SEO
Lesson 2

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.

post.ts
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.

page.tsx
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.

post.ts
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.

page.tsx
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:

/app/posts/[slug]/page.tsx
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