seofields
docs

Frontend Integration

Query SEO data via GROQ and render meta tags in your frontend framework.

GROQ Query

Fetch SEO fields from any document:

GROQ query
*[_type == "page" && slug.current == $slug][0]{
  title,
  seo {
    title,
    description,
    canonicalUrl,
    metaImage { asset-> { url } },
    keywords,
    robots { noIndex, noFollow },
    openGraph {
      title, description, url, siteName, type,
      image { asset-> { url } }
    },
    twitter {
      card, site, creator, title, description,
      image { asset-> { url } }
    },
    metaAttributes[] {
      _key, attributeKey, attributeType, attributeValueString,
      attributeValueImage { asset-> { url } }
    }
  }
}

Next.js

components/SEOHead.tsx
import Head from 'next/head'

export function SEOHead({ seo }) {
  if (!seo) return null

  return (
    <Head>
      {seo.title && <title>{seo.title}</title>}
      {seo.description && (
        <meta name="description" content={seo.description} />
      )}

      {/* Open Graph */}
      {seo.openGraph?.title && (
        <meta property="og:title" content={seo.openGraph.title} />
      )}
      {seo.openGraph?.description && (
        <meta property="og:description" content={seo.openGraph.description} />
      )}
      {seo.openGraph?.url && (
        <meta property="og:url" content={seo.openGraph.url} />
      )}
      {seo.openGraph?.siteName && (
        <meta property="og:site_name" content={seo.openGraph.siteName} />
      )}
      {seo.openGraph?.type && (
        <meta property="og:type" content={seo.openGraph.type} />
      )}
      {seo.openGraph?.image?.asset?.url && (
        <meta property="og:image" content={seo.openGraph.image.asset.url} />
      )}

      {/* X (Twitter) Card */}
      {seo.twitter?.card && (
        <meta name="twitter:card" content={seo.twitter.card} />
      )}
      {seo.twitter?.site && (
        <meta name="twitter:site" content={seo.twitter.site} />
      )}
      {seo.twitter?.creator && (
        <meta name="twitter:creator" content={seo.twitter.creator} />
      )}
      {seo.twitter?.title && (
        <meta name="twitter:title" content={seo.twitter.title} />
      )}
      {seo.twitter?.description && (
        <meta name="twitter:description" content={seo.twitter.description} />
      )}

      {/* Robots */}
      {(seo.robots?.noIndex || seo.robots?.noFollow) && (
        <meta
          name="robots"
          content={[
            seo.robots.noIndex && 'noindex',
            seo.robots.noFollow && 'nofollow',
          ].filter(Boolean).join(', ')}
        />
      )}

      {/* Canonical URL */}
      {seo.canonicalUrl && (
        <link rel="canonical" href={seo.canonicalUrl} />
      )}

      {/* Keywords */}
      {seo.keywords?.length > 0 && (
        <meta name="keywords" content={seo.keywords.join(', ')} />
      )}
    </Head>
  )
}

Next.js App Router (Metadata API)

app/[slug]/page.tsx
import type { Metadata } from 'next'
import { client } from '@/sanity/client'

export async function generateMetadata({ params }): Promise<Metadata> {
  const page = await client.fetch(
    `*[_type == "page" && slug.current == $slug][0]{ seo }`,
    { slug: params.slug }
  )
  const seo = page?.seo

  return {
    title: seo?.title,
    description: seo?.description,
    keywords: seo?.keywords,
    alternates: {
      canonical: seo?.canonicalUrl,
    },
    openGraph: {
      title: seo?.openGraph?.title,
      description: seo?.openGraph?.description,
      url: seo?.openGraph?.url,
      siteName: seo?.openGraph?.siteName,
      type: seo?.openGraph?.type,
      images: seo?.openGraph?.image?.asset?.url
        ? [{ url: seo.openGraph.image.asset.url }]
        : undefined,
    },
    twitter: {
      card: seo?.twitter?.card,
      site: seo?.twitter?.site,
      creator: seo?.twitter?.creator,
      title: seo?.twitter?.title,
      description: seo?.twitter?.description,
    },
    robots: {
      index: !seo?.robots?.noIndex,
      follow: !seo?.robots?.noFollow,
    },
  }
}

React / Gatsby

SEO.tsx
import { Helmet } from 'react-helmet'

export function SEO({ seo }) {
  return (
    <Helmet>
      <title>{seo?.title}</title>
      <meta name="description" content={seo?.description} />
      {seo?.keywords && (
        <meta name="keywords" content={seo.keywords.join(', ')} />
      )}

      {/* Open Graph */}
      <meta property="og:title" content={seo?.openGraph?.title} />
      <meta property="og:description" content={seo?.openGraph?.description} />
      <meta property="og:url" content={seo?.openGraph?.url} />
      <meta property="og:type" content={seo?.openGraph?.type || 'website'} />

      {/* X (Twitter) Card */}
      {seo?.twitter?.card && (
        <meta name="twitter:card" content={seo.twitter.card} />
      )}
      {seo?.twitter?.site && (
        <meta name="twitter:site" content={seo.twitter.site} />
      )}
      {seo?.twitter?.creator && (
        <meta name="twitter:creator" content={seo.twitter.creator} />
      )}
    </Helmet>
  )
}