Building SEO overrides
Conditional operators are frequently used when building Sanity on the frontend. This section follows the same pattern, providing multiple overrides and fallbacks for SEO fields.
Why this approach?
This method allows content editors to override SEO fields while also having fallbacks if they don't provide content. It ensures a fast authoring experience with the option to add more complex SEO configurations later if needed.
Working with overrides
Let's build a schema that has a title
field, and then add SEO overrides for the seoTitle
field.
import { defineField, defineType } from "sanity";
export const post = defineType({
name: "post",
type: "document",
fields: [
defineField({
name: "title",
type: "string",
validation: (Rule) => Rule.required(),
}),
// I will override the title field if you fill in this field
defineField({
name: "seoTitle",
type: "string",
}),
],
});
To cater for the SEO overrides, you will need to add a nullish coalescing operator (opens in a new tab) on the frontend.
In this example if an seoTitle
is provided, it will be used to populate the <title>
tag, but if it's not provided, the title
field will be used as a fallback.
export const PostPage = ({ data }) => {
const { title, seoTitle } = data ?? {};
return (
<main>
<Head>
<title>{seoTitle ?? title}</title>
</Head>
<h1>{title}</h1>
</main>
);
};
Following the same pattern, you can add SEO overrides for the seoDescription
field. This will then be passed to the <meta name="description" />
tag. This is what Google uses to display a description of your page in the search results.
Again, you can also add an override for the seoImage
field, which will be used to populate the <meta property="og:image" />
tag.
The most important takeaway from this, is that you always want to have an override, and a fallback. It keeps consistency in your content, and standardizes the way you query your SEO fields.
Let's jump to a more comprehensive example, where you have a page builder, and you want to add SEO overrides for the title
, description
, and image
fields.
import { defineField, defineType } from "sanity";
export const post = defineType({
name: "post",
type: "document",
fields: [
defineField({
name: "title",
type: "string",
validation: (Rule) => Rule.required(),
}),
defineField({
name: "description",
type: "text",
validation: (Rule) => Rule.required(),
}),
defineField({
name: "image",
type: "image",
}),
defineField({
name: "seoTitle",
type: "string",
}),
defineField({
name: "seoDescription",
type: "text",
}),
defineField({
name: "seoImage",
type: "image",
}),
],
});
To emphasize the importance of having an override and a fallback, let's take one last look at the frontend code, before moving onto the kitchen-sink example.
export const PostPage = ({ data }) => {
const { title, seoTitle, description, seoDescription, image, seoImage } =
data ?? {};
return (
<>
{/* Keeping this simple for now */}
<title>{seoTitle ?? title}</title>
<meta name="description" content={seoDescription ?? description} />
<meta
property="og:image"
content={seoImage?.asset?.url ?? image?.asset?.url}
/>
</>
);
};
You may have noticed that on this version, there is a metadata object to set the title, description, and image. This is a modern way to set the metadata for your page, and it's what Next.js recommends.
Make sure to check out the metadata docs (opens in a new tab) for more information.
Here's how you implement this with batteries included:
import type { Metadata } from "next";
import { Image } from "next/image";
// Sanity client setup earlier
import { client } from "@/sanity/lib/client";
async function getPostData(slug: string) {
const post = await client.fetch(
groq`*[_type == "post" && slug.current == $slug][0]{
title,
description,
image,
seoTitle,
seoDescription,
seoImage,
}`,
{ slug }
);
return post;
}
export const generateMetadata = async ({
params,
}: Params): Promise<Metadata> => {
const { slug } = await params;
const { title, description, image, seoTitle, seoDescription, seoImage } =
await getPostData(slug);
return {
// Here's all the overrides and fallbacks
title: seoTitle ?? title,
description: seoDescription ?? description,
image: seoImage?.asset?.url ?? image?.asset?.url,
};
};
export default async function PostPage({ params }: Params) {
const { slug } = params;
const { title, description, image } = await getPostData(slug);
return (
<main>
<h1>{title}</h1>
{/* ...rest of your page content */}
</main>
);
}
At this point, you can see how the pattern works, and how this is easy to extend to other SEO fields.
This approach can be easily abstracted into a reusable <SEO/>
component that you can use across your site, while still maintaining consistent SEO data with fallbacks. Always include fallbacks.
In the next lesson, you will enhance your SEO functionality by adding:
- Open Graph fields for social sharing
- A
seoNoIndex
field to control search engine indexing - A
seoHideFromLists
field to manage content visibility in listings