sanity-plugin-seofields 1.5.1 → 1.5.3
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/LICENSE +1 -1
- package/README.md +97 -1085
- package/dist/SeoHealthDashboard-KPBNXSL4.cjs +10 -0
- package/dist/{SeoHealthDashboard-L4D4F3LO.cjs.map → SeoHealthDashboard-KPBNXSL4.cjs.map} +1 -1
- package/dist/SeoHealthDashboard-QKVB5HK3.js +4 -0
- package/dist/{SeoHealthDashboard-6RBRIYBX.js.map → SeoHealthDashboard-QKVB5HK3.js.map} +1 -1
- package/dist/{SeoHealthTool-FXNQPNPG.js → SeoHealthTool-EPPOEDTW.js} +3 -3
- package/dist/{SeoHealthTool-FXNQPNPG.js.map → SeoHealthTool-EPPOEDTW.js.map} +1 -1
- package/dist/{SeoHealthTool-FM2HXDPU.cjs → SeoHealthTool-ON3SRXCF.cjs} +4 -4
- package/dist/{SeoHealthTool-FM2HXDPU.cjs.map → SeoHealthTool-ON3SRXCF.cjs.map} +1 -1
- package/dist/{chunk-Z63UP35O.cjs → chunk-G2SVI2SP.cjs} +595 -306
- package/dist/chunk-G2SVI2SP.cjs.map +1 -0
- package/dist/{chunk-CNBJAXVH.js → chunk-UCVSMPEJ.js} +596 -307
- package/dist/chunk-UCVSMPEJ.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +19 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +53 -33
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +39 -3
- package/dist/next.d.ts +39 -3
- package/dist/next.js +53 -33
- package/dist/next.js.map +1 -1
- package/package.json +18 -1
- package/dist/SeoHealthDashboard-6RBRIYBX.js +0 -4
- package/dist/SeoHealthDashboard-L4D4F3LO.cjs +0 -10
- package/dist/chunk-CNBJAXVH.js.map +0 -1
- package/dist/chunk-Z63UP35O.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -3,1192 +3,204 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/sanity-plugin-seofields)
|
|
4
4
|
[](https://www.npmjs.com/package/sanity-plugin-seofields)
|
|
5
5
|
[](./LICENSE)
|
|
6
|
-
[](https://github.com/hardik-143/sanity-plugin-seofields/issues)
|
|
7
6
|
[](https://github.com/hardik-143/sanity-plugin-seofields)
|
|
8
7
|
|
|
9
|
-
A
|
|
8
|
+
A Sanity Studio (v3/v4/v5) plugin to manage SEO fields — meta tags, Open Graph, Twitter Cards, robots directives, and structured data.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
📖 **[Full Documentation →](https://sanity-plugin-seofields.thehardik.in/docs)**
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
- 📱 **Open Graph**: Complete social media sharing optimization
|
|
15
|
-
- 🐦 **X (Formerly Twitter) Cards**: X-specific meta tags with image support
|
|
16
|
-
- 🤖 **Robots Control**: Index/follow settings for search engines
|
|
17
|
-
- 🖼️ **Image Management**: Optimized image handling for social sharing
|
|
18
|
-
- 📋 **Live Preview**: Real-time SEO preview as you edit
|
|
19
|
-
- 🔧 **TypeScript Support**: Full type definitions included
|
|
20
|
-
- 📊 **Custom Attributes**: Flexible meta attribute system
|
|
21
|
-
- ✅ **Validation**: Built-in character limits and best practices
|
|
22
|
-
- 🎛️ **Field Visibility**: Hide sitewide fields on specific content types
|
|
23
|
-
- 📊 **SEO Health Dashboard**: Studio-wide overview of SEO completeness with scores, issue highlights, and direct document links
|
|
24
|
-
- 🗂️ **Desk Structure Pane**: Embed the dashboard inside the Structure tool with `createSeoHealthPane` — supports split-pane document editing
|
|
12
|
+
---
|
|
25
13
|
|
|
26
|
-
##
|
|
14
|
+
## Installation
|
|
27
15
|
|
|
28
16
|
```bash
|
|
29
17
|
npm install sanity-plugin-seofields
|
|
30
18
|
```
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
yarn add sanity-plugin-seofields
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## 🚀 Quick Start
|
|
39
|
-
|
|
40
|
-
### 1. Add the Plugin
|
|
20
|
+
## Quick Start
|
|
41
21
|
|
|
42
|
-
|
|
22
|
+
### 1. Register the plugin
|
|
43
23
|
|
|
44
|
-
```
|
|
24
|
+
```ts
|
|
25
|
+
// sanity.config.ts
|
|
45
26
|
import {defineConfig} from 'sanity'
|
|
46
27
|
import seofields from 'sanity-plugin-seofields'
|
|
47
28
|
|
|
48
29
|
export default defineConfig({
|
|
49
|
-
|
|
50
|
-
title: 'Your Project',
|
|
51
|
-
projectId: 'your-project-id',
|
|
52
|
-
dataset: 'production',
|
|
53
|
-
|
|
54
|
-
plugins: [
|
|
55
|
-
seofields(), // Add the SEO fields plugin
|
|
56
|
-
// ... other plugins
|
|
57
|
-
],
|
|
58
|
-
|
|
59
|
-
schema: {
|
|
60
|
-
types: [
|
|
61
|
-
// ... your schema types
|
|
62
|
-
],
|
|
63
|
-
},
|
|
30
|
+
plugins: [seofields()],
|
|
64
31
|
})
|
|
65
32
|
```
|
|
66
33
|
|
|
67
|
-
### 2. Add SEO
|
|
34
|
+
### 2. Add SEO fields to a document
|
|
68
35
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
36
|
+
```ts
|
|
72
37
|
import {defineField, defineType} from 'sanity'
|
|
73
38
|
|
|
74
39
|
export default defineType({
|
|
75
40
|
name: 'page',
|
|
76
|
-
title: 'Page',
|
|
77
|
-
type: 'document',
|
|
78
|
-
fields: [
|
|
79
|
-
defineField({
|
|
80
|
-
name: 'title',
|
|
81
|
-
title: 'Title',
|
|
82
|
-
type: 'string',
|
|
83
|
-
}),
|
|
84
|
-
defineField({
|
|
85
|
-
name: 'content',
|
|
86
|
-
title: 'Content',
|
|
87
|
-
type: 'text',
|
|
88
|
-
}),
|
|
89
|
-
// Add SEO fields
|
|
90
|
-
defineField({
|
|
91
|
-
name: 'seo',
|
|
92
|
-
title: 'SEO',
|
|
93
|
-
type: 'seoFields',
|
|
94
|
-
}),
|
|
95
|
-
],
|
|
96
|
-
})
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### 3. Using Individual SEO Components
|
|
100
|
-
|
|
101
|
-
You can also use individual components:
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
import {defineField, defineType} from 'sanity'
|
|
105
|
-
|
|
106
|
-
export default defineType({
|
|
107
|
-
name: 'article',
|
|
108
|
-
title: 'Article',
|
|
109
41
|
type: 'document',
|
|
110
42
|
fields: [
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Individual SEO components
|
|
114
|
-
defineField({
|
|
115
|
-
name: 'openGraph',
|
|
116
|
-
title: 'Open Graph',
|
|
117
|
-
type: 'openGraph',
|
|
118
|
-
}),
|
|
119
|
-
defineField({
|
|
120
|
-
name: 'twitterCard',
|
|
121
|
-
title: 'X (Formerly Twitter) Card',
|
|
122
|
-
type: 'twitter',
|
|
123
|
-
}),
|
|
124
|
-
defineField({
|
|
125
|
-
name: 'metaAttributes',
|
|
126
|
-
title: 'Custom Meta Tags',
|
|
127
|
-
type: 'metaTag',
|
|
128
|
-
}),
|
|
43
|
+
defineField({name: 'title', type: 'string'}),
|
|
44
|
+
defineField({name: 'seo', type: 'seoFields'}),
|
|
129
45
|
],
|
|
130
46
|
})
|
|
131
47
|
```
|
|
132
48
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
| Type | Description | Use Case |
|
|
136
|
-
| --------------- | ---------------------------------- | -------------------------------- |
|
|
137
|
-
| `seoFields` | Complete SEO package | Main SEO fields for any document |
|
|
138
|
-
| `openGraph` | Open Graph meta tags | Social media sharing |
|
|
139
|
-
| `twitter` | X (Formerly Twitter) Card settings | X-specific optimization |
|
|
140
|
-
| `metaTag` | Custom meta attributes | Advanced meta tag management |
|
|
141
|
-
| `metaAttribute` | Individual meta attribute | Building custom meta tags |
|
|
142
|
-
| `robots` | Search engine directives | Control indexing and crawling |
|
|
143
|
-
|
|
144
|
-
## 🔧 Configuration Options
|
|
145
|
-
|
|
146
|
-
### Basic Configuration
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
import seofields from 'sanity-plugin-seofields'
|
|
150
|
-
|
|
151
|
-
export default defineConfig({
|
|
152
|
-
plugins: [
|
|
153
|
-
seofields(), // Use default configuration
|
|
154
|
-
],
|
|
155
|
-
})
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Advanced Configuration
|
|
159
|
-
|
|
160
|
-
You can customize field titles and descriptions, control SEO preview functionality, and manage field visibility:
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
import seofields, {SeoFieldsPluginConfig} from 'sanity-plugin-seofields'
|
|
164
|
-
|
|
165
|
-
export default defineConfig({
|
|
166
|
-
plugins: [
|
|
167
|
-
seofields({
|
|
168
|
-
seoPreview: true, // Enable/disable SEO preview (default: true)
|
|
169
|
-
fieldOverrides: {
|
|
170
|
-
title: {
|
|
171
|
-
title: 'Page Title',
|
|
172
|
-
description: 'The main title that appears in search results',
|
|
173
|
-
},
|
|
174
|
-
description: {
|
|
175
|
-
title: 'Meta Description',
|
|
176
|
-
description: 'A brief description of the page content for search engines',
|
|
177
|
-
},
|
|
178
|
-
canonicalUrl: {
|
|
179
|
-
title: 'Canonical URL',
|
|
180
|
-
description: 'The preferred URL for this page to avoid duplicate content issues',
|
|
181
|
-
},
|
|
182
|
-
metaImage: {
|
|
183
|
-
title: 'Social Media Image',
|
|
184
|
-
description: 'Image used when sharing this page on social media',
|
|
185
|
-
},
|
|
186
|
-
keywords: {
|
|
187
|
-
title: 'SEO Keywords',
|
|
188
|
-
description: 'Keywords that describe the content of this page',
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
// Hide sitewide fields on specific content types
|
|
192
|
-
fieldVisibility: {
|
|
193
|
-
page: {
|
|
194
|
-
hiddenFields: ['openGraphSiteName', 'twitterSite'],
|
|
195
|
-
},
|
|
196
|
-
post: {
|
|
197
|
-
hiddenFields: ['openGraphSiteName', 'twitterSite'],
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
// Or hide fields globally
|
|
201
|
-
defaultHiddenFields: ['openGraphSiteName', 'twitterSite'],
|
|
202
|
-
} satisfies SeoFieldsPluginConfig),
|
|
203
|
-
],
|
|
204
|
-
})
|
|
205
|
-
```
|
|
49
|
+
That's it. The `seoFields` type is automatically registered by the plugin.
|
|
206
50
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
| Option | Type | Default | Description |
|
|
210
|
-
| --------------------- | --------- | ------- | ------------------------------------------- |
|
|
211
|
-
| `seoPreview` | `boolean` | `true` | Enable/disable the live SEO preview feature |
|
|
212
|
-
| `fieldOverrides` | `object` | `{}` | Customize field titles and descriptions |
|
|
213
|
-
| `fieldVisibility` | `object` | `{}` | Hide sitewide fields on specific post types |
|
|
214
|
-
| `defaultHiddenFields` | `array` | `[]` | Hide sitewide fields globally |
|
|
215
|
-
|
|
216
|
-
#### Field Configuration
|
|
217
|
-
|
|
218
|
-
Each field in the `fieldOverrides` object can have:
|
|
219
|
-
|
|
220
|
-
- `title` - Custom title for the field
|
|
221
|
-
- `description` - Custom description/help text for the field
|
|
222
|
-
|
|
223
|
-
**Available field keys:**
|
|
224
|
-
|
|
225
|
-
- `title`, `description`, `canonicalUrl`, `metaImage`, `keywords`, `metaAttributes`, `robots`
|
|
226
|
-
- `openGraphUrl`, `openGraphTitle`, `openGraphDescription`, `openGraphSiteName`, `openGraphType`, `openGraphImage`
|
|
227
|
-
- `twitterCard`, `twitterSite`, `twitterCreator`, `twitterTitle`, `twitterDescription`, `twitterImage`
|
|
228
|
-
|
|
229
|
-
#### Field Visibility Configuration
|
|
230
|
-
|
|
231
|
-
Control which fields are visible on different content types. You can hide any SEO field on any post type:
|
|
51
|
+
---
|
|
232
52
|
|
|
233
|
-
|
|
53
|
+
## Available Schema Types
|
|
234
54
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
55
|
+
| Type | Description |
|
|
56
|
+
| --------------- | ------------------------------------ |
|
|
57
|
+
| `seoFields` | Complete SEO bundle (recommended) |
|
|
58
|
+
| `openGraph` | Open Graph tags for social sharing |
|
|
59
|
+
| `twitter` | X (Twitter) Card settings |
|
|
60
|
+
| `metaTag` | Container for custom meta attributes |
|
|
61
|
+
| `metaAttribute` | Single key/value meta attribute |
|
|
62
|
+
| `robots` | noindex / nofollow directives |
|
|
238
63
|
|
|
239
|
-
|
|
64
|
+
---
|
|
240
65
|
|
|
241
|
-
|
|
66
|
+
## Configuration
|
|
242
67
|
|
|
243
|
-
```
|
|
244
|
-
// Hide fields globally
|
|
68
|
+
```ts
|
|
245
69
|
seofields({
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Hide fields on specific content types
|
|
250
|
-
seofields({
|
|
251
|
-
fieldVisibility: {
|
|
252
|
-
page: {
|
|
253
|
-
hiddenFields: ['openGraphSiteName', 'twitterSite', 'keywords'],
|
|
254
|
-
},
|
|
255
|
-
post: {
|
|
256
|
-
hiddenFields: ['openGraphSiteName', 'metaAttributes'],
|
|
257
|
-
},
|
|
258
|
-
product: {
|
|
259
|
-
hiddenFields: ['canonicalUrl', 'robots'],
|
|
260
|
-
},
|
|
70
|
+
seoPreview: true,
|
|
71
|
+
fieldOverrides: {
|
|
72
|
+
title: {title: 'Page Title'},
|
|
261
73
|
},
|
|
262
|
-
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
This is particularly useful when you want to:
|
|
266
|
-
|
|
267
|
-
- Manage sitewide settings (like site name and X handle) in a dedicated Site Settings document
|
|
268
|
-
- Simplify the editing experience by hiding fields that aren't relevant for certain content types
|
|
269
|
-
- Create different SEO workflows for different content types
|
|
270
|
-
|
|
271
|
-
### Field Specifications
|
|
272
|
-
|
|
273
|
-
#### Meta Title
|
|
274
|
-
|
|
275
|
-
- **Max Length**: 70 characters (warning at 60)
|
|
276
|
-
- **Purpose**: Search engine result headlines
|
|
277
|
-
- **Best Practice**: Include primary keywords, keep under 60 chars
|
|
278
|
-
|
|
279
|
-
#### Meta Description
|
|
280
|
-
|
|
281
|
-
- **Max Length**: 160 characters (warning at 150)
|
|
282
|
-
- **Purpose**: Search result descriptions
|
|
283
|
-
- **Best Practice**: Compelling summary with keywords
|
|
284
|
-
|
|
285
|
-
#### Canonical URL
|
|
286
|
-
|
|
287
|
-
- **Format**: Must include protocol (https://)
|
|
288
|
-
- **Purpose**: Signals the preferred URL when duplicate or paginated content exists
|
|
289
|
-
- **Best Practice**: Mirror the resolved frontend route exactly to avoid mismatched indexing
|
|
290
|
-
|
|
291
|
-
#### Meta Image
|
|
292
|
-
|
|
293
|
-
- **Recommended Size**: 1200x630px minimum 600x315px
|
|
294
|
-
- **Purpose**: Default share image when Open Graph/Twitter images are absent
|
|
295
|
-
- **Best Practice**: Provide descriptive alt text and keep file size under 5MB
|
|
296
|
-
|
|
297
|
-
#### Meta Attributes
|
|
298
|
-
|
|
299
|
-
- **Structure**: Key/value pairs or key/image pairs
|
|
300
|
-
- **Purpose**: Add bespoke `<meta>` tags (for example `theme-color`, `author`, verification tokens)
|
|
301
|
-
- **Best Practice**: Avoid duplicating tags already generated elsewhere to limit head bloat
|
|
302
|
-
|
|
303
|
-
#### Keywords
|
|
304
|
-
|
|
305
|
-
- **Type**: Array of short strings
|
|
306
|
-
- **Purpose**: Editorial helper; not surfaced automatically to search engines
|
|
307
|
-
- **Best Practice**: Keep entries concise (1-3 words) and limit to high-intent topics
|
|
308
|
-
|
|
309
|
-
#### Open Graph Image
|
|
310
|
-
|
|
311
|
-
- **Recommended Size**: 1200x630px
|
|
312
|
-
- **Minimum Size**: 600x315px
|
|
313
|
-
- **Aspect Ratio**: 1.91:1
|
|
314
|
-
- **Formats**: JPG, PNG, WebP
|
|
315
|
-
|
|
316
|
-
#### Open Graph URL
|
|
317
|
-
|
|
318
|
-
- **Purpose**: Canonical URL for social media sharing
|
|
319
|
-
- **Format**: Full URL with protocol (https://)
|
|
320
|
-
- **Best Practice**: Use the preferred URL for the page to avoid duplicate content issues
|
|
321
|
-
- **Required**: Should match the actual page URL for consistency
|
|
322
|
-
|
|
323
|
-
#### Open Graph Site Name
|
|
324
|
-
|
|
325
|
-
- **Purpose**: Displays publisher name on share previews
|
|
326
|
-
- **Best Practice**: Keep consistent with brand name used across marketing channels
|
|
327
|
-
|
|
328
|
-
#### Open Graph Type
|
|
329
|
-
|
|
330
|
-
- **Options**: `website`, `article`, `profile`, `book`, `music`, `video`, `product`
|
|
331
|
-
- **Best Practice**: Pick the narrowest type applicable to unlock platform-specific rendering
|
|
332
|
-
|
|
333
|
-
#### X (Formerly Twitter) Card Image
|
|
334
|
-
|
|
335
|
-
- **Summary Card**: Minimum 120x120px
|
|
336
|
-
- **Large Image**: Minimum 280x150px
|
|
337
|
-
- **Required**: Alt text for accessibility
|
|
338
|
-
|
|
339
|
-
#### X (Formerly Twitter) Card Creator
|
|
340
|
-
|
|
341
|
-
- **Purpose**: Attribution to content creator on X (formerly Twitter)
|
|
342
|
-
- **Format**: X handle with @ symbol (e.g., @creator)
|
|
343
|
-
- **Usage**: Identifies the individual author of the content
|
|
344
|
-
- **Best Practice**: Use actual X handles for proper attribution
|
|
345
|
-
|
|
346
|
-
#### X (Formerly Twitter) Card Type
|
|
347
|
-
|
|
348
|
-
- **Options**: `summary`, `summary_large_image`, `app`, `player`
|
|
349
|
-
- **Best Practice**: Use `summary_large_image` for rich media, fall back to `summary` when imagery is square or minimal
|
|
350
|
-
|
|
351
|
-
#### X (Formerly Twitter) Site Handle
|
|
352
|
-
|
|
353
|
-
- **Purpose**: Publisher attribution when multiple authors contribute
|
|
354
|
-
- **Format**: X handle with @ symbol (e.g., @brand)
|
|
355
|
-
- **Best Practice**: Configure once in site settings and hide on document types that inherit it
|
|
356
|
-
|
|
357
|
-
#### Robots Settings
|
|
358
|
-
|
|
359
|
-
- **Options**: `noIndex`, `noFollow`
|
|
360
|
-
- **Purpose**: Control whether pages are indexed or links followed by crawlers
|
|
361
|
-
- **Best Practice**: Only enable when intentionally blocking content (for example gated pages or previews)
|
|
362
|
-
|
|
363
|
-
## 🎛️ Field Visibility Feature
|
|
364
|
-
|
|
365
|
-
The field visibility feature allows you to hide any SEO field on specific content types. This is perfect for managing sitewide settings in a dedicated Site Settings document or creating customized editing experiences for different content types.
|
|
366
|
-
|
|
367
|
-
### Quick Example
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
// Hide specific fields on different content types
|
|
371
|
-
seofields({
|
|
74
|
+
defaultHiddenFields: ['openGraphSiteName', 'twitterSite'],
|
|
372
75
|
fieldVisibility: {
|
|
373
|
-
|
|
374
|
-
hiddenFields: ['openGraphSiteName', 'twitterSite', 'keywords'],
|
|
375
|
-
},
|
|
376
|
-
post: {
|
|
377
|
-
hiddenFields: ['openGraphSiteName', 'metaAttributes'],
|
|
378
|
-
},
|
|
379
|
-
product: {
|
|
380
|
-
hiddenFields: ['canonicalUrl', 'robots'],
|
|
381
|
-
},
|
|
76
|
+
post: {hiddenFields: ['twitterSite']},
|
|
382
77
|
},
|
|
383
78
|
})
|
|
384
79
|
```
|
|
385
80
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
export default defineType({
|
|
393
|
-
name: 'siteSettings',
|
|
394
|
-
title: 'Site Settings',
|
|
395
|
-
type: 'document',
|
|
396
|
-
fields: [
|
|
397
|
-
defineField({
|
|
398
|
-
name: 'openGraphSiteName',
|
|
399
|
-
title: 'Open Graph Site Name',
|
|
400
|
-
type: 'string',
|
|
401
|
-
}),
|
|
402
|
-
defineField({
|
|
403
|
-
name: 'twitterSite',
|
|
404
|
-
title: 'X (Formerly Twitter) Site Handle',
|
|
405
|
-
type: 'string',
|
|
406
|
-
}),
|
|
407
|
-
],
|
|
408
|
-
})
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### Complete SEO Setup
|
|
412
|
-
|
|
413
|
-
```typescript
|
|
414
|
-
// In your schema
|
|
415
|
-
defineField({
|
|
416
|
-
name: 'seo',
|
|
417
|
-
title: 'SEO & Social Media',
|
|
418
|
-
type: 'seoFields',
|
|
419
|
-
group: 'seo', // Optional: group in a tab
|
|
420
|
-
})
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### Custom Meta Tags
|
|
81
|
+
| Option | Type | Default | Description |
|
|
82
|
+
| --------------------- | --------- | ------- | --------------------------------------- |
|
|
83
|
+
| `seoPreview` | `boolean` | `true` | Enable/disable live SEO preview |
|
|
84
|
+
| `fieldOverrides` | `object` | `{}` | Customize field titles and descriptions |
|
|
85
|
+
| `defaultHiddenFields` | `array` | `[]` | Hide sitewide fields globally |
|
|
86
|
+
| `fieldVisibility` | `object` | `{}` | Hide fields per document type |
|
|
424
87
|
|
|
425
|
-
|
|
426
|
-
// For advanced users who need custom meta tags
|
|
427
|
-
defineField({
|
|
428
|
-
name: 'customMeta',
|
|
429
|
-
title: 'Custom Meta Tags',
|
|
430
|
-
type: 'metaTag',
|
|
431
|
-
description: 'Add custom meta attributes for specific needs',
|
|
432
|
-
})
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Open Graph Only
|
|
436
|
-
|
|
437
|
-
```typescript
|
|
438
|
-
// If you only need Open Graph
|
|
439
|
-
defineField({
|
|
440
|
-
name: 'socialSharing',
|
|
441
|
-
title: 'Social Media Sharing',
|
|
442
|
-
type: 'openGraph',
|
|
443
|
-
})
|
|
444
|
-
```
|
|
88
|
+
→ [Full configuration reference](https://sanity-plugin-seofields.thehardik.in/docs/configuration)
|
|
445
89
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
The plugin includes a built-in **SEO Health Dashboard** tool accessible directly from Sanity Studio. It scans all documents that contain an `seo` field and gives you an at-a-glance picture of your site's SEO completeness.
|
|
449
|
-
|
|
450
|
-
### 🔑 License Key Required
|
|
90
|
+
---
|
|
451
91
|
|
|
452
|
-
|
|
92
|
+
## SEO Health Dashboard
|
|
453
93
|
|
|
454
|
-
|
|
94
|
+
An optional Studio tool that scores SEO completeness across all documents, highlights missing fields, and links directly to documents.
|
|
455
95
|
|
|
456
|
-
|
|
96
|
+
Requires a free license key — [get yours here](https://sanity-plugin-seofields.thehardik.in/get-license).
|
|
457
97
|
|
|
458
|
-
```
|
|
459
|
-
// Minimal — just add your license key
|
|
98
|
+
```ts
|
|
460
99
|
seofields({
|
|
461
|
-
|
|
462
|
-
|
|
100
|
+
dashboard: {
|
|
101
|
+
enabled: true,
|
|
102
|
+
licenseKey: process.env.SANITY_STUDIO_SEO_LICENSE_KEY,
|
|
463
103
|
},
|
|
464
104
|
})
|
|
465
|
-
|
|
466
|
-
// Full options — all nested under healthDashboard
|
|
467
|
-
seofields({
|
|
468
|
-
healthDashboard: {
|
|
469
|
-
// Required
|
|
470
|
-
licenseKey: 'YOUR_LICENSE_KEY',
|
|
471
|
-
|
|
472
|
-
// Studio nav tab
|
|
473
|
-
tool: {
|
|
474
|
-
title: 'SEO Audit', // tab label in Studio sidebar (default: 'SEO Health')
|
|
475
|
-
name: 'seo-health-dashboard', // internal tool slug
|
|
476
|
-
},
|
|
477
|
-
|
|
478
|
-
// Dashboard page content
|
|
479
|
-
content: {
|
|
480
|
-
icon: '🔍', // emoji before the page heading
|
|
481
|
-
title: 'SEO Audit', // page heading (default: tool.title)
|
|
482
|
-
description: 'Track SEO quality across all published content.',
|
|
483
|
-
},
|
|
484
|
-
|
|
485
|
-
// Table columns (flat keys — replaces the deprecated display.* object)
|
|
486
|
-
showTypeColumn: true, // show document type column (default: true)
|
|
487
|
-
showDocumentId: false, // show document _id under titles (default: true)
|
|
488
|
-
|
|
489
|
-
// Document query
|
|
490
|
-
query: {
|
|
491
|
-
types: ['post', 'page'], // limit to specific document types
|
|
492
|
-
requireSeo: true, // only include docs with seo != null (default: true)
|
|
493
|
-
// groq: '*[seo != null] { _id, _type, title, seo, _updatedAt }',
|
|
494
|
-
// ^ custom GROQ takes precedence over types + requireSeo
|
|
495
|
-
},
|
|
496
|
-
|
|
497
|
-
// Human-readable labels for document type names
|
|
498
|
-
typeDisplayLabels: {productDrug: 'Products', landingPage: 'Landing Page'},
|
|
499
|
-
|
|
500
|
-
// Custom badge next to the document title
|
|
501
|
-
getDocumentBadge: (doc) => {
|
|
502
|
-
if (doc.status === 'draft') return {label: 'Draft', bgColor: '#f3f4f6', textColor: '#6b7280'}
|
|
503
|
-
},
|
|
504
|
-
|
|
505
|
-
apiVersion: '2023-01-01', // Sanity API version (default: '2023-01-01')
|
|
506
|
-
},
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
// Or disable the dashboard entirely
|
|
510
|
-
seofields({
|
|
511
|
-
healthDashboard: false,
|
|
512
|
-
})
|
|
513
105
|
```
|
|
514
106
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
The following keys were renamed in **v1.3.2** for clarity. The old keys still work but will print a console warning and show an amber banner inside the dashboard. They will be removed in a future major release.
|
|
518
|
-
|
|
519
|
-
| Deprecated (old) | Replacement (new) |
|
|
520
|
-
| ------------------------------------ | ----------------------------------- |
|
|
521
|
-
| `healthDashboard.display.typeColumn` | `healthDashboard.showTypeColumn` |
|
|
522
|
-
| `healthDashboard.display.documentId` | `healthDashboard.showDocumentId` |
|
|
523
|
-
| `healthDashboard.typeLabels` | `healthDashboard.typeDisplayLabels` |
|
|
524
|
-
| `healthDashboard.docBadge` | `healthDashboard.getDocumentBadge` |
|
|
525
|
-
|
|
526
|
-
The same renames apply to `SeoHealthDashboardProps` when using `createSeoHealthPane` directly.
|
|
527
|
-
|
|
528
|
-
See the [v1.3.2 changelog](./CHANGELOG.md#132--2026-03-23) for the full migration diff.
|
|
107
|
+
→ [Dashboard docs](https://sanity-plugin-seofields.thehardik.in/docs/dashboard)
|
|
529
108
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
| Feature | Details |
|
|
533
|
-
| ------------------------ | -------------------------------------------------------------------------------- |
|
|
534
|
-
| **Summary stats** | Total documents, average score, and count per health tier |
|
|
535
|
-
| **Per-document score** | 0–95 score based on which SEO fields are filled in |
|
|
536
|
-
| **Color-coded badges** | 🟢 Excellent (≥ 80) · 🟡 Good (≥ 60) · 🟠 Fair (≥ 40) · 🔴 Poor / Missing (< 40) |
|
|
537
|
-
| **Inline issues** | Top 2 issues per document shown inline; overflow count displayed |
|
|
538
|
-
| **Direct document link** | Click the document title to open it in the desk (new tab) |
|
|
539
|
-
| **Search & filter** | Filter by health status, sort by score or title, and full-text search |
|
|
540
|
-
|
|
541
|
-
### Scoring breakdown
|
|
542
|
-
|
|
543
|
-
| Field | Max Points |
|
|
544
|
-
| ------------------- | ---------- |
|
|
545
|
-
| Meta Title | 25 |
|
|
546
|
-
| Meta Description | 20 |
|
|
547
|
-
| OG Title | 15 |
|
|
548
|
-
| OG Description | 10 |
|
|
549
|
-
| Twitter Title | 10 |
|
|
550
|
-
| Twitter Description | 10 |
|
|
551
|
-
| Robots / No-Index | 5 |
|
|
552
|
-
| **Total** | **95** |
|
|
553
|
-
|
|
554
|
-
> **Scoring logic:** each field earns its full points when a non-empty value is present, zero when missing. `query.groq` lets you control exactly which documents are included in the audit.
|
|
555
|
-
|
|
556
|
-
## 🗂️ Desk Structure Pane
|
|
557
|
-
|
|
558
|
-
Embed the SEO Health Dashboard **directly inside the Structure tool** as a pane with split-pane document editing — clicking any row opens the document editor to the right.
|
|
559
|
-
|
|
560
|
-
### Import
|
|
561
|
-
|
|
562
|
-
```typescript
|
|
563
|
-
import {createSeoHealthPane} from 'sanity-plugin-seofields'
|
|
564
|
-
```
|
|
109
|
+
---
|
|
565
110
|
|
|
566
|
-
|
|
111
|
+
## Schema.org / JSON-LD
|
|
567
112
|
|
|
568
|
-
|
|
113
|
+
The plugin ships 24 Schema.org types as Sanity schema definitions + React components that render `<script type="application/ld+json">` tags.
|
|
569
114
|
|
|
570
|
-
|
|
115
|
+
### 1. Register schema types in Studio
|
|
571
116
|
|
|
572
|
-
```
|
|
117
|
+
```ts
|
|
573
118
|
// sanity.config.ts
|
|
574
|
-
import {
|
|
575
|
-
import {structureTool} from 'sanity/structure'
|
|
576
|
-
import seofields, {createSeoHealthPane} from 'sanity-plugin-seofields'
|
|
119
|
+
import {schemaOrg} from 'sanity-plugin-seofields/schema'
|
|
577
120
|
|
|
578
121
|
export default defineConfig({
|
|
579
|
-
plugins: [
|
|
580
|
-
seofields({healthDashboard: false}), // optional: hide the top-level tool tab
|
|
581
|
-
structureTool({
|
|
582
|
-
structure: (S) =>
|
|
583
|
-
S.list()
|
|
584
|
-
.title('Content')
|
|
585
|
-
.items([
|
|
586
|
-
S.documentTypeListItem('post').title('Posts'),
|
|
587
|
-
S.divider(),
|
|
588
|
-
S.listItem()
|
|
589
|
-
.title('SEO Health')
|
|
590
|
-
.child(
|
|
591
|
-
createSeoHealthPane(S, {
|
|
592
|
-
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
|
|
593
|
-
query: `*[_type == "post" && defined(seo)]{
|
|
594
|
-
_id, _type, title, slug, seo, _updatedAt
|
|
595
|
-
}`,
|
|
596
|
-
title: 'Posts SEO Health',
|
|
597
|
-
}),
|
|
598
|
-
),
|
|
599
|
-
]),
|
|
600
|
-
}),
|
|
601
|
-
],
|
|
122
|
+
plugins: [seofields(), schemaOrg()], // all 24 types at once
|
|
602
123
|
})
|
|
603
124
|
```
|
|
604
125
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
| Option | Type | Default | Description |
|
|
608
|
-
| ------------ | --------- | -------------- | ---------------------------------------------------------------------- |
|
|
609
|
-
| `licenseKey` | `string` | **required** | License key (format `SEOF-XXXX-XXXX-XXXX`). |
|
|
610
|
-
| `query` | `string` | — | GROQ query. Must return `_id`, `_type`, `title`, `seo`, `_updatedAt`. |
|
|
611
|
-
| `title` | `string` | `'SEO Health'` | Pane title shown in breadcrumb |
|
|
612
|
-
| `openInPane` | `boolean` | `true` | Enable row links that open the document editor as a pane to the right. |
|
|
613
|
-
| `...rest` | — | — | All other `SeoHealthDashboardProps` |
|
|
614
|
-
|
|
615
|
-
## 🌐 Frontend Integration
|
|
616
|
-
|
|
617
|
-
### Next.js Example
|
|
618
|
-
|
|
619
|
-
```tsx
|
|
620
|
-
import Head from 'next/head'
|
|
621
|
-
|
|
622
|
-
export function SEOHead({seo}) {
|
|
623
|
-
if (!seo) return null
|
|
624
|
-
|
|
625
|
-
return (
|
|
626
|
-
<Head>
|
|
627
|
-
{seo.title && <title>{seo.title}</title>}
|
|
628
|
-
{seo.description && <meta name="description" content={seo.description} />}
|
|
629
|
-
|
|
630
|
-
{/* Open Graph */}
|
|
631
|
-
{seo.openGraph?.title && <meta property="og:title" content={seo.openGraph.title} />}
|
|
632
|
-
{seo.openGraph?.description && (
|
|
633
|
-
<meta property="og:description" content={seo.openGraph.description} />
|
|
634
|
-
)}
|
|
635
|
-
{seo.openGraph?.url && <meta property="og:url" content={seo.openGraph.url} />}
|
|
636
|
-
|
|
637
|
-
{/* Twitter */}
|
|
638
|
-
{seo.twitter?.card && <meta name="twitter:card" content={seo.twitter.card} />}
|
|
639
|
-
{seo.twitter?.site && <meta name="twitter:site" content={seo.twitter.site} />}
|
|
640
|
-
{seo.twitter?.creator && <meta name="twitter:creator" content={seo.twitter.creator} />}
|
|
641
|
-
|
|
642
|
-
{/* Robots */}
|
|
643
|
-
{seo.robots?.noIndex && <meta name="robots" content="noindex" />}
|
|
644
|
-
{seo.robots?.noFollow && <meta name="robots" content="nofollow" />}
|
|
645
|
-
|
|
646
|
-
{/* Canonical URL */}
|
|
647
|
-
{seo.canonicalUrl && <link rel="canonical" href={seo.canonicalUrl} />}
|
|
648
|
-
</Head>
|
|
649
|
-
)
|
|
650
|
-
}
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
### React/Gatsby Example
|
|
654
|
-
|
|
655
|
-
```tsx
|
|
656
|
-
import {Helmet} from 'react-helmet'
|
|
657
|
-
|
|
658
|
-
export function SEO({seo}) {
|
|
659
|
-
return (
|
|
660
|
-
<Helmet>
|
|
661
|
-
<title>{seo?.title}</title>
|
|
662
|
-
<meta name="description" content={seo?.description} />
|
|
663
|
-
|
|
664
|
-
{/* Keywords */}
|
|
665
|
-
{seo?.keywords && <meta name="keywords" content={seo.keywords.join(', ')} />}
|
|
666
|
-
|
|
667
|
-
{/* Open Graph */}
|
|
668
|
-
<meta property="og:title" content={seo?.openGraph?.title} />
|
|
669
|
-
<meta property="og:description" content={seo?.openGraph?.description} />
|
|
670
|
-
<meta property="og:url" content={seo?.openGraph?.url} />
|
|
671
|
-
<meta property="og:type" content={seo?.openGraph?.type || 'website'} />
|
|
672
|
-
|
|
673
|
-
{/* Twitter */}
|
|
674
|
-
{seo?.twitter?.card && <meta name="twitter:card" content={seo.twitter.card} />}
|
|
675
|
-
{seo?.twitter?.site && <meta name="twitter:site" content={seo.twitter.site} />}
|
|
676
|
-
{seo?.twitter?.creator && <meta name="twitter:creator" content={seo.twitter.creator} />}
|
|
677
|
-
</Helmet>
|
|
678
|
-
)
|
|
679
|
-
}
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
## 🎯 Framework Integration Examples
|
|
683
|
-
|
|
684
|
-
### Remix (Loader + Action Approach)
|
|
685
|
-
|
|
686
|
-
Handle SEO metadata in Remix loaders for server-side rendering with JSON responses:
|
|
687
|
-
|
|
688
|
-
```typescript
|
|
689
|
-
// routes/posts.$slug.tsx
|
|
690
|
-
import {json, type LoaderFunction} from '@remix-run/node'
|
|
691
|
-
import {useLoaderData} from '@remix-run/react'
|
|
692
|
-
import {buildSeoMeta} from 'sanity-plugin-seofields/utils'
|
|
693
|
-
|
|
694
|
-
export const loader: LoaderFunction = async ({params}) => {
|
|
695
|
-
// Fetch post with SEO fields from Sanity
|
|
696
|
-
const post = await sanityClient.fetch(
|
|
697
|
-
`*[_type == "post" && slug.current == $slug][0]{
|
|
698
|
-
title, content, seo, slug
|
|
699
|
-
}`,
|
|
700
|
-
{slug: params.slug},
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
// Use buildSeoMeta to generate meta tags
|
|
704
|
-
const seoMeta = buildSeoMeta(post.seo, {
|
|
705
|
-
defaultTitle: 'Blog',
|
|
706
|
-
siteUrl: 'https://example.com',
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
return json({post, seoMeta})
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
713
|
-
return data?.seoMeta || []
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
export default function PostRoute() {
|
|
717
|
-
const {post} = useLoaderData<typeof loader>()
|
|
718
|
-
return <article>{post.title}</article>
|
|
719
|
-
}
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
### Nuxt 3 (Composable Approach)
|
|
723
|
-
|
|
724
|
-
Create a composable for SSR-friendly SEO management:
|
|
126
|
+
Or register only what you need:
|
|
725
127
|
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
import {buildSeoMeta} from 'sanity-plugin-seofields/utils'
|
|
128
|
+
```ts
|
|
129
|
+
import {schemaOrgArticlePlugin, schemaOrgOrganizationPlugin} from 'sanity-plugin-seofields/schema'
|
|
729
130
|
|
|
730
|
-
export
|
|
731
|
-
|
|
732
|
-
defaultTitle = 'My Site',
|
|
733
|
-
siteUrl = 'https://example.com',
|
|
734
|
-
} = options
|
|
735
|
-
|
|
736
|
-
const meta = buildSeoMeta(seo, {defaultTitle, siteUrl})
|
|
737
|
-
|
|
738
|
-
// useHead() handles SSR + client-side rendering
|
|
739
|
-
useHead({
|
|
740
|
-
title: seo?.title || defaultTitle,
|
|
741
|
-
meta: meta.map(m => ({
|
|
742
|
-
name: m.name || m.property,
|
|
743
|
-
content: m.content,
|
|
744
|
-
})),
|
|
745
|
-
link: seo?.canonicalUrl
|
|
746
|
-
? [{rel: 'canonical', href: seo.canonicalUrl}]
|
|
747
|
-
: [],
|
|
748
|
-
})
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// pages/blog/[slug].vue
|
|
752
|
-
<script setup lang="ts">
|
|
753
|
-
const route = useRoute()
|
|
754
|
-
const {data: post} = await useFetch(`/api/posts/${route.params.slug}`)
|
|
755
|
-
|
|
756
|
-
useSanityMeta(post.value?.seo, {
|
|
757
|
-
siteUrl: 'https://example.com',
|
|
131
|
+
export default defineConfig({
|
|
132
|
+
plugins: [seofields(), schemaOrgArticlePlugin(), schemaOrgOrganizationPlugin()],
|
|
758
133
|
})
|
|
759
|
-
</script>
|
|
760
|
-
|
|
761
|
-
<template>
|
|
762
|
-
<article v-if="post">
|
|
763
|
-
<h1>{{ post.title }}</h1>
|
|
764
|
-
</article>
|
|
765
|
-
</template>
|
|
766
134
|
```
|
|
767
135
|
|
|
768
|
-
###
|
|
769
|
-
|
|
770
|
-
Leverage Astro's component-level SEO with static generation:
|
|
136
|
+
### 2. Add to a document schema
|
|
771
137
|
|
|
772
|
-
```
|
|
773
|
-
//
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
import Layout from '../../layouts/Layout.astro'
|
|
777
|
-
|
|
778
|
-
// Fetch from Sanity at build time
|
|
779
|
-
const {slug} = Astro.params
|
|
780
|
-
const post = await sanityClient.fetch(
|
|
781
|
-
`*[_type == "post" && slug.current == $slug][0]{
|
|
782
|
-
title, content, seo, slug
|
|
783
|
-
}`,
|
|
784
|
-
{slug},
|
|
785
|
-
)
|
|
786
|
-
|
|
787
|
-
// Generate meta tags for static HTML
|
|
788
|
-
const seoMeta = buildSeoMeta(post.seo, {
|
|
789
|
-
defaultTitle: 'Blog',
|
|
790
|
-
siteUrl: Astro.site,
|
|
791
|
-
})
|
|
792
|
-
---
|
|
793
|
-
|
|
794
|
-
<Layout
|
|
795
|
-
title={post.seo?.title}
|
|
796
|
-
meta={seoMeta}
|
|
797
|
-
canonicalUrl={post.seo?.canonicalUrl}
|
|
798
|
-
>
|
|
799
|
-
<article>
|
|
800
|
-
<h1>{post.title}</h1>
|
|
801
|
-
</article>
|
|
802
|
-
</Layout>
|
|
803
|
-
|
|
804
|
-
<!-- Astro layouts handle meta tag rendering -->
|
|
138
|
+
```ts
|
|
139
|
+
defineField({name: 'schemaOrg', type: 'schemaOrg'}) // combined array field
|
|
140
|
+
// or individual types:
|
|
141
|
+
defineField({name: 'article', type: 'schemaOrgArticle'})
|
|
805
142
|
```
|
|
806
143
|
|
|
807
|
-
###
|
|
808
|
-
|
|
809
|
-
For client-rendered React apps without SSR:
|
|
144
|
+
### 3. Render in Next.js
|
|
810
145
|
|
|
811
|
-
```
|
|
812
|
-
//
|
|
813
|
-
import {
|
|
814
|
-
import type {SEOFields} from 'sanity-plugin-seofields'
|
|
146
|
+
```tsx
|
|
147
|
+
// Combined renderer
|
|
148
|
+
import {SchemaOrgScripts} from 'sanity-plugin-seofields/schema/next'
|
|
815
149
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
fallbackTitle: string
|
|
150
|
+
export default function Layout({data}) {
|
|
151
|
+
return <SchemaOrgScripts items={data.schemaOrg} />
|
|
819
152
|
}
|
|
820
153
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
<Helmet>
|
|
824
|
-
{/* Basic Meta */}
|
|
825
|
-
<title>{seo?.title || fallbackTitle}</title>
|
|
826
|
-
<meta name="description" content={seo?.description || ''} />
|
|
154
|
+
// Or individual components
|
|
155
|
+
import {ArticleSchema, OrganizationSchema} from 'sanity-plugin-seofields/schema/next'
|
|
827
156
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
{
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
{/* Robots */}
|
|
836
|
-
{seo?.robots?.noIndex && <meta name="robots" content="noindex" />}
|
|
837
|
-
|
|
838
|
-
{/* Canonical (limit crawl budget) */}
|
|
839
|
-
{seo?.canonicalUrl && (
|
|
840
|
-
<link rel="canonical" href={seo.canonicalUrl} />
|
|
841
|
-
)}
|
|
842
|
-
</Helmet>
|
|
157
|
+
export default function Page({data}) {
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<ArticleSchema data={data.article} />
|
|
161
|
+
<OrganizationSchema data={data.org} />
|
|
162
|
+
</>
|
|
843
163
|
)
|
|
844
164
|
}
|
|
845
|
-
|
|
846
|
-
// Usage in page component
|
|
847
|
-
// Note: Client-side rendering cannot inject meta tags pre-page-load.
|
|
848
|
-
// For public pages, use SSR or static generation instead.
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
---
|
|
852
|
-
|
|
853
|
-
## 🚀 Migrating from Other SEO Plugins
|
|
854
|
-
|
|
855
|
-
Coming from **Yoast**, **All in One SEO**, or **RankMath**?
|
|
856
|
-
|
|
857
|
-
| Feature | Yoast | All in One SEO | RankMath | sanity-plugin-seofields |
|
|
858
|
-
| -------------------------- | ---------- | -------------- | -------- | ------------------------ |
|
|
859
|
-
| **Meta Title/Description** | ✅ | ✅ | ✅ | ✅ |
|
|
860
|
-
| **Open Graph Tags** | ✅ | ✅ | ✅ | ✅ |
|
|
861
|
-
| **Twitter Cards** | ⚠️ Limited | ✅ | ✅ | ✅ |
|
|
862
|
-
| **Readability Analysis** | ✅ | ✅ | ✅ | ❌ (Sanity-native focus) |
|
|
863
|
-
| **Keyword Density** | ✅ | ✅ | ✅ | ❌ (External tools) |
|
|
864
|
-
| **Custom Meta Attributes** | ⚠️ Limited | ✅ | ✅ | ✅ |
|
|
865
|
-
| **Robots/Canonical** | ✅ | ✅ | ✅ | ✅ |
|
|
866
|
-
| **Headless-First** | ❌ | ❌ | ❌ | ✅ Framework-agnostic |
|
|
867
|
-
| **SSR-Ready** | N/A | N/A | N/A | ✅ All frameworks |
|
|
868
|
-
|
|
869
|
-
### Migration Path
|
|
870
|
-
|
|
871
|
-
1. **Export existing metadata** from your old plugin (title, description, OG tags)
|
|
872
|
-
2. **Create a Sanity schema** matching your current fields — map to `seoFields` type
|
|
873
|
-
3. **Bulk import** using Sanity's API or migration scripts
|
|
874
|
-
4. **Update your frontend** to use `buildSeoMeta` utilities instead of plugin hooks
|
|
875
|
-
5. **Test meta rendering** in browsers DevTools and social preview tools
|
|
876
|
-
|
|
877
|
-
For detailed migration guides, see [Migration Guides](#) in our documentation.
|
|
878
|
-
|
|
879
|
-
---
|
|
880
|
-
|
|
881
|
-
## 📚 API Reference
|
|
882
|
-
|
|
883
|
-
### Main Export
|
|
884
|
-
|
|
885
|
-
```typescript
|
|
886
|
-
import seofields from 'sanity-plugin-seofields'
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
### Schema Types
|
|
890
|
-
|
|
891
|
-
- `seoFields` - Complete SEO fields object
|
|
892
|
-
- `openGraph` - Open Graph meta tags
|
|
893
|
-
- `twitter` - Twitter Card settings
|
|
894
|
-
- `metaTag` - Custom meta tag collection
|
|
895
|
-
- `metaAttribute` - Individual meta attribute
|
|
896
|
-
- `robots` - Search engine robots settings
|
|
897
|
-
|
|
898
|
-
## 🔧 Troubleshooting
|
|
899
|
-
|
|
900
|
-
### TypeScript auto-import not working
|
|
901
|
-
|
|
902
|
-
**Problem:** `buildSeoMeta` doesn't appear in IDE autocomplete
|
|
903
|
-
|
|
904
|
-
**Solution:**
|
|
905
|
-
|
|
906
|
-
1. Check your `package.json` exports field has a `"types"` condition:
|
|
907
|
-
|
|
908
|
-
```json
|
|
909
|
-
{
|
|
910
|
-
"exports": {
|
|
911
|
-
".": {
|
|
912
|
-
"types": "./dist/index.d.ts",
|
|
913
|
-
"default": "./dist/index.js"
|
|
914
|
-
},
|
|
915
|
-
"./next": {
|
|
916
|
-
"types": "./dist/next.d.ts",
|
|
917
|
-
"default": "./dist/next.js"
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
```
|
|
922
|
-
|
|
923
|
-
2. Verify your `tsconfig.json` has the correct `moduleResolution`:
|
|
924
|
-
|
|
925
|
-
```json
|
|
926
|
-
{
|
|
927
|
-
"compilerOptions": {
|
|
928
|
-
"moduleResolution": "bundler",
|
|
929
|
-
"resolveJsonModule": true
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
---
|
|
935
|
-
|
|
936
|
-
### "Cannot find module 'sanity-plugin-seofields/next'"
|
|
937
|
-
|
|
938
|
-
**Problem:** Runtime import error when trying to use Next.js utilities
|
|
939
|
-
|
|
940
|
-
**Solution:**
|
|
941
|
-
|
|
942
|
-
1. Ensure built files exist in `dist/next.js`:
|
|
943
|
-
|
|
944
|
-
```bash
|
|
945
|
-
npm run build
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
2. Clear and reinstall node_modules:
|
|
949
|
-
|
|
950
|
-
```bash
|
|
951
|
-
rm -rf node_modules package-lock.json
|
|
952
|
-
npm install
|
|
953
|
-
```
|
|
954
|
-
|
|
955
|
-
3. Verify `package.json` exports includes the next export:
|
|
956
|
-
|
|
957
|
-
```json
|
|
958
|
-
{
|
|
959
|
-
"exports": {
|
|
960
|
-
"./next": {
|
|
961
|
-
"types": "./dist/next.d.ts",
|
|
962
|
-
"default": "./dist/next.js"
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
165
|
```
|
|
967
166
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
### Type inference in generateMetadata()
|
|
167
|
+
**Available types:** `Article`, `BlogPosting`, `BreadcrumbList`, `Course`, `Event`, `FAQPage`, `HowTo`, `ImageObject`, `LocalBusiness`, `Offer`, `Organization`, `Person`, `Place`, `Product`, `Review`, `SoftwareApplication`, `VideoObject`, `WebApplication`, `WebPage`, `Website`, and more.
|
|
971
168
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
**Solution:** Explicitly type the return value:
|
|
975
|
-
|
|
976
|
-
```tsx
|
|
977
|
-
import type {Metadata} from 'next'
|
|
978
|
-
import {buildSeoMeta} from 'sanity-plugin-seofields/next'
|
|
979
|
-
|
|
980
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
981
|
-
const seoData = await fetchSeoData()
|
|
982
|
-
const metadata = buildSeoMeta(seoData)
|
|
983
|
-
|
|
984
|
-
return {
|
|
985
|
-
title: metadata.title,
|
|
986
|
-
description: metadata.description,
|
|
987
|
-
openGraph: {
|
|
988
|
-
title: metadata.openGraph?.title,
|
|
989
|
-
description: metadata.openGraph?.description,
|
|
990
|
-
url: metadata.openGraph?.url,
|
|
991
|
-
},
|
|
992
|
-
twitter: {
|
|
993
|
-
card: metadata.twitter?.card as any,
|
|
994
|
-
site: metadata.twitter?.site,
|
|
995
|
-
creator: metadata.twitter?.creator,
|
|
996
|
-
},
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
```
|
|
169
|
+
→ [Schema.org docs](https://sanity-plugin-seofields.thehardik.in/docs/schema-org)
|
|
1000
170
|
|
|
1001
171
|
---
|
|
1002
172
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
**Problem:** SEO Health tool doesn't appear in the studio
|
|
1006
|
-
|
|
1007
|
-
**Solution:**
|
|
1008
|
-
|
|
1009
|
-
1. Ensure the plugin is added to `sanity.config.ts`:
|
|
173
|
+
## Next.js Integration
|
|
1010
174
|
|
|
1011
|
-
```
|
|
1012
|
-
import
|
|
1013
|
-
|
|
1014
|
-
export default defineConfig({
|
|
1015
|
-
// ... other config
|
|
1016
|
-
plugins: [
|
|
1017
|
-
seofields({
|
|
1018
|
-
documentTypes: ['post', 'page', 'product'],
|
|
1019
|
-
// other options
|
|
1020
|
-
}),
|
|
1021
|
-
],
|
|
1022
|
-
})
|
|
175
|
+
```ts
|
|
176
|
+
import {buildSeoMeta, SeoMetaTags} from 'sanity-plugin-seofields/next'
|
|
1023
177
|
```
|
|
1024
178
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
```typescript
|
|
1028
|
-
seofields({
|
|
1029
|
-
documentTypes: ['post', 'page'], // Add your document types here
|
|
1030
|
-
})
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
3. Verify plugin config fieldVisibility is not hiding SEO fields:
|
|
1034
|
-
|
|
1035
|
-
```typescript
|
|
1036
|
-
seofields({
|
|
1037
|
-
documentTypes: ['post'],
|
|
1038
|
-
fieldVisibility: {
|
|
1039
|
-
// Make sure SEO fields aren't set to hidden
|
|
1040
|
-
},
|
|
1041
|
-
})
|
|
1042
|
-
```
|
|
179
|
+
→ [Next.js integration guide](https://sanity-plugin-seofields.thehardik.in/docs/nextjs)
|
|
1043
180
|
|
|
1044
181
|
---
|
|
1045
182
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
**Problem:** OG/Twitter images show as `undefined` in meta tags
|
|
1049
|
-
|
|
1050
|
-
**Solution:** Provide an `imageUrlResolver` function:
|
|
1051
|
-
|
|
1052
|
-
```tsx
|
|
1053
|
-
import imageUrlBuilder from '@sanity/image-url'
|
|
1054
|
-
import {client} from './sanity.client'
|
|
1055
|
-
|
|
1056
|
-
const imageBuilder = imageUrlBuilder(client)
|
|
1057
|
-
|
|
1058
|
-
export function buildImageUrl(source) {
|
|
1059
|
-
if (!source) return undefined
|
|
1060
|
-
return imageBuilder.image(source).url()
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// In your buildSeoMeta call:
|
|
1064
|
-
const metadata = buildSeoMeta({
|
|
1065
|
-
...seoData,
|
|
1066
|
-
imageUrlResolver: buildImageUrl,
|
|
1067
|
-
})
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
Or use it in your Next.js layout:
|
|
1071
|
-
|
|
1072
|
-
```tsx
|
|
1073
|
-
import {buildSeoMeta} from 'sanity-plugin-seofields/next'
|
|
1074
|
-
import imageUrlBuilder from '@sanity/image-url'
|
|
1075
|
-
|
|
1076
|
-
const imageBuilder = imageUrlBuilder(client)
|
|
1077
|
-
|
|
1078
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
1079
|
-
const seoData = await sanityFetch(SeoQuery)
|
|
1080
|
-
|
|
1081
|
-
const metadata = buildSeoMeta({
|
|
1082
|
-
...seoData,
|
|
1083
|
-
imageUrlResolver: (image) => imageBuilder.image(image).url(),
|
|
1084
|
-
})
|
|
1085
|
-
|
|
1086
|
-
return metadata
|
|
1087
|
-
}
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
---
|
|
1091
|
-
|
|
1092
|
-
### generateMetadata() not finding Sanity data
|
|
1093
|
-
|
|
1094
|
-
**Problem:** Data is `undefined` when trying to fetch from Sanity in Next.js
|
|
1095
|
-
|
|
1096
|
-
**Solution:**
|
|
1097
|
-
|
|
1098
|
-
1. Ensure `sanityFetch` is properly awaited:
|
|
1099
|
-
|
|
1100
|
-
```tsx
|
|
1101
|
-
import {sanityFetch} from '@/lib/sanity.client'
|
|
1102
|
-
|
|
1103
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
1104
|
-
try {
|
|
1105
|
-
const seoData = await sanityFetch(SeoQuery) // Don't forget await!
|
|
1106
|
-
return buildSeoMeta(seoData)
|
|
1107
|
-
} catch (error) {
|
|
1108
|
-
console.error('Failed to fetch SEO data:', error)
|
|
1109
|
-
return {title: 'Default Title'}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
2. Verify environment variables are set:
|
|
183
|
+
## CLI
|
|
1115
184
|
|
|
1116
185
|
```bash
|
|
1117
|
-
|
|
1118
|
-
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id
|
|
1119
|
-
NEXT_PUBLIC_SANITY_DATASET=production
|
|
1120
|
-
SANITY_API_TOKEN=your_token (if using authenticated fetches)
|
|
186
|
+
npx seofields
|
|
1121
187
|
```
|
|
1122
188
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
```tsx
|
|
1126
|
-
import type {Metadata} from 'next'
|
|
1127
|
-
import {buildSeoMeta} from 'sanity-plugin-seofields/next'
|
|
1128
|
-
import {sanityFetch} from '@/lib/sanity.client'
|
|
1129
|
-
|
|
1130
|
-
const SeoQuery = `*[_type == "post" && slug.current == $slug][0] {
|
|
1131
|
-
title,
|
|
1132
|
-
seo {
|
|
1133
|
-
title,
|
|
1134
|
-
description,
|
|
1135
|
-
openGraph {
|
|
1136
|
-
title,
|
|
1137
|
-
description,
|
|
1138
|
-
image,
|
|
1139
|
-
},
|
|
1140
|
-
twitter {
|
|
1141
|
-
card,
|
|
1142
|
-
site,
|
|
1143
|
-
creator,
|
|
1144
|
-
},
|
|
1145
|
-
},
|
|
1146
|
-
}`
|
|
1147
|
-
|
|
1148
|
-
export async function generateMetadata({params}: {params: {slug: string}}): Promise<Metadata> {
|
|
1149
|
-
try {
|
|
1150
|
-
const doc = await sanityFetch(SeoQuery, {slug: params.slug})
|
|
1151
|
-
|
|
1152
|
-
if (!doc) {
|
|
1153
|
-
return {title: 'Post not found'}
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
return buildSeoMeta(doc.seo || {})
|
|
1157
|
-
} catch (error) {
|
|
1158
|
-
console.error('SEO metadata error:', error)
|
|
1159
|
-
return {title: 'Error loading page'}
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
```
|
|
189
|
+
→ [CLI docs](https://sanity-plugin-seofields.thehardik.in/docs/cli)
|
|
1163
190
|
|
|
1164
191
|
---
|
|
1165
192
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
- 📖 [Full Documentation](./TYPES_SCHEMA_DOCS.md)
|
|
1169
|
-
- 🐛 [GitHub Issues](https://github.com/hardik-143/sanity-plugin-seofields/issues)
|
|
1170
|
-
- 📧 [Email Support](mailto:dhardik1430@gmail.com)
|
|
193
|
+
## Links
|
|
1171
194
|
|
|
1172
|
-
|
|
195
|
+
- 📖 [Documentation](https://sanity-plugin-seofields.thehardik.in/docs)
|
|
196
|
+
- 🐛 [Issues](https://github.com/hardik-143/sanity-plugin-seofields/issues)
|
|
197
|
+
- 📦 [npm](https://www.npmjs.com/package/sanity-plugin-seofields)
|
|
198
|
+
- 📝 [Changelog](./CHANGELOG.md)
|
|
1173
199
|
|
|
1174
|
-
|
|
200
|
+
## Contributing
|
|
1175
201
|
|
|
1176
|
-
|
|
1177
|
-
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
1178
|
-
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
1179
|
-
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
1180
|
-
5. Open a Pull Request
|
|
202
|
+
PRs and issues are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
1181
203
|
|
|
1182
|
-
##
|
|
1183
|
-
|
|
1184
|
-
[MIT](LICENSE) © [Desai Hardik](https://github.com/hardik-143)
|
|
1185
|
-
|
|
1186
|
-
## 🆘 Support
|
|
1187
|
-
|
|
1188
|
-
- 📧 Email: dhardik1430@gmail.com
|
|
1189
|
-
- 🐛 Issues: [GitHub Issues](https://github.com/hardik-143/sanity-plugin-seofields/issues)
|
|
1190
|
-
- 📖 Documentation: [Types & Schema Docs](./TYPES_SCHEMA_DOCS.md)
|
|
1191
|
-
|
|
1192
|
-
---
|
|
204
|
+
## License
|
|
1193
205
|
|
|
1194
|
-
|
|
206
|
+
[MIT](./LICENSE)
|