sanity-plugin-seofields 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +508 -26
  2. package/dist/SeoHealthTool-2COI27KI.cjs +8 -0
  3. package/dist/SeoHealthTool-2COI27KI.cjs.map +1 -0
  4. package/dist/SeoHealthTool-CWI3KB2V.js +8 -0
  5. package/dist/SeoHealthTool-CWI3KB2V.js.map +1 -0
  6. package/dist/{SeoPreview-3EXR6FD4.cjs → SeoPreview-LMZAWWOE.cjs} +35 -38
  7. package/dist/SeoPreview-LMZAWWOE.cjs.map +1 -0
  8. package/dist/{SeoPreview-4PPAF4NO.js → SeoPreview-UNQBKTQO.js} +14 -11
  9. package/dist/SeoPreview-UNQBKTQO.js.map +1 -0
  10. package/dist/{chunk-B5IVI5LP.cjs → chunk-5XTQRILL.cjs} +286 -236
  11. package/dist/chunk-5XTQRILL.cjs.map +1 -0
  12. package/dist/{chunk-254YHUN3.cjs → chunk-7N4MLTMR.js} +8 -6
  13. package/dist/chunk-7N4MLTMR.js.map +1 -0
  14. package/dist/{chunk-DDAAVRWG.js → chunk-A57XNQC7.cjs} +9 -4
  15. package/dist/chunk-A57XNQC7.cjs.map +1 -0
  16. package/dist/chunk-BWLJDK5J.js +624 -0
  17. package/dist/chunk-BWLJDK5J.js.map +1 -0
  18. package/dist/chunk-CRCXC45D.js +56 -0
  19. package/dist/chunk-CRCXC45D.js.map +1 -0
  20. package/dist/chunk-EUG4MNRC.cjs +713 -0
  21. package/dist/chunk-EUG4MNRC.cjs.map +1 -0
  22. package/dist/{chunk-ZBHLMQTS.cjs → chunk-HHO2AKAP.js} +44 -17
  23. package/dist/chunk-HHO2AKAP.js.map +1 -0
  24. package/dist/chunk-NLEB47UV.js +713 -0
  25. package/dist/chunk-NLEB47UV.js.map +1 -0
  26. package/dist/chunk-OULUDJPI.cjs +254 -0
  27. package/dist/chunk-OULUDJPI.cjs.map +1 -0
  28. package/dist/chunk-R2U7JF7U.cjs +624 -0
  29. package/dist/chunk-R2U7JF7U.cjs.map +1 -0
  30. package/dist/chunk-XD4HKLEA.js +254 -0
  31. package/dist/chunk-XD4HKLEA.js.map +1 -0
  32. package/dist/chunk-XYAZJ3WX.cjs +56 -0
  33. package/dist/chunk-XYAZJ3WX.cjs.map +1 -0
  34. package/dist/{chunk-VR44E62I.js → chunk-XZKQWV2H.js} +204 -33
  35. package/dist/chunk-XZKQWV2H.js.map +1 -0
  36. package/dist/{chunk-HDZZQCH7.js → chunk-Y46ACXM4.cjs} +45 -5
  37. package/dist/chunk-Y46ACXM4.cjs.map +1 -0
  38. package/dist/cli.js +27 -27
  39. package/dist/{component-DEXwtemT.d.ts → component-CjT1hvxh.d.ts} +2 -2
  40. package/dist/{component-BzI-LHPw.d.cts → component-WYh0z8as.d.cts} +2 -2
  41. package/dist/define-cli.cjs +2 -4
  42. package/dist/define-cli.cjs.map +1 -1
  43. package/dist/define-cli.js +4 -4
  44. package/dist/define-cli.js.map +1 -1
  45. package/dist/head.cjs +22 -0
  46. package/dist/head.cjs.map +1 -0
  47. package/dist/head.d.cts +314 -0
  48. package/dist/head.d.ts +314 -0
  49. package/dist/head.js +22 -0
  50. package/dist/head.js.map +1 -0
  51. package/dist/hreflang-_VbSuWYD.d.cts +29 -0
  52. package/dist/hreflang-_VbSuWYD.d.ts +29 -0
  53. package/dist/index.cjs +1072 -368
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.cts +7 -374
  56. package/dist/index.d.ts +7 -374
  57. package/dist/index.js +1015 -288
  58. package/dist/index.js.map +1 -1
  59. package/dist/next.cjs +212 -455
  60. package/dist/next.cjs.map +1 -1
  61. package/dist/next.d.cts +9 -206
  62. package/dist/next.d.ts +9 -206
  63. package/dist/next.js +189 -111
  64. package/dist/next.js.map +1 -1
  65. package/dist/plugin-HFuWZsJu.d.cts +516 -0
  66. package/dist/plugin-oQUCim56.d.ts +516 -0
  67. package/dist/schema/next.cjs +173 -327
  68. package/dist/schema/next.cjs.map +1 -1
  69. package/dist/schema/next.d.cts +4 -4
  70. package/dist/schema/next.d.ts +4 -4
  71. package/dist/schema/next.js +172 -8
  72. package/dist/schema/next.js.map +1 -1
  73. package/dist/schema.cjs +376 -517
  74. package/dist/schema.cjs.map +1 -1
  75. package/dist/schema.d.cts +5 -5
  76. package/dist/schema.d.ts +5 -5
  77. package/dist/schema.js +216 -11
  78. package/dist/schema.js.map +1 -1
  79. package/dist/server.cjs +149 -0
  80. package/dist/server.cjs.map +1 -0
  81. package/dist/server.d.cts +100 -0
  82. package/dist/server.d.ts +100 -0
  83. package/dist/server.js +149 -0
  84. package/dist/server.js.map +1 -0
  85. package/dist/{types-CW8qFdAn.d.ts → types-BAdN1RMu.d.ts} +2 -2
  86. package/dist/{types-B_vjMPtM.d.cts → types-C5i3KMgs.d.cts} +2 -2
  87. package/dist/{types-BXSCbl6p.d.cts → types-DP-DiW6f.d.cts} +2 -2
  88. package/dist/{types-C19cQx5D.d.ts → types-DRLUGLtX.d.ts} +2 -2
  89. package/dist/{types-yVmQfby9.d.cts → types-DxlPXihz.d.cts} +1 -1
  90. package/dist/{types-yVmQfby9.d.ts → types-DxlPXihz.d.ts} +1 -1
  91. package/package.json +25 -4
  92. package/dist/SeoHealthDashboard-AEKVVMUR-7NWV3C3Y.js +0 -4
  93. package/dist/SeoHealthDashboard-AEKVVMUR-7NWV3C3Y.js.map +0 -1
  94. package/dist/SeoHealthDashboard-AEKVVMUR-XWKJUH24.cjs +0 -10
  95. package/dist/SeoHealthDashboard-AEKVVMUR-XWKJUH24.cjs.map +0 -1
  96. package/dist/SeoHealthTool-2KKQJLJL.cjs +0 -11
  97. package/dist/SeoHealthTool-2KKQJLJL.cjs.map +0 -1
  98. package/dist/SeoHealthTool-3CUKGADZ.js +0 -5
  99. package/dist/SeoHealthTool-3CUKGADZ.js.map +0 -1
  100. package/dist/SeoPreview-3EXR6FD4.cjs.map +0 -1
  101. package/dist/SeoPreview-4PPAF4NO.js.map +0 -1
  102. package/dist/chunk-254YHUN3.cjs.map +0 -1
  103. package/dist/chunk-27XSYENH.js +0 -2215
  104. package/dist/chunk-27XSYENH.js.map +0 -1
  105. package/dist/chunk-B5IVI5LP.cjs.map +0 -1
  106. package/dist/chunk-BNXMFD3T.cjs +0 -431
  107. package/dist/chunk-BNXMFD3T.cjs.map +0 -1
  108. package/dist/chunk-DDAAVRWG.js.map +0 -1
  109. package/dist/chunk-HDZZQCH7.js.map +0 -1
  110. package/dist/chunk-P6IOWIO2.cjs +0 -480
  111. package/dist/chunk-P6IOWIO2.cjs.map +0 -1
  112. package/dist/chunk-UOCZFYYP.js +0 -407
  113. package/dist/chunk-UOCZFYYP.js.map +0 -1
  114. package/dist/chunk-VR44E62I.js.map +0 -1
  115. package/dist/chunk-WKXHX3GO.js +0 -424
  116. package/dist/chunk-WKXHX3GO.js.map +0 -1
  117. package/dist/chunk-XZDLJ2N6.cjs +0 -2224
  118. package/dist/chunk-XZDLJ2N6.cjs.map +0 -1
  119. package/dist/chunk-ZBHLMQTS.cjs.map +0 -1
@@ -0,0 +1,516 @@
1
+ import * as sanity from 'sanity';
2
+ import { a as HreflangTranslation } from './hreflang-_VbSuWYD.cjs';
3
+ import { ComponentType } from 'react';
4
+ import { D as DocumentWithSeoHealth } from './types-DxlPXihz.cjs';
5
+
6
+ type SeoGenField = 'title' | 'description' | 'focusKeyword' | 'keywords' | 'ogTitle' | 'ogDescription' | 'twitterTitle' | 'twitterDescription';
7
+ type MetaContext = {
8
+ title?: string;
9
+ description?: string;
10
+ slug?: string;
11
+ };
12
+
13
+ /** Auto-populate configuration for the `hreflangs` field. */
14
+ interface HreflangConfig {
15
+ /** Show the "Sync from translations" button on the hreflangs field. */
16
+ autoFill?: boolean;
17
+ /** Field holding the language tag on translated documents. Defaults to 'language'. */
18
+ localeField?: string;
19
+ /** Build the path for a translation. Defaults to `/${slug}`. */
20
+ resolvePath?: (t: HreflangTranslation) => string;
21
+ }
22
+ type AiIndustry = 'blog' | 'ecommerce' | 'healthcare' | 'pharmacy' | 'saas' | 'finance' | 'realestate' | 'education' | 'restaurant' | 'travel' | 'fitness' | 'hospitality' | 'nonprofit' | 'legal' | 'insurance' | 'automotive' | 'homeServices';
23
+ /**
24
+ * All the generic document values the plugin extracts, handed to a custom prompt function so you can
25
+ * template your own prompt exactly the way the built-in prompts are built. `content` is the extracted
26
+ * document text (same output as the plugin's own content extraction).
27
+ */
28
+ type CustomPromptValues = {
29
+ field: SeoGenField;
30
+ content: string;
31
+ focusKeyword: string;
32
+ keywords: string[];
33
+ meta?: MetaContext;
34
+ industry?: AiIndustry;
35
+ };
36
+ /** Receives all generic document values, returns the prompt string sent to the AI provider. */
37
+ type CustomPromptFn = (values: CustomPromptValues) => string;
38
+ interface AiConfig {
39
+ /** AI provider to use for text generation. */
40
+ provider?: 'openai' | 'anthropic' | 'groq' | 'gemini' | 'ollama';
41
+ /**
42
+ * API key for the provider. Not required for ollama or when using a proxy endpoint.
43
+ * Warning: this is bundled into Studio's client-side JS and readable by anyone with Studio
44
+ * access. For production, use `endpoint` instead — see `sanity-plugin-seofields/server`
45
+ * for ready-made proxy handlers (Next.js, Express, Node, Cloudflare Workers, etc.).
46
+ */
47
+ apiKey?: string;
48
+ /** Override the provider's default model. */
49
+ model?: string;
50
+ /** Override the provider's base URL. Accepts any OpenAI-compatible endpoint (DeepSeek, xAI Grok, Azure OpenAI, etc.). */
51
+ baseUrl?: string;
52
+ /**
53
+ * Proxy endpoint — POST { field, content, focusKeyword, keywords, meta } → { result: string }.
54
+ * Key stays server-side. Build one with `sanity-plugin-seofields/server`, e.g.
55
+ * `createNextRouteHandler({ provider: 'openai', apiKey: process.env.OPENAI_API_KEY })`.
56
+ */
57
+ endpoint?: string;
58
+ /** Sampling temperature. Defaults to 0.7. */
59
+ temperature?: number;
60
+ /**
61
+ * Root document field(s) used as content for the AI prompt. Defaults to 'body'.
62
+ *
63
+ * - `string` — one field, used for every document type
64
+ * - `string[]` — multiple fields in priority order, used for every document type
65
+ * - `Record<string, string | string[]>` — per-document-type field(s); use a `default`
66
+ * key for unlisted types (falls back to `'body'` if omitted)
67
+ *
68
+ * @example
69
+ * content: ['title', 'excerpt', 'body']
70
+ *
71
+ * @example
72
+ * content: {
73
+ * page: ['sections'],
74
+ * news: 'content',
75
+ * work: ['content', 'contentExtended'],
76
+ * author: 'bio',
77
+ * default: 'body',
78
+ * }
79
+ */
80
+ content?: string | string[] | Record<string, string | string[]>;
81
+ /** Industry context for prompts. When set, uses domain-specific vocabulary. Omit for generic prompts. */
82
+ industry?: AiIndustry;
83
+ /**
84
+ * FREE tier: a single custom prompt (generic or industry-specific — the function decides based on the
85
+ * `field`/`industry` it receives). Ignored if `customPrompts` is also set.
86
+ *
87
+ * In proxy mode (`endpoint`), custom prompts must be set on the **server** handler config
88
+ * (`createSeoAiHandler`) — a function cannot be sent over HTTP.
89
+ *
90
+ * @example
91
+ * customPrompt: (v) => `Write a 55-char SEO ${v.field} about: ${v.content.slice(0, 200)}`
92
+ */
93
+ customPrompt?: CustomPromptFn;
94
+ /**
95
+ * PAID tier: up to 5 generic + up to 5 per-industry custom prompts. The count and per-industry
96
+ * pools are unlocked only behind a validated license via `seofields-pro`; without a valid license
97
+ * this collapses to a single prompt (same as the free `customPrompt`).
98
+ *
99
+ * By default custom prompts **replace** the built-in angle pool for a field. Set `merge: true` to
100
+ * mix them into the built-in pool instead.
101
+ */
102
+ customPrompts?: {
103
+ generic?: CustomPromptFn[];
104
+ byIndustry?: Partial<Record<AiIndustry, CustomPromptFn[]>>;
105
+ /** Mix custom prompts with the built-in angle pool. Default false = replace. */
106
+ merge?: boolean;
107
+ };
108
+ /** Enable test mode — uses static pre-written outputs without any API call. Shows test warning in UI. */
109
+ testMode?: boolean;
110
+ /** Number of full generation attempts before giving up. Defaults to 2. */
111
+ maxRetries?: number;
112
+ /**
113
+ * Controls what is returned when all attempts fail validation:
114
+ * - true: returns the first raw response (before refinement passes)
115
+ * - false / undefined (default): returns the last attempt's output
116
+ */
117
+ keepFirstOnValidationFail?: boolean;
118
+ /**
119
+ * Width of the "Generate with AI" button.
120
+ * - 'full' (default): stretches to the field's full width.
121
+ * - 'auto': normal button width, left-aligned.
122
+ */
123
+ buttonWidth?: 'full' | 'auto';
124
+ /** @internal — forwarded from root licenseKey at plugin setup time. Do not set manually. */
125
+ _licenseKey?: string;
126
+ /** @internal — Sanity project ID used for pro feature validation. Do not set manually. */
127
+ _projectId?: string;
128
+ }
129
+
130
+ interface SeoFieldConfig {
131
+ title?: string;
132
+ description?: string;
133
+ }
134
+ type SeoFieldKeys = 'title' | 'description' | 'canonicalUrl' | 'metaImage' | 'keywords' | 'metaAttributes' | 'robots' | 'focusKeyword' | 'hreflangs' | 'geoChecklist' | 'metaTagsPreview';
135
+ type openGraphFieldKeys = 'openGraphUrl' | 'openGraphTitle' | 'openGraphDescription' | 'openGraphSiteName' | 'openGraphType' | 'openGraphImageType' | 'openGraphImage' | 'openGraphImageUrl';
136
+ type twitterFieldKeys = 'twitterCard' | 'twitterSite' | 'twitterCreator' | 'twitterTitle' | 'twitterDescription' | 'twitterImageType' | 'twitterImage' | 'twitterImageUrl';
137
+ type AllFieldKeys = SeoFieldKeys | openGraphFieldKeys | twitterFieldKeys;
138
+ /**
139
+ * Names of top-level fields inside the `seoFields` object type.
140
+ * Use these when assigning fields to groups in `fieldGroups`.
141
+ */
142
+ type SeoObjectFieldName = 'robots' | 'preview' | 'title' | 'description' | 'metaImage' | 'metaAttributes' | 'keywords' | 'canonicalUrl' | 'openGraph' | 'twitter' | 'focusKeyword' | 'hreflangs' | 'geoChecklist' | 'metaTagsPreview';
143
+ /**
144
+ * Defines a single tab/group within the `seoFields` object.
145
+ */
146
+ interface SeoFieldGroup {
147
+ /** Unique key for this group (used internally by Sanity). */
148
+ name: string;
149
+ /** Human-readable label shown as the tab title. */
150
+ title: string;
151
+ /** Whether this tab is selected by default. Only one group should be `true`. */
152
+ default?: boolean;
153
+ /**
154
+ * Field names to include in this group.
155
+ * Use the top-level seoFields field names: `'title'`, `'description'`, `'metaImage'`,
156
+ * `'keywords'`, `'canonicalUrl'`, `'metaAttributes'`, `'robots'`, `'preview'`,
157
+ * `'openGraph'`, `'twitter'`.
158
+ */
159
+ fields: SeoObjectFieldName[];
160
+ /** Optional icon displayed next to the tab title. Must be a React component. */
161
+ icon?: ComponentType;
162
+ }
163
+ type ValidHiddenFieldKeys = Exclude<AllFieldKeys, 'openGraphImageUrl' | 'twitterImageUrl' | 'openGraphImageType' | 'twitterImageType'>;
164
+ interface FieldVisibilityConfig {
165
+ hiddenFields?: ValidHiddenFieldKeys[];
166
+ }
167
+ /**
168
+ * Individual SEO fields that must be non-empty before publishing.
169
+ * Dot-notation for nested fields (e.g. 'openGraph.title').
170
+ */
171
+ type PublishGateRequiredField = 'title' | 'description' | 'metaImage' | 'keywords' | 'canonicalUrl' | 'openGraph.title' | 'openGraph.description' | 'openGraph.image' | 'openGraph.type' | 'twitter.title' | 'twitter.description' | 'twitter.image' | 'twitter.card';
172
+ interface SeoFieldsPluginConfig {
173
+ /**
174
+ * License key for pro features (SEO Health Dashboard, Publish Gate).
175
+ * Obtain at https://sanity-plugin-seofields.thehardik.in
176
+ */
177
+ licenseKey?: string;
178
+ /**
179
+ * Enable or configure the SEO preview feature.
180
+ * If set to `true`, the SEO preview will be enabled with default settings.
181
+ * If set to an object, you can provide a custom `prefix` function to modify the URL prefix
182
+ * and/or a `titleSuffix` to append text (e.g. a brand name) after the meta title in the preview.
183
+ * The plugin automatically adds a `|` separator — only provide the suffix text itself.
184
+ *
185
+ * Example:
186
+ * ```
187
+ * seoPreview: {
188
+ * prefix: (doc) => `/${doc.slug?.current || 'untitled'}`,
189
+ * titleSuffix: 'Acme Corp',
190
+ * // or dynamically:
191
+ * titleSuffix: (doc) => doc.brandName || 'Acme Corp',
192
+ * }
193
+ * ```
194
+ */
195
+ seoPreview?: boolean | {
196
+ prefix?: (doc: {
197
+ _type?: string;
198
+ } & Record<string, unknown>) => string;
199
+ /**
200
+ * A static string or function appended to the meta title in the Live Preview.
201
+ * Useful for showing a brand suffix (e.g. `'Acme Corp'`) that is added
202
+ * via your Next.js title template but not stored in the Sanity field.
203
+ * The plugin automatically prepends a `|` separator so only provide the
204
+ * text itself. The suffix is rendered in a muted style so editors can
205
+ * distinguish it from the typed title. The combined length (including the
206
+ * separator) is checked against the 60-character SERP limit.
207
+ */
208
+ titleSuffix?: string | ((doc: {
209
+ _type?: string;
210
+ } & Record<string, unknown>) => string);
211
+ /**
212
+ * When `true`, the `titleSuffix` is rendered in the same color and weight
213
+ * as the main title (`#1a0dab`, `fontWeight: 500`) instead of the default
214
+ * muted style (`#70757a`, `fontWeight: 400`).
215
+ *
216
+ * @default false
217
+ */
218
+ titleSuffixInheritColor?: boolean;
219
+ /**
220
+ * A GROQ query string whose result is used as the title suffix in the Live Preview.
221
+ * The query runs against your dataset at Studio load time, making it useful for
222
+ * fetching a dynamic value from another document (e.g. a settings singleton).
223
+ * Takes priority over `titleSuffix` when both are provided.
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * titleSuffixQuery: '*[_type == "siteSettings"][0].siteName'
228
+ * ```
229
+ */
230
+ titleSuffixQuery?: string;
231
+ };
232
+ /**
233
+ * A mapping of field keys to their configuration settings.
234
+ * This allows customization of field titles and descriptions.
235
+ * For example, to change the title of the 'title' field:
236
+ */
237
+ fieldOverrides?: Partial<Record<AllFieldKeys, SeoFieldConfig>>;
238
+ /**
239
+ * A mapping of document types to field visibility configurations.
240
+ * This allows you to specify which fields should be hidden for specific document types.
241
+ */
242
+ fieldVisibility?: Record<string, FieldVisibilityConfig>;
243
+ /**
244
+ * A list of fields that should be hidden by default in all document types.
245
+ * This can be overridden by specific document type settings in `fieldVisibility`.
246
+ */
247
+ defaultHiddenFields?: ValidHiddenFieldKeys[];
248
+ /**
249
+ * Group the SEO fields into tabbed sections inside the `seoFields` object.
250
+ * When configured, the Studio shows tabs (Sanity groups) so editors can
251
+ * switch between e.g. "Meta", "Open Graph", and "Twitter Card" panels.
252
+ *
253
+ * @example
254
+ * fieldGroups: [
255
+ * { name: 'meta', title: 'Meta', default: true,
256
+ * fields: ['title', 'description', 'metaImage', 'keywords', 'canonicalUrl', 'metaAttributes', 'robots', 'preview'] },
257
+ * { name: 'openGraph', title: 'Open Graph', fields: ['openGraph'] },
258
+ * { name: 'twitter', title: 'Twitter Card', fields: ['twitter'] },
259
+ * ]
260
+ */
261
+ fieldGroups?: SeoFieldGroup[];
262
+ /**
263
+ * Show the GEO / AI Overview readiness checklist inside seoFields.
264
+ * Defaults to `true`.
265
+ */
266
+ geo?: boolean;
267
+ /**
268
+ * Show the Meta Tags HTML preview inside seoFields.
269
+ * Defaults to `true`.
270
+ */
271
+ metaTagsPreview?: boolean;
272
+ /**
273
+ * The base URL of your website, used for generating full URLs in the SEO preview.
274
+ * Defaults to 'https://www.example.com' if not provided.
275
+ */
276
+ baseUrl?: string;
277
+ /**
278
+ * The Sanity API version to use for all plugin clients (SEO Preview, Health Dashboard).
279
+ * Defaults to '2024-01-01'.
280
+ * @example '2024-01-01'
281
+ */
282
+ apiVersion?: string;
283
+ /**
284
+ * Auto-populate the `hreflangs` field from document translations. Designed for
285
+ * `@sanity/document-internationalization` — adds a "Sync from translations" button to the field
286
+ * that reads the `translation.metadata` references and fills alternate entries (still editable).
287
+ * URLs are built from the root `baseUrl` + each translation's slug.
288
+ *
289
+ * @example
290
+ * hreflang: { autoFill: true, resolvePath: (t) => `/${t.language}/${t.slug}` }
291
+ */
292
+ hreflang?: HreflangConfig;
293
+ /**
294
+ * Enable or configure the SEO Health Dashboard tool.
295
+ * If set to `true`, the dashboard is enabled with all defaults.
296
+ * If set to an object, you can customise the tool and dashboard settings.
297
+ * Defaults to `true`.
298
+ * Example:
299
+ * ```
300
+ * healthDashboard: {
301
+ * toolTitle: 'SEO Overview', // Studio nav tab label
302
+ * content: {
303
+ * icon: '🔍', // Emoji icon shown before the page heading
304
+ * title: 'My SEO Dashboard',// Page heading inside the tool (no emoji)
305
+ * description: 'Track SEO across all documents', // Subtitle under the heading
306
+ * },
307
+ * display: {
308
+ * typeColumn: false, // Hide the document type column (default: true)
309
+ * documentId: false, // Hide the document ID under titles (default: true)
310
+ * },
311
+ * query: {
312
+ * // Option 1 – filter by specific document types
313
+ * types: ['post', 'page'],
314
+ * // Option 2 – provide a full custom GROQ query (takes precedence over `types`)
315
+ * // Must return documents with at least: _id, _type, title, seo, _updatedAt
316
+ * groq: `*[seo != null && defined(slug.current)]{ _id, _type, title, slug, seo, _updatedAt }`,
317
+ * },
318
+ * }
319
+ * ```
320
+ */
321
+ healthDashboard?: boolean | {
322
+ tool?: {
323
+ title?: string;
324
+ name?: string;
325
+ };
326
+ toolTitle?: string;
327
+ content?: {
328
+ icon?: string;
329
+ title?: string;
330
+ description?: string;
331
+ /** Text shown while the license key is being verified. Defaults to "Verifying license…" */
332
+ loadingLicense?: string;
333
+ /** Text shown while documents are being fetched. Defaults to "Loading documents…" */
334
+ loadingDocuments?: string;
335
+ /** Text shown when the query returns zero results. Defaults to "No documents found" */
336
+ noDocuments?: string;
337
+ };
338
+ /**
339
+ * Show or hide the document type column in the results table.
340
+ * Defaults to `true`.
341
+ */
342
+ showTypeColumn?: boolean;
343
+ /**
344
+ * Show or hide the Sanity document `_id` under each title.
345
+ * Defaults to `true`.
346
+ */
347
+ showDocumentId?: boolean;
348
+ query?: {
349
+ /**
350
+ * Limit the dashboard to specific document types.
351
+ * Example: `['post', 'page']`
352
+ */
353
+ types?: string[];
354
+ /**
355
+ * When using `types`, also require the `seo` field to be non-null.
356
+ * Set to `false` to include documents of those types even if `seo` is missing.
357
+ * Defaults to `true`.
358
+ */
359
+ requireSeo?: boolean;
360
+ /**
361
+ * Provide a fully custom GROQ query. Takes precedence over `types`.
362
+ * The query must return documents with at least: _id, _type, title, seo, _updatedAt
363
+ */
364
+ groq?: string;
365
+ };
366
+ /**
367
+ * The Sanity API version to use for the client (e.g. '2023-01-01').
368
+ * Defaults to '2023-01-01'.
369
+ * @deprecated Use the root-level `apiVersion` option instead.
370
+ * @example
371
+ * // Before (deprecated):
372
+ * healthDashboard: { apiVersion: '2024-01-01' }
373
+ * // After:
374
+ * apiVersion: '2024-01-01'
375
+ */
376
+ apiVersion?: string;
377
+ /**
378
+ * License key for the SEO Health Dashboard pro feature.
379
+ * @deprecated Use the root-level `licenseKey` option instead.
380
+ */
381
+ licenseKey?: string;
382
+ /**
383
+ * Map raw `_type` values to human-readable display labels.
384
+ * Used in both the Type column and the Type filter dropdown.
385
+ * Any type without an entry falls back to the raw `_type` string.
386
+ *
387
+ * @example
388
+ * typeDisplayLabels: { productDrug: 'Products', singleCondition: 'Condition' }
389
+ */
390
+ typeDisplayLabels?: Record<string, string>;
391
+ /**
392
+ * Controls how the document type is rendered in the Type column.
393
+ * - `'badge'` (default) — coloured pill
394
+ * - `'text'` — plain text, useful for dense layouts
395
+ */
396
+ typeColumnMode?: 'badge' | 'text';
397
+ /**
398
+ * The document field to use as the display title in the dashboard.
399
+ *
400
+ * - `string` — use this field for every document type (e.g. `'name'`)
401
+ * - `Record<string, string>` — per-type mapping; unmapped types fall back to `title`
402
+ *
403
+ * @example
404
+ * titleField: 'name'
405
+ *
406
+ * @example
407
+ * titleField: { post: 'title', product: 'name', category: 'label' }
408
+ */
409
+ titleField?: string | Record<string, string>;
410
+ /**
411
+ * Callback function to render a custom badge next to the document title.
412
+ * Receives the full document and should return badge data or undefined.
413
+ *
414
+ * @example
415
+ * getDocumentBadge: (doc) => {
416
+ * if (doc.services === 'NHS')
417
+ * return { label: 'NHS', bgColor: '#e0f2fe', textColor: '#0369a1' }
418
+ * if (doc.services === 'Private')
419
+ * return { label: 'Private', bgColor: '#fef3c7', textColor: '#92400e' }
420
+ * }
421
+ */
422
+ getDocumentBadge?: (doc: DocumentWithSeoHealth & Record<string, unknown>) => {
423
+ label: string;
424
+ bgColor?: string;
425
+ textColor?: string;
426
+ fontSize?: string;
427
+ } | undefined;
428
+ /**
429
+ * The `name` of the Sanity structure tool that contains the monitored documents.
430
+ * Required when you have multiple structure tools and the documents live in a
431
+ * non-default one. Clicking a title will navigate to
432
+ * `/{basePath}/{structureTool}/intent/edit/…` directly.
433
+ *
434
+ * @example
435
+ * structureTool: 'common'
436
+ */
437
+ structureTool?: string;
438
+ /**
439
+ * Enable preview/demo mode to show dummy data.
440
+ * Useful for testing, documentation, or showcasing the dashboard.
441
+ * When enabled, displays realistic sample documents with various SEO scores.
442
+ * Defaults to `false`.
443
+ *
444
+ * @example
445
+ * previewMode: true
446
+ */
447
+ previewMode?: boolean;
448
+ /**
449
+ * Export options for the SEO Health Dashboard.
450
+ * Set to `true` (default) to enable both CSV and JSON export,
451
+ * or configure per-format.
452
+ */
453
+ export?: boolean | {
454
+ enabled?: boolean;
455
+ formats?: Array<'csv' | 'json'>;
456
+ };
457
+ /**
458
+ * Show compact inline stat pills in the header row instead of the
459
+ * full 6-card stats grid. Useful for saving vertical space.
460
+ * Defaults to `false`.
461
+ */
462
+ compactStats?: boolean;
463
+ };
464
+ /**
465
+ * Gate publishing based on SEO score or required field presence.
466
+ * Requires a valid root-level `licenseKey` — silently disabled if key is absent or invalid.
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * publishGate: {
471
+ * mode: 'warn',
472
+ * minScore: 70,
473
+ * requiredFields: ['title', 'description', 'openGraph.title'],
474
+ * types: ['post', 'page'],
475
+ * }
476
+ * ```
477
+ */
478
+ /**
479
+ * AI text generation configuration.
480
+ * Supports OpenAI, Anthropic, Groq, Gemini, Ollama, or a custom proxy endpoint.
481
+ * Set `testMode: true` to preview generation without an API key.
482
+ */
483
+ ai?: AiConfig;
484
+ publishGate?: {
485
+ /**
486
+ * 'block' — disables the Publish button with a tooltip reason (default).
487
+ * 'warn' — shows a confirm dialog; editor can bypass and publish anyway.
488
+ * @default 'block'
489
+ */
490
+ mode?: 'block' | 'warn';
491
+ /**
492
+ * Minimum overall SEO health score (0–100) required to publish.
493
+ * Gate fires when the document's score is below this threshold.
494
+ * @default 60
495
+ */
496
+ minScore?: number;
497
+ /**
498
+ * Individual fields that MUST be non-empty. Gate fires if any are missing.
499
+ * Use dot-notation for nested fields: 'openGraph.title', 'twitter.image', etc.
500
+ */
501
+ requiredFields?: PublishGateRequiredField[];
502
+ /**
503
+ * Restrict the gate to specific document types.
504
+ * Omit to apply to all document types that have an `seo` field.
505
+ */
506
+ types?: string[];
507
+ /**
508
+ * Custom message shown in the block tooltip or warn dialog.
509
+ * Accepts a string or a function receiving (score, issues[]).
510
+ */
511
+ message?: string | ((score: number, issues: string[]) => string);
512
+ };
513
+ }
514
+ declare const seofields: sanity.Plugin<void | SeoFieldsPluginConfig>;
515
+
516
+ export { type AiConfig as A, type CustomPromptFn as C, type FieldVisibilityConfig as F, type HreflangConfig as H, type MetaContext as M, type PublishGateRequiredField as P, type SeoGenField as S, type ValidHiddenFieldKeys as V, type AiIndustry as a, type SeoFieldsPluginConfig as b, type AllFieldKeys as c, type CustomPromptValues as d, type SeoFieldConfig as e, type SeoFieldGroup as f, type SeoFieldKeys as g, type SeoObjectFieldName as h, type openGraphFieldKeys as o, seofields as s, type twitterFieldKeys as t };