@duffcloudservices/cms 0.3.17 → 0.5.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/dist/chunk-TIGZ7RKI.js +456 -0
- package/dist/chunk-TIGZ7RKI.js.map +1 -0
- package/dist/index.d.ts +199 -263
- package/dist/index.js +8 -162
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +94 -16
- package/dist/plugins/index.js +168 -61
- package/dist/plugins/index.js.map +1 -1
- package/dist/vitepressTransform-DAhmD_YQ.d.ts +471 -0
- package/package.json +1 -1
- package/src/composables/useSEO.ts +21 -259
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Provides SEO configuration with build-time injection support from .dcs/seo.yaml.
|
|
5
5
|
* Generates meta tags, Open Graph, Twitter Cards, and JSON-LD structured data.
|
|
6
6
|
*
|
|
7
|
+
* The actual tag resolution lives in the framework-agnostic `../seo/headTags`
|
|
8
|
+
* module so that the build-time static-HTML emitter (`dcsSeoPlugin`) produces
|
|
9
|
+
* byte-identical output. This composable is a thin Vue/unhead wrapper over it.
|
|
10
|
+
*
|
|
7
11
|
* @example
|
|
8
12
|
* ```vue
|
|
9
13
|
* <script setup lang="ts">
|
|
@@ -28,13 +32,11 @@ import { useHead } from '@unhead/vue'
|
|
|
28
32
|
import type {
|
|
29
33
|
SeoConfiguration,
|
|
30
34
|
GlobalSeoConfig,
|
|
31
|
-
SeoOpenGraphConfig,
|
|
32
|
-
SeoTwitterConfig,
|
|
33
|
-
SeoSchemaConfig,
|
|
34
35
|
ResolvedPageSeo,
|
|
35
36
|
UseSeoReturn,
|
|
36
37
|
HeadOverrides,
|
|
37
38
|
} from '../types/seo'
|
|
39
|
+
import { buildHeadTags, resolvePageSeo, generateJsonLd } from '../seo/headTags'
|
|
38
40
|
|
|
39
41
|
// Declare the global injected by dcsSeoPlugin
|
|
40
42
|
declare const __DCS_SEO__: SeoConfiguration | undefined
|
|
@@ -54,199 +56,6 @@ function getBuildTimeSeo(): SeoConfiguration | undefined {
|
|
|
54
56
|
return undefined
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
// =============================================================================
|
|
58
|
-
// Meta Tag Generation Utilities
|
|
59
|
-
// =============================================================================
|
|
60
|
-
|
|
61
|
-
interface HeadInput {
|
|
62
|
-
title?: string
|
|
63
|
-
titleTemplate?: string | ((title: string) => string)
|
|
64
|
-
meta?: Array<{ name?: string; property?: string; content: string }>
|
|
65
|
-
link?: Array<{ rel: string; href: string; hreflang?: string }>
|
|
66
|
-
script?: Array<{ type: string; children: string }>
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Generate Open Graph meta tags from config
|
|
71
|
-
*/
|
|
72
|
-
function generateOpenGraphMeta(
|
|
73
|
-
og: SeoOpenGraphConfig,
|
|
74
|
-
global: GlobalSeoConfig,
|
|
75
|
-
pageTitle: string,
|
|
76
|
-
pageDescription: string,
|
|
77
|
-
canonical: string
|
|
78
|
-
): Array<{ property: string; content: string }> {
|
|
79
|
-
const tags: Array<{ property: string; content: string }> = []
|
|
80
|
-
|
|
81
|
-
tags.push({ property: 'og:title', content: og.title || pageTitle })
|
|
82
|
-
tags.push({ property: 'og:description', content: og.description || pageDescription })
|
|
83
|
-
tags.push({ property: 'og:url', content: og.url || canonical })
|
|
84
|
-
tags.push({ property: 'og:type', content: og.type || 'website' })
|
|
85
|
-
|
|
86
|
-
const image = og.image || global.images?.ogDefault
|
|
87
|
-
if (image) {
|
|
88
|
-
tags.push({ property: 'og:image', content: image })
|
|
89
|
-
if (og.imageAlt || pageTitle) {
|
|
90
|
-
tags.push({ property: 'og:image:alt', content: og.imageAlt || pageTitle })
|
|
91
|
-
}
|
|
92
|
-
if (og.imageWidth) {
|
|
93
|
-
tags.push({ property: 'og:image:width', content: String(og.imageWidth) })
|
|
94
|
-
}
|
|
95
|
-
if (og.imageHeight) {
|
|
96
|
-
tags.push({ property: 'og:image:height', content: String(og.imageHeight) })
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (global.siteName) {
|
|
101
|
-
tags.push({ property: 'og:site_name', content: global.siteName })
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (global.locale) {
|
|
105
|
-
tags.push({ property: 'og:locale', content: global.locale })
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Article-specific tags
|
|
109
|
-
if (og.type === 'article') {
|
|
110
|
-
if (og.publishedTime) {
|
|
111
|
-
tags.push({ property: 'article:published_time', content: og.publishedTime })
|
|
112
|
-
}
|
|
113
|
-
if (og.modifiedTime) {
|
|
114
|
-
tags.push({ property: 'article:modified_time', content: og.modifiedTime })
|
|
115
|
-
}
|
|
116
|
-
if (og.author) {
|
|
117
|
-
tags.push({ property: 'article:author', content: og.author })
|
|
118
|
-
}
|
|
119
|
-
if (og.section) {
|
|
120
|
-
tags.push({ property: 'article:section', content: og.section })
|
|
121
|
-
}
|
|
122
|
-
if (og.tags) {
|
|
123
|
-
og.tags.forEach((tag) => {
|
|
124
|
-
tags.push({ property: 'article:tag', content: tag })
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return tags
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Generate Twitter Card meta tags from config
|
|
134
|
-
*/
|
|
135
|
-
function generateTwitterMeta(
|
|
136
|
-
twitter: SeoTwitterConfig,
|
|
137
|
-
global: GlobalSeoConfig,
|
|
138
|
-
pageTitle: string,
|
|
139
|
-
pageDescription: string
|
|
140
|
-
): Array<{ name: string; content: string }> {
|
|
141
|
-
const tags: Array<{ name: string; content: string }> = []
|
|
142
|
-
|
|
143
|
-
tags.push({ name: 'twitter:card', content: twitter.card || 'summary_large_image' })
|
|
144
|
-
tags.push({ name: 'twitter:title', content: twitter.title || pageTitle })
|
|
145
|
-
tags.push({ name: 'twitter:description', content: twitter.description || pageDescription })
|
|
146
|
-
|
|
147
|
-
const image = twitter.image || global.images?.twitterDefault
|
|
148
|
-
if (image) {
|
|
149
|
-
tags.push({ name: 'twitter:image', content: image })
|
|
150
|
-
if (twitter.imageAlt || pageTitle) {
|
|
151
|
-
tags.push({ name: 'twitter:image:alt', content: twitter.imageAlt || pageTitle })
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const site = twitter.site || global.social?.twitter
|
|
156
|
-
if (site) {
|
|
157
|
-
tags.push({ name: 'twitter:site', content: site.startsWith('@') ? site : `@${site}` })
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (twitter.creator) {
|
|
161
|
-
tags.push({
|
|
162
|
-
name: 'twitter:creator',
|
|
163
|
-
content: twitter.creator.startsWith('@') ? twitter.creator : `@${twitter.creator}`,
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return tags
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Generate JSON-LD script content from schemas
|
|
172
|
-
*/
|
|
173
|
-
function generateJsonLd(schemas: SeoSchemaConfig[], global: GlobalSeoConfig): object[] {
|
|
174
|
-
return schemas.map((schema) => {
|
|
175
|
-
const base: Record<string, unknown> = {
|
|
176
|
-
'@context': 'https://schema.org',
|
|
177
|
-
'@type': schema.type,
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Merge properties
|
|
181
|
-
if (schema.properties) {
|
|
182
|
-
Object.assign(base, schema.properties)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Auto-populate common properties from global config
|
|
186
|
-
if (schema.type === 'WebSite' && global.siteUrl && !base.url) {
|
|
187
|
-
base.url = global.siteUrl
|
|
188
|
-
}
|
|
189
|
-
if (schema.type === 'WebSite' && global.siteName && !base.name) {
|
|
190
|
-
base.name = global.siteName
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return base
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Resolve page SEO by merging global defaults with page-specific config
|
|
199
|
-
*/
|
|
200
|
-
function resolvePageSeo(
|
|
201
|
-
pageSlug: string,
|
|
202
|
-
pagePath: string | undefined,
|
|
203
|
-
seoConfig: SeoConfiguration | undefined
|
|
204
|
-
): ResolvedPageSeo {
|
|
205
|
-
const global = seoConfig?.global ?? {}
|
|
206
|
-
const page = seoConfig?.pages?.[pageSlug] ?? {}
|
|
207
|
-
|
|
208
|
-
// Build canonical URL
|
|
209
|
-
let canonical = page.canonical || ''
|
|
210
|
-
if (!canonical && global.siteUrl) {
|
|
211
|
-
const path = pagePath ?? (pageSlug === 'home' ? '/' : `/${pageSlug}`)
|
|
212
|
-
canonical = `${global.siteUrl.replace(/\/$/, '')}${path}`
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Build title
|
|
216
|
-
let title = page.title || global.defaultTitle || pageSlug
|
|
217
|
-
if (!page.noTitleTemplate && global.titleTemplate) {
|
|
218
|
-
title = global.titleTemplate.replace('%s', title)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Merge Open Graph
|
|
222
|
-
const openGraph: ResolvedPageSeo['openGraph'] = {
|
|
223
|
-
type: page.openGraph?.type || 'website',
|
|
224
|
-
title: page.openGraph?.title || page.title || global.defaultTitle || '',
|
|
225
|
-
description: page.openGraph?.description || page.description || global.defaultDescription || '',
|
|
226
|
-
...page.openGraph,
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Merge Twitter
|
|
230
|
-
const twitter: ResolvedPageSeo['twitter'] = {
|
|
231
|
-
card: page.twitter?.card || 'summary_large_image',
|
|
232
|
-
...page.twitter,
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Combine schemas (global + page)
|
|
236
|
-
const schemas = [...(global.schemas ?? []), ...(page.schemas ?? [])]
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
title,
|
|
240
|
-
description: page.description || global.defaultDescription || '',
|
|
241
|
-
canonical,
|
|
242
|
-
robots: page.robots || global.robots || 'index, follow',
|
|
243
|
-
openGraph,
|
|
244
|
-
twitter,
|
|
245
|
-
schemas,
|
|
246
|
-
alternates: page.alternates ?? [],
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
59
|
/**
|
|
251
60
|
* useSEO composable for DCS-managed SEO configuration.
|
|
252
61
|
*
|
|
@@ -278,78 +87,31 @@ export function useSEO(pageSlug: string, pagePath?: string): UseSeoReturn {
|
|
|
278
87
|
}
|
|
279
88
|
|
|
280
89
|
/**
|
|
281
|
-
* Apply all meta tags via useHead
|
|
90
|
+
* Apply all meta tags via useHead.
|
|
91
|
+
*
|
|
92
|
+
* Delegates to the shared `buildHeadTags` resolver so the emitted tags match
|
|
93
|
+
* the build-time static-HTML emitter exactly. Keywords are intentionally not
|
|
94
|
+
* emitted at runtime (historical behaviour), so `includeKeywords` is omitted.
|
|
282
95
|
*/
|
|
283
96
|
function applyHead(overrides?: HeadOverrides): void {
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// Build meta tags
|
|
291
|
-
const meta: HeadInput['meta'] = []
|
|
292
|
-
|
|
293
|
-
// Basic meta
|
|
294
|
-
meta.push({ name: 'description', content: description })
|
|
295
|
-
if (resolved.robots) {
|
|
296
|
-
meta.push({ name: 'robots', content: resolved.robots })
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Verification codes
|
|
300
|
-
if (global.verification?.google) {
|
|
301
|
-
meta.push({ name: 'google-site-verification', content: global.verification.google })
|
|
302
|
-
}
|
|
303
|
-
if (global.verification?.bing) {
|
|
304
|
-
meta.push({ name: 'msvalidate.01', content: global.verification.bing })
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Open Graph
|
|
308
|
-
const ogMeta = generateOpenGraphMeta(
|
|
309
|
-
resolved.openGraph,
|
|
310
|
-
global,
|
|
311
|
-
title,
|
|
312
|
-
description,
|
|
313
|
-
resolved.canonical
|
|
314
|
-
)
|
|
315
|
-
meta.push(...ogMeta.map((t) => ({ property: t.property, content: t.content })))
|
|
316
|
-
|
|
317
|
-
// Twitter
|
|
318
|
-
const twitterMeta = generateTwitterMeta(resolved.twitter, global, title, description)
|
|
319
|
-
meta.push(...twitterMeta.map((t) => ({ name: t.name, content: t.content })))
|
|
320
|
-
|
|
321
|
-
// Additional overrides
|
|
322
|
-
if (overrides?.meta) {
|
|
323
|
-
meta.push(...overrides.meta)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Build links
|
|
327
|
-
const link: HeadInput['link'] = []
|
|
328
|
-
|
|
329
|
-
// Canonical
|
|
330
|
-
if (resolved.canonical) {
|
|
331
|
-
link.push({ rel: 'canonical', href: resolved.canonical })
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Alternate languages
|
|
335
|
-
resolved.alternates.forEach((alt) => {
|
|
336
|
-
link.push({ rel: 'alternate', href: alt.href, hreflang: alt.hreflang })
|
|
97
|
+
const { title, meta, link, script } = buildHeadTags(pageSlug, pagePath, seoConfig, {
|
|
98
|
+
title: overrides?.title,
|
|
99
|
+
description: overrides?.description,
|
|
100
|
+
keywords: overrides?.keywords,
|
|
101
|
+
schemas: overrides?.schemas,
|
|
102
|
+
meta: overrides?.meta,
|
|
337
103
|
})
|
|
338
104
|
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
children: JSON.stringify(schema),
|
|
344
|
-
}))
|
|
345
|
-
|
|
346
|
-
// Apply via useHead
|
|
105
|
+
// Apply via useHead. The shared resolver returns framework-agnostic tag
|
|
106
|
+
// shapes (HeadMetaTag/HeadLinkTag/HeadScriptTag); unhead's input types are
|
|
107
|
+
// structurally compatible but add an open-ended `data-*` index signature,
|
|
108
|
+
// so we hand them over via useHead's Head input type. Runtime is identical.
|
|
347
109
|
useHead({
|
|
348
110
|
title,
|
|
349
111
|
meta,
|
|
350
112
|
link,
|
|
351
113
|
script,
|
|
352
|
-
})
|
|
114
|
+
} as unknown as Parameters<typeof useHead>[0])
|
|
353
115
|
}
|
|
354
116
|
|
|
355
117
|
return {
|