All skills
sanity-io avatar

/sanity-best-practices

@fc8116b official
by Sanitysanity-io/agent-toolkit187 stars
30

Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, webhooks, Blueprints, and framework integrations such as Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and the App SDK. Use this skill whenever working with Sanity schemas, defineType or defineField, GROQ or defineQuery, content modeling, Presentation or preview setups, Sanity-powered frontend integrations, event-driven content automation, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase.

Use this Skill: https://skilld.dev/gh/sanity-io/agent-toolkit/sanity-best-practices

This session only. Nothing lands on disk.

referencesseo.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Sanity SEO Best Practices

1. Core Philosophy

SEO doesn't require complex configurations. A few core principles, applied consistently:

  • Smart defaults with optional overrides — Don't require SEO fields; use existing content as fallback
  • Use GROQ for fallback logic — Move conditional logic into queries, not components
  • Leverage Next.js APIs — Use generateMetadata, sitemap.ts, not manual <meta> tags
  • Structured content = structured data — Your content model is already SEO-ready

2. SEO Schema Type (Reusable)

Create a reusable SEO object type for consistent metadata across document types.

// schemaTypes/seoType.ts
import { defineField, defineType } from "sanity";

export const seoType = defineType({
  name: "seo",
  title: "SEO",
  type: "object",
  fields: [
    defineField({
      name: "title",
      description: "Overrides the page title if provided",
      type: "string",
    }),
    defineField({
      name: "description",
      type: "text",
      rows: 3,
    }),
    defineField({
      name: "image",
      description: "Image for social sharing (1200x630 recommended)",
      type: "image",
      options: { hotspot: true },
    }),
    defineField({
      name: "noIndex",
      description: "Hide this page from search engines",
      type: "boolean",
      initialValue: false,
    }),
  ],
});

Usage in document types:

defineField({
  name: "seo",
  type: "seo",
})

3. GROQ Queries with Fallbacks

Use coalesce() to provide fallback values. This keeps frontend logic clean.

*[_type == "page" && slug.current == $slug][0]{
  ...,
  "seo": {
    // Use SEO field if provided, otherwise fall back to main title
    "title": coalesce(seo.title, title, ""),
    "description": coalesce(seo.description, ""),
    "image": seo.image,
    "noIndex": seo.noIndex == true
  }
}

Key principle: seo.title will never be null — it contains either the SEO override, the page title, or empty string.

4. Next.js Metadata (The Right Way)

Use generateMetadata — never render <title> or <meta> tags directly in components.

// app/(frontend)/[slug]/page.tsx
import type { Metadata } from "next";
import { urlFor } from "@/sanity/lib/image";

type RouteProps = {
  params: Promise<{ slug: string }>;
};

// Extract fetch to reuse in both functions
const getPage = async (params: RouteProps["params"]) =>
  sanityFetch({
    query: PAGE_QUERY,
    params: await params,
    stega: false, // Critical for SEO!
  });

export async function generateMetadata({ params }: RouteProps): Promise<Metadata> {
  const { data: page } = await getPage(params);

  if (!page) return {};

  const metadata: Metadata = {
    title: page.seo.title,
    description: page.seo.description,
  };

  // Open Graph image
  if (page.seo.image) {
    metadata.openGraph = {
      images: {
        url: urlFor(page.seo.image).width(1200).height(630).url(),
        width: 1200,
        height: 630,
      },
    };
  }

  // noIndex robots directive
  if (page.seo.noIndex) {
    metadata.robots = "noindex";
  }

  return metadata;
}

export default async function Page({ params }: RouteProps) {
  const { data: page } = await getPage(params);
  // ... render page
}

Critical: Always set stega: false when fetching for metadata. Stega characters in <title> destroy SEO.

5. Dynamic Sitemap

Use Next.js sitemap.ts convention to auto-generate from Sanity content.

GROQ Query

*[_type in ["page", "post"] && defined(slug.current) && seo.noIndex != true] {
  "href": select(
    _type == "page" => "/" + slug.current,
    _type == "post" => "/posts/" + slug.current,
    slug.current
  ),
  _updatedAt
}

Route Implementation

// app/sitemap.ts
import { MetadataRoute } from "next";
import { client } from "@/sanity/lib/client";
import { SITEMAP_QUERY } from "@/sanity/lib/queries";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = process.env.VERCEL_URL
    ? `https://${process.env.VERCEL_URL}`
    : "http://localhost:3000";

  try {
    const paths = await client.fetch(SITEMAP_QUERY);
    if (!paths) return [];

    return paths.map((path) => ({
      url: new URL(path.href!, baseUrl).toString(),
      lastModified: new Date(path._updatedAt),
      changeFrequency: "weekly",
      priority: 1,
    }));
  } catch (error) {
    console.error("Sitemap generation failed:", error);
    return [];
  }
}

Note: Sitemap limit is 50,000 URLs per file. For larger sites, use sitemap index.

6. Redirects (Managed in Sanity)

Create a redirect document type for content team management.

Schema

// schemaTypes/redirectType.ts
import { defineField, defineType, SanityDocumentLike } from "sanity";
import { LinkIcon } from "@sanity/icons/Link";

function isValidPath(value: string | undefined) {
  if (!value) return "Required";
  if (!value.startsWith("/")) return "Must start with /";
  if (/[^a-zA-Z0-9\-_/:]/.test(value)) return "Invalid characters";
  return true;
}

export const redirectType = defineType({
  name: "redirect",
  title: "Redirect",
  type: "document",
  icon: LinkIcon,
  validation: (Rule) =>
    Rule.custom((doc: SanityDocumentLike | undefined) => {
      if (doc?.source === doc?.destination) {
        return "Source and destination cannot be the same";
      }
      return true;
    }),
  fields: [
    defineField({
      name: "source",
      type: "string",
      validation: (Rule) => Rule.required().custom(isValidPath),
    }),
    defineField({
      name: "destination",
      type: "string",
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "permanent",
      description: "301 (permanent) or 302 (temporary)",
      type: "boolean",
      initialValue: true,
    }),
    defineField({
      name: "isEnabled",
      type: "boolean",
      initialValue: true,
    }),
  ],
});

Next.js Config

// next.config.ts
import { fetchRedirects } from "@/sanity/lib/fetchRedirects";

const nextConfig: NextConfig = {
  async redirects() {
    return await fetchRedirects();
  },
};

Limits: Vercel allows max 1,024 redirects in next.config. For more, use middleware.

7. Dynamic Open Graph Images

Generate OG images on-the-fly using Next.js Edge Runtime at /api/og.

// app/api/og/route.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";

export async function GET(request: Request) {
  const id = new URL(request.url).searchParams.get("id");
  if (!id) return new Response("Missing id", { status: 400 });

  const data = await client.fetch(`*[_id == $id][0]{ title }`, { id });

  return new ImageResponse(
    <div tw="flex w-full h-full bg-blue-500 text-white p-10">
      <h1 tw="text-6xl font-bold">{data?.title || "Untitled"}</h1>
    </div>,
    { width: 1200, height: 630 }
  );
}

Use as fallback in metadata: url: page.seo.image ? urlFor(page.seo.image).url() : \/api/og?id=${page._id}``

8. JSON-LD Structured Data

Use schema-dts for type-safe structured data.

npm install schema-dts

FAQ Example

import { FAQPage, WithContext } from "schema-dts";

const generateFaqData = (faqs: FAQ[]): WithContext<FAQPage> => ({
  "@context": "https://schema.org",
  "@type": "FAQPage",
  mainEntity: faqs.map((faq) => ({
    "@type": "Question",
    name: faq.title,
    acceptedAnswer: {
      "@type": "Answer",
      text: faq.text, // Use pt::text() in GROQ to get plain text
    },
  })),
});

// In component
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(generateFaqData(faqs)) }}
/>

GROQ for Plain Text

faqs[]->{
  _id,
  title,
  body,
  "text": pt::text(body)  // Convert Portable Text to plain string
}

9. Testing Tools

Source: SKILL.md on GitHub

2 warnings14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    This skill provides comprehensive Sanity.io development best practices, covering schema design, GROQ queries, and integration with major frontend frameworks. It promotes secure development habits, such as proper management of API tokens and environment variables.

  • Socket14d

    1 alert: gptAnomaly

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer6mo

    8/24 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at fc8116b. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
  • Next.js
  • Nuxt
  • sanity
  • groq
  • schema
  • typegen
  • portable-text
  • visual-editing
  • astro
  • remix
  • sveltekit
  • migrations
  • localization
  • cms

README badge

README badge for sanity-io/agent-toolkit/sanity-best-practices

Provides guidelines and reference materials for Sanity schema design, GROQ queries, TypeGen, Visual Editing, Portable Text, Studio structure, localization, migrations, Sanity Functions, and framework integrations including Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, and Hydrogen. Use this skill when setting up Sanity projects, designing content models, writing queries, implementing live preview, or integrating Sanity with a frontend framework.

Generated from the current SKILL.md.

Does this skill cover framework integrations like Next.js, Nuxt, and Astro?
Yes. The skill includes integration guides for Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and standalone Studio patterns.
What does this skill cover for GROQ queries?
The skill provides GROQ query patterns, type safety approaches, and performance optimization guidelines, with reference materials for detailed examples.
Does this skill include guidance on Sanity Functions and event automation?
Yes. The skill covers Sanity Functions for automating content workflows, including documentEventHandler and defineDocumentFunction patterns.
Can I use this skill for schema design and content modeling?
Yes. The skill provides schema design best practices, field definitions, validation patterns, and content modeling guidance for different use cases.
Does this cover Visual Editing and live preview setup?
Yes. The skill includes guidance on the Presentation Tool, Stega, overlays, and live preview configuration for frontend frameworks.

Generated from the current SKILL.md. These answers refresh after source changes.