sanity-plugin-seofields 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +508 -26
  2. package/dist/SeoHealthTool-2COI27KI.cjs +8 -0
  3. package/dist/SeoHealthTool-2COI27KI.cjs.map +1 -0
  4. package/dist/SeoHealthTool-CWI3KB2V.js +8 -0
  5. package/dist/SeoHealthTool-CWI3KB2V.js.map +1 -0
  6. package/dist/{SeoPreview-3EXR6FD4.cjs → SeoPreview-LMZAWWOE.cjs} +35 -38
  7. package/dist/SeoPreview-LMZAWWOE.cjs.map +1 -0
  8. package/dist/{SeoPreview-4PPAF4NO.js → SeoPreview-UNQBKTQO.js} +14 -11
  9. package/dist/SeoPreview-UNQBKTQO.js.map +1 -0
  10. package/dist/{chunk-B5IVI5LP.cjs → chunk-5XTQRILL.cjs} +286 -236
  11. package/dist/chunk-5XTQRILL.cjs.map +1 -0
  12. package/dist/{chunk-254YHUN3.cjs → chunk-7N4MLTMR.js} +8 -6
  13. package/dist/chunk-7N4MLTMR.js.map +1 -0
  14. package/dist/{chunk-DDAAVRWG.js → chunk-A57XNQC7.cjs} +9 -4
  15. package/dist/chunk-A57XNQC7.cjs.map +1 -0
  16. package/dist/chunk-BWLJDK5J.js +624 -0
  17. package/dist/chunk-BWLJDK5J.js.map +1 -0
  18. package/dist/chunk-CRCXC45D.js +56 -0
  19. package/dist/chunk-CRCXC45D.js.map +1 -0
  20. package/dist/chunk-EUG4MNRC.cjs +713 -0
  21. package/dist/chunk-EUG4MNRC.cjs.map +1 -0
  22. package/dist/{chunk-ZBHLMQTS.cjs → chunk-HHO2AKAP.js} +44 -17
  23. package/dist/chunk-HHO2AKAP.js.map +1 -0
  24. package/dist/chunk-NLEB47UV.js +713 -0
  25. package/dist/chunk-NLEB47UV.js.map +1 -0
  26. package/dist/chunk-OULUDJPI.cjs +254 -0
  27. package/dist/chunk-OULUDJPI.cjs.map +1 -0
  28. package/dist/chunk-R2U7JF7U.cjs +624 -0
  29. package/dist/chunk-R2U7JF7U.cjs.map +1 -0
  30. package/dist/chunk-XD4HKLEA.js +254 -0
  31. package/dist/chunk-XD4HKLEA.js.map +1 -0
  32. package/dist/chunk-XYAZJ3WX.cjs +56 -0
  33. package/dist/chunk-XYAZJ3WX.cjs.map +1 -0
  34. package/dist/{chunk-VR44E62I.js → chunk-XZKQWV2H.js} +204 -33
  35. package/dist/chunk-XZKQWV2H.js.map +1 -0
  36. package/dist/{chunk-HDZZQCH7.js → chunk-Y46ACXM4.cjs} +45 -5
  37. package/dist/chunk-Y46ACXM4.cjs.map +1 -0
  38. package/dist/cli.js +27 -27
  39. package/dist/{component-DEXwtemT.d.ts → component-CjT1hvxh.d.ts} +2 -2
  40. package/dist/{component-BzI-LHPw.d.cts → component-WYh0z8as.d.cts} +2 -2
  41. package/dist/define-cli.cjs +2 -4
  42. package/dist/define-cli.cjs.map +1 -1
  43. package/dist/define-cli.js +4 -4
  44. package/dist/define-cli.js.map +1 -1
  45. package/dist/head.cjs +22 -0
  46. package/dist/head.cjs.map +1 -0
  47. package/dist/head.d.cts +314 -0
  48. package/dist/head.d.ts +314 -0
  49. package/dist/head.js +22 -0
  50. package/dist/head.js.map +1 -0
  51. package/dist/hreflang-_VbSuWYD.d.cts +29 -0
  52. package/dist/hreflang-_VbSuWYD.d.ts +29 -0
  53. package/dist/index.cjs +1072 -368
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.cts +7 -374
  56. package/dist/index.d.ts +7 -374
  57. package/dist/index.js +1015 -288
  58. package/dist/index.js.map +1 -1
  59. package/dist/next.cjs +212 -455
  60. package/dist/next.cjs.map +1 -1
  61. package/dist/next.d.cts +9 -206
  62. package/dist/next.d.ts +9 -206
  63. package/dist/next.js +189 -111
  64. package/dist/next.js.map +1 -1
  65. package/dist/plugin-HFuWZsJu.d.cts +516 -0
  66. package/dist/plugin-oQUCim56.d.ts +516 -0
  67. package/dist/schema/next.cjs +173 -327
  68. package/dist/schema/next.cjs.map +1 -1
  69. package/dist/schema/next.d.cts +4 -4
  70. package/dist/schema/next.d.ts +4 -4
  71. package/dist/schema/next.js +172 -8
  72. package/dist/schema/next.js.map +1 -1
  73. package/dist/schema.cjs +376 -517
  74. package/dist/schema.cjs.map +1 -1
  75. package/dist/schema.d.cts +5 -5
  76. package/dist/schema.d.ts +5 -5
  77. package/dist/schema.js +216 -11
  78. package/dist/schema.js.map +1 -1
  79. package/dist/server.cjs +149 -0
  80. package/dist/server.cjs.map +1 -0
  81. package/dist/server.d.cts +100 -0
  82. package/dist/server.d.ts +100 -0
  83. package/dist/server.js +149 -0
  84. package/dist/server.js.map +1 -0
  85. package/dist/{types-CW8qFdAn.d.ts → types-BAdN1RMu.d.ts} +2 -2
  86. package/dist/{types-B_vjMPtM.d.cts → types-C5i3KMgs.d.cts} +2 -2
  87. package/dist/{types-BXSCbl6p.d.cts → types-DP-DiW6f.d.cts} +2 -2
  88. package/dist/{types-C19cQx5D.d.ts → types-DRLUGLtX.d.ts} +2 -2
  89. package/dist/{types-yVmQfby9.d.cts → types-DxlPXihz.d.cts} +1 -1
  90. package/dist/{types-yVmQfby9.d.ts → types-DxlPXihz.d.ts} +1 -1
  91. package/package.json +25 -4
  92. package/dist/SeoHealthDashboard-AEKVVMUR-7NWV3C3Y.js +0 -4
  93. package/dist/SeoHealthDashboard-AEKVVMUR-7NWV3C3Y.js.map +0 -1
  94. package/dist/SeoHealthDashboard-AEKVVMUR-XWKJUH24.cjs +0 -10
  95. package/dist/SeoHealthDashboard-AEKVVMUR-XWKJUH24.cjs.map +0 -1
  96. package/dist/SeoHealthTool-2KKQJLJL.cjs +0 -11
  97. package/dist/SeoHealthTool-2KKQJLJL.cjs.map +0 -1
  98. package/dist/SeoHealthTool-3CUKGADZ.js +0 -5
  99. package/dist/SeoHealthTool-3CUKGADZ.js.map +0 -1
  100. package/dist/SeoPreview-3EXR6FD4.cjs.map +0 -1
  101. package/dist/SeoPreview-4PPAF4NO.js.map +0 -1
  102. package/dist/chunk-254YHUN3.cjs.map +0 -1
  103. package/dist/chunk-27XSYENH.js +0 -2215
  104. package/dist/chunk-27XSYENH.js.map +0 -1
  105. package/dist/chunk-B5IVI5LP.cjs.map +0 -1
  106. package/dist/chunk-BNXMFD3T.cjs +0 -431
  107. package/dist/chunk-BNXMFD3T.cjs.map +0 -1
  108. package/dist/chunk-DDAAVRWG.js.map +0 -1
  109. package/dist/chunk-HDZZQCH7.js.map +0 -1
  110. package/dist/chunk-P6IOWIO2.cjs +0 -480
  111. package/dist/chunk-P6IOWIO2.cjs.map +0 -1
  112. package/dist/chunk-UOCZFYYP.js +0 -407
  113. package/dist/chunk-UOCZFYYP.js.map +0 -1
  114. package/dist/chunk-VR44E62I.js.map +0 -1
  115. package/dist/chunk-WKXHX3GO.js +0 -424
  116. package/dist/chunk-WKXHX3GO.js.map +0 -1
  117. package/dist/chunk-XZDLJ2N6.cjs +0 -2224
  118. package/dist/chunk-XZDLJ2N6.cjs.map +0 -1
  119. package/dist/chunk-ZBHLMQTS.cjs.map +0 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ Manage SEO fields, social previews, robots directives, canonical URLs, Schema.or
8
8
 
9
9
  <p><a href="https://www.npmjs.com/package/sanity-plugin-seofields"><img src="https://img.shields.io/npm/v/sanity-plugin-seofields.svg?color=10b981&label=npm" alt="npm version" /></a> <a href="https://www.npmjs.com/package/sanity-plugin-seofields"><img src="https://img.shields.io/npm/dm/sanity-plugin-seofields.svg?color=2563eb&label=downloads" alt="npm downloads" /></a> <a href="./LICENSE"><img src="https://img.shields.io/npm/l/sanity-plugin-seofields.svg?color=f59e0b" alt="license" /></a> <a href="https://github.com/hardik-143/sanity-plugin-seofields"><img src="https://img.shields.io/github/stars/hardik-143/sanity-plugin-seofields?style=social" alt="GitHub stars" /></a> <a href="https://www.sanity.io"><img src="https://img.shields.io/badge/Sanity-v3%20%7C%20v4%20%7C%20v5-f03e2f?logo=sanity" alt="Sanity" /></a> <a href="#compatibility"><img src="https://img.shields.io/badge/TypeScript-ready-3178c6?logo=typescript&logoColor=white" alt="TypeScript" /></a></p>
10
10
 
11
- [**Documentation**](https://sanity-plugin-seofields.thehardik.in/docs) &nbsp;•&nbsp; [Quick Start](#quick-start) &nbsp;•&nbsp; [Configuration](#configuration) &nbsp;•&nbsp; [Schema.org](#schemaorg-structured-data) &nbsp;•&nbsp; [CLI](#cli)
11
+ [**Documentation**](https://sanity-plugin-seofields.thehardik.in/docs) &nbsp;•&nbsp; [Quick Start](#quick-start) &nbsp;•&nbsp; [Configuration](#configuration) &nbsp;•&nbsp; [AI](#ai-content-generation) &nbsp;•&nbsp; [Schema.org](#schemaorg-structured-data) &nbsp;•&nbsp; [CLI](#cli)
12
12
 
13
13
  ---
14
14
 
@@ -16,17 +16,18 @@ Manage SEO fields, social previews, robots directives, canonical URLs, Schema.or
16
16
 
17
17
  Most Sanity SEO plugins stop at title and description fields. `sanity-plugin-seofields` gives editors and developers a full SEO workflow — from per-document fields all the way to studio-wide audits.
18
18
 
19
- | Feature | What you get |
20
- | :--------------------- | :----------------------------------------- |
21
- | Structured SEO fields | Complete field group for every document |
22
- | Live SERP preview | See the Google result while editing |
23
- | Open Graph + X/Twitter | Full social card controls |
24
- | Robots + canonical | Indexing directives and canonical URLs |
25
- | Custom meta tags | Reusable `metaTag` / `metaAttribute` types |
26
- | Schema.org JSON-LD | 39 types for structured data |
27
- | Next.js helpers | Metadata and script rendering |
28
- | SEO Health Dashboard | Audit documents across the whole studio |
29
- | CLI | Setup, reports, and exports |
19
+ | Feature | What you get |
20
+ | :--------------------- | :----------------------------------------------------- |
21
+ | Structured SEO fields | Complete field group for every document |
22
+ | Live SERP preview | See the Google result while editing |
23
+ | Open Graph + X/Twitter | Full social card controls |
24
+ | Robots + canonical | Indexing directives and canonical URLs |
25
+ | Custom meta tags | Reusable `metaTag` / `metaAttribute` types |
26
+ | Schema.org JSON-LD | 39 types for structured data |
27
+ | Frontend helpers | Next.js Metadata, React tags, and plain head data |
28
+ | AI content generation | Generate + refine SEO copy, 5 providers, 17 industries |
29
+ | SEO Health Dashboard | Audit documents across the whole studio |
30
+ | CLI | Setup, reports, and exports |
30
31
 
31
32
  Use it as a simple field plugin, a structured data system, or a complete SEO operations layer for your Sanity projects.
32
33
 
@@ -39,8 +40,11 @@ Use it as a simple field plugin, a structured data system, or a complete SEO ope
39
40
  - [Quick Start](#quick-start)
40
41
  - [Registered Schema Types](#registered-schema-types)
41
42
  - [Configuration](#configuration)
43
+ - [AI Content Generation](#ai-content-generation)
42
44
  - [SEO Health Dashboard](#seo-health-dashboard)
43
45
  - [Schema.org Structured Data](#schemaorg-structured-data)
46
+ - [Frontend Integration](#frontend-integration)
47
+ - [Frontend Head Exports](#frontend-head-exports)
44
48
  - [Next.js & React Exports](#nextjs--react-exports)
45
49
  - [CLI](#cli)
46
50
  - [Package Exports](#package-exports)
@@ -75,6 +79,7 @@ Use it as a simple field plugin, a structured data system, or a complete SEO ope
75
79
 
76
80
  - Studio tool for scanning SEO coverage across documents
77
81
  - 0–100 scoring per document
82
+ - Keyword and focus-keyword scoring rewards actual placement/prominence in title and description, not just presence
78
83
  - Missing title, description, image, canonical, robots, and social metadata checks
79
84
  - Document-type filters
80
85
  - Query customization
@@ -105,14 +110,41 @@ Use it as a simple field plugin, a structured data system, or a complete SEO ope
105
110
 
106
111
  <br />
107
112
 
113
+ <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/nextjs.svg" width="20" height="20" align="center" alt="Next.js"/> Next.js&nbsp;&nbsp;
114
+ <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/astro.svg" width="20" height="20" align="center" alt="Astro"/> Astro&nbsp;&nbsp;
115
+ <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/nuxtjs.svg" width="20" height="20" align="center" alt="Nuxt"/> Nuxt&nbsp;&nbsp;
116
+ <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/vue.svg" width="20" height="20" align="center" alt="Vue"/> Vue&nbsp;&nbsp;
117
+ <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/svelte.svg" width="20" height="20" align="center" alt="SvelteKit"/> SvelteKit&nbsp;&nbsp;
118
+ Remix
119
+
120
+ <br />
121
+ <br />
122
+
108
123
  - `buildSeoMeta()` for Next.js App Router `generateMetadata()`
109
124
  - `<SeoMetaTags />` for framework-agnostic React rendering
110
- - Schema.org React components for Next.js and other React frameworks
125
+ - `buildSeoHead()` for Astro, Nuxt, Vue, SvelteKit, Remix, and custom head renderers
126
+ - Schema.org React components for Next.js/React plus JSON-LD builders for custom frontends
111
127
  - Image URL resolver hooks for Sanity asset pipelines
112
128
  - Sanitizers for Open Graph type and Twitter Card values
113
129
 
114
130
  </details>
115
131
 
132
+ <details>
133
+ <summary><b>AI Content Generation</b></summary>
134
+
135
+ <br />
136
+
137
+ - "Generate with AI" button for title, description, focus keyword, keywords, Open Graph, and X/Twitter fields
138
+ - 5 providers — OpenAI, Anthropic, Groq, Gemini, Ollama — plus any OpenAI-compatible `baseUrl`
139
+ - 17 industry-specific prompt contexts (8 free, 9 pro) at up to 10 prompt variations per field
140
+ - Secure server-side proxy adapters for Next.js, Express, Node, and Fetch-API runtimes — keeps API keys off the client
141
+ - Per-document-type content source mapping, including nested Portable Text
142
+ - Automatic length, keyword, and readability refinement passes with retry
143
+
144
+ See [AI.md](./AI.md) for full setup, security notes, and configuration reference.
145
+
146
+ </details>
147
+
116
148
  <details>
117
149
  <summary><b>CLI</b></summary>
118
150
 
@@ -134,6 +166,9 @@ Use it as a simple field plugin, a structured data system, or a complete SEO ope
134
166
  npm install sanity-plugin-seofields
135
167
  ```
136
168
 
169
+ This single install also brings in the licensed helper package used internally for pro features.
170
+ You do **not** need to install `seofields-pro` separately.
171
+
137
172
  Peer dependencies:
138
173
 
139
174
  ```txt
@@ -210,6 +245,48 @@ Full guide: [Frontend integration](https://sanity-plugin-seofields.thehardik.in/
210
245
 
211
246
  ---
212
247
 
248
+ ### 4. Render SEO Metadata Outside Next.js
249
+
250
+ Use `buildSeoHead()` when your framework expects plain head tag data instead of
251
+ Next.js `Metadata` or React elements.
252
+
253
+ ```ts
254
+ import {buildSeoHead} from 'sanity-plugin-seofields/head'
255
+
256
+ const head = buildSeoHead({
257
+ seo: page.seo,
258
+ baseUrl: 'https://example.com',
259
+ path: `/${page.slug}`,
260
+ defaults: {
261
+ title: page.title,
262
+ description: 'Default site description',
263
+ siteName: 'My Site',
264
+ },
265
+ imageUrlResolver: (image) => urlFor(image).width(1200).height(630).url(),
266
+ })
267
+ ```
268
+
269
+ `head` is serializable and framework-neutral:
270
+
271
+ ```ts
272
+ {
273
+ title: 'Page title',
274
+ meta: [
275
+ {name: 'description', content: '...'},
276
+ {property: 'og:title', content: '...'},
277
+ {name: 'twitter:card', content: 'summary_large_image'},
278
+ ],
279
+ link: [
280
+ {rel: 'canonical', href: 'https://example.com/page'},
281
+ {rel: 'alternate', hreflang: 'fr-FR', href: 'https://example.com/fr/page'},
282
+ ],
283
+ }
284
+ ```
285
+
286
+ Full guide: [Frontend integration](https://sanity-plugin-seofields.thehardik.in/docs/frontend-integration)
287
+
288
+ ---
289
+
213
290
  ## Registered Schema Types
214
291
 
215
292
  | Type | Purpose |
@@ -244,25 +321,87 @@ seofields({
244
321
  },
245
322
  dashboard: {
246
323
  enabled: true,
247
- licenseKey: process.env.SANITY_STUDIO_SEO_LICENSE_KEY,
248
324
  },
325
+ licenseKey: process.env.SANITY_STUDIO_SEO_LICENSE_KEY,
249
326
  })
250
327
  ```
251
328
 
252
- | Option | Description |
253
- | :-------------------- | :------------------------------------------------------------------- |
254
- | `seoPreview` | Enable or disable the live preview shown inside SEO fields |
255
- | `fieldOverrides` | Customize field titles, descriptions, validation, and field metadata |
256
- | `defaultHiddenFields` | Hide specific SEO fields globally |
257
- | `fieldVisibility` | Hide specific SEO fields for specific document types |
258
- | `fieldGroups` | Customize how fields are grouped in the `seoFields` object |
259
- | `apiVersion` | Sanity API version used by plugin clients |
260
- | `dashboard` | Enable and configure the SEO Health Dashboard tool |
329
+ | Option | Description |
330
+ | :-------------------- | :------------------------------------------------------------------------------------- |
331
+ | `seoPreview` | Enable or disable the live preview shown inside SEO fields |
332
+ | `fieldOverrides` | Customize field titles, descriptions, validation, and field metadata |
333
+ | `defaultHiddenFields` | Hide specific SEO fields globally |
334
+ | `fieldVisibility` | Hide specific SEO fields for specific document types |
335
+ | `fieldGroups` | Customize how fields are grouped in the `seoFields` object |
336
+ | `apiVersion` | Sanity API version used by plugin clients |
337
+ | `hreflang` | Auto-populate hreflangs from translations see [Hreflang auto-populate](#hreflang-auto-populate) |
338
+ | `dashboard` | Enable and configure the SEO Health Dashboard tool |
339
+ | `licenseKey` | License key for pro features (dashboard, publish gate, pro industries) |
340
+ | `ai` | Enable "Generate with AI" fields — see [AI Content Generation](#ai-content-generation) |
261
341
 
262
342
  Full reference: [Configuration docs](https://sanity-plugin-seofields.thehardik.in/docs/configuration)
263
343
 
264
344
  ---
265
345
 
346
+ ## AI Content Generation
347
+
348
+ Add an `ai` object to the plugin config to show a **Generate with AI** button on `title`, `description`, `focusKeyword`, `keywords`, `ogTitle`, `ogDescription`, `twitterTitle`, and `twitterDescription`.
349
+
350
+ ```ts
351
+ seofields({
352
+ ai: {
353
+ provider: 'openai',
354
+ apiKey: process.env.SANITY_STUDIO_OPENAI_API_KEY,
355
+ industry: 'blog',
356
+ },
357
+ })
358
+ ```
359
+
360
+ **Providers** — `openai` (default), `anthropic`, `groq`, `gemini`, `ollama` (local models, no key required), or any OpenAI-compatible `baseUrl` (DeepSeek, xAI Grok, Azure OpenAI, self-hosted).
361
+
362
+ **API key security** — Sanity Studio is client-side; an `apiKey` set directly is bundled into browser JS and readable by anyone with Studio access, regardless of env-var naming. For production, use `endpoint` with a server-side proxy adapter instead:
363
+
364
+ ```ts
365
+ // Next.js — app/api/seo/generate/route.ts
366
+ import {createNextRouteHandler} from 'sanity-plugin-seofields/server'
367
+
368
+ export const {POST, OPTIONS} = createNextRouteHandler({
369
+ provider: 'openai',
370
+ apiKey: process.env.OPENAI_API_KEY, // stays server-side
371
+ })
372
+ ```
373
+
374
+ `sanity-plugin-seofields/server` also ships `createExpressHandler`, `createNodeHandler`, and `createFetchHandler` (Cloudflare Workers, Deno, Bun). Each wraps the same generation pipeline as direct-provider mode — only the key location changes.
375
+
376
+ **Industries & prompt tiers** — 17 industries, 10 prompt variations per field:
377
+
378
+ | Tier | Industries | Prompts per field |
379
+ | :------- | :------------------------------------------------------------------------------------------------------------ | :--------------------------------------- |
380
+ | Free (8) | `blog`, `restaurant`, `travel`, `ecommerce`, `education`, `fitness`, `hospitality`, `nonprofit` | 4 free, 6 more with a license (10 total) |
381
+ | Pro (9) | `healthcare`, `pharmacy`, `finance`, `realestate`, `saas`, `legal`, `insurance`, `automotive`, `homeServices` | All 10 require a license |
382
+
383
+ Without `industry` set, generation uses generic prompts, which are always free.
384
+
385
+ **Custom prompts** — write your own prompt wording with `customPrompt` (free: one function) or `customPrompts` (paid: up to 5 generic + 5 per industry, behind a license). Your function receives all extracted document values (`content`, `focusKeyword`, `keywords`, `meta`, `field`, `industry`) and returns the prompt string. Custom prompts replace the built-in pool by default; set `merge: true` to mix them in. In proxy mode set them on the server handler config. See [AI.md → Custom Prompts](./AI.md#custom-prompts).
386
+
387
+ ```ts
388
+ seofields({
389
+ ai: {
390
+ provider: 'openai',
391
+ apiKey: process.env.SANITY_STUDIO_OPENAI_API_KEY,
392
+ customPrompt: (v) => `Write a 55-char SEO ${v.field} about: ${v.content.slice(0, 200)}`,
393
+ },
394
+ })
395
+ ```
396
+
397
+ **Content source** (`ai.content`) — defaults to the document's `body` field. Accepts a single field, an array of fields in priority order, or a per-document-type mapping with a `default` fallback. Nested Portable Text is found automatically.
398
+
399
+ **Refinement pipeline** — each generation attempt is checked for target character length, required keyword presence (injected if missing), readability (simplified if too complex), and focus-keyword verbatim match against existing title/description. Controlled by `maxRetries` and `keepFirstOnValidationFail`.
400
+
401
+ Full guide: [AI.md](./AI.md) &nbsp;•&nbsp; [AI Integration docs](https://sanity-plugin-seofields.thehardik.in/docs/ai)
402
+
403
+ ---
404
+
266
405
  ## SEO Health Dashboard
267
406
 
268
407
  The dashboard is a Studio tool that helps teams find SEO gaps before they ship content.
@@ -275,9 +414,9 @@ import seofields from 'sanity-plugin-seofields'
275
414
  export default defineConfig({
276
415
  plugins: [
277
416
  seofields({
417
+ licenseKey: process.env.SANITY_STUDIO_SEO_LICENSE_KEY,
278
418
  dashboard: {
279
419
  enabled: true,
280
- licenseKey: process.env.SANITY_STUDIO_SEO_LICENSE_KEY,
281
420
  query: {
282
421
  types: ['page', 'post', 'product'],
283
422
  },
@@ -300,7 +439,7 @@ Dashboard capabilities:
300
439
  - Open the exact document that needs updates
301
440
  - Customize document title, subtitle, and preview display
302
441
 
303
- Get a dashboard license: [Get license](https://sanity-plugin-seofields.thehardik.in/get-license)
442
+ Get a license key: [Get license](https://sanity-plugin-seofields.thehardik.in/get-license)
304
443
 
305
444
  Dashboard docs: [SEO Health Dashboard](https://sanity-plugin-seofields.thehardik.in/docs/dashboard)
306
445
 
@@ -409,6 +548,289 @@ Schema.org docs: [Structured data guide](https://sanity-plugin-seofields.thehard
409
548
 
410
549
  ---
411
550
 
551
+ ## Frontend Integration
552
+
553
+ The plugin stores SEO fields under the `seoFields` object. If you are comparing
554
+ examples from other Sanity SEO plugins, map the names carefully:
555
+
556
+ | Common name in other plugins | `sanity-plugin-seofields` field |
557
+ | :--------------------------- | :------------------------------ |
558
+ | `seo.metaTitle` | `seo.title` |
559
+ | `seo.metaDescription` | `seo.description` |
560
+ | `seo.metaImage` | `seo.metaImage` |
561
+ | `seo.twitter.cardType` | `seo.twitter.card` |
562
+ | `seo.hreflang` | `seo.hreflangs[]` |
563
+
564
+ ### Shared GROQ Fragment
565
+
566
+ ```ts
567
+ export const SEO_FRAGMENT = `{
568
+ title,
569
+ seo {
570
+ title,
571
+ description,
572
+ canonicalUrl,
573
+ metaImage { asset-> { url }, alt },
574
+ keywords,
575
+ robots { noIndex, noFollow, noTranslate, noImageIndex },
576
+ hreflangs[] { locale, url },
577
+ openGraph {
578
+ title, description, url, siteName, type,
579
+ imageType, imageUrl,
580
+ image { asset-> { url }, alt }
581
+ },
582
+ twitter {
583
+ card, site, creator, title, description,
584
+ imageType, imageUrl,
585
+ image { asset-> { url }, alt }
586
+ },
587
+ metaAttributes[] { _key, key, type, value }
588
+ }
589
+ }`
590
+
591
+ export const PAGE_QUERY = `
592
+ *[_type == "page" && slug.current == $slug][0] ${SEO_FRAGMENT}
593
+ `
594
+ ```
595
+
596
+ ### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/nextjs.svg" width="20" height="20" align="center" alt=""/> Next.js App Router
597
+
598
+ ```tsx
599
+ // app/[slug]/page.tsx
600
+ import type {Metadata} from 'next'
601
+ import {buildSeoMeta} from 'sanity-plugin-seofields/next'
602
+ import {client} from '@/sanity/lib/client'
603
+ import {urlFor} from '@/sanity/lib/image'
604
+ import {PAGE_QUERY} from '@/sanity/lib/queries'
605
+
606
+ export async function generateMetadata(props: {
607
+ params: Promise<{slug: string}>
608
+ }): Promise<Metadata> {
609
+ const {slug} = await props.params
610
+ const page = await client.fetch(PAGE_QUERY, {slug})
611
+
612
+ return buildSeoMeta({
613
+ seo: page?.seo,
614
+ baseUrl: 'https://example.com',
615
+ path: '/' + slug,
616
+ defaults: {
617
+ title: page?.title || 'My Site',
618
+ description: 'Default site description',
619
+ siteName: 'My Site',
620
+ twitterSite: '@mysite',
621
+ },
622
+ imageUrlResolver: (image) => urlFor(image).width(1200).height(630).url(),
623
+ })
624
+ }
625
+ ```
626
+
627
+ ### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/astro.svg" width="20" height="20" align="center" alt=""/> Astro
628
+
629
+ ```astro
630
+ --- // src/pages/[slug].astro
631
+ import {buildSeoHead} from 'sanity-plugin-seofields/head'
632
+ import {client} from '@/lib/sanity'
633
+ import Layout from '@/layouts/Layout.astro'
634
+ import {PAGE_QUERY} from '@/lib/queries'
635
+
636
+ const {slug} = Astro.params
637
+ const page = await client.fetch(PAGE_QUERY, {slug})
638
+
639
+ if (!page) return Astro.redirect('/404')
640
+
641
+ const head = buildSeoHead({
642
+ seo: page.seo,
643
+ baseUrl: 'https://example.com',
644
+ path: '/' + slug,
645
+ defaults: {
646
+ title: page.title,
647
+ description: 'Default site description',
648
+ siteName: 'My Site',
649
+ },
650
+ })
651
+ ---
652
+
653
+ <Layout head={head}>
654
+ <h1>{page.title}</h1>
655
+ </Layout>
656
+ ```
657
+
658
+ Render the tags in your layout:
659
+
660
+ ```astro
661
+ --- // src/layouts/Layout.astro
662
+ const {head} = Astro.props
663
+ ---
664
+
665
+ <html lang="en">
666
+ <head>
667
+ <title>{head.title}</title>
668
+ {head.meta.map((tag) =>
669
+ 'property' in tag ? (
670
+ <meta property={tag.property} content={tag.content} />
671
+ ) : (
672
+ <meta name={tag.name} content={tag.content} />
673
+ )
674
+ )}
675
+ {head.link.map((tag) => (
676
+ <link rel={tag.rel} href={tag.href} hreflang={tag.hreflang} />
677
+ ))}
678
+ </head>
679
+ <body>
680
+ <slot />
681
+ </body>
682
+ </html>
683
+ ```
684
+
685
+ ### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/nuxtjs.svg" width="20" height="20" align="center" alt=""/> Nuxt 3
686
+
687
+ ```vue
688
+ <!-- pages/[slug].vue -->
689
+ <script setup lang="ts">
690
+ import {buildSeoHead} from 'sanity-plugin-seofields/head'
691
+
692
+ const route = useRoute()
693
+ const page = await useSeoData(route.params.slug as string)
694
+
695
+ const head = buildSeoHead({
696
+ seo: page?.seo,
697
+ baseUrl: 'https://example.com',
698
+ path: '/' + route.params.slug,
699
+ defaults: {
700
+ title: page?.title || 'My Site',
701
+ description: 'Default site description',
702
+ siteName: 'My Site',
703
+ },
704
+ })
705
+
706
+ useHead({
707
+ title: head.title,
708
+ meta: head.meta,
709
+ link: head.link,
710
+ })
711
+ </script>
712
+
713
+ <template>
714
+ <main>
715
+ <h1>{{ page?.title }}</h1>
716
+ </main>
717
+ </template>
718
+ ```
719
+
720
+ ### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/vue.svg" width="20" height="20" align="center" alt=""/> Vue 3 Standalone
721
+
722
+ Use `@unhead/vue` or another Vue head manager:
723
+
724
+ ```vue
725
+ <script setup lang="ts">
726
+ import {useHead} from '@unhead/vue'
727
+ import {buildSeoHead} from 'sanity-plugin-seofields/head'
728
+ import {client} from '@/lib/sanity'
729
+ import {PAGE_QUERY} from '@/lib/queries'
730
+
731
+ const props = defineProps<{slug: string}>()
732
+ const page = await client.fetch(PAGE_QUERY, {slug: props.slug})
733
+
734
+ const head = buildSeoHead({
735
+ seo: page?.seo,
736
+ baseUrl: 'https://example.com',
737
+ path: '/' + props.slug,
738
+ defaults: {title: page?.title || 'My Site', siteName: 'My Site'},
739
+ })
740
+
741
+ useHead({
742
+ title: head.title,
743
+ meta: head.meta,
744
+ link: head.link,
745
+ })
746
+ </script>
747
+ ```
748
+
749
+ ### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/svelte.svg" width="20" height="20" align="center" alt=""/> SvelteKit
750
+
751
+ ```ts
752
+ // src/routes/[slug]/+page.ts
753
+ import {buildSeoHead} from 'sanity-plugin-seofields/head'
754
+ import type {PageLoad} from './$types'
755
+ import {client} from '$lib/sanity'
756
+ import {PAGE_QUERY} from '$lib/queries'
757
+
758
+ export const load: PageLoad = async ({params}) => {
759
+ const page = await client.fetch(PAGE_QUERY, {slug: params.slug})
760
+
761
+ return {
762
+ page,
763
+ head: buildSeoHead({
764
+ seo: page?.seo,
765
+ baseUrl: 'https://example.com',
766
+ path: '/' + params.slug,
767
+ defaults: {
768
+ title: page?.title || 'My Site',
769
+ description: 'Default site description',
770
+ siteName: 'My Site',
771
+ },
772
+ }),
773
+ }
774
+ }
775
+ ```
776
+
777
+ ```svelte
778
+ <!-- src/routes/[slug]/+page.svelte -->
779
+ <script lang="ts">
780
+ import type {PageData} from './$types'
781
+ export let data: PageData
782
+ </script>
783
+
784
+ <svelte:head>
785
+ {#if data.head.title}
786
+ <title>{data.head.title}</title>
787
+ {/if}
788
+
789
+ {#each data.head.meta as tag}
790
+ {#if 'property' in tag}
791
+ <meta property={tag.property} content={tag.content} />
792
+ {:else}
793
+ <meta name={tag.name} content={tag.content} />
794
+ {/if}
795
+ {/each}
796
+
797
+ {#each data.head.link as tag}
798
+ <link rel={tag.rel} href={tag.href} hreflang={tag.hreflang} />
799
+ {/each}
800
+ </svelte:head>
801
+
802
+ <main>
803
+ <h1>{data.page?.title}</h1>
804
+ </main>
805
+ ```
806
+
807
+ Detailed guide: [Frontend integration](https://sanity-plugin-seofields.thehardik.in/docs/frontend-integration)
808
+
809
+ ---
810
+
811
+ ## Frontend Head Exports
812
+
813
+ Use this entry point for Astro, Nuxt, Vue, SvelteKit, Remix, and any frontend
814
+ that wants plain serializable head data without importing the Studio plugin or
815
+ React helpers.
816
+
817
+ ```ts
818
+ import {
819
+ buildSeoHead,
820
+ buildSeoMeta,
821
+ sanitizeOGType,
822
+ sanitizeTwitterCard,
823
+ } from 'sanity-plugin-seofields/head'
824
+ ```
825
+
826
+ Common usage:
827
+
828
+ - Use `buildSeoHead()` in Astro, Nuxt, Vue, SvelteKit, Remix, and custom renderers
829
+ - Use `buildSeoMeta()` if you want the normalized metadata object but not React components
830
+ - Pass an `imageUrlResolver` when your Sanity image data needs URL building
831
+
832
+ ---
833
+
412
834
  ## Next.js & React Exports
413
835
 
414
836
  ```ts
@@ -431,6 +853,64 @@ Docs: [Frontend integration](https://sanity-plugin-seofields.thehardik.in/docs/f
431
853
 
432
854
  ---
433
855
 
856
+ ## Hreflang auto-populate
857
+
858
+ For projects using [`@sanity/document-internationalization`](https://github.com/sanity-io/document-internationalization), derive hreflang alternates from your translation references instead of typing them by hand.
859
+
860
+ **Frontend** — `buildHreflangs()` turns the resolved `_translations` array into entries and feeds `buildSeoMeta`:
861
+
862
+ ```ts
863
+ import {buildSeoMeta, buildHreflangs} from 'sanity-plugin-seofields/next' // or /head
864
+
865
+ // GROQ: "_translations": *[_type=="translation.metadata" && references(^._id)].translations[].value->{ language, "slug": slug.current }
866
+ export async function generateMetadata() {
867
+ return buildSeoMeta({
868
+ seo: data.seo,
869
+ baseUrl: 'https://example.com',
870
+ path: `/${data.slug.current}`,
871
+ hreflangs: buildHreflangs(data._translations, {
872
+ baseUrl: 'https://example.com',
873
+ xDefault: 'en',
874
+ // resolvePath: (t) => `/${t.language}/${t.slug}`,
875
+ }),
876
+ })
877
+ }
878
+ ```
879
+
880
+ **Studio** — enable `hreflang.autoFill` to add a **Sync from translations** button to the `hreflangs` field (entries stay editable):
881
+
882
+ ```ts
883
+ seofields({
884
+ baseUrl: 'https://example.com',
885
+ hreflang: {autoFill: true /*, localeField: 'language', resolvePath */},
886
+ })
887
+ ```
888
+
889
+ The Studio sync reads `translation.metadata` via the standard client — no extra dependency required.
890
+
891
+ ---
892
+
893
+ ## llms.txt generator
894
+
895
+ Generate an [llms.txt](https://llmstxt.org) file from your Sanity content with `buildLlmsTxt()` + `docsToLlmsSection()` (framework-neutral, exported from `/head` and `/next`):
896
+
897
+ ```ts
898
+ import {buildLlmsTxt, docsToLlmsSection} from 'sanity-plugin-seofields/head'
899
+
900
+ const body = buildLlmsTxt({
901
+ title: 'Acme',
902
+ summary: 'Everything Acme, for humans and LLMs.',
903
+ baseUrl: 'https://acme.com',
904
+ sections: [
905
+ docsToLlmsSection(posts, {title: 'Blog', baseUrl: 'https://acme.com'}),
906
+ docsToLlmsSection(docsPages, {title: 'Docs', baseUrl: 'https://acme.com'}),
907
+ ],
908
+ })
909
+ // serve `body` from /llms.txt (route handler or build step)
910
+ ```
911
+
912
+ ---
913
+
434
914
  ## CLI
435
915
 
436
916
  Run the CLI:
@@ -458,7 +938,9 @@ CLI docs: [CLI guide](https://sanity-plugin-seofields.thehardik.in/docs/cli)
458
938
  | Import path | Use |
459
939
  | :------------------------------------ | :--------------------------------------------------------------------- |
460
940
  | `sanity-plugin-seofields` | Studio plugin, base schema types, dashboard pane factory, shared types |
461
- | `sanity-plugin-seofields/next` | SEO metadata helpers and Schema.org React components |
941
+ | `sanity-plugin-seofields/head` | Framework-neutral SEO helpers (`buildSeoHead`, `buildSeoMeta`) |
942
+ | `sanity-plugin-seofields/server` | Server-side AI proxy adapters (Next.js, Express, Node, Fetch API) |
943
+ | `sanity-plugin-seofields/next` | Next.js metadata helpers, React meta tags, and Schema.org components |
462
944
  | `sanity-plugin-seofields/schema` | Schema.org Sanity schema plugins and type exports |
463
945
  | `sanity-plugin-seofields/schema/next` | Schema.org React JSON-LD components |
464
946
  | `sanity-plugin-seofields/define-cli` | CLI configuration helper |
@@ -0,0 +1,8 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});require('./chunk-A57XNQC7.cjs');
2
+
3
+ // src/components/SeoHealthTool.tsx
4
+ var _seofieldspro = require('seofields-pro');
5
+
6
+
7
+ exports.default = _seofieldspro.SeoHealthTool;
8
+ //# sourceMappingURL=SeoHealthTool-2COI27KI.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/hardik/GITHUB/sanity-plugin-seofields/npm/dist/SeoHealthTool-2COI27KI.cjs","../src/components/SeoHealthTool.tsx"],"names":[],"mappings":"AAAA,yGAA6B;AAC7B;AACA;ACFA,6CAAuC;ADIvC;AACE;AACF,8CAAC","file":"/Users/hardik/GITHUB/sanity-plugin-seofields/npm/dist/SeoHealthTool-2COI27KI.cjs","sourcesContent":[null,"export {SeoHealthTool as default} from 'seofields-pro'\n"]}
@@ -0,0 +1,8 @@
1
+ import "./chunk-7N4MLTMR.js";
2
+
3
+ // src/components/SeoHealthTool.tsx
4
+ import { SeoHealthTool } from "seofields-pro";
5
+ export {
6
+ SeoHealthTool as default
7
+ };
8
+ //# sourceMappingURL=SeoHealthTool-CWI3KB2V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/SeoHealthTool.tsx"],"sourcesContent":["export {SeoHealthTool as default} from 'seofields-pro'\n"],"mappings":";;;AAAA,SAAyB,qBAAc;","names":[]}