Frontend Integration
Query SEO data via GROQ and render meta tags in your frontend framework. Use the built-in helpers for zero-boilerplate integration.
✨ Built-in helpers (v1.2.4+)
The plugin ships buildSeoMeta() and <SeoMetaTags> so you don't have to map every field manually. Both are exported from sanity-plugin-seofields/next — a dedicated entry point for use in Next.js Server Components and generateMetadata() functions.
GROQ Query
Fetch SEO fields from any document:
*[_type == "page" && slug.current == $slug][0]{
title,
seo {
title,
description,
canonicalUrl,
metaImage { asset-> { url } },
keywords,
robots { noIndex, noFollow },
openGraph {
title, description, url, siteName, type,
imageType, imageUrl,
image { asset-> { url }, alt }
},
twitter {
card, site, creator, title, description,
imageType, imageUrl,
image { asset-> { url }, alt }
},
metaAttributes[] { _key, key, type, value }
}
}Next.js App Router — buildSeoMeta()
Use buildSeoMeta() inside generateMetadata(). The return value is structurally compatible with Next.js's Metadata type — no manual field mapping needed.
import type { Metadata } from 'next'
import { buildSeoMeta } from 'sanity-plugin-seofields/next'
import { sanityFetch } from '@/sanity/lib/live'
import { urlFor } from '@/sanity/lib/image'
import { SEO_QUERY } from '@/sanity/lib/queries'
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params
const { data } = await sanityFetch({ query: SEO_QUERY, params: { slug } })
return buildSeoMeta({
seo: data?.seo,
baseUrl: 'https://example.com',
path: `/blog/${slug}`,
defaults: {
title: 'My Site',
siteName: 'My Site',
twitterSite: '@mysite',
},
// Optional: resolve Sanity image refs to full URLs
imageUrlResolver: (img) => urlFor(img).width(1200).url(),
})
}Next.js App Router — <SeoMetaTags>
Renders <title>, <meta>, and <link rel="canonical"> tags as plain React elements. Next.js hoists them to <head> automatically. Import from sanity-plugin-seofields/next to use it in Next.js Server Components and generateMetadata() functions.
import { SeoMetaTags } from 'sanity-plugin-seofields/next'
import type { SeoFields } from 'sanity-plugin-seofields'
import { sanityFetch } from '@/sanity/lib/live'
import { urlFor } from '@/sanity/lib/image'
import { BLOG_QUERY } from '@/sanity/lib/queries'
export default async function Page({ params }) {
const { slug } = await params
const { data } = await sanityFetch({ query: BLOG_QUERY, params: { slug } })
return (
<>
{data.seo && (
<SeoMetaTags
data={data.seo as SeoFields}
baseUrl="https://example.com"
path={`/blog/${slug}`}
defaults={{ title: 'My Site', siteName: 'My Site' }}
imageUrlResolver={(img) => urlFor(img).width(1200).url()}
/>
)}
<main>...</main>
</>
)
}Next.js Pages Router
Place <SeoMetaTags> inside Next.js <Head>.
import Head from 'next/head'
import { SeoMetaTags } from 'sanity-plugin-seofields/next'
import { urlFor } from '@/sanity/lib/image'
export default function Page({ data }) {
return (
<>
<Head>
<SeoMetaTags
data={data.seo}
baseUrl="https://example.com"
path={`/blog/${data.slug}`}
defaults={{ title: 'My Site', siteName: 'My Site' }}
imageUrlResolver={(img) => urlFor(img).width(1200).url()}
/>
</Head>
<main>...</main>
</>
)
}buildSeoMeta() Options
interface BuildSeoMetaOptions {
/** Raw SEO object from Sanity. Pass null/undefined to use only defaults. */
seo?: SeoFieldsInput | null
/** Base URL of your site, e.g. "https://example.com". Used for canonical + og:url. */
baseUrl?: string
/** Current page path, e.g. "/about". Combined with baseUrl for canonical + og:url. */
path?: string
/** Fallback values used when SEO fields are missing. */
defaults?: {
title?: string
description?: string
siteName?: string
twitterSite?: string
twitterCreator?: string
/** Fallback OG/Twitter image URL when no image is set in Sanity. */
ogImage?: string
}
/** Resolve a Sanity image asset reference to a full URL string. */
imageUrlResolver?: (image: SanityImage | SanityImageWithAlt) => string | null | undefined
}React / Gatsby (manual)
For non-Next.js frameworks, use <SeoMetaTags> inside react-helmet or any head management library, or map fields manually:
import { Helmet } from 'react-helmet'
export function SEO({ seo }) {
if (!seo) return null
const robotsContent = [
seo.robots?.noIndex ? 'noindex' : 'index',
seo.robots?.noFollow ? 'nofollow' : 'follow',
].join(', ')
return (
<Helmet>
{seo.title && <title>{seo.title}</title>}
{seo.description && <meta name="description" content={seo.description} />}
{seo.keywords?.length > 0 && <meta name="keywords" content={seo.keywords.join(', ')} />}
<meta name="robots" content={robotsContent} />
{seo.canonicalUrl && <link rel="canonical" href={seo.canonicalUrl} />}
{/* 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} />}
{/* 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} />}
</Helmet>
)
}Was this page helpful?