{"_id":"@crawl-me-maybe/sanity-plugin-seo","_rev":"3-b0384a5ca506c3ea35984d3d47cf46ba","name":"@crawl-me-maybe/sanity-plugin-seo","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@crawl-me-maybe/sanity-plugin-seo","version":"0.1.0","author":{"name":"Nathan Nye"},"license":"MIT","_id":"@crawl-me-maybe/sanity-plugin-seo@0.1.0","maintainers":[{"name":"autopsyaardvark","email":"nathan@nye.design"}],"dist":{"shasum":"3763ab38d4cd5ba78709d900f9a4f2d3e5b2567c","tarball":"https://registry.npmjs.org/@crawl-me-maybe/sanity-plugin-seo/-/sanity-plugin-seo-0.1.0.tgz","fileCount":38,"integrity":"sha512-IbexZRmby4jm38XqNRLMCQuWvP6AWV63+bm5iNEVj0YBmNSvZjf+f0kvEUY6bz22Cc7LUeKHlJSGWrN4T+MY7A==","signatures":[{"sig":"MEYCIQDRaWH7Ks8K63WyfZXbKhaFoVnlPdfEVDLkcm0bMrgywwIhAIEqQoJc0ilm7UZEv51hoSIDhx6pY6Q0NgQZ0IJEQpYi","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":165833},"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"ea29b941627b726f11e8fc6064be62334c1936bf","scripts":{"build":"bun run build.ts && tsc -p tsconfig.json --declaration --emitDeclarationOnly --declarationDir dist --rootDir src --noCheck"},"_npmUser":{"name":"autopsyaardvark","email":"nathan@nye.design"},"_npmVersion":"11.6.2","description":"SEO fields, global defaults, and social preview cards for Sanity Studio.","directories":{},"_nodeVersion":"22.20.0","dependencies":{"react-icons":"^5.6.0","@sanity-image/url-builder":"^1.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.7","sanity":"^6.1.0","react-dom":"^19.2.7","@sanity/ui":"^3.2.0","@types/react":"^19.2.17"},"peerDependencies":{"react":"^18.3 || ^19","sanity":"^5 || ^6.0.0","react-dom":"^18.3 || ^19","@sanity/ui":"^2.0 || ^3.0"},"_npmOperationalInternal":{"tmp":"tmp/sanity-plugin-seo_0.1.0_1782344896925_0.3880995010303554","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@crawl-me-maybe/sanity-plugin-seo","version":"0.1.1","author":{"name":"Nathan Nye"},"license":"MIT","_id":"@crawl-me-maybe/sanity-plugin-seo@0.1.1","maintainers":[{"name":"autopsyaardvark","email":"nathan@nye.design"}],"dist":{"shasum":"4c4b16d988053f985485a654bb2865e340b5a071","tarball":"https://registry.npmjs.org/@crawl-me-maybe/sanity-plugin-seo/-/sanity-plugin-seo-0.1.1.tgz","fileCount":38,"integrity":"sha512-D+Q6mrC6BINAjK+PgL8oy42FIjNm5lmqi7mAdgJ9PGm6rIvYMJ0LECdCC5rbYJhKEnuoX0XpaMizEmrlJAuaAg==","signatures":[{"sig":"MEUCIGe9riCtH+cEE+OhuV6ELKhTgDoZtRSaLWW0ZXB3KH+8AiEAle3K5jfx6daj/uNkb4HkrB0RPvl3Z+0KdylrUQVtER8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":165986},"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"497f987e3a4746b15cc305954c0d284da6e343da","scripts":{"build":"bun run build.ts && tsc -p tsconfig.json --declaration --emitDeclarationOnly --declarationDir dist --rootDir src --noCheck"},"_npmUser":{"name":"autopsyaardvark","email":"nathan@nye.design"},"_npmVersion":"11.6.2","description":"<img width=\"1590\" height=\"408\" alt=\"sanity-plugin-thumb\" src=\"https://github.com/user-attachments/assets/1069b4d7-7a3c-4ceb-b538-b0f3bcbe4c71\" /> <br/>","directories":{},"_nodeVersion":"22.20.0","dependencies":{"react-icons":"^5.6.0","@sanity-image/url-builder":"^1.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.7","sanity":"^6.1.0","react-dom":"^19.2.7","@sanity/ui":"^3.2.0","@types/react":"^19.2.17"},"peerDependencies":{"react":"^18.3 || ^19","sanity":"^5 || ^6.0.0","react-dom":"^18.3 || ^19","@sanity/ui":"^2.0 || ^3.0"},"_npmOperationalInternal":{"tmp":"tmp/sanity-plugin-seo_0.1.1_1782347188790_0.9510164968375252","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@crawl-me-maybe/sanity-plugin-seo","version":"0.1.2","author":"Nathan Nye","main":"./dist/index.js","types":"./dist/index.d.ts","license":"MIT","publishConfig":{"access":"public"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"bun run build.ts && tsc -p tsconfig.json --declaration --emitDeclarationOnly --declarationDir dist --rootDir src --noCheck","prepublishOnly":"bun run build"},"dependencies":{"@sanity-image/url-builder":"^1.1.0","react-icons":"^5.6.0"},"peerDependencies":{"@sanity/ui":"^2.0 || ^3.0","react":"^18.3 || ^19","react-dom":"^18.3 || ^19","sanity":"^5 || ^6.0.0"},"devDependencies":{"@sanity/ui":"^3.2.0","@types/react":"^19.2.17","react":"^19.2.7","react-dom":"^19.2.7","sanity":"^6.1.0"},"_id":"@crawl-me-maybe/sanity-plugin-seo@0.1.2","_integrity":"sha512-M1wBv4Waza91LN5KS15XtKgrrOTEgnXIQgrxbn/pKAazC81eBX2KqrqjRlfBwOn/NnVd8Mn7iRuOXCeoIhHmCA==","_nodeVersion":"24.3.0","_npmVersion":"10.8.3","shasum":"21d6acfded1799bac0e90af7fd81b8b6e7392b82","dist":{"integrity":"sha512-M1wBv4Waza91LN5KS15XtKgrrOTEgnXIQgrxbn/pKAazC81eBX2KqrqjRlfBwOn/NnVd8Mn7iRuOXCeoIhHmCA==","shasum":"21d6acfded1799bac0e90af7fd81b8b6e7392b82","tarball":"https://registry.npmjs.org/@crawl-me-maybe/sanity-plugin-seo/-/sanity-plugin-seo-0.1.2.tgz","fileCount":39,"unpackedSize":175100,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIH+zBIWhozbuCEVqWEzsekgXxj6I1daUyFwYwye+3QyzAiEAqMkGhTnME/zh+x1DqaF0+5rHtOkhtKd5cqVI/DX0bTw="}]},"_npmUser":{"name":"autopsyaardvark","email":"nathan@nye.design"},"directories":{},"maintainers":[{"name":"autopsyaardvark","email":"nathan@nye.design"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sanity-plugin-seo_0.1.2_1785780535144_0.36324384524549314"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-24T23:48:16.725Z","modified":"2026-08-03T18:08:55.474Z","0.1.0":"2026-06-24T23:48:17.078Z","0.1.1":"2026-06-25T00:26:28.941Z","0.1.2":"2026-08-03T18:08:55.270Z"},"author":"Nathan Nye","license":"MIT","maintainers":[{"name":"autopsyaardvark","email":"nathan@nye.design"}],"readme":"<img width=\"1590\" height=\"408\" alt=\"sanity-plugin-thumb\" src=\"https://github.com/user-attachments/assets/1069b4d7-7a3c-4ceb-b538-b0f3bcbe4c71\" />\n<br/>\n\n# @crawl-me-maybe/sanity-plugin-seo\n\nSEO fields, global defaults, and social preview cards for Sanity Studio.\n\n> Built for Sanity v5 and v6\n\nDesigned to pair with [`@crawl-me-maybe/meta`](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/meta) on the frontend. The `globalSeoSettings` document and `pageMetadata` object use the same field names and shapes that `buildMetadata()` expects — query your content, resolve image URLs in GROQ, and pass the result straight through.\n\n## Table of contents\n\n- [Install](#install)\n- [Features](#features)\n- [Getting started](#getting-started)\n  - [Plugin configuration](#plugin-configuration)\n  - [Custom canonical URLs](#custom-canonical-urls)\n  - [Adding SEO fields to a page document](#adding-seo-fields-to-a-page-document)\n  - [Studio structure — surfacing Global SEO Settings](#studio-structure--surfacing-global-seo-settings)\n  - [Field-level i18n (i18n-fields)](#field-level-i18n-i18n-fields)\n- [Favicons](#favicons)\n- [Robots.txt](#robotstxt)\n- [Frontend integration](#frontend-integration)\n  - [With `@crawl-me-maybe/meta`](#with-crawl-me-maybemeta)\n- [Schemas](#schemas)\n  - [`globalSeoSettings` document](#globalseoSettings-document)\n  - [`pageMetadata` object](#pagemetadata-object)\n  - [Shared field types](#shared-field-types)\n\n## Install\n\n```bash\nnpm install @crawl-me-maybe/sanity-plugin-seo\npnpm add @crawl-me-maybe/sanity-plugin-seo\nbun add @crawl-me-maybe/sanity-plugin-seo\nyarn add @crawl-me-maybe/sanity-plugin-seo\n```\n\n---\n\n## Features\n\n- **Global defaults** — a `globalSeoSettings` singleton document that feeds fallback values to all page-level fields; editors always see what fallback is being displayed\n- **Page metadata** — a `pageMetadata` object type with meta description, meta image, and search indexing controls that each show and override the global default\n- **Social preview cards** — live editable previews for Facebook, Twitter/X, LinkedIn, and Google Search, built from page data\n- **Favicon browser-tab preview** — renders your favicon and site title in a mock browser tab with light/dark toggle\n- **Robots.txt rules editor** — structured rule builder with a read-only `robots.txt` preview tab\n\n## Getting started\n\n### Plugin configuration\n\nPass options to the plugin to enable or disable individual features. All options default to `true`.\n\n```ts\n// sanity.config.ts\nimport crawlMeMaybeSeo from \"@crawl-me-maybe/sanity-plugin-seo\";\n\nexport default defineConfig({\n  plugins: [\n    crawlMeMaybeSeo({\n      global: {\n        favicon: true,   // favicon field + browser-tab preview in Global Settings\n        robots: true,    // robots.txt rule builder in Global Settings\n      },\n      page: {\n        searchIndexing: true, // noIndex / noFollow controls on page metadata\n        canonicalUrl: true,   // canonical URL override on page metadata\n      },\n    }),\n  ],\n});\n```\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `global.favicon` | `boolean` | `true` | Includes the favicon field (with browser-tab preview) in Global SEO Settings |\n| `global.robots` | `boolean` | `true` | Includes the robots rules builder in Global SEO Settings |\n| `page.searchIndexing` | `boolean` | `true` | Includes noIndex / noFollow controls on the `pageMetadata` field |\n| `page.canonicalUrl` | `boolean` | `true` | Includes the canonical URL field on `pageMetadata`. Disable if you manage canonical via a custom reference field on the document |\n| `fieldTypes.metaDescription` | `string` | `\"text\"` | Base Sanity type for the shared `metaDescription` field |\n| `fieldTypes.metaTitle` | `string` | `\"string\"` | Base Sanity type for the shared `metaTitle` field |\n| `resolveValue` | `(value: unknown) => unknown` | identity | Studio helper that turns stored field values into scalars for placeholders, `hasContent`, and social previews |\n\n### Custom canonical URLs\n\nBy default, `pageMetadata` includes a `canonicalUrl` field for explicit URL overrides. Disable it when you prefer a reference to another document:\n\n```ts\n// sanity.config.ts\ncrawlMeMaybeSeo({\n  page: {\n    canonicalUrl: false,\n  },\n});\n```\n\nThen add your own field on the page document:\n\n```ts\ndefineField({\n  name: \"canonical\",\n  type: \"reference\",\n  to: [{ type: \"page\" }, { type: \"post\" }],\n})\n```\n\nResolve the referenced document to a URL in your frontend query and pass it to `@crawl-me-maybe/meta` with `disableSelfCanonical: true`. When the built-in field is enabled, leave it empty and `@crawl-me-maybe/meta` generates a self-referential canonical from `siteUrl` + slug automatically.\n\n\n### Adding SEO fields to a page document\n\nInclude the `pageMetadata` object type on any document schema. Global defaults are displayed automatically in Studio; use [`@crawl-me-maybe/meta`](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/meta)'s `buildMetadata()` on the frontend to apply the same merge logic.\n\n```ts\n// schemas/page.ts\nimport { defineType, defineField } from \"sanity\";\n\nexport default defineType({\n  name: \"page\",\n  title: \"Page\",\n  type: \"document\",\n  fields: [\n    defineField({\n      name: \"title\",\n      title: \"Title\",\n      type: \"string\",\n    }),\n    defineField({\n      name: \"slug\",\n      title: \"Slug\",\n      type: \"slug\",\n      options: { source: \"title\" },\n    }),\n    defineField({\n      name: \"seo\",\n      title: \"SEO\",\n      type: \"pageMetadata\", // registered by the plugin\n    }),\n  ],\n});\n```\n\nEach `pageMetadata` field shows the live global default as a placeholder when the field is empty, and renders social preview cards (Facebook, Twitter/X, LinkedIn, Google) in a tabbed panel.\n\n<img width=\"814\" height=\"686\" alt=\"Screenshot 2026-06-23 at 1 48 58 PM\" src=\"https://github.com/user-attachments/assets/542b0804-4036-4d8c-b2ab-d6579f84523c\" />\n<br/>\n\n### Studio structure — surfacing Global SEO Settings\n\nRegister the plugin, then add the `globalSeoSettings` singleton to your Studio's structure so editors can reach it:\n\n```ts\n// sanity.config.ts\nimport { defineConfig } from \"sanity\";\nimport { structureTool } from \"sanity/structure\";\nimport crawlMeMaybeSeo from \"@crawl-me-maybe/sanity-plugin-seo\";\n\nexport default defineConfig({\n  plugins: [\n    structureTool({\n      structure: (S) =>\n        S.list()\n          .title(\"Content\")\n          .items([\n            // ... your other list items ...\n            S.divider(),\n            S.listItem()\n              .title(\"Global SEO Settings\")\n              .child(\n                S.document()\n                  .schemaType(\"globalSeoSettings\")\n                  .documentId(\"globalSeoSettings\")\n              ),\n          ]),\n    }),\n    crawlMeMaybeSeo(),\n  ],\n});\n```\n\n### Field-level i18n (i18n-fields)\n\nWhen using [sanity-plugin-i18n-fields](https://www.sanity.io/plugins/i18n-fields), swap the base types for `metaDescription` / `metaTitle` and provide a `resolveValue` so Studio can read a locale string out of the stored locale object.\n\n`i18n.text` / `i18n.string` store values like `{ _type: \"i18n.text\", en: \"...\", fr: \"...\" }` instead of plain strings. Without `resolveValue`, global-default placeholders and social preview cards cannot display those values.\n\n```ts\n// sanity.config.ts\nimport { defineConfig } from \"sanity\";\nimport crawlMeMaybeSeo from \"@crawl-me-maybe/sanity-plugin-seo\";\nimport { I18nFields } from \"sanity-plugin-i18n-fields\";\n\nexport default defineConfig({\n  plugins: [\n    I18nFields({\n      locales: [\n        { code: \"en\", title: \"English\", default: true },\n        { code: \"fr\", title: \"French\" },\n      ],\n    }),\n    crawlMeMaybeSeo({\n      fieldTypes: {\n        metaDescription: \"i18n.text\",\n        metaTitle: \"i18n.string\",\n      },\n      resolveValue: (value) => {\n        if (typeof value === \"string\") return value;\n        if (value && typeof value === \"object\" && !Array.isArray(value)) {\n          // Locale used for Studio placeholders + social previews\n          return (value as { en?: string }).en ?? null;\n        }\n        return null;\n      },\n    }),\n  ],\n});\n```\n\n`resolveValue` only affects Studio UI (placeholders, empty-state messaging, and preview cards). On the frontend, resolve the active locale before passing data to [`@crawl-me-maybe/meta`](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/meta), which expects strings:\n\n```groq\n*[_type == \"page\" && slug.current == $slug][0]{\n  title,\n  \"slug\": { \"current\": slug.current },\n  \"description\": coalesce(seo.description[$locale], seo.description.en),\n  \"canonicalUrl\": seo.canonicalUrl,\n  \"searchIndexing\": seo.searchIndexing,\n  \"metaImage\": seo.metaImage.asset->url\n}\n```\n\n```groq\n*[_type == \"globalSeoSettings\"][0]{\n  siteTitle,\n  pageTitleTemplate,\n  \"metaDescription\": coalesce(metaDescription[$locale], metaDescription.en),\n  siteUrl,\n  twitterHandle,\n  \"defaultMetaImage\": defaultMetaImage.asset->url,\n  \"faviconUrl\": favicon.asset->url\n}\n```\n\n---\n\n## Favicons\nBrowser-tab preview for dynamic favicons with theme-switching button. Can be disabled by setting `global.favicon` to `false`.\n\n<img width=\"661\" height=\"355\" alt=\"Screenshot 2026-06-23 at 1 22 48 PM\" src=\"https://github.com/user-attachments/assets/69600f60-2e93-4df4-88f6-ec50fcd3adb5\" />\n<br/>\n\n## Robots.txt\nPreview-enabled array field for robots.txt entries. Disabled by setting `global.robots` to `false`.\n\n> 🧠 Note: This plugin only generates a preview, you are still responsible for exposing and formatting this to your frontend.\n\n<img width=\"697\" height=\"318\" alt=\"Screenshot 2026-06-23 at 1 18 44 PM\" src=\"https://github.com/user-attachments/assets/7592bdbc-a39c-44d9-b801-4e566e499919\" />\n<img width=\"682\" height=\"333\" alt=\"Screenshot 2026-06-23 at 1 18 50 PM\" src=\"https://github.com/user-attachments/assets/f8d7444c-cb8a-4571-80a8-352699dd36fc\" />\n<br/>\n\n---\n\n## Frontend integration\n\nThis plugin only stores SEO metadata inside Sanity. Your app is responsible for reading these fields and rendering meta tags, canonical URLs, schema markup, robots.txt, and sitemap.xml.\n\n[`@crawl-me-maybe/meta`](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/meta) handles the metadata side — merging page and global defaults, title templates, Open Graph, Twitter cards, and canonical URLs. [`@crawl-me-maybe/sitemap`](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/sitemap) and [`@crawl-me-maybe/schema`](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/schema) cover sitemaps, robots.txt, and JSON-LD.\n\n### With `@crawl-me-maybe/meta`\n\nThe plugin schemas map directly to `buildMetadata()` inputs — no adapter layer required:\n\n| Plugin schema | `buildMetadata` argument | Notes |\n|---|---|---|\n| `globalSeoSettings` | `globalSeoDefaults` (`GlobalSeoSettings`) | `siteTitle`, `pageTitleTemplate`, `siteUrl`, `metaDescription`, `twitterHandle` |\n| `globalSeoSettings.defaultMetaImage` | `defaultMetaImage` | Resolve to a URL string in GROQ (`defaultMetaImage.asset->url`) |\n| `globalSeoSettings.favicon` | `faviconUrl` | Resolve to a URL string in GROQ (`favicon.asset->url`) |\n| Page `title` + `slug` | `page.title`, `page.slug` | Required for title templates and self-canonical URLs |\n| `pageMetadata.description` | `page.description` | Falls back to `globalSeoSettings.metaDescription` |\n| `pageMetadata.metaImage` | `page.metaImage` | Resolve to a URL string in GROQ; falls back to `defaultMetaImage` |\n| `pageMetadata.canonicalUrl` | `page.canonicalUrl` | Path or full URL; empty values become a self-canonical from `siteUrl` + slug |\n| `pageMetadata.searchIndexing` | `page.searchIndexing` | `noIndex` / `noFollow` become a `robots` meta tag |\n\n**Page query:**\n\n```groq\n*[_type == \"page\" && slug.current == $slug][0]{\n  title,\n  \"slug\": { \"current\": slug.current },\n  \"description\": seo.description,\n  \"canonicalUrl\": seo.canonicalUrl,\n  \"searchIndexing\": seo.searchIndexing,\n  \"metaImage\": seo.metaImage.asset->url\n}\n```\n\n**Global defaults query:**\n\n```groq\n*[_type == \"globalSeoSettings\"][0]{\n  siteTitle,\n  pageTitleTemplate,\n  metaDescription,\n  siteUrl,\n  twitterHandle,\n  \"defaultMetaImage\": defaultMetaImage.asset->url,\n  \"faviconUrl\": favicon.asset->url\n}\n```\n\n**Next.js example:**\n\n```ts\n// app/[slug]/page.tsx\nimport type { Metadata } from \"next\";\nimport { buildMetadata } from \"@crawl-me-maybe/meta\";\nimport { toNextMeta } from \"@crawl-me-maybe/meta/next\";\nimport { client } from \"@/lib/sanity\";\n\nexport async function generateMetadata({ params }): Promise<Metadata> {\n  const page = await client.fetch(PAGE_QUERY, { slug: params.slug });\n  const seoDefaults = await client.fetch(GLOBAL_SEO_QUERY);\n\n  return toNextMeta(buildMetadata(page, seoDefaults));\n}\n```\n\nSee the [`@crawl-me-maybe/meta` docs](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/meta) for Nuxt, raw HTML, canonical URL options, and build-time overrides.\n\n---\n\n## Schemas\n\n### `globalSeoSettings` document\n\nA singleton document that provides site-wide defaults. Page-level fields display these values in Studio when empty; [`@crawl-me-maybe/meta`](https://github.com/nathannye/crawl-me-maybe/tree/main/packages/meta)'s `buildMetadata()` applies the same fallbacks on the frontend.\n\n| Field | Sanity type | Required | Validation | Notes |\n|---|---|---|---|---|\n| `siteTitle` | `string` | ✅ | Required | Injected into `pageTitleTemplate` via `{siteTitle}` |\n| `pageTitleTemplate` | `string` | ✅ | Required | Custom token input; initial value `{pageTitle} - {siteTitle}` |\n| `siteUrl` | `url` | ✅ | Must start with `https://` | Used when generating canonical URLs, Open Graph URLs, and sitemap entries. |\n| `metaDescription` | `metaDescription` | — | Warn < 120 or > 160 chars | Default description displayed by page metadata fields |\n| `defaultMetaImage` | `metaImage` | — | — | Default OG image displayed by page metadata fields |\n| `favicon` | `image` | — | — | Browser-tab preview in Studio. Controlled by `global.favicon` option |\n| `twitterHandle` | `string` | — | Must start with `@` | Social group |\n| `logo` | `image` | — | — | Used for Organization / WebSite schema markup |\n| `advanced.robots` | `robots[]` | — | Paths must start with `/` | robots.txt rule builder with preview tab. Controlled by `global.robots` option |\n\n---\n\n### `pageMetadata` object\n\nAdd this type to any document that needs per-page SEO. Fields display the active global default as placeholder when empty.\n\n| Field | Sanity type | Required | Validation | Notes |\n|---|---|---|---|---|\n| `description` | `metaDescription` | — | Warn < 120 or > 160 chars | Overrides `globalSeoSettings.metaDescription` |\n| `canonicalUrl` | `string` | — | Path (`/about`) or `https://` URL | Optional override; leave empty for auto self-canonical on the frontend. Controlled by `page.canonicalUrl` option |\n| `searchIndexing` | `searchIndexing` | — | — | noIndex / noFollow controls. Controlled by `page.searchIndexing` option |\n| `metaImage` | `metaImage` | — | — | Overrides `globalSeoSettings.defaultMetaImage`; shows thumbnail of active default when empty |\n\n---\n\n### Shared field types\n\nThese named types are registered by the plugin and can be reused in your own schemas:\n\n| Type name | Base type | Description |\n|---|---|---|\n| `metaDescription` | `text` (configurable via `fieldTypes.metaDescription`) | 3-row textarea with 120–160 char warnings when base type is `text` |\n| `metaTitle` | `string` (configurable via `fieldTypes.metaTitle`) | Single-line title field with 50–60 char warnings when base type is `string` |\n| `metaImage` | `image` | Standard image field for OG/social use |\n| `favicon` | `image` | Image with browser-tab preview component |\n| `searchIndexing` | `object` | noIndex + noFollow boolean controls |\n| `robots` | `array` | Rule builder with userAgent / allow / disallow fields and robots.txt preview |\n\n","readmeFilename":"README.md"}