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.
Files changed (32) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +97 -1085
  3. package/dist/SeoHealthDashboard-KPBNXSL4.cjs +10 -0
  4. package/dist/{SeoHealthDashboard-L4D4F3LO.cjs.map → SeoHealthDashboard-KPBNXSL4.cjs.map} +1 -1
  5. package/dist/SeoHealthDashboard-QKVB5HK3.js +4 -0
  6. package/dist/{SeoHealthDashboard-6RBRIYBX.js.map → SeoHealthDashboard-QKVB5HK3.js.map} +1 -1
  7. package/dist/{SeoHealthTool-FXNQPNPG.js → SeoHealthTool-EPPOEDTW.js} +3 -3
  8. package/dist/{SeoHealthTool-FXNQPNPG.js.map → SeoHealthTool-EPPOEDTW.js.map} +1 -1
  9. package/dist/{SeoHealthTool-FM2HXDPU.cjs → SeoHealthTool-ON3SRXCF.cjs} +4 -4
  10. package/dist/{SeoHealthTool-FM2HXDPU.cjs.map → SeoHealthTool-ON3SRXCF.cjs.map} +1 -1
  11. package/dist/{chunk-Z63UP35O.cjs → chunk-G2SVI2SP.cjs} +595 -306
  12. package/dist/chunk-G2SVI2SP.cjs.map +1 -0
  13. package/dist/{chunk-CNBJAXVH.js → chunk-UCVSMPEJ.js} +596 -307
  14. package/dist/chunk-UCVSMPEJ.js.map +1 -0
  15. package/dist/cli.js +1 -1
  16. package/dist/index.cjs +19 -3
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +30 -0
  19. package/dist/index.d.ts +30 -0
  20. package/dist/index.js +19 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/next.cjs +53 -33
  23. package/dist/next.cjs.map +1 -1
  24. package/dist/next.d.cts +39 -3
  25. package/dist/next.d.ts +39 -3
  26. package/dist/next.js +53 -33
  27. package/dist/next.js.map +1 -1
  28. package/package.json +18 -1
  29. package/dist/SeoHealthDashboard-6RBRIYBX.js +0 -4
  30. package/dist/SeoHealthDashboard-L4D4F3LO.cjs +0 -10
  31. package/dist/chunk-CNBJAXVH.js.map +0 -1
  32. package/dist/chunk-Z63UP35O.cjs.map +0 -1
package/README.md CHANGED
@@ -3,1192 +3,204 @@
3
3
  [![npm version](https://img.shields.io/npm/v/sanity-plugin-seofields.svg?color=brightgreen&label=npm)](https://www.npmjs.com/package/sanity-plugin-seofields)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/sanity-plugin-seofields.svg?color=blue)](https://www.npmjs.com/package/sanity-plugin-seofields)
5
5
  [![license](https://img.shields.io/npm/l/sanity-plugin-seofields.svg?color=yellow)](./LICENSE)
6
- [![GitHub issues](https://img.shields.io/github/issues/hardik-143/sanity-plugin-seofields?color=orange)](https://github.com/hardik-143/sanity-plugin-seofields/issues)
7
6
  [![GitHub stars](https://img.shields.io/github/stars/hardik-143/sanity-plugin-seofields?style=social)](https://github.com/hardik-143/sanity-plugin-seofields)
8
7
 
9
- A comprehensive Sanity Studio (v3/v4/v5) plugin to manage SEO fields like meta titles, descriptions, Open Graph tags, and X (Formerly Twitter) Cards for structured, search-optimized content.
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
- ## Features
10
+ 📖 **[Full Documentation →](https://sanity-plugin-seofields.thehardik.in/docs)**
12
11
 
13
- - 🎯 **Meta Tags**: Title, description, keywords, and canonical URLs
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
- ## 📦 Installation
14
+ ## Installation
27
15
 
28
16
  ```bash
29
17
  npm install sanity-plugin-seofields
30
18
  ```
31
19
 
32
- or
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
- Add the plugin to your `sanity.config.ts` (or `.js`) file:
22
+ ### 1. Register the plugin
43
23
 
44
- ```typescript
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
- name: 'your-project',
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 Fields to Your Documents
34
+ ### 2. Add SEO fields to a document
68
35
 
69
- Add the SEO fields to any document type in your schema:
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
- // ... other fields
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
- ## 🎨 Available Schema Types
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
- ### Configuration Options
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
- **Available field keys:**
53
+ ## Available Schema Types
234
54
 
235
- - `title`, `description`, `canonicalUrl`, `metaImage`, `keywords`, `metaAttributes`, `robots`
236
- - `openGraphUrl`, `openGraphTitle`, `openGraphDescription`, `openGraphSiteName`, `openGraphType`, `openGraphImage`
237
- - `twitterCard`, `twitterSite`, `twitterCreator`, `twitterTitle`, `twitterDescription`, `twitterImage`
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
- > ℹ️ Hiding `openGraphImage` or `twitterImage` also hides their URL and type variants to keep the editor experience consistent.
64
+ ---
240
65
 
241
- **Example configurations:**
66
+ ## Configuration
242
67
 
243
- ```typescript
244
- // Hide fields globally
68
+ ```ts
245
69
  seofields({
246
- defaultHiddenFields: ['openGraphSiteName', 'twitterSite', 'keywords'],
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
- page: {
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
- ### Site Settings Integration
387
-
388
- Create a Site Settings document to manage sitewide fields:
389
-
390
- ```typescript
391
- // schemas/siteSettings.ts
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
- ```typescript
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
- ## 📊 SEO Health Dashboard
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
- The SEO Health Dashboard requires a valid license key to use. **Good news**: it's completely free during the current period (2–3 months). When we transition to a paid model, your existing key will remain valid for a one-time $10 fee.
92
+ ## SEO Health Dashboard
453
93
 
454
- [Get your free license key →](https://sanity-plugin-seofields.thehardik.in/get-license)
94
+ An optional Studio tool that scores SEO completeness across all documents, highlights missing fields, and links directly to documents.
455
95
 
456
- ### Configuration
96
+ Requires a free license key — [get yours here](https://sanity-plugin-seofields.thehardik.in/get-license).
457
97
 
458
- ```typescript
459
- // Minimal — just add your license key
98
+ ```ts
460
99
  seofields({
461
- healthDashboard: {
462
- licenseKey: 'YOUR_LICENSE_KEY',
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
- ### Deprecated keys (v1.3.2)
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
- ### What it shows
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
- ### Usage
111
+ ## Schema.org / JSON-LD
567
112
 
568
- `createSeoHealthPane(S, options)` requires both arguments: Sanity's structure builder `S` and an options object with a required `licenseKey`. It returns a **`ComponentBuilder`** — use it **directly** as the `.child()` value.
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
- > ⚠️ **Do NOT wrap in `S.component()`.** The function already calls `S.component()` internally. Wrapping it again causes: _"component is required for component structure item"_.
115
+ ### 1. Register schema types in Studio
571
116
 
572
- ```typescript
117
+ ```ts
573
118
  // sanity.config.ts
574
- import {defineConfig} from 'sanity'
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
- ### `createSeoHealthPane` options
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
- ```typescript
727
- // composables/useSanityMeta.ts
728
- import {buildSeoMeta} from 'sanity-plugin-seofields/utils'
128
+ ```ts
129
+ import {schemaOrgArticlePlugin, schemaOrgOrganizationPlugin} from 'sanity-plugin-seofields/schema'
729
130
 
730
- export const useSanityMeta = (seo: SEOFields, options = {}) => {
731
- const {
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
- ### Astro (Server-Side Rendering)
769
-
770
- Leverage Astro's component-level SEO with static generation:
136
+ ### 2. Add to a document schema
771
137
 
772
- ```typescript
773
- // src/pages/blog/[slug].astro
774
- ---
775
- import {buildSeoMeta} from 'sanity-plugin-seofields/utils'
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
- ### React SPA (Client-Side with Helmet)
808
-
809
- For client-rendered React apps without SSR:
144
+ ### 3. Render in Next.js
810
145
 
811
- ```typescript
812
- // components/PostHead.tsx
813
- import {Helmet} from 'react-helmet-async'
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
- interface PostHeadProps {
817
- seo?: SEOFields
818
- fallbackTitle: string
150
+ export default function Layout({data}) {
151
+ return <SchemaOrgScripts items={data.schemaOrg} />
819
152
  }
820
153
 
821
- export function PostHead({seo, fallbackTitle}: PostHeadProps) {
822
- return (
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
- {/* Open Graph - critical for social shares */}
829
- <meta property="og:title" content={seo?.openGraph?.title} />
830
- <meta property="og:description" content={seo?.openGraph?.description} />
831
- {seo?.openGraph?.image?.url && (
832
- <meta property="og:image" content={seo.openGraph.image.url} />
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
- **Problem:** `buildSeoMeta()` return type is not recognized as Next.js `Metadata`
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
- ### Dashboard not showing in Sanity Studio
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
- ```typescript
1012
- import seofields from 'sanity-plugin-seofields'
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
- 2. Check that `documentTypes` array includes your document types:
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
- ### Image URLs not resolving
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
- # .env.local
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
- 3. Complete example with proper error handling:
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
- **Still stuck?** Check our:
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
- ## 🤝 Contributing
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
- Contributions are welcome! Please feel free to submit a Pull Request.
200
+ ## Contributing
1175
201
 
1176
- 1. Fork the repository
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
- ## 📄 License
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
- Made with ❤️ for the Sanity community
206
+ [MIT](./LICENSE)