sanity-plugin-seofields 1.0.10 β†’ 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@ A comprehensive Sanity Studio v3 plugin to manage SEO fields like meta titles, d
20
20
  - πŸ“Š **Custom Attributes**: Flexible meta attribute system
21
21
  - βœ… **Validation**: Built-in character limits and best practices
22
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
23
24
 
24
25
  ## πŸ“¦ Installation
25
26
 
@@ -441,6 +442,95 @@ defineField({
441
442
  })
442
443
  ```
443
444
 
445
+ ## πŸ“Š SEO Health Dashboard
446
+
447
+ 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.
448
+
449
+ ### πŸ”‘ License Key Required
450
+
451
+ 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.
452
+
453
+ [Get your free license key β†’](https://sanity-plugin-seofields.thehardik.in/get-license)
454
+
455
+ ### Configuration
456
+
457
+ ```typescript
458
+ // Minimal β€” just add your license key
459
+ seofields({
460
+ healthDashboard: {
461
+ licenseKey: 'YOUR_LICENSE_KEY',
462
+ },
463
+ })
464
+
465
+ // Full options β€” all nested under healthDashboard
466
+ seofields({
467
+ healthDashboard: {
468
+ // Required
469
+ licenseKey: 'YOUR_LICENSE_KEY',
470
+
471
+ // Studio nav tab
472
+ tool: {
473
+ title: 'SEO Audit', // tab label in Studio sidebar (default: 'SEO Health')
474
+ name: 'seo-health-dashboard', // internal tool slug
475
+ },
476
+
477
+ // Dashboard page content
478
+ content: {
479
+ icon: 'πŸ”', // emoji before the page heading
480
+ title: 'SEO Audit', // page heading (default: tool.title)
481
+ description: 'Track SEO quality across all published content.',
482
+ },
483
+
484
+ // Table columns
485
+ display: {
486
+ typeColumn: true, // show document type column (default: true)
487
+ documentId: false, // show document _id (default: true)
488
+ },
489
+
490
+ // Document query
491
+ query: {
492
+ types: ['post', 'page'], // limit to specific document types
493
+ requireSeo: true, // only include docs with seo != null (default: true)
494
+ // groq: '*[seo != null] { _id, _type, title, seo, _updatedAt }',
495
+ // ^ custom GROQ takes precedence over types + requireSeo
496
+ },
497
+
498
+ apiVersion: '2023-01-01', // Sanity API version (default: '2023-01-01')
499
+ },
500
+ })
501
+
502
+ // Or disable the dashboard entirely
503
+ seofields({
504
+ healthDashboard: false,
505
+ })
506
+ ```
507
+
508
+ ### What it shows
509
+
510
+ | Feature | Details |
511
+ | ------------------------ | -------------------------------------------------------------------------------- |
512
+ | **Summary stats** | Total documents, average score, and count per health tier |
513
+ | **Per-document score** | 0–95 score based on which SEO fields are filled in |
514
+ | **Color-coded badges** | 🟒 Excellent (β‰₯ 80) Β· 🟑 Good (β‰₯ 60) Β· 🟠 Fair (β‰₯ 40) Β· πŸ”΄ Poor / Missing (< 40) |
515
+ | **Inline issues** | Top 2 issues per document shown inline; overflow count displayed |
516
+ | **Direct document link** | Click the document title to open it in the desk (new tab) |
517
+ | **Search & filter** | Filter by health status, sort by score or title, and full-text search |
518
+
519
+ ### Scoring breakdown
520
+
521
+ | Field | Max Points |
522
+ | ------------------- | ---------- |
523
+ | Meta Title | 25 |
524
+ | Meta Description | 20 |
525
+ | OG Title | 15 |
526
+ | OG Description | 10 |
527
+ | Twitter Title | 10 |
528
+ | Twitter Description | 10 |
529
+ | Robots / No-Index | 5 |
530
+ | **Total** | **95** |
531
+
532
+ > **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.
533
+
444
534
  ## 🌐 Frontend Integration
445
535
 
446
536
  ### Next.js Example
package/dist/index.d.mts CHANGED
@@ -1,12 +1,26 @@
1
+ import {JSX} from 'react'
1
2
  import {ObjectDefinition} from 'sanity'
2
3
  import {Plugin as Plugin_2} from 'sanity'
3
4
  import {PreviewConfig} from 'sanity'
5
+ import {default as React_2} from 'react'
4
6
  import {SchemaTypeDefinition} from 'sanity'
5
7
 
6
8
  export declare type AllFieldKeys = SeoFieldKeys | openGraphFieldKeys | twitterFieldKeys
7
9
 
8
10
  export declare function allSchemas(config?: SeoFieldsPluginConfig): SchemaTypeDefinition[]
9
11
 
12
+ export declare interface DocumentWithSeoHealth {
13
+ _id: string
14
+ _type: string
15
+ title?: string
16
+ slug?: {
17
+ current: string
18
+ }
19
+ seo?: SeoFields
20
+ _updatedAt?: string
21
+ health: SeoHealthMetrics
22
+ }
23
+
10
24
  export declare interface FieldVisibilityConfig {
11
25
  hiddenFields?: ValidHiddenFieldKeys[]
12
26
  }
@@ -46,6 +60,17 @@ export declare type openGraphFieldKeys =
46
60
 
47
61
  export declare function openGraphSchema(config?: SeoFieldsPluginConfig): SchemaTypeDefinition
48
62
 
63
+ declare interface OpenGraphSettings {
64
+ _type: 'openGraph'
65
+ title?: string
66
+ description?: string
67
+ siteName?: string
68
+ type?: 'website' | 'article' | 'profile' | 'book' | 'music' | 'video' | 'product'
69
+ imageType?: 'upload' | 'url'
70
+ image?: SanityImageWithAlt
71
+ imageUrl?: string
72
+ }
73
+
49
74
  export declare const robotsSchema: {
50
75
  type: 'object'
51
76
  name: 'robots'
@@ -53,6 +78,36 @@ export declare const robotsSchema: {
53
78
  preview?: PreviewConfig<Record<string, string>, Record<never, any>> | undefined
54
79
  }
55
80
 
81
+ declare interface RobotsSettings {
82
+ noIndex?: boolean
83
+ noFollow?: boolean
84
+ }
85
+
86
+ declare interface SanityImage {
87
+ _type: 'image'
88
+ asset: {
89
+ _ref: string
90
+ _type: 'reference'
91
+ }
92
+ hotspot?: {
93
+ x: number
94
+ y: number
95
+ height: number
96
+ width: number
97
+ }
98
+ crop?: {
99
+ top: number
100
+ bottom: number
101
+ left: number
102
+ right: number
103
+ }
104
+ alt?: string
105
+ }
106
+
107
+ declare interface SanityImageWithAlt extends SanityImage {
108
+ alt: string
109
+ }
110
+
56
111
  export declare interface SeoFieldConfig {
57
112
  title?: string
58
113
  description?: string
@@ -67,6 +122,19 @@ export declare type SeoFieldKeys =
67
122
  | 'metaAttributes'
68
123
  | 'robots'
69
124
 
125
+ declare interface SeoFields {
126
+ _type: 'seoFields'
127
+ robots?: RobotsSettings
128
+ preview?: string
129
+ title?: string
130
+ description?: string
131
+ metaImage?: SanityImage
132
+ keywords?: string[]
133
+ canonicalUrl?: string
134
+ openGraph?: OpenGraphSettings
135
+ twitter?: TwitterCardSettings
136
+ }
137
+
70
138
  declare const seofields: Plugin_2<void | SeoFieldsPluginConfig>
71
139
  export default seofields
72
140
 
@@ -113,10 +181,265 @@ export declare interface SeoFieldsPluginConfig {
113
181
  * Defaults to 'https://www.example.com' if not provided.
114
182
  */
115
183
  baseUrl?: string
184
+ /**
185
+ * Enable or configure the SEO Health Dashboard tool.
186
+ * If set to `true`, the dashboard is enabled with all defaults.
187
+ * If set to an object, you can customise the tool and dashboard settings.
188
+ * Defaults to `true`.
189
+ * Example:
190
+ * ```
191
+ * healthDashboard: {
192
+ * toolTitle: 'SEO Overview', // Studio nav tab label
193
+ * content: {
194
+ * icon: 'πŸ”', // Emoji icon shown before the page heading
195
+ * title: 'My SEO Dashboard',// Page heading inside the tool (no emoji)
196
+ * description: 'Track SEO across all documents', // Subtitle under the heading
197
+ * },
198
+ * display: {
199
+ * typeColumn: false, // Hide the document type column (default: true)
200
+ * documentId: false, // Hide the document ID under titles (default: true)
201
+ * },
202
+ * query: {
203
+ * // Option 1 – filter by specific document types
204
+ * types: ['post', 'page'],
205
+ * // Option 2 – provide a full custom GROQ query (takes precedence over `types`)
206
+ * // Must return documents with at least: _id, _type, title, seo, _updatedAt
207
+ * groq: `*[seo != null && defined(slug.current)]{ _id, _type, title, slug, seo, _updatedAt }`,
208
+ * },
209
+ * }
210
+ * ```
211
+ */
212
+ healthDashboard?:
213
+ | boolean
214
+ | {
215
+ tool?: {
216
+ title?: string
217
+ name?: string
218
+ }
219
+ toolTitle?: string
220
+ content?: {
221
+ icon?: string
222
+ title?: string
223
+ description?: string
224
+ /** Text shown while the license key is being verified. Defaults to "Verifying license…" */
225
+ loadingLicense?: string
226
+ /** Text shown while documents are being fetched. Defaults to "Loading documents…" */
227
+ loadingDocuments?: string
228
+ /** Text shown when the query returns zero results. Defaults to "No documents found" */
229
+ noDocuments?: string
230
+ }
231
+ display?: {
232
+ typeColumn?: boolean
233
+ documentId?: boolean
234
+ }
235
+ query?: {
236
+ /**
237
+ * Limit the dashboard to specific document types.
238
+ * Example: `['post', 'page']`
239
+ */
240
+ types?: string[]
241
+ /**
242
+ * When using `types`, also require the `seo` field to be non-null.
243
+ * Set to `false` to include documents of those types even if `seo` is missing.
244
+ * Defaults to `true`.
245
+ */
246
+ requireSeo?: boolean
247
+ /**
248
+ * Provide a fully custom GROQ query. Takes precedence over `types`.
249
+ * The query must return documents with at least: _id, _type, title, seo, _updatedAt
250
+ */
251
+ groq?: string
252
+ }
253
+ /**
254
+ * The Sanity API version to use for the client (e.g. '2023-01-01').
255
+ * Defaults to '2023-01-01'.
256
+ */
257
+ apiVersion?: string
258
+ /**
259
+ * License key for the SEO Health Dashboard pro feature.
260
+ * Obtain a license at https://sanity-plugin-seofields.thehardik.in
261
+ */
262
+ licenseKey?: string
263
+ /**
264
+ * Map raw `_type` values to human-readable display labels.
265
+ * Used in both the Type column and the Type filter dropdown.
266
+ * Any type without an entry falls back to the raw `_type` string.
267
+ *
268
+ * @example
269
+ * typeLabels: { productDrug: 'Products', singleCondition: 'Condition' }
270
+ */
271
+ typeLabels?: Record<string, string>
272
+ /**
273
+ * Controls how the document type is rendered in the Type column.
274
+ * - `'badge'` (default) β€” coloured pill
275
+ * - `'text'` β€” plain text, useful for dense layouts
276
+ */
277
+ typeColumnMode?: 'badge' | 'text'
278
+ /**
279
+ * The document field to use as the display title in the dashboard.
280
+ *
281
+ * - `string` β€” use this field for every document type (e.g. `'name'`)
282
+ * - `Record<string, string>` β€” per-type mapping; unmapped types fall back to `title`
283
+ *
284
+ * @example
285
+ * titleField: 'name'
286
+ *
287
+ * @example
288
+ * titleField: { post: 'title', product: 'name', category: 'label' }
289
+ */
290
+ titleField?: string | Record<string, string>
291
+ /**
292
+ * Callback function to render a custom badge next to the document title.
293
+ * Receives the full document and should return badge data or undefined.
294
+ *
295
+ * @example
296
+ * docBadge: (doc) => {
297
+ * if (doc.services === 'NHS')
298
+ * return { label: 'NHS', bgColor: '#e0f2fe', textColor: '#0369a1' }
299
+ * if (doc.services === 'Private')
300
+ * return { label: 'Private', bgColor: '#fef3c7', textColor: '#92400e' }
301
+ * }
302
+ */
303
+ docBadge?: (doc: DocumentWithSeoHealth & Record<string, unknown>) =>
304
+ | {
305
+ label: string
306
+ bgColor?: string
307
+ textColor?: string
308
+ fontSize?: string
309
+ }
310
+ | undefined
311
+ }
116
312
  }
117
313
 
118
314
  export declare function seoFieldsSchema(config?: SeoFieldsPluginConfig): SchemaTypeDefinition
119
315
 
316
+ export declare const SeoHealthDashboard: React_2.FC<SeoHealthDashboardProps>
317
+
318
+ declare interface SeoHealthDashboardProps {
319
+ icon?: string
320
+ title?: string
321
+ description?: string
322
+ showTypeColumn?: boolean
323
+ showDocumentId?: boolean
324
+ /**
325
+ * Limit the dashboard to specific document type names.
326
+ * If both queryTypes and customQuery are provided, customQuery takes precedence.
327
+ */
328
+ queryTypes?: string[]
329
+ /**
330
+ * When using `queryTypes`, also filter by `seo != null`.
331
+ * Set to `false` to include documents of those types even without an seo field.
332
+ * Defaults to `true`.
333
+ */
334
+ queryRequireSeo?: boolean
335
+ /**
336
+ * A fully custom GROQ query used to fetch documents.
337
+ * Must return objects with at least: _id, _type, title, seo, _updatedAt
338
+ * Takes precedence over queryTypes.
339
+ */
340
+ customQuery?: string
341
+ /**
342
+ * The Sanity API version to use for the client (e.g. '2023-01-01').
343
+ * Defaults to '2023-01-01'.
344
+ */
345
+ apiVersion?: string
346
+ /**
347
+ * License key for the SEO Health Dashboard.
348
+ * Obtain a key at https://sanity-plugin-seofields.thehardik.in
349
+ */
350
+ licenseKey?: string
351
+ /**
352
+ * Map raw `_type` values to human-readable display labels used in the
353
+ * Type column and the Type filter dropdown.
354
+ * Any type without an entry falls back to the raw `_type` string.
355
+ *
356
+ * @example
357
+ * typeLabels={{ productDrug: 'Products', singleCondition: 'Condition' }}
358
+ */
359
+ typeLabels?: Record<string, string>
360
+ /**
361
+ * Controls how the type is rendered in the Type column.
362
+ * - `'badge'` (default) β€” coloured pill, consistent with score badges
363
+ * - `'text'` β€” plain text, useful for dense layouts
364
+ */
365
+ typeColumnMode?: 'badge' | 'text'
366
+ /**
367
+ * The document field to use as the display title.
368
+ *
369
+ * - `string` β€” use this field for every document type (e.g. `'name'`)
370
+ * - `Record<string, string>` β€” per-type mapping; unmapped types fall back to `title`
371
+ *
372
+ * @example
373
+ * // Same field for all types
374
+ * titleField: 'name'
375
+ *
376
+ * @example
377
+ * // Different field per type
378
+ * titleField: { post: 'title', product: 'name', category: 'label' }
379
+ */
380
+ titleField?: string | Record<string, string>
381
+ /**
382
+ * Callback function to render a custom badge next to the document title.
383
+ * Receives the full document and should return badge data or undefined.
384
+ *
385
+ * @example
386
+ * docBadge: (doc) => {
387
+ * if (doc.services === 'NHS')
388
+ * return { label: 'NHS', bgColor: '#e0f2fe', textColor: '#0369a1' }
389
+ * if (doc.services === 'Private')
390
+ * return { label: 'Private', bgColor: '#fef3c7', textColor: '#92400e' }
391
+ * }
392
+ */
393
+ docBadge?: (doc: DocumentWithSeoHealth & Record<string, unknown>) =>
394
+ | {
395
+ label: string
396
+ bgColor?: string
397
+ textColor?: string
398
+ fontSize?: string
399
+ }
400
+ | undefined
401
+ /**
402
+ * Custom text shown while the license key is being verified.
403
+ * Defaults to `"Verifying license…"`.
404
+ */
405
+ loadingLicense?: React_2.ReactNode
406
+ /**
407
+ * Custom text shown while documents are being fetched.
408
+ * Defaults to `"Loading documents…"`.
409
+ */
410
+ loadingDocuments?: React_2.ReactNode
411
+ /**
412
+ * Custom text shown when the query returns zero results.
413
+ * Defaults to `"No documents found"`.
414
+ */
415
+ noDocuments?: React_2.ReactNode
416
+ }
417
+
418
+ export declare interface SeoHealthMetrics {
419
+ score: number
420
+ status: SeoHealthStatus
421
+ issues: string[]
422
+ }
423
+
424
+ export declare type SeoHealthStatus = 'excellent' | 'good' | 'fair' | 'poor' | 'missing'
425
+
426
+ /**
427
+ * Sanity Tool component for the SEO Health Dashboard
428
+ * This component wraps the SeoHealthDashboard for use as a custom tool in Sanity Studio
429
+ */
430
+ export declare const SeoHealthTool: (props: SeoHealthDashboardProps) => JSX.Element
431
+
432
+ declare interface TwitterCardSettings {
433
+ _type: 'twitter'
434
+ card?: 'summary' | 'summary_large_image' | 'app' | 'player'
435
+ site?: string
436
+ title?: string
437
+ description?: string
438
+ imageType?: 'upload' | 'url'
439
+ image?: SanityImageWithAlt
440
+ imageUrl?: string
441
+ }
442
+
120
443
  export declare type twitterFieldKeys =
121
444
  | 'twitterCard'
122
445
  | 'twitterSite'