sanity-plugin-seofields 1.7.0 → 1.9.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.
- package/README.md +440 -29
- package/dist/SchemaOrgJsonLdPreview-DJLkIPq4.d.cts +78 -0
- package/dist/SchemaOrgJsonLdPreview-DJLkIPq4.d.ts +78 -0
- package/dist/SeoHealthTool-2COI27KI.cjs +8 -0
- package/dist/SeoHealthTool-2COI27KI.cjs.map +1 -0
- package/dist/SeoHealthTool-CWI3KB2V.js +8 -0
- package/dist/SeoHealthTool-CWI3KB2V.js.map +1 -0
- package/dist/{SeoPreview-WYH7NYNM.cjs → SeoPreview-LMZAWWOE.cjs} +35 -38
- package/dist/SeoPreview-LMZAWWOE.cjs.map +1 -0
- package/dist/{SeoPreview-KEGGQSPJ.js → SeoPreview-UNQBKTQO.js} +14 -11
- package/dist/SeoPreview-UNQBKTQO.js.map +1 -0
- package/dist/chunk-3OK3S2F7.cjs +673 -0
- package/dist/chunk-3OK3S2F7.cjs.map +1 -0
- package/dist/{chunk-GWPOJSGW.cjs → chunk-5XTQRILL.cjs} +1022 -307
- package/dist/chunk-5XTQRILL.cjs.map +1 -0
- package/dist/{chunk-2NMEKWO5.js → chunk-7N4MLTMR.js} +8 -15
- package/dist/chunk-7N4MLTMR.js.map +1 -0
- package/dist/{chunk-S367Y35J.cjs → chunk-A57XNQC7.cjs} +9 -20
- package/dist/chunk-A57XNQC7.cjs.map +1 -0
- package/dist/chunk-BWLJDK5J.js +624 -0
- package/dist/chunk-BWLJDK5J.js.map +1 -0
- package/dist/chunk-FUUWBQE2.cjs +195 -0
- package/dist/chunk-FUUWBQE2.cjs.map +1 -0
- package/dist/{chunk-ZBHLMQTS.cjs → chunk-HHO2AKAP.js} +44 -17
- package/dist/chunk-HHO2AKAP.js.map +1 -0
- package/dist/chunk-KIEO6QG3.js +195 -0
- package/dist/chunk-KIEO6QG3.js.map +1 -0
- package/dist/chunk-R2U7JF7U.cjs +624 -0
- package/dist/chunk-R2U7JF7U.cjs.map +1 -0
- package/dist/{chunk-HRKRV2X7.js → chunk-XZKQWV2H.js} +974 -148
- package/dist/chunk-XZKQWV2H.js.map +1 -0
- package/dist/{chunk-HDZZQCH7.js → chunk-Y46ACXM4.cjs} +45 -5
- package/dist/chunk-Y46ACXM4.cjs.map +1 -0
- package/dist/chunk-Z5AOUU4H.js +673 -0
- package/dist/chunk-Z5AOUU4H.js.map +1 -0
- package/dist/cli.js +27 -27
- package/dist/{component-DQ4KuQQD.d.ts → component-CjT1hvxh.d.ts} +3 -27
- package/dist/{component-D1sBm6ts.d.cts → component-WYh0z8as.d.cts} +3 -27
- package/dist/define-cli.cjs +2 -4
- package/dist/define-cli.cjs.map +1 -1
- package/dist/define-cli.js +4 -4
- package/dist/define-cli.js.map +1 -1
- package/dist/head.cjs +14 -0
- package/dist/head.cjs.map +1 -0
- package/dist/head.d.cts +254 -0
- package/dist/head.d.ts +254 -0
- package/dist/head.js +14 -0
- package/dist/head.js.map +1 -0
- package/dist/index.cjs +1797 -230
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -598
- package/dist/index.d.ts +11 -598
- package/dist/index.js +1777 -197
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +210 -447
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +8 -200
- package/dist/next.d.ts +8 -200
- package/dist/next.js +200 -124
- package/dist/next.js.map +1 -1
- package/dist/plugin-DQdThyxG.d.ts +449 -0
- package/dist/plugin-DoGeZP79.d.cts +449 -0
- package/dist/schema/next.cjs +173 -319
- package/dist/schema/next.cjs.map +1 -1
- package/dist/schema/next.d.cts +4 -4
- package/dist/schema/next.d.ts +4 -4
- package/dist/schema/next.js +172 -8
- package/dist/schema/next.js.map +1 -1
- package/dist/schema.cjs +390 -489
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +26 -16
- package/dist/schema.d.ts +26 -16
- package/dist/schema.js +236 -13
- package/dist/schema.js.map +1 -1
- package/dist/server.cjs +149 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +102 -0
- package/dist/server.d.ts +102 -0
- package/dist/server.js +149 -0
- package/dist/server.js.map +1 -0
- package/dist/{types-CeBULNFF.d.ts → types-BAdN1RMu.d.ts} +2 -2
- package/dist/{types-_wBLg6RV.d.cts → types-C5i3KMgs.d.cts} +2 -2
- package/dist/{types-9_wbCIi9.d.ts → types-DP-DiW6f.d.cts} +29 -2
- package/dist/{types-Bb49oRBV.d.cts → types-DRLUGLtX.d.ts} +29 -2
- package/dist/{types-B91ena4g.d.cts → types-DxlPXihz.d.cts} +8 -1
- package/dist/{types-B91ena4g.d.ts → types-DxlPXihz.d.ts} +8 -1
- package/package.json +25 -3
- package/dist/SeoHealthDashboard-IP5BJ322.cjs +0 -10
- package/dist/SeoHealthDashboard-IP5BJ322.cjs.map +0 -1
- package/dist/SeoHealthDashboard-PTUEVYZL.js +0 -4
- package/dist/SeoHealthDashboard-PTUEVYZL.js.map +0 -1
- package/dist/SeoHealthTool-CZZRMWQK.cjs +0 -14
- package/dist/SeoHealthTool-CZZRMWQK.cjs.map +0 -1
- package/dist/SeoHealthTool-IULT4G25.js +0 -12
- package/dist/SeoHealthTool-IULT4G25.js.map +0 -1
- package/dist/SeoPreview-KEGGQSPJ.js.map +0 -1
- package/dist/SeoPreview-WYH7NYNM.cjs.map +0 -1
- package/dist/chunk-2NMEKWO5.js.map +0 -1
- package/dist/chunk-5YRH47VQ.cjs +0 -2167
- package/dist/chunk-5YRH47VQ.cjs.map +0 -1
- package/dist/chunk-ARELX3GK.cjs +0 -689
- package/dist/chunk-ARELX3GK.cjs.map +0 -1
- package/dist/chunk-AW7OKBQQ.js +0 -2161
- package/dist/chunk-AW7OKBQQ.js.map +0 -1
- package/dist/chunk-FONJYVF2.js +0 -610
- package/dist/chunk-FONJYVF2.js.map +0 -1
- package/dist/chunk-GWPOJSGW.cjs.map +0 -1
- package/dist/chunk-HDZZQCH7.js.map +0 -1
- package/dist/chunk-HRKRV2X7.js.map +0 -1
- package/dist/chunk-S367Y35J.cjs.map +0 -1
- package/dist/chunk-ZBHLMQTS.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-

|
|
2
2
|
|
|
3
3
|
# sanity-plugin-seofields
|
|
4
4
|
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
Manage SEO fields, social previews, robots directives, canonical URLs, Schema.org JSON-LD, and studio-wide SEO health checks — directly inside Sanity Studio v3, v4, or v5.
|
|
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) • [Quick Start](#quick-start) • [Configuration](#configuration) • [Schema.org](#schemaorg-structured-data) • [CLI](#cli)
|
|
11
|
+
[**Documentation**](https://sanity-plugin-seofields.thehardik.in/docs) • [Quick Start](#quick-start) • [Configuration](#configuration) • [AI](#ai-content-generation) • [Schema.org](#schemaorg-structured-data) • [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
|
|
28
|
-
|
|
|
29
|
-
|
|
|
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
|
|
114
|
+
<img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/astro.svg" width="20" height="20" align="center" alt="Astro"/> Astro
|
|
115
|
+
<img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/nuxtjs.svg" width="20" height="20" align="center" alt="Nuxt"/> Nuxt
|
|
116
|
+
<img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/vue.svg" width="20" height="20" align="center" alt="Vue"/> Vue
|
|
117
|
+
<img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/svelte.svg" width="20" height="20" align="center" alt="SvelteKit"/> SvelteKit
|
|
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
|
-
-
|
|
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 |
|
|
@@ -217,7 +294,7 @@ Full guide: [Frontend integration](https://sanity-plugin-seofields.thehardik.in/
|
|
|
217
294
|
| `seoFields` | Complete SEO field bundle for document schemas |
|
|
218
295
|
| `openGraph` | Open Graph metadata for Facebook, LinkedIn, Slack, and other social surfaces |
|
|
219
296
|
| `twitter` | X/Twitter Card metadata |
|
|
220
|
-
| `robots` | Indexing and
|
|
297
|
+
| `robots` | Indexing, crawling, translation, and image indexing directives |
|
|
221
298
|
| `metaTag` | Custom meta tag container |
|
|
222
299
|
| `metaAttribute` | Single custom meta attribute |
|
|
223
300
|
|
|
@@ -244,25 +321,74 @@ 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
|
+
| `dashboard` | Enable and configure the SEO Health Dashboard tool |
|
|
338
|
+
| `licenseKey` | License key for pro features (dashboard, publish gate, pro industries) |
|
|
339
|
+
| `ai` | Enable "Generate with AI" fields — see [AI Content Generation](#ai-content-generation) |
|
|
261
340
|
|
|
262
341
|
Full reference: [Configuration docs](https://sanity-plugin-seofields.thehardik.in/docs/configuration)
|
|
263
342
|
|
|
264
343
|
---
|
|
265
344
|
|
|
345
|
+
## AI Content Generation
|
|
346
|
+
|
|
347
|
+
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`.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
seofields({
|
|
351
|
+
ai: {
|
|
352
|
+
provider: 'openai',
|
|
353
|
+
apiKey: process.env.SANITY_STUDIO_OPENAI_API_KEY,
|
|
354
|
+
industry: 'blog',
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Providers** — `openai` (default), `anthropic`, `groq`, `gemini`, `ollama` (local models, no key required), or any OpenAI-compatible `baseUrl` (DeepSeek, xAI Grok, Azure OpenAI, self-hosted).
|
|
360
|
+
|
|
361
|
+
**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:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
// Next.js — app/api/seo/generate/route.ts
|
|
365
|
+
import {createNextRouteHandler} from 'sanity-plugin-seofields/server'
|
|
366
|
+
|
|
367
|
+
export const {POST, OPTIONS} = createNextRouteHandler({
|
|
368
|
+
provider: 'openai',
|
|
369
|
+
apiKey: process.env.OPENAI_API_KEY, // stays server-side
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
`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.
|
|
374
|
+
|
|
375
|
+
**Industries & prompt tiers** — 17 industries, 10 prompt variations per field:
|
|
376
|
+
|
|
377
|
+
| Tier | Industries | Prompts per field |
|
|
378
|
+
| :------- | :------------------------------------------------------------------------------------------------------------ | :--------------------------------------- |
|
|
379
|
+
| Free (8) | `blog`, `restaurant`, `travel`, `ecommerce`, `education`, `fitness`, `hospitality`, `nonprofit` | 4 free, 6 more with a license (10 total) |
|
|
380
|
+
| Pro (9) | `healthcare`, `pharmacy`, `finance`, `realestate`, `saas`, `legal`, `insurance`, `automotive`, `homeServices` | All 10 require a license |
|
|
381
|
+
|
|
382
|
+
Without `industry` set, generation uses generic prompts, which are always free.
|
|
383
|
+
|
|
384
|
+
**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.
|
|
385
|
+
|
|
386
|
+
**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`.
|
|
387
|
+
|
|
388
|
+
Full guide: [AI.md](./AI.md) • [AI Integration docs](https://sanity-plugin-seofields.thehardik.in/docs/ai)
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
266
392
|
## SEO Health Dashboard
|
|
267
393
|
|
|
268
394
|
The dashboard is a Studio tool that helps teams find SEO gaps before they ship content.
|
|
@@ -275,9 +401,9 @@ import seofields from 'sanity-plugin-seofields'
|
|
|
275
401
|
export default defineConfig({
|
|
276
402
|
plugins: [
|
|
277
403
|
seofields({
|
|
404
|
+
licenseKey: process.env.SANITY_STUDIO_SEO_LICENSE_KEY,
|
|
278
405
|
dashboard: {
|
|
279
406
|
enabled: true,
|
|
280
|
-
licenseKey: process.env.SANITY_STUDIO_SEO_LICENSE_KEY,
|
|
281
407
|
query: {
|
|
282
408
|
types: ['page', 'post', 'product'],
|
|
283
409
|
},
|
|
@@ -300,7 +426,7 @@ Dashboard capabilities:
|
|
|
300
426
|
- Open the exact document that needs updates
|
|
301
427
|
- Customize document title, subtitle, and preview display
|
|
302
428
|
|
|
303
|
-
Get a
|
|
429
|
+
Get a license key: [Get license](https://sanity-plugin-seofields.thehardik.in/get-license)
|
|
304
430
|
|
|
305
431
|
Dashboard docs: [SEO Health Dashboard](https://sanity-plugin-seofields.thehardik.in/docs/dashboard)
|
|
306
432
|
|
|
@@ -409,6 +535,289 @@ Schema.org docs: [Structured data guide](https://sanity-plugin-seofields.thehard
|
|
|
409
535
|
|
|
410
536
|
---
|
|
411
537
|
|
|
538
|
+
## Frontend Integration
|
|
539
|
+
|
|
540
|
+
The plugin stores SEO fields under the `seoFields` object. If you are comparing
|
|
541
|
+
examples from other Sanity SEO plugins, map the names carefully:
|
|
542
|
+
|
|
543
|
+
| Common name in other plugins | `sanity-plugin-seofields` field |
|
|
544
|
+
| :--------------------------- | :------------------------------ |
|
|
545
|
+
| `seo.metaTitle` | `seo.title` |
|
|
546
|
+
| `seo.metaDescription` | `seo.description` |
|
|
547
|
+
| `seo.metaImage` | `seo.metaImage` |
|
|
548
|
+
| `seo.twitter.cardType` | `seo.twitter.card` |
|
|
549
|
+
| `seo.hreflang` | `seo.hreflangs[]` |
|
|
550
|
+
|
|
551
|
+
### Shared GROQ Fragment
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
export const SEO_FRAGMENT = `{
|
|
555
|
+
title,
|
|
556
|
+
seo {
|
|
557
|
+
title,
|
|
558
|
+
description,
|
|
559
|
+
canonicalUrl,
|
|
560
|
+
metaImage { asset-> { url }, alt },
|
|
561
|
+
keywords,
|
|
562
|
+
robots { noIndex, noFollow, noTranslate, noImageIndex },
|
|
563
|
+
hreflangs[] { locale, url },
|
|
564
|
+
openGraph {
|
|
565
|
+
title, description, url, siteName, type,
|
|
566
|
+
imageType, imageUrl,
|
|
567
|
+
image { asset-> { url }, alt }
|
|
568
|
+
},
|
|
569
|
+
twitter {
|
|
570
|
+
card, site, creator, title, description,
|
|
571
|
+
imageType, imageUrl,
|
|
572
|
+
image { asset-> { url }, alt }
|
|
573
|
+
},
|
|
574
|
+
metaAttributes[] { _key, key, type, value }
|
|
575
|
+
}
|
|
576
|
+
}`
|
|
577
|
+
|
|
578
|
+
export const PAGE_QUERY = `
|
|
579
|
+
*[_type == "page" && slug.current == $slug][0] ${SEO_FRAGMENT}
|
|
580
|
+
`
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/nextjs.svg" width="20" height="20" align="center" alt=""/> Next.js App Router
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
// app/[slug]/page.tsx
|
|
587
|
+
import type {Metadata} from 'next'
|
|
588
|
+
import {buildSeoMeta} from 'sanity-plugin-seofields/next'
|
|
589
|
+
import {client} from '@/sanity/lib/client'
|
|
590
|
+
import {urlFor} from '@/sanity/lib/image'
|
|
591
|
+
import {PAGE_QUERY} from '@/sanity/lib/queries'
|
|
592
|
+
|
|
593
|
+
export async function generateMetadata(props: {
|
|
594
|
+
params: Promise<{slug: string}>
|
|
595
|
+
}): Promise<Metadata> {
|
|
596
|
+
const {slug} = await props.params
|
|
597
|
+
const page = await client.fetch(PAGE_QUERY, {slug})
|
|
598
|
+
|
|
599
|
+
return buildSeoMeta({
|
|
600
|
+
seo: page?.seo,
|
|
601
|
+
baseUrl: 'https://example.com',
|
|
602
|
+
path: '/' + slug,
|
|
603
|
+
defaults: {
|
|
604
|
+
title: page?.title || 'My Site',
|
|
605
|
+
description: 'Default site description',
|
|
606
|
+
siteName: 'My Site',
|
|
607
|
+
twitterSite: '@mysite',
|
|
608
|
+
},
|
|
609
|
+
imageUrlResolver: (image) => urlFor(image).width(1200).height(630).url(),
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/astro.svg" width="20" height="20" align="center" alt=""/> Astro
|
|
615
|
+
|
|
616
|
+
```astro
|
|
617
|
+
--- // src/pages/[slug].astro
|
|
618
|
+
import {buildSeoHead} from 'sanity-plugin-seofields/head'
|
|
619
|
+
import {client} from '@/lib/sanity'
|
|
620
|
+
import Layout from '@/layouts/Layout.astro'
|
|
621
|
+
import {PAGE_QUERY} from '@/lib/queries'
|
|
622
|
+
|
|
623
|
+
const {slug} = Astro.params
|
|
624
|
+
const page = await client.fetch(PAGE_QUERY, {slug})
|
|
625
|
+
|
|
626
|
+
if (!page) return Astro.redirect('/404')
|
|
627
|
+
|
|
628
|
+
const head = buildSeoHead({
|
|
629
|
+
seo: page.seo,
|
|
630
|
+
baseUrl: 'https://example.com',
|
|
631
|
+
path: '/' + slug,
|
|
632
|
+
defaults: {
|
|
633
|
+
title: page.title,
|
|
634
|
+
description: 'Default site description',
|
|
635
|
+
siteName: 'My Site',
|
|
636
|
+
},
|
|
637
|
+
})
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
<Layout head={head}>
|
|
641
|
+
<h1>{page.title}</h1>
|
|
642
|
+
</Layout>
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Render the tags in your layout:
|
|
646
|
+
|
|
647
|
+
```astro
|
|
648
|
+
--- // src/layouts/Layout.astro
|
|
649
|
+
const {head} = Astro.props
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
<html lang="en">
|
|
653
|
+
<head>
|
|
654
|
+
<title>{head.title}</title>
|
|
655
|
+
{head.meta.map((tag) =>
|
|
656
|
+
'property' in tag ? (
|
|
657
|
+
<meta property={tag.property} content={tag.content} />
|
|
658
|
+
) : (
|
|
659
|
+
<meta name={tag.name} content={tag.content} />
|
|
660
|
+
)
|
|
661
|
+
)}
|
|
662
|
+
{head.link.map((tag) => (
|
|
663
|
+
<link rel={tag.rel} href={tag.href} hreflang={tag.hreflang} />
|
|
664
|
+
))}
|
|
665
|
+
</head>
|
|
666
|
+
<body>
|
|
667
|
+
<slot />
|
|
668
|
+
</body>
|
|
669
|
+
</html>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/nuxtjs.svg" width="20" height="20" align="center" alt=""/> Nuxt 3
|
|
673
|
+
|
|
674
|
+
```vue
|
|
675
|
+
<!-- pages/[slug].vue -->
|
|
676
|
+
<script setup lang="ts">
|
|
677
|
+
import {buildSeoHead} from 'sanity-plugin-seofields/head'
|
|
678
|
+
|
|
679
|
+
const route = useRoute()
|
|
680
|
+
const page = await useSeoData(route.params.slug as string)
|
|
681
|
+
|
|
682
|
+
const head = buildSeoHead({
|
|
683
|
+
seo: page?.seo,
|
|
684
|
+
baseUrl: 'https://example.com',
|
|
685
|
+
path: '/' + route.params.slug,
|
|
686
|
+
defaults: {
|
|
687
|
+
title: page?.title || 'My Site',
|
|
688
|
+
description: 'Default site description',
|
|
689
|
+
siteName: 'My Site',
|
|
690
|
+
},
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
useHead({
|
|
694
|
+
title: head.title,
|
|
695
|
+
meta: head.meta,
|
|
696
|
+
link: head.link,
|
|
697
|
+
})
|
|
698
|
+
</script>
|
|
699
|
+
|
|
700
|
+
<template>
|
|
701
|
+
<main>
|
|
702
|
+
<h1>{{ page?.title }}</h1>
|
|
703
|
+
</main>
|
|
704
|
+
</template>
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/vue.svg" width="20" height="20" align="center" alt=""/> Vue 3 Standalone
|
|
708
|
+
|
|
709
|
+
Use `@unhead/vue` or another Vue head manager:
|
|
710
|
+
|
|
711
|
+
```vue
|
|
712
|
+
<script setup lang="ts">
|
|
713
|
+
import {useHead} from '@unhead/vue'
|
|
714
|
+
import {buildSeoHead} from 'sanity-plugin-seofields/head'
|
|
715
|
+
import {client} from '@/lib/sanity'
|
|
716
|
+
import {PAGE_QUERY} from '@/lib/queries'
|
|
717
|
+
|
|
718
|
+
const props = defineProps<{slug: string}>()
|
|
719
|
+
const page = await client.fetch(PAGE_QUERY, {slug: props.slug})
|
|
720
|
+
|
|
721
|
+
const head = buildSeoHead({
|
|
722
|
+
seo: page?.seo,
|
|
723
|
+
baseUrl: 'https://example.com',
|
|
724
|
+
path: '/' + props.slug,
|
|
725
|
+
defaults: {title: page?.title || 'My Site', siteName: 'My Site'},
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
useHead({
|
|
729
|
+
title: head.title,
|
|
730
|
+
meta: head.meta,
|
|
731
|
+
link: head.link,
|
|
732
|
+
})
|
|
733
|
+
</script>
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### <img src="https://sanity-plugin-seofields.thehardik.in/icons/frameworks/svelte.svg" width="20" height="20" align="center" alt=""/> SvelteKit
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
// src/routes/[slug]/+page.ts
|
|
740
|
+
import {buildSeoHead} from 'sanity-plugin-seofields/head'
|
|
741
|
+
import type {PageLoad} from './$types'
|
|
742
|
+
import {client} from '$lib/sanity'
|
|
743
|
+
import {PAGE_QUERY} from '$lib/queries'
|
|
744
|
+
|
|
745
|
+
export const load: PageLoad = async ({params}) => {
|
|
746
|
+
const page = await client.fetch(PAGE_QUERY, {slug: params.slug})
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
page,
|
|
750
|
+
head: buildSeoHead({
|
|
751
|
+
seo: page?.seo,
|
|
752
|
+
baseUrl: 'https://example.com',
|
|
753
|
+
path: '/' + params.slug,
|
|
754
|
+
defaults: {
|
|
755
|
+
title: page?.title || 'My Site',
|
|
756
|
+
description: 'Default site description',
|
|
757
|
+
siteName: 'My Site',
|
|
758
|
+
},
|
|
759
|
+
}),
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
```svelte
|
|
765
|
+
<!-- src/routes/[slug]/+page.svelte -->
|
|
766
|
+
<script lang="ts">
|
|
767
|
+
import type {PageData} from './$types'
|
|
768
|
+
export let data: PageData
|
|
769
|
+
</script>
|
|
770
|
+
|
|
771
|
+
<svelte:head>
|
|
772
|
+
{#if data.head.title}
|
|
773
|
+
<title>{data.head.title}</title>
|
|
774
|
+
{/if}
|
|
775
|
+
|
|
776
|
+
{#each data.head.meta as tag}
|
|
777
|
+
{#if 'property' in tag}
|
|
778
|
+
<meta property={tag.property} content={tag.content} />
|
|
779
|
+
{:else}
|
|
780
|
+
<meta name={tag.name} content={tag.content} />
|
|
781
|
+
{/if}
|
|
782
|
+
{/each}
|
|
783
|
+
|
|
784
|
+
{#each data.head.link as tag}
|
|
785
|
+
<link rel={tag.rel} href={tag.href} hreflang={tag.hreflang} />
|
|
786
|
+
{/each}
|
|
787
|
+
</svelte:head>
|
|
788
|
+
|
|
789
|
+
<main>
|
|
790
|
+
<h1>{data.page?.title}</h1>
|
|
791
|
+
</main>
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Detailed guide: [Frontend integration](https://sanity-plugin-seofields.thehardik.in/docs/frontend-integration)
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## Frontend Head Exports
|
|
799
|
+
|
|
800
|
+
Use this entry point for Astro, Nuxt, Vue, SvelteKit, Remix, and any frontend
|
|
801
|
+
that wants plain serializable head data without importing the Studio plugin or
|
|
802
|
+
React helpers.
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
import {
|
|
806
|
+
buildSeoHead,
|
|
807
|
+
buildSeoMeta,
|
|
808
|
+
sanitizeOGType,
|
|
809
|
+
sanitizeTwitterCard,
|
|
810
|
+
} from 'sanity-plugin-seofields/head'
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
Common usage:
|
|
814
|
+
|
|
815
|
+
- Use `buildSeoHead()` in Astro, Nuxt, Vue, SvelteKit, Remix, and custom renderers
|
|
816
|
+
- Use `buildSeoMeta()` if you want the normalized metadata object but not React components
|
|
817
|
+
- Pass an `imageUrlResolver` when your Sanity image data needs URL building
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
412
821
|
## Next.js & React Exports
|
|
413
822
|
|
|
414
823
|
```ts
|
|
@@ -458,7 +867,9 @@ CLI docs: [CLI guide](https://sanity-plugin-seofields.thehardik.in/docs/cli)
|
|
|
458
867
|
| Import path | Use |
|
|
459
868
|
| :------------------------------------ | :--------------------------------------------------------------------- |
|
|
460
869
|
| `sanity-plugin-seofields` | Studio plugin, base schema types, dashboard pane factory, shared types |
|
|
461
|
-
| `sanity-plugin-seofields/
|
|
870
|
+
| `sanity-plugin-seofields/head` | Framework-neutral SEO helpers (`buildSeoHead`, `buildSeoMeta`) |
|
|
871
|
+
| `sanity-plugin-seofields/server` | Server-side AI proxy adapters (Next.js, Express, Node, Fetch API) |
|
|
872
|
+
| `sanity-plugin-seofields/next` | Next.js metadata helpers, React meta tags, and Schema.org components |
|
|
462
873
|
| `sanity-plugin-seofields/schema` | Schema.org Sanity schema plugins and type exports |
|
|
463
874
|
| `sanity-plugin-seofields/schema/next` | Schema.org React JSON-LD components |
|
|
464
875
|
| `sanity-plugin-seofields/define-cli` | CLI configuration helper |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { JSX } from 'react';
|
|
2
|
+
import { InputProps } from 'sanity';
|
|
3
|
+
|
|
4
|
+
type DocumentRecord = {
|
|
5
|
+
_id?: string;
|
|
6
|
+
_type?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
} & Record<string, unknown>;
|
|
9
|
+
type DocumentViewDocument = DocumentRecord | {
|
|
10
|
+
displayed?: DocumentRecord | null;
|
|
11
|
+
draft?: DocumentRecord | null;
|
|
12
|
+
published?: DocumentRecord | null;
|
|
13
|
+
} | null;
|
|
14
|
+
interface SchemaOrgJsonLdPreviewOptions {
|
|
15
|
+
/**
|
|
16
|
+
* The base site URL used with the document slug to show a resolved page URL.
|
|
17
|
+
* Defaults to `https://www.example.com`.
|
|
18
|
+
*/
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Document field used for the slug. Supports dot paths such as `seo.slug`.
|
|
22
|
+
* Defaults to `slug`.
|
|
23
|
+
*/
|
|
24
|
+
slugField?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Combined Schema.org field on the document. Defaults to `schemaOrg`.
|
|
27
|
+
*/
|
|
28
|
+
schemaOrgField?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional path segment placed before the document slug, e.g. `blog`.
|
|
31
|
+
* Can be a function of the current document.
|
|
32
|
+
*/
|
|
33
|
+
pathPrefix?: string | ((document: DocumentRecord) => string);
|
|
34
|
+
/**
|
|
35
|
+
* Full override for building the resolved document URL.
|
|
36
|
+
*/
|
|
37
|
+
urlBuilder?: (document: DocumentRecord) => string;
|
|
38
|
+
/**
|
|
39
|
+
* API version for the slug-list query.
|
|
40
|
+
*/
|
|
41
|
+
apiVersion?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Custom GROQ query for the slug list. Should return `_id`, optional `title`,
|
|
44
|
+
* and `slug` as a string.
|
|
45
|
+
*/
|
|
46
|
+
slugQuery?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Show existing slugs for the same document type.
|
|
49
|
+
* Defaults to `true` for document previews and `false` for field input previews.
|
|
50
|
+
*/
|
|
51
|
+
showSlugList?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Max number of existing slugs to fetch. Defaults to `25`.
|
|
54
|
+
*/
|
|
55
|
+
maxSlugItems?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Title field used in the slug list. Defaults to `title`.
|
|
58
|
+
*/
|
|
59
|
+
titleField?: string;
|
|
60
|
+
}
|
|
61
|
+
interface SchemaOrgJsonLdPreviewProps extends SchemaOrgJsonLdPreviewOptions {
|
|
62
|
+
/** Schema.org value to preview. Uses `document[schemaOrgField]` when omitted. */
|
|
63
|
+
value?: Record<string, unknown> | Array<Record<string, unknown>> | null;
|
|
64
|
+
/** Current Sanity document or document-view `document` prop. */
|
|
65
|
+
document?: DocumentViewDocument;
|
|
66
|
+
/** Optional document id from a Sanity document-node component view. */
|
|
67
|
+
documentId?: string;
|
|
68
|
+
/** Optional schema type from a Sanity document-node component view. */
|
|
69
|
+
schemaType?: string;
|
|
70
|
+
/** Fallback Schema.org type name for individual object inputs. */
|
|
71
|
+
schemaTypeName?: string;
|
|
72
|
+
/** Internal flag used by field input previews. */
|
|
73
|
+
fieldPreview?: boolean;
|
|
74
|
+
}
|
|
75
|
+
declare function SchemaOrgJsonLdPreview({ value, document, documentId, schemaType, schemaTypeName, fieldPreview, baseUrl, slugField, schemaOrgField, pathPrefix, urlBuilder, apiVersion, slugQuery, showSlugList, maxSlugItems, titleField, }: SchemaOrgJsonLdPreviewProps): JSX.Element;
|
|
76
|
+
declare function SchemaOrgJsonLdPreviewInput(props: InputProps): JSX.Element;
|
|
77
|
+
|
|
78
|
+
export { SchemaOrgJsonLdPreview as S, type SchemaOrgJsonLdPreviewOptions as a, type SchemaOrgJsonLdPreviewProps as b, SchemaOrgJsonLdPreviewInput as c };
|