sanity-plugin-seofields 1.2.4 → 1.2.6
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/dist/index.cjs +2604 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +422 -0
- package/dist/index.d.ts +339 -492
- package/dist/index.js +1284 -2013
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +182 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +241 -0
- package/dist/next.d.ts +202 -295
- package/dist/next.js +110 -70
- package/dist/next.js.map +1 -1
- package/dist/types-B91ena4g.d.cts +89 -0
- package/dist/types-B91ena4g.d.ts +89 -0
- package/package.json +37 -18
- package/dist/index.d.mts +0 -575
- package/dist/index.mjs +0 -3292
- package/dist/index.mjs.map +0 -1
- package/dist/next.d.mts +0 -334
- package/dist/next.mjs +0 -102
- package/dist/next.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/components/SeoHealthDashboard.tsx +0 -1568
- package/src/components/SeoHealthPane.tsx +0 -81
- package/src/components/SeoHealthTool.tsx +0 -11
- package/src/components/SeoPreview.tsx +0 -178
- package/src/components/meta/MetaDescription.tsx +0 -39
- package/src/components/meta/MetaTitle.tsx +0 -44
- package/src/components/openGraph/OgDescription.tsx +0 -46
- package/src/components/openGraph/OgTitle.tsx +0 -45
- package/src/components/twitter/twitterDescription.tsx +0 -45
- package/src/components/twitter/twitterTitle.tsx +0 -45
- package/src/helpers/SeoMetaTags.tsx +0 -154
- package/src/helpers/seoMeta.ts +0 -283
- package/src/index.ts +0 -26
- package/src/next.ts +0 -12
- package/src/plugin.ts +0 -344
- package/src/schemas/index.ts +0 -121
- package/src/schemas/types/index.ts +0 -20
- package/src/schemas/types/metaAttribute/index.ts +0 -60
- package/src/schemas/types/metaTag/index.ts +0 -17
- package/src/schemas/types/openGraph/index.ts +0 -114
- package/src/schemas/types/robots/index.ts +0 -26
- package/src/schemas/types/twitter/index.ts +0 -108
- package/src/types.ts +0 -108
- package/src/utils/fieldsUtils.ts +0 -160
- package/src/utils/seoUtils.ts +0 -423
- package/src/utils/utils.ts +0 -9
- package/v2-incompatible.js +0 -11
package/src/helpers/seoMeta.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Headless CMS integration helpers for sanity-plugin-seofields
|
|
3
|
-
*
|
|
4
|
-
* Provides framework-agnostic SEO metadata utilities for use with:
|
|
5
|
-
* - Next.js App Router → buildSeoMeta() inside generateMetadata()
|
|
6
|
-
* - Next.js Pages Router → <SeoMetaTags> inside Next.js <Head>
|
|
7
|
-
* - Nuxt / Remix / any SSR → <SeoMetaTags> inside your <head> slot
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {SanityImage, SanityImageWithAlt, SeoFields} from '../types'
|
|
11
|
-
|
|
12
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
/** Structured metadata returned by buildSeoMeta(). Compatible with Next.js Metadata (App Router). */
|
|
15
|
-
export interface SeoMetadata {
|
|
16
|
-
title?: string | null
|
|
17
|
-
description?: string | null
|
|
18
|
-
keywords?: string[]
|
|
19
|
-
robots?: {
|
|
20
|
-
index?: boolean
|
|
21
|
-
follow?: boolean
|
|
22
|
-
googleBot?: {
|
|
23
|
-
index?: boolean
|
|
24
|
-
follow?: boolean
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
openGraph?: {
|
|
28
|
-
type?: string
|
|
29
|
-
url?: string
|
|
30
|
-
title?: string
|
|
31
|
-
description?: string
|
|
32
|
-
siteName?: string
|
|
33
|
-
images?: Array<{url: string; width?: number; height?: number; alt?: string}>
|
|
34
|
-
}
|
|
35
|
-
twitter?: {
|
|
36
|
-
card?: string
|
|
37
|
-
site?: string
|
|
38
|
-
creator?: string
|
|
39
|
-
title?: string
|
|
40
|
-
description?: string
|
|
41
|
-
images?: string[]
|
|
42
|
-
}
|
|
43
|
-
alternates?: {
|
|
44
|
-
canonical?: string
|
|
45
|
-
}
|
|
46
|
-
/** Any custom meta attributes from seo.metaAttributes */
|
|
47
|
-
other?: Record<string, string>
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Default values used when SEO fields are missing. */
|
|
51
|
-
export interface SeoMetaDefaults {
|
|
52
|
-
title?: string
|
|
53
|
-
description?: string
|
|
54
|
-
siteName?: string
|
|
55
|
-
twitterSite?: string
|
|
56
|
-
twitterCreator?: string
|
|
57
|
-
/** Fallback image URL when no OG / Twitter image is set. */
|
|
58
|
-
ogImage?: string
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Permissive image shape accepted by buildSeoMeta — compatible with both the
|
|
63
|
-
* plugin's SanityImage and Sanity's code-generated image type (where `asset`
|
|
64
|
-
* and `alt` are optional).
|
|
65
|
-
*/
|
|
66
|
-
interface SeoImageInput {
|
|
67
|
-
_type?: string
|
|
68
|
-
asset?: {_ref: string; _type: string; _weak?: boolean; [key: string]: unknown}
|
|
69
|
-
hotspot?: unknown
|
|
70
|
-
crop?: unknown
|
|
71
|
-
alt?: string
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Input-compatible variant of SeoFields. Structurally matches Sanity's
|
|
76
|
-
* code-generated types (where `asset`, `alt`, `key`, and `type` are all
|
|
77
|
-
* optional), so you can pass `data.seo` from a sanityFetch result directly
|
|
78
|
-
* without any `as any` or manual casting.
|
|
79
|
-
*/
|
|
80
|
-
export interface SeoFieldsInput {
|
|
81
|
-
_type?: string
|
|
82
|
-
robots?: {noIndex?: boolean | null; noFollow?: boolean | null} | null
|
|
83
|
-
title?: string | null
|
|
84
|
-
description?: string | null
|
|
85
|
-
metaImage?: SeoImageInput | null
|
|
86
|
-
metaAttributes?: Array<{_key?: string; key?: string; value?: string; type?: string}> | null
|
|
87
|
-
keywords?: string[] | null
|
|
88
|
-
canonicalUrl?: string | null
|
|
89
|
-
openGraph?: {
|
|
90
|
-
_type?: string
|
|
91
|
-
url?: string | null
|
|
92
|
-
title?: string | null
|
|
93
|
-
description?: string | null
|
|
94
|
-
siteName?: string | null
|
|
95
|
-
type?: string | null
|
|
96
|
-
imageType?: string | null
|
|
97
|
-
image?: SeoImageInput | null
|
|
98
|
-
imageUrl?: string | null
|
|
99
|
-
} | null
|
|
100
|
-
twitter?: {
|
|
101
|
-
_type?: string
|
|
102
|
-
card?: string | null
|
|
103
|
-
site?: string | null
|
|
104
|
-
creator?: string | null
|
|
105
|
-
title?: string | null
|
|
106
|
-
description?: string | null
|
|
107
|
-
imageType?: string | null
|
|
108
|
-
image?: SeoImageInput | null
|
|
109
|
-
imageUrl?: string | null
|
|
110
|
-
} | null
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Options accepted by buildSeoMeta(). */
|
|
114
|
-
export interface BuildSeoMetaOptions {
|
|
115
|
-
/**
|
|
116
|
-
* The raw SEO object from Sanity (_type excluded or included — both work).
|
|
117
|
-
* Pass `null` or `undefined` to fall back entirely to `defaults`.
|
|
118
|
-
*
|
|
119
|
-
* Accepts both the strict plugin `SeoFields` type and Sanity's code-generated
|
|
120
|
-
* type (which has all nested fields optional) without any `as any` cast.
|
|
121
|
-
*/
|
|
122
|
-
seo?: SeoFieldsInput | null
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The base URL of your site, e.g. "https://example.com".
|
|
126
|
-
* Used for canonical URL and OpenGraph URL construction.
|
|
127
|
-
*/
|
|
128
|
-
baseUrl?: string
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The path for the current page, e.g. "/about".
|
|
132
|
-
* Combined with baseUrl to produce the canonical + OG url.
|
|
133
|
-
* Defaults to "".
|
|
134
|
-
*/
|
|
135
|
-
path?: string
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Default values used when the Sanity SEO fields are empty / missing.
|
|
139
|
-
*/
|
|
140
|
-
defaults?: SeoMetaDefaults
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Resolve a Sanity image asset to a plain URL string.
|
|
144
|
-
*
|
|
145
|
-
* @example (using @sanity/image-url)
|
|
146
|
-
* imageUrlResolver: (img) => urlFor(img).width(1200).url()
|
|
147
|
-
*/
|
|
148
|
-
imageUrlResolver?: (image: SanityImage | SanityImageWithAlt) => string | null | undefined
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
const VALID_OG_TYPES = [
|
|
154
|
-
'website',
|
|
155
|
-
'article',
|
|
156
|
-
'profile',
|
|
157
|
-
'book',
|
|
158
|
-
'music',
|
|
159
|
-
'video',
|
|
160
|
-
'product',
|
|
161
|
-
] as const
|
|
162
|
-
type OGType = (typeof VALID_OG_TYPES)[number]
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Coerce an arbitrary string to a valid OpenGraph type.
|
|
166
|
-
* Falls back to "website" when the value is invalid.
|
|
167
|
-
*/
|
|
168
|
-
export function sanitizeOGType(value?: string): OGType {
|
|
169
|
-
if (value && (VALID_OG_TYPES as readonly string[]).includes(value)) {
|
|
170
|
-
return value as OGType
|
|
171
|
-
}
|
|
172
|
-
return 'website'
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const VALID_TWITTER_CARDS = ['summary', 'summary_large_image', 'app', 'player'] as const
|
|
176
|
-
type TwitterCard = (typeof VALID_TWITTER_CARDS)[number]
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Coerce an arbitrary string to a valid Twitter card type.
|
|
180
|
-
* Falls back to "summary_large_image" when the value is invalid.
|
|
181
|
-
*/
|
|
182
|
-
export function sanitizeTwitterCard(value?: string): TwitterCard {
|
|
183
|
-
if (value && (VALID_TWITTER_CARDS as readonly string[]).includes(value)) {
|
|
184
|
-
return value as TwitterCard
|
|
185
|
-
}
|
|
186
|
-
return 'summary_large_image'
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ─── Core builder ─────────────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Convert a Sanity SEO object into a structured metadata object.
|
|
193
|
-
*
|
|
194
|
-
* The return value is structurally compatible with Next.js App Router's
|
|
195
|
-
* `Metadata` type, so you can return it directly from `generateMetadata()`.
|
|
196
|
-
*
|
|
197
|
-
* @example Next.js App Router
|
|
198
|
-
* ```ts
|
|
199
|
-
* import { buildSeoMeta } from 'sanity-plugin-seofields'
|
|
200
|
-
* import { urlFor } from '@/sanity/lib/image'
|
|
201
|
-
*
|
|
202
|
-
* export async function generateMetadata(): Promise<Metadata> {
|
|
203
|
-
* const { seo } = await sanityFetch({ query: PAGE_SEO_QUERY })
|
|
204
|
-
* return buildSeoMeta({
|
|
205
|
-
* seo,
|
|
206
|
-
* baseUrl: process.env.NEXT_PUBLIC_SITE_URL,
|
|
207
|
-
* path: '/about',
|
|
208
|
-
* defaults: { title: 'My Site', siteName: 'My Site' },
|
|
209
|
-
* imageUrlResolver: (img) => urlFor(img).width(1200).url(),
|
|
210
|
-
* })
|
|
211
|
-
* }
|
|
212
|
-
* ```
|
|
213
|
-
*/
|
|
214
|
-
export function buildSeoMeta(options: BuildSeoMetaOptions): SeoMetadata {
|
|
215
|
-
const {seo, baseUrl = '', path = '', defaults = {}, imageUrlResolver} = options
|
|
216
|
-
|
|
217
|
-
const normalizedBase = baseUrl.replace(/\/+$/, '') // remove trailing /
|
|
218
|
-
const normalizedPath = path.replace(/^\/+/, '') // remove leading /
|
|
219
|
-
|
|
220
|
-
const fullUrl = [normalizedBase, normalizedPath].filter(Boolean).join('/')
|
|
221
|
-
|
|
222
|
-
// ── OG image resolution ──
|
|
223
|
-
let ogImageURL: string = defaults.ogImage || ''
|
|
224
|
-
if (seo?.openGraph?.imageType === 'url' && seo.openGraph.imageUrl) {
|
|
225
|
-
ogImageURL = seo.openGraph.imageUrl
|
|
226
|
-
} else if (seo?.openGraph?.image && imageUrlResolver) {
|
|
227
|
-
ogImageURL = imageUrlResolver(seo.openGraph.image as SanityImage) || ogImageURL
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── Twitter image resolution ──
|
|
231
|
-
let twitterImageURL: string = ogImageURL // reuse OG image as fallback
|
|
232
|
-
if (seo?.twitter?.imageType === 'url' && seo.twitter.imageUrl) {
|
|
233
|
-
twitterImageURL = seo.twitter.imageUrl
|
|
234
|
-
} else if (seo?.twitter?.image && imageUrlResolver) {
|
|
235
|
-
twitterImageURL = imageUrlResolver(seo.twitter.image as SanityImage) || twitterImageURL
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ── Custom meta attributes → `other` map ──
|
|
239
|
-
const other: Record<string, string> = {}
|
|
240
|
-
if (Array.isArray(seo?.metaAttributes)) {
|
|
241
|
-
for (const attr of seo!.metaAttributes!) {
|
|
242
|
-
if (attr.key && attr.value) {
|
|
243
|
-
other[attr.key] = attr.value
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const ogUrl = seo?.openGraph?.url || fullUrl
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
title: seo?.title ?? defaults.title ?? null,
|
|
252
|
-
description: seo?.description ?? defaults.description ?? null,
|
|
253
|
-
keywords: seo?.keywords?.length ? (seo.keywords as string[]) : undefined,
|
|
254
|
-
robots: {
|
|
255
|
-
index: !seo?.robots?.noIndex,
|
|
256
|
-
follow: !seo?.robots?.noFollow,
|
|
257
|
-
googleBot: {
|
|
258
|
-
index: !seo?.robots?.noIndex,
|
|
259
|
-
follow: !seo?.robots?.noFollow,
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
openGraph: {
|
|
263
|
-
type: sanitizeOGType(seo?.openGraph?.type ?? undefined),
|
|
264
|
-
url: ogUrl || undefined,
|
|
265
|
-
title: seo?.openGraph?.title ?? defaults.title,
|
|
266
|
-
description: seo?.openGraph?.description ?? defaults.description,
|
|
267
|
-
siteName: seo?.openGraph?.siteName ?? defaults.siteName,
|
|
268
|
-
images: ogImageURL ? [{url: ogImageURL}] : [],
|
|
269
|
-
},
|
|
270
|
-
twitter: {
|
|
271
|
-
card: sanitizeTwitterCard(seo?.twitter?.card ?? undefined),
|
|
272
|
-
site: seo?.twitter?.site ?? defaults.twitterSite,
|
|
273
|
-
creator: seo?.twitter?.creator ?? defaults.twitterCreator,
|
|
274
|
-
title: seo?.twitter?.title ?? defaults.title,
|
|
275
|
-
description: seo?.twitter?.description ?? defaults.description,
|
|
276
|
-
images: twitterImageURL ? [twitterImageURL] : [],
|
|
277
|
-
},
|
|
278
|
-
alternates: {
|
|
279
|
-
canonical: fullUrl || undefined,
|
|
280
|
-
},
|
|
281
|
-
...(Object.keys(other).length > 0 ? {other} : {}),
|
|
282
|
-
}
|
|
283
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// Import the plugin
|
|
2
|
-
import seofields from './plugin'
|
|
3
|
-
|
|
4
|
-
// Default export the plugin
|
|
5
|
-
export default seofields
|
|
6
|
-
|
|
7
|
-
// Re-export everything from plugin.ts
|
|
8
|
-
export * from './plugin'
|
|
9
|
-
|
|
10
|
-
// Export schema types for external use
|
|
11
|
-
export {default as seoFieldsSchema} from './schemas'
|
|
12
|
-
export {default as allSchemas} from './schemas/types'
|
|
13
|
-
export {default as metaAttributeSchema} from './schemas/types/metaAttribute'
|
|
14
|
-
export {default as metaTagSchema} from './schemas/types/metaTag'
|
|
15
|
-
export {default as openGraphSchema} from './schemas/types/openGraph'
|
|
16
|
-
export {default as robotsSchema} from './schemas/types/robots'
|
|
17
|
-
export {default as twitterSchema} from './schemas/types/twitter'
|
|
18
|
-
|
|
19
|
-
// Export dashboard components and types
|
|
20
|
-
export {default as SeoHealthDashboard} from './components/SeoHealthDashboard'
|
|
21
|
-
export {default as SeoHealthTool} from './components/SeoHealthTool'
|
|
22
|
-
export {createSeoHealthPane} from './components/SeoHealthPane'
|
|
23
|
-
export type {SeoHealthPaneOptions} from './components/SeoHealthPane'
|
|
24
|
-
|
|
25
|
-
// Export types
|
|
26
|
-
export type {DocumentWithSeoHealth, SeoHealthMetrics, SeoHealthStatus} from './types'
|
package/src/next.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Next.js App Router helpers — safe to import in Server Components.
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* import { buildSeoMeta, SeoMetaTags } from 'sanity-plugin-seofields/next'
|
|
6
|
-
*/
|
|
7
|
-
export {buildSeoMeta, sanitizeOGType, sanitizeTwitterCard} from './helpers/seoMeta'
|
|
8
|
-
|
|
9
|
-
export type {BuildSeoMetaOptions, SeoMetaDefaults, SeoMetadata} from './helpers/seoMeta'
|
|
10
|
-
|
|
11
|
-
export {SeoMetaTags} from './helpers/SeoMetaTags'
|
|
12
|
-
export type {SeoMetaTagsProps} from './helpers/SeoMetaTags'
|
package/src/plugin.ts
DELETED
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
// plugin.ts
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import {definePlugin} from 'sanity'
|
|
4
|
-
|
|
5
|
-
import SeoHealthTool from './components/SeoHealthTool'
|
|
6
|
-
import types from './schemas/types'
|
|
7
|
-
import type {DocumentWithSeoHealth} from './types'
|
|
8
|
-
|
|
9
|
-
export interface SeoFieldConfig {
|
|
10
|
-
title?: string
|
|
11
|
-
description?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type SeoFieldKeys =
|
|
15
|
-
| 'title'
|
|
16
|
-
| 'description'
|
|
17
|
-
| 'canonicalUrl'
|
|
18
|
-
| 'metaImage'
|
|
19
|
-
| 'keywords'
|
|
20
|
-
| 'metaAttributes'
|
|
21
|
-
| 'robots'
|
|
22
|
-
|
|
23
|
-
export type openGraphFieldKeys =
|
|
24
|
-
| 'openGraphUrl'
|
|
25
|
-
| 'openGraphTitle'
|
|
26
|
-
| 'openGraphDescription'
|
|
27
|
-
| 'openGraphSiteName'
|
|
28
|
-
| 'openGraphType'
|
|
29
|
-
| 'openGraphImageType'
|
|
30
|
-
| 'openGraphImage'
|
|
31
|
-
| 'openGraphImageUrl'
|
|
32
|
-
|
|
33
|
-
export type twitterFieldKeys =
|
|
34
|
-
| 'twitterCard'
|
|
35
|
-
| 'twitterSite'
|
|
36
|
-
| 'twitterCreator'
|
|
37
|
-
| 'twitterTitle'
|
|
38
|
-
| 'twitterDescription'
|
|
39
|
-
| 'twitterImageType'
|
|
40
|
-
| 'twitterImage'
|
|
41
|
-
| 'twitterImageUrl'
|
|
42
|
-
|
|
43
|
-
export type AllFieldKeys = SeoFieldKeys | openGraphFieldKeys | twitterFieldKeys
|
|
44
|
-
|
|
45
|
-
export type ValidHiddenFieldKeys = Exclude<
|
|
46
|
-
AllFieldKeys,
|
|
47
|
-
'openGraphImageUrl' | 'twitterImageUrl' | 'openGraphImageType' | 'twitterImageType'
|
|
48
|
-
>
|
|
49
|
-
|
|
50
|
-
export interface FieldVisibilityConfig {
|
|
51
|
-
hiddenFields?: ValidHiddenFieldKeys[]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface SeoFieldsPluginConfig {
|
|
55
|
-
/**
|
|
56
|
-
* Enable or configure the SEO preview feature.
|
|
57
|
-
* If set to `true`, the SEO preview will be enabled with default settings.
|
|
58
|
-
* If set to an object, you can provide a custom prefix function to modify the URL prefix in the preview.
|
|
59
|
-
* The prefix function receives the current document as an argument and should return a string.
|
|
60
|
-
* Example:
|
|
61
|
-
* ```
|
|
62
|
-
* seoPreview: {
|
|
63
|
-
* prefix: (doc) => `/${doc.slug?.current || 'untitled'}`
|
|
64
|
-
* }
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
seoPreview?:
|
|
68
|
-
| boolean
|
|
69
|
-
| {
|
|
70
|
-
prefix?: (doc: {_type?: string} & Record<string, unknown>) => string
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* A mapping of field keys to their configuration settings.
|
|
75
|
-
* This allows customization of field titles and descriptions.
|
|
76
|
-
* For example, to change the title of the 'title' field:
|
|
77
|
-
*/
|
|
78
|
-
fieldOverrides?: Partial<Record<AllFieldKeys, SeoFieldConfig>>
|
|
79
|
-
/**
|
|
80
|
-
* A mapping of document types to field visibility configurations.
|
|
81
|
-
* This allows you to specify which fields should be hidden for specific document types.
|
|
82
|
-
*/
|
|
83
|
-
fieldVisibility?: Record<string, FieldVisibilityConfig>
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* A list of fields that should be hidden by default in all document types.
|
|
87
|
-
* This can be overridden by specific document type settings in `fieldVisibility`.
|
|
88
|
-
*/
|
|
89
|
-
defaultHiddenFields?: ValidHiddenFieldKeys[]
|
|
90
|
-
/**
|
|
91
|
-
* The base URL of your website, used for generating full URLs in the SEO preview.
|
|
92
|
-
* Defaults to 'https://www.example.com' if not provided.
|
|
93
|
-
*/
|
|
94
|
-
baseUrl?: string
|
|
95
|
-
/**
|
|
96
|
-
* Enable or configure the SEO Health Dashboard tool.
|
|
97
|
-
* If set to `true`, the dashboard is enabled with all defaults.
|
|
98
|
-
* If set to an object, you can customise the tool and dashboard settings.
|
|
99
|
-
* Defaults to `true`.
|
|
100
|
-
* Example:
|
|
101
|
-
* ```
|
|
102
|
-
* healthDashboard: {
|
|
103
|
-
* toolTitle: 'SEO Overview', // Studio nav tab label
|
|
104
|
-
* content: {
|
|
105
|
-
* icon: '🔍', // Emoji icon shown before the page heading
|
|
106
|
-
* title: 'My SEO Dashboard',// Page heading inside the tool (no emoji)
|
|
107
|
-
* description: 'Track SEO across all documents', // Subtitle under the heading
|
|
108
|
-
* },
|
|
109
|
-
* display: {
|
|
110
|
-
* typeColumn: false, // Hide the document type column (default: true)
|
|
111
|
-
* documentId: false, // Hide the document ID under titles (default: true)
|
|
112
|
-
* },
|
|
113
|
-
* query: {
|
|
114
|
-
* // Option 1 – filter by specific document types
|
|
115
|
-
* types: ['post', 'page'],
|
|
116
|
-
* // Option 2 – provide a full custom GROQ query (takes precedence over `types`)
|
|
117
|
-
* // Must return documents with at least: _id, _type, title, seo, _updatedAt
|
|
118
|
-
* groq: `*[seo != null && defined(slug.current)]{ _id, _type, title, slug, seo, _updatedAt }`,
|
|
119
|
-
* },
|
|
120
|
-
* }
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
|
-
healthDashboard?:
|
|
124
|
-
| boolean
|
|
125
|
-
| {
|
|
126
|
-
tool?: {
|
|
127
|
-
title?: string
|
|
128
|
-
name?: string
|
|
129
|
-
}
|
|
130
|
-
toolTitle?: string
|
|
131
|
-
content?: {
|
|
132
|
-
icon?: string
|
|
133
|
-
title?: string
|
|
134
|
-
description?: string
|
|
135
|
-
/** Text shown while the license key is being verified. Defaults to "Verifying license…" */
|
|
136
|
-
loadingLicense?: string
|
|
137
|
-
/** Text shown while documents are being fetched. Defaults to "Loading documents…" */
|
|
138
|
-
loadingDocuments?: string
|
|
139
|
-
/** Text shown when the query returns zero results. Defaults to "No documents found" */
|
|
140
|
-
noDocuments?: string
|
|
141
|
-
}
|
|
142
|
-
display?: {
|
|
143
|
-
typeColumn?: boolean
|
|
144
|
-
documentId?: boolean
|
|
145
|
-
}
|
|
146
|
-
query?: {
|
|
147
|
-
/**
|
|
148
|
-
* Limit the dashboard to specific document types.
|
|
149
|
-
* Example: `['post', 'page']`
|
|
150
|
-
*/
|
|
151
|
-
types?: string[]
|
|
152
|
-
/**
|
|
153
|
-
* When using `types`, also require the `seo` field to be non-null.
|
|
154
|
-
* Set to `false` to include documents of those types even if `seo` is missing.
|
|
155
|
-
* Defaults to `true`.
|
|
156
|
-
*/
|
|
157
|
-
requireSeo?: boolean
|
|
158
|
-
/**
|
|
159
|
-
* Provide a fully custom GROQ query. Takes precedence over `types`.
|
|
160
|
-
* The query must return documents with at least: _id, _type, title, seo, _updatedAt
|
|
161
|
-
*/
|
|
162
|
-
groq?: string
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* The Sanity API version to use for the client (e.g. '2023-01-01').
|
|
166
|
-
* Defaults to '2023-01-01'.
|
|
167
|
-
*/
|
|
168
|
-
apiVersion?: string
|
|
169
|
-
/**
|
|
170
|
-
* License key for the SEO Health Dashboard pro feature.
|
|
171
|
-
* Obtain a license at https://sanity-plugin-seofields.thehardik.in
|
|
172
|
-
*/
|
|
173
|
-
licenseKey?: string
|
|
174
|
-
/**
|
|
175
|
-
* Map raw `_type` values to human-readable display labels.
|
|
176
|
-
* Used in both the Type column and the Type filter dropdown.
|
|
177
|
-
* Any type without an entry falls back to the raw `_type` string.
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* typeLabels: { productDrug: 'Products', singleCondition: 'Condition' }
|
|
181
|
-
*/
|
|
182
|
-
typeLabels?: Record<string, string>
|
|
183
|
-
/**
|
|
184
|
-
* Controls how the document type is rendered in the Type column.
|
|
185
|
-
* - `'badge'` (default) — coloured pill
|
|
186
|
-
* - `'text'` — plain text, useful for dense layouts
|
|
187
|
-
*/
|
|
188
|
-
typeColumnMode?: 'badge' | 'text'
|
|
189
|
-
/**
|
|
190
|
-
* The document field to use as the display title in the dashboard.
|
|
191
|
-
*
|
|
192
|
-
* - `string` — use this field for every document type (e.g. `'name'`)
|
|
193
|
-
* - `Record<string, string>` — per-type mapping; unmapped types fall back to `title`
|
|
194
|
-
*
|
|
195
|
-
* @example
|
|
196
|
-
* titleField: 'name'
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* titleField: { post: 'title', product: 'name', category: 'label' }
|
|
200
|
-
*/
|
|
201
|
-
titleField?: string | Record<string, string>
|
|
202
|
-
/**
|
|
203
|
-
* Callback function to render a custom badge next to the document title.
|
|
204
|
-
* Receives the full document and should return badge data or undefined.
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* docBadge: (doc) => {
|
|
208
|
-
* if (doc.services === 'NHS')
|
|
209
|
-
* return { label: 'NHS', bgColor: '#e0f2fe', textColor: '#0369a1' }
|
|
210
|
-
* if (doc.services === 'Private')
|
|
211
|
-
* return { label: 'Private', bgColor: '#fef3c7', textColor: '#92400e' }
|
|
212
|
-
* }
|
|
213
|
-
*/
|
|
214
|
-
docBadge?: (
|
|
215
|
-
doc: DocumentWithSeoHealth & Record<string, unknown>,
|
|
216
|
-
) => {label: string; bgColor?: string; textColor?: string; fontSize?: string} | undefined
|
|
217
|
-
/**
|
|
218
|
-
* The `name` of the Sanity structure tool that contains the monitored documents.
|
|
219
|
-
* Required when you have multiple structure tools and the documents live in a
|
|
220
|
-
* non-default one. Clicking a title will navigate to
|
|
221
|
-
* `/{basePath}/{structureTool}/intent/edit/…` directly.
|
|
222
|
-
*
|
|
223
|
-
* @example
|
|
224
|
-
* structureTool: 'common'
|
|
225
|
-
*/
|
|
226
|
-
structureTool?: string
|
|
227
|
-
/**
|
|
228
|
-
* Enable preview/demo mode to show dummy data.
|
|
229
|
-
* Useful for testing, documentation, or showcasing the dashboard.
|
|
230
|
-
* When enabled, displays realistic sample documents with various SEO scores.
|
|
231
|
-
* Defaults to `false`.
|
|
232
|
-
*
|
|
233
|
-
* @example
|
|
234
|
-
* previewMode: true
|
|
235
|
-
*/
|
|
236
|
-
previewMode?: boolean
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
interface ResolvedDashboardConfig {
|
|
241
|
-
enabled: boolean
|
|
242
|
-
toolTitle: string
|
|
243
|
-
toolName: string
|
|
244
|
-
icon: string | undefined
|
|
245
|
-
title: string | undefined
|
|
246
|
-
description: string | undefined
|
|
247
|
-
showTypeColumn: boolean | undefined
|
|
248
|
-
showDocumentId: boolean | undefined
|
|
249
|
-
queryTypes: string[] | undefined
|
|
250
|
-
queryRequireSeo: boolean | undefined
|
|
251
|
-
queryGroq: string | undefined
|
|
252
|
-
apiVersion: string | undefined
|
|
253
|
-
licenseKey: string | undefined
|
|
254
|
-
typeLabels: Record<string, string> | undefined
|
|
255
|
-
typeColumnMode: 'badge' | 'text' | undefined
|
|
256
|
-
titleField: string | Record<string, string> | undefined
|
|
257
|
-
docBadge:
|
|
258
|
-
| ((
|
|
259
|
-
doc: DocumentWithSeoHealth & Record<string, unknown>,
|
|
260
|
-
) => {label: string; bgColor?: string; textColor?: string; fontSize?: string} | undefined)
|
|
261
|
-
| undefined
|
|
262
|
-
loadingLicense: string | undefined
|
|
263
|
-
loadingDocuments: string | undefined
|
|
264
|
-
noDocuments: string | undefined
|
|
265
|
-
previewMode: boolean | undefined
|
|
266
|
-
structureTool: string | undefined
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const resolveDashboardConfig = (
|
|
270
|
-
healthDashboard: SeoFieldsPluginConfig['healthDashboard'],
|
|
271
|
-
): ResolvedDashboardConfig => {
|
|
272
|
-
const cfg = typeof healthDashboard === 'object' ? healthDashboard : undefined
|
|
273
|
-
return {
|
|
274
|
-
enabled: healthDashboard !== false,
|
|
275
|
-
toolTitle: cfg?.tool?.title ?? 'SEO Health',
|
|
276
|
-
toolName: cfg?.tool?.name ?? 'seo-health-dashboard',
|
|
277
|
-
icon: cfg?.content?.icon,
|
|
278
|
-
title: cfg?.content?.title,
|
|
279
|
-
description: cfg?.content?.description,
|
|
280
|
-
showTypeColumn: cfg?.display?.typeColumn,
|
|
281
|
-
showDocumentId: cfg?.display?.documentId,
|
|
282
|
-
queryTypes: cfg?.query?.types,
|
|
283
|
-
queryRequireSeo: cfg?.query?.requireSeo,
|
|
284
|
-
queryGroq: cfg?.query?.groq,
|
|
285
|
-
apiVersion: cfg?.apiVersion,
|
|
286
|
-
licenseKey: cfg?.licenseKey,
|
|
287
|
-
typeLabels: cfg?.typeLabels,
|
|
288
|
-
typeColumnMode: cfg?.typeColumnMode,
|
|
289
|
-
titleField: cfg?.titleField,
|
|
290
|
-
docBadge: cfg?.docBadge,
|
|
291
|
-
loadingLicense: cfg?.content?.loadingLicense,
|
|
292
|
-
loadingDocuments: cfg?.content?.loadingDocuments,
|
|
293
|
-
noDocuments: cfg?.content?.noDocuments,
|
|
294
|
-
previewMode: cfg?.previewMode,
|
|
295
|
-
structureTool: cfg?.structureTool,
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const seofields = definePlugin<SeoFieldsPluginConfig | void>((config = {}) => {
|
|
300
|
-
const {healthDashboard = true} = config as SeoFieldsPluginConfig
|
|
301
|
-
const dash = resolveDashboardConfig(healthDashboard)
|
|
302
|
-
|
|
303
|
-
const BoundSeoHealthTool = () =>
|
|
304
|
-
React.createElement(SeoHealthTool, {
|
|
305
|
-
icon: dash.icon,
|
|
306
|
-
title: dash.title,
|
|
307
|
-
description: dash.description,
|
|
308
|
-
showTypeColumn: dash.showTypeColumn,
|
|
309
|
-
showDocumentId: dash.showDocumentId,
|
|
310
|
-
queryTypes: dash.queryTypes,
|
|
311
|
-
queryRequireSeo: dash.queryRequireSeo,
|
|
312
|
-
customQuery: dash.queryGroq,
|
|
313
|
-
apiVersion: dash.apiVersion,
|
|
314
|
-
licenseKey: dash.licenseKey,
|
|
315
|
-
typeLabels: dash.typeLabels,
|
|
316
|
-
typeColumnMode: dash.typeColumnMode,
|
|
317
|
-
titleField: dash.titleField,
|
|
318
|
-
docBadge: dash.docBadge,
|
|
319
|
-
loadingLicense: dash.loadingLicense,
|
|
320
|
-
loadingDocuments: dash.loadingDocuments,
|
|
321
|
-
noDocuments: dash.noDocuments,
|
|
322
|
-
previewMode: dash.previewMode,
|
|
323
|
-
structureTool: dash.structureTool,
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
name: 'sanity-plugin-seofields',
|
|
328
|
-
schema: {
|
|
329
|
-
types: types(config as SeoFieldsPluginConfig),
|
|
330
|
-
},
|
|
331
|
-
...(dash.enabled && {
|
|
332
|
-
tools: [
|
|
333
|
-
{
|
|
334
|
-
name: dash.toolName,
|
|
335
|
-
title: dash.toolTitle,
|
|
336
|
-
component: BoundSeoHealthTool,
|
|
337
|
-
icon: () => '📊',
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
}),
|
|
341
|
-
}
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
export default seofields
|