sanity-plugin-seofields 1.2.3 → 1.2.5
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.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +570 -448
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +556 -434
- package/dist/index.mjs.map +1 -1
- package/dist/next.d.mts +334 -0
- package/dist/next.d.ts +334 -0
- package/dist/next.js +105 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +102 -0
- package/dist/next.mjs.map +1 -0
- package/package.json +13 -1
- package/src/helpers/SeoMetaTags.tsx +154 -0
- package/src/helpers/seoMeta.ts +283 -0
- package/src/next.ts +12 -0
- package/src/types.ts +6 -2
|
@@ -0,0 +1,283 @@
|
|
|
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/next.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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/types.ts
CHANGED
|
@@ -35,8 +35,8 @@ export interface RobotsSettings {
|
|
|
35
35
|
// Meta Attribute
|
|
36
36
|
export interface MetaAttribute {
|
|
37
37
|
_type: 'metaAttribute'
|
|
38
|
-
key
|
|
39
|
-
type
|
|
38
|
+
key?: string
|
|
39
|
+
type?: 'string' | 'image'
|
|
40
40
|
value?: string
|
|
41
41
|
image?: SanityImage
|
|
42
42
|
}
|
|
@@ -44,6 +44,8 @@ export interface MetaAttribute {
|
|
|
44
44
|
// Open Graph settings
|
|
45
45
|
export interface OpenGraphSettings {
|
|
46
46
|
_type: 'openGraph'
|
|
47
|
+
/** The canonical URL for OpenGraph (og:url). Maps to the `url` field in Sanity. */
|
|
48
|
+
url?: string
|
|
47
49
|
title?: string
|
|
48
50
|
description?: string
|
|
49
51
|
siteName?: string
|
|
@@ -58,6 +60,7 @@ export interface TwitterCardSettings {
|
|
|
58
60
|
_type: 'twitter'
|
|
59
61
|
card?: 'summary' | 'summary_large_image' | 'app' | 'player'
|
|
60
62
|
site?: string
|
|
63
|
+
creator?: string
|
|
61
64
|
title?: string
|
|
62
65
|
description?: string
|
|
63
66
|
imageType?: 'upload' | 'url'
|
|
@@ -73,6 +76,7 @@ export interface SeoFields {
|
|
|
73
76
|
title?: string
|
|
74
77
|
description?: string
|
|
75
78
|
metaImage?: SanityImage
|
|
79
|
+
metaAttributes?: MetaAttribute[]
|
|
76
80
|
keywords?: string[]
|
|
77
81
|
canonicalUrl?: string
|
|
78
82
|
openGraph?: OpenGraphSettings
|