@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
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as vue from 'vue';
|
|
2
2
|
import { ComputedRef, MaybeRefOrGetter } from 'vue';
|
|
3
|
+
import { U as UseSeoReturn, G as GlobalSeoConfig, S as SeoConfiguration, R as ResolvedPageSeo, a as SeoSchemaConfig, b as SeoOpenGraphConfig, c as SeoTwitterConfig } from './vitepressTransform-DAhmD_YQ.js';
|
|
4
|
+
export { C as CreateSeoTransformPageDataOptions, H as HeadOverrides, P as PageSeoConfig, i as ResolvedPageOverrides, n as SeoAlternateConfig, k as SeoAuthorConfig, m as SeoImagesConfig, g as SeoPageContext, h as SeoPageTypeRule, l as SeoSocialConfig, o as SeoVerificationConfig, p as UseSeoConfig, j as VitePressHeadConfig, V as VitePressPageData, e as buildVitePressSeoHead, d as createSeoTransformPageData, f as defaultRelativePathToRoute } from './vitepressTransform-DAhmD_YQ.js';
|
|
3
5
|
import { ImageContext, ResponsiveImageResult } from '@duffcloudservices/cms-core';
|
|
4
6
|
export { ImageContext, ResponsiveImageOptions, ResponsiveImageResult, ResponsiveSource, isCdnAssetUrl, resolveResponsiveImage } from '@duffcloudservices/cms-core';
|
|
5
7
|
|
|
@@ -106,274 +108,16 @@ interface TextContentReturn {
|
|
|
106
108
|
*/
|
|
107
109
|
declare function useTextContent(config: TextContentConfig): TextContentReturn;
|
|
108
110
|
|
|
109
|
-
/**
|
|
110
|
-
* Types for .dcs/seo.yaml structure
|
|
111
|
-
* Matches contracts/generated/schemas/seo.json
|
|
112
|
-
*/
|
|
113
|
-
/**
|
|
114
|
-
* Root structure of .dcs/seo.yaml
|
|
115
|
-
*/
|
|
116
|
-
interface SeoConfiguration {
|
|
117
|
-
/** Schema version */
|
|
118
|
-
version: number;
|
|
119
|
-
/** ISO timestamp of last update */
|
|
120
|
-
lastUpdated?: string;
|
|
121
|
-
/** Email or identifier of who made the update */
|
|
122
|
-
updatedBy?: string;
|
|
123
|
-
/** Global/site-wide SEO defaults */
|
|
124
|
-
global?: GlobalSeoConfig;
|
|
125
|
-
/** Page-specific SEO configurations keyed by page slug */
|
|
126
|
-
pages?: Record<string, PageSeoConfig>;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Global/site-wide SEO configuration
|
|
130
|
-
*/
|
|
131
|
-
interface GlobalSeoConfig {
|
|
132
|
-
/** Site name used in titles and structured data */
|
|
133
|
-
siteName?: string;
|
|
134
|
-
/** Base URL of the site (e.g., https://example.com) */
|
|
135
|
-
siteUrl?: string;
|
|
136
|
-
/** Locale for Open Graph (e.g., en_US) */
|
|
137
|
-
locale?: string;
|
|
138
|
-
/** Default page title */
|
|
139
|
-
defaultTitle?: string;
|
|
140
|
-
/** Default meta description */
|
|
141
|
-
defaultDescription?: string;
|
|
142
|
-
/** Title template with %s placeholder (e.g., "%s | Site Name") */
|
|
143
|
-
titleTemplate?: string;
|
|
144
|
-
/** Author information for structured data */
|
|
145
|
-
author?: SeoAuthorConfig;
|
|
146
|
-
/** Social media handles */
|
|
147
|
-
social?: SeoSocialConfig;
|
|
148
|
-
/** Default images for social sharing */
|
|
149
|
-
images?: SeoImagesConfig;
|
|
150
|
-
/** Default robots directive (e.g., "index, follow") */
|
|
151
|
-
robots?: string;
|
|
152
|
-
/** Global JSON-LD schemas (Organization, WebSite, etc.) */
|
|
153
|
-
schemas?: SeoSchemaConfig[];
|
|
154
|
-
/** Search engine verification codes */
|
|
155
|
-
verification?: SeoVerificationConfig;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Page-specific SEO configuration
|
|
159
|
-
*/
|
|
160
|
-
interface PageSeoConfig {
|
|
161
|
-
/** Page title */
|
|
162
|
-
title?: string;
|
|
163
|
-
/** Meta description */
|
|
164
|
-
description?: string;
|
|
165
|
-
/** Meta keywords (comma-separated) */
|
|
166
|
-
keywords?: string;
|
|
167
|
-
/** Canonical URL */
|
|
168
|
-
canonical?: string;
|
|
169
|
-
/** Page-specific robots directive */
|
|
170
|
-
robots?: string;
|
|
171
|
-
/** Open Graph configuration */
|
|
172
|
-
openGraph?: SeoOpenGraphConfig;
|
|
173
|
-
/** Twitter Card configuration */
|
|
174
|
-
twitter?: SeoTwitterConfig;
|
|
175
|
-
/** Page-specific JSON-LD schemas */
|
|
176
|
-
schemas?: SeoSchemaConfig[];
|
|
177
|
-
/** Alternate language links */
|
|
178
|
-
alternates?: SeoAlternateConfig[];
|
|
179
|
-
/** If true, don't apply titleTemplate to this page */
|
|
180
|
-
noTitleTemplate?: boolean;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Author information for structured data
|
|
184
|
-
*/
|
|
185
|
-
interface SeoAuthorConfig {
|
|
186
|
-
/** Author name */
|
|
187
|
-
name?: string;
|
|
188
|
-
/** Author email */
|
|
189
|
-
email?: string;
|
|
190
|
-
/** Author image URL */
|
|
191
|
-
image?: string;
|
|
192
|
-
/** Job title */
|
|
193
|
-
jobTitle?: string;
|
|
194
|
-
/** Social profile URLs */
|
|
195
|
-
sameAs?: string[];
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Social media handles
|
|
199
|
-
*/
|
|
200
|
-
interface SeoSocialConfig {
|
|
201
|
-
/** Twitter handle (without @) */
|
|
202
|
-
twitter?: string;
|
|
203
|
-
/** LinkedIn company or profile slug */
|
|
204
|
-
linkedin?: string;
|
|
205
|
-
/** GitHub username */
|
|
206
|
-
github?: string;
|
|
207
|
-
/** Facebook page name */
|
|
208
|
-
facebook?: string;
|
|
209
|
-
/** Instagram username */
|
|
210
|
-
instagram?: string;
|
|
211
|
-
/** YouTube channel */
|
|
212
|
-
youtube?: string;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Default images for social sharing
|
|
216
|
-
*/
|
|
217
|
-
interface SeoImagesConfig {
|
|
218
|
-
/** Logo image URL */
|
|
219
|
-
logo?: string;
|
|
220
|
-
/** Default Open Graph image */
|
|
221
|
-
ogDefault?: string;
|
|
222
|
-
/** Default Twitter Card image */
|
|
223
|
-
twitterDefault?: string;
|
|
224
|
-
/** Favicon URL */
|
|
225
|
-
favicon?: string;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Open Graph meta configuration
|
|
229
|
-
*/
|
|
230
|
-
interface SeoOpenGraphConfig {
|
|
231
|
-
/** OG title (defaults to page title) */
|
|
232
|
-
title?: string;
|
|
233
|
-
/** OG description (defaults to page description) */
|
|
234
|
-
description?: string;
|
|
235
|
-
/** OG image URL */
|
|
236
|
-
image?: string;
|
|
237
|
-
/** Alt text for OG image */
|
|
238
|
-
imageAlt?: string;
|
|
239
|
-
/** OG image width in pixels */
|
|
240
|
-
imageWidth?: number;
|
|
241
|
-
/** OG image height in pixels */
|
|
242
|
-
imageHeight?: number;
|
|
243
|
-
/** OG type */
|
|
244
|
-
type?: 'website' | 'article' | 'profile' | 'book' | 'music.song' | 'music.album' | 'video.movie' | 'video.episode' | 'video.tv_show' | 'video.other';
|
|
245
|
-
/** OG URL (defaults to canonical) */
|
|
246
|
-
url?: string;
|
|
247
|
-
/** Article published time (ISO 8601) */
|
|
248
|
-
publishedTime?: string;
|
|
249
|
-
/** Article modified time (ISO 8601) */
|
|
250
|
-
modifiedTime?: string;
|
|
251
|
-
/** Article author */
|
|
252
|
-
author?: string;
|
|
253
|
-
/** Article section/category */
|
|
254
|
-
section?: string;
|
|
255
|
-
/** Article tags */
|
|
256
|
-
tags?: string[];
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Twitter Card configuration
|
|
260
|
-
*/
|
|
261
|
-
interface SeoTwitterConfig {
|
|
262
|
-
/** Card type */
|
|
263
|
-
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
264
|
-
/** Twitter title */
|
|
265
|
-
title?: string;
|
|
266
|
-
/** Twitter description */
|
|
267
|
-
description?: string;
|
|
268
|
-
/** Twitter image URL */
|
|
269
|
-
image?: string;
|
|
270
|
-
/** Alt text for Twitter image */
|
|
271
|
-
imageAlt?: string;
|
|
272
|
-
/** Site's Twitter handle (without @) */
|
|
273
|
-
site?: string;
|
|
274
|
-
/** Content creator's Twitter handle (without @) */
|
|
275
|
-
creator?: string;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* JSON-LD schema configuration
|
|
279
|
-
*/
|
|
280
|
-
interface SeoSchemaConfig {
|
|
281
|
-
/** Schema.org type (e.g., "WebSite", "Organization", "Article") */
|
|
282
|
-
type: string;
|
|
283
|
-
/** Schema properties */
|
|
284
|
-
properties?: Record<string, unknown>;
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Alternate language link
|
|
288
|
-
*/
|
|
289
|
-
interface SeoAlternateConfig {
|
|
290
|
-
/** Language code (e.g., "en", "es", "x-default") */
|
|
291
|
-
hreflang: string;
|
|
292
|
-
/** URL of alternate version */
|
|
293
|
-
href: string;
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Search engine verification codes
|
|
297
|
-
*/
|
|
298
|
-
interface SeoVerificationConfig {
|
|
299
|
-
/** Google Search Console verification code */
|
|
300
|
-
google?: string;
|
|
301
|
-
/** Bing Webmaster Tools verification code */
|
|
302
|
-
bing?: string;
|
|
303
|
-
/** DuckDuckGo verification (reserved for future use) */
|
|
304
|
-
duckduckgo?: string;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Resolved page SEO configuration (after merging global + page)
|
|
308
|
-
*/
|
|
309
|
-
interface ResolvedPageSeo {
|
|
310
|
-
/** Final page title */
|
|
311
|
-
title: string;
|
|
312
|
-
/** Final meta description */
|
|
313
|
-
description: string;
|
|
314
|
-
/** Final canonical URL */
|
|
315
|
-
canonical: string;
|
|
316
|
-
/** Final robots directive */
|
|
317
|
-
robots: string;
|
|
318
|
-
/** Merged Open Graph configuration */
|
|
319
|
-
openGraph: Required<Pick<SeoOpenGraphConfig, 'title' | 'description' | 'type'>> & SeoOpenGraphConfig;
|
|
320
|
-
/** Merged Twitter configuration */
|
|
321
|
-
twitter: Required<Pick<SeoTwitterConfig, 'card'>> & SeoTwitterConfig;
|
|
322
|
-
/** All schemas (global + page) */
|
|
323
|
-
schemas: SeoSchemaConfig[];
|
|
324
|
-
/** Alternate links */
|
|
325
|
-
alternates: SeoAlternateConfig[];
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Configuration for useSEO composable
|
|
329
|
-
*/
|
|
330
|
-
interface UseSeoConfig {
|
|
331
|
-
/** Page slug matching entry in seo.yaml */
|
|
332
|
-
pageSlug: string;
|
|
333
|
-
/** Optional page path for canonical URL generation */
|
|
334
|
-
pagePath?: string;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Return type of useSEO composable
|
|
338
|
-
*/
|
|
339
|
-
interface UseSeoReturn {
|
|
340
|
-
/** Computed page SEO configuration */
|
|
341
|
-
config: vue.ComputedRef<ResolvedPageSeo>;
|
|
342
|
-
/** Apply all meta tags via useHead */
|
|
343
|
-
applyHead: (overrides?: HeadOverrides) => void;
|
|
344
|
-
/** Get JSON-LD schema objects for the page */
|
|
345
|
-
getSchema: () => object[];
|
|
346
|
-
/** Get canonical URL for the page */
|
|
347
|
-
getCanonical: () => string;
|
|
348
|
-
/** Whether SEO config was loaded from build-time */
|
|
349
|
-
hasBuildTimeSeo: boolean;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Overrides that can be passed to applyHead
|
|
353
|
-
*/
|
|
354
|
-
interface HeadOverrides {
|
|
355
|
-
/** Override title */
|
|
356
|
-
title?: string;
|
|
357
|
-
/** Override description */
|
|
358
|
-
description?: string;
|
|
359
|
-
/** Override keywords meta tag */
|
|
360
|
-
keywords?: string;
|
|
361
|
-
/** Additional or replacement schemas */
|
|
362
|
-
schemas?: object[];
|
|
363
|
-
/** Additional meta tags */
|
|
364
|
-
meta?: Array<{
|
|
365
|
-
name?: string;
|
|
366
|
-
property?: string;
|
|
367
|
-
content: string;
|
|
368
|
-
}>;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
111
|
/**
|
|
372
112
|
* useSEO Composable
|
|
373
113
|
*
|
|
374
114
|
* Provides SEO configuration with build-time injection support from .dcs/seo.yaml.
|
|
375
115
|
* Generates meta tags, Open Graph, Twitter Cards, and JSON-LD structured data.
|
|
376
116
|
*
|
|
117
|
+
* The actual tag resolution lives in the framework-agnostic `../seo/headTags`
|
|
118
|
+
* module so that the build-time static-HTML emitter (`dcsSeoPlugin`) produces
|
|
119
|
+
* byte-identical output. This composable is a thin Vue/unhead wrapper over it.
|
|
120
|
+
*
|
|
377
121
|
* @example
|
|
378
122
|
* ```vue
|
|
379
123
|
* <script setup lang="ts">
|
|
@@ -418,6 +162,198 @@ declare function useSEO(pageSlug: string, pagePath?: string): UseSeoReturn;
|
|
|
418
162
|
*/
|
|
419
163
|
declare function createSiteSEO(_siteDefaults: Partial<GlobalSeoConfig>): (pageSlug: string, pagePath?: string) => UseSeoReturn;
|
|
420
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Framework-agnostic SEO head-tag resolution.
|
|
167
|
+
*
|
|
168
|
+
* This module is the single source of truth for turning a
|
|
169
|
+
* (`pageSlug`, `pagePath`, `SeoConfiguration`) triple into a plain,
|
|
170
|
+
* serialisable description of the `<head>` tags a page should carry:
|
|
171
|
+
* resolved title, meta[], link[], and JSON-LD script[].
|
|
172
|
+
*
|
|
173
|
+
* It is consumed by:
|
|
174
|
+
* - `useSEO` (runtime, via `@unhead/vue`) — see `../composables/useSEO.ts`
|
|
175
|
+
* - `dcsSeoPlugin`'s build-time static-HTML emitter — see
|
|
176
|
+
* `../plugins/dcsSeoPlugin.ts`
|
|
177
|
+
*
|
|
178
|
+
* Keeping the resolution here (rather than inside the Vue composable) means
|
|
179
|
+
* the runtime and the build-time emitter produce byte-identical tags from the
|
|
180
|
+
* same `seo.yaml`, with no Vue/unhead dependency required at build time.
|
|
181
|
+
*
|
|
182
|
+
* Runtime behaviour is intentionally identical to the previous in-composable
|
|
183
|
+
* logic **except** for one corrected bug: `og:title` now falls back to the
|
|
184
|
+
* fully-resolved (template-applied) page title instead of the raw, untemplated
|
|
185
|
+
* `page.title`. Previously `og:title` could diverge from the `<title>` element
|
|
186
|
+
* (e.g. `<title>Iron Oak Contractors | Our Services</title>` but
|
|
187
|
+
* `og:title = "Our Services"`).
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/** A `<meta>` tag — either a `name=`/`content=` or `property=`/`content=` pair. */
|
|
191
|
+
interface HeadMetaTag {
|
|
192
|
+
name?: string;
|
|
193
|
+
property?: string;
|
|
194
|
+
content: string;
|
|
195
|
+
}
|
|
196
|
+
/** A `<link>` tag (canonical, alternate, etc.). */
|
|
197
|
+
interface HeadLinkTag {
|
|
198
|
+
rel: string;
|
|
199
|
+
href: string;
|
|
200
|
+
hreflang?: string;
|
|
201
|
+
}
|
|
202
|
+
/** A `<script type="application/ld+json">` tag carrying serialised JSON-LD. */
|
|
203
|
+
interface HeadScriptTag {
|
|
204
|
+
type: string;
|
|
205
|
+
/** Pre-serialised JSON-LD string (already `JSON.stringify`-ed). */
|
|
206
|
+
children: string;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The complete, framework-agnostic set of resolved `<head>` tags for a page.
|
|
210
|
+
*
|
|
211
|
+
* - `title` is the final, template-applied title (what goes in `<title>`).
|
|
212
|
+
* - `meta` covers description, keywords, robots, verification, OG, Twitter.
|
|
213
|
+
* - `link` covers canonical + hreflang alternates.
|
|
214
|
+
* - `script` covers JSON-LD (global + page schemas).
|
|
215
|
+
* - `jsonLd` is the same JSON-LD as parsed objects, for callers that want the
|
|
216
|
+
* structured form (e.g. `useSEO().getSchema()`).
|
|
217
|
+
*/
|
|
218
|
+
interface ResolvedHeadTags {
|
|
219
|
+
title: string;
|
|
220
|
+
meta: HeadMetaTag[];
|
|
221
|
+
link: HeadLinkTag[];
|
|
222
|
+
script: HeadScriptTag[];
|
|
223
|
+
jsonLd: object[];
|
|
224
|
+
/** The fully-resolved page SEO (merged global + page) used to build tags. */
|
|
225
|
+
resolved: ResolvedPageSeo;
|
|
226
|
+
}
|
|
227
|
+
/** Optional overrides applied on top of the resolved config when building tags. */
|
|
228
|
+
interface HeadTagOverrides {
|
|
229
|
+
/** Override the resolved `<title>`. */
|
|
230
|
+
title?: string;
|
|
231
|
+
/**
|
|
232
|
+
* Page-specific title fallback used when `seo.yaml` has no `title` for this
|
|
233
|
+
* page (e.g. the route `title` from `pages.yaml`). Unlike `global.defaultTitle`
|
|
234
|
+
* this IS run through `titleTemplate`, so un-configured routes (blog posts,
|
|
235
|
+
* etc.) get unique titles rather than the global default.
|
|
236
|
+
*/
|
|
237
|
+
fallbackTitle?: string;
|
|
238
|
+
/** Override the resolved meta description. */
|
|
239
|
+
description?: string;
|
|
240
|
+
/** Override the meta keywords value. */
|
|
241
|
+
keywords?: string;
|
|
242
|
+
/**
|
|
243
|
+
* Force the robots directive (e.g. `'noindex, nofollow'`). When supplied this
|
|
244
|
+
* wins over both page- and global-level robots.
|
|
245
|
+
*/
|
|
246
|
+
robots?: string;
|
|
247
|
+
/** Replace the JSON-LD schema objects entirely (already-built objects). */
|
|
248
|
+
schemas?: object[];
|
|
249
|
+
/** Extra meta tags appended after the generated ones. */
|
|
250
|
+
meta?: HeadMetaTag[];
|
|
251
|
+
/**
|
|
252
|
+
* Emit a `<meta name="keywords">` tag from the page's `keywords` field.
|
|
253
|
+
*
|
|
254
|
+
* Defaults to `false` so the `useSEO` runtime path stays byte-identical to
|
|
255
|
+
* its historical output (which never emitted keywords). The static-HTML
|
|
256
|
+
* emitter opts in (`true`) to surface page keywords in the baked `<head>`.
|
|
257
|
+
*/
|
|
258
|
+
includeKeywords?: boolean;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Generate Open Graph meta tags from config.
|
|
262
|
+
*
|
|
263
|
+
* @param resolvedTitle - the final, template-applied page title. Used as the
|
|
264
|
+
* `og:title` fallback so OG stays consistent with `<title>`.
|
|
265
|
+
*/
|
|
266
|
+
declare function generateOpenGraphMeta(og: SeoOpenGraphConfig, global: GlobalSeoConfig, resolvedTitle: string, pageDescription: string, canonical: string): Array<{
|
|
267
|
+
property: string;
|
|
268
|
+
content: string;
|
|
269
|
+
}>;
|
|
270
|
+
/**
|
|
271
|
+
* Generate Twitter Card meta tags from config.
|
|
272
|
+
*
|
|
273
|
+
* @param resolvedTitle - the final, template-applied page title (Twitter title
|
|
274
|
+
* fallback), mirroring the OG behaviour.
|
|
275
|
+
*/
|
|
276
|
+
declare function generateTwitterMeta(twitter: SeoTwitterConfig, global: GlobalSeoConfig, resolvedTitle: string, pageDescription: string): Array<{
|
|
277
|
+
name: string;
|
|
278
|
+
content: string;
|
|
279
|
+
}>;
|
|
280
|
+
/**
|
|
281
|
+
* Generate JSON-LD schema objects from schema configs, auto-populating common
|
|
282
|
+
* WebSite properties from global config.
|
|
283
|
+
*/
|
|
284
|
+
declare function generateJsonLd(schemas: SeoSchemaConfig[], global: GlobalSeoConfig): object[];
|
|
285
|
+
/**
|
|
286
|
+
* Resolve page SEO by merging global defaults with page-specific config.
|
|
287
|
+
*
|
|
288
|
+
* Behavioural changes from the historical in-composable version (both bug
|
|
289
|
+
* fixes):
|
|
290
|
+
* 1. `openGraph.title` falls back to the **template-applied** page title, not
|
|
291
|
+
* the raw `page.title`, so `og:title` matches `<title>`.
|
|
292
|
+
* 2. The `titleTemplate` is applied **only** to a page-specific title
|
|
293
|
+
* (`page.title`, or the `fallbackTitle` arg). It is no longer applied to
|
|
294
|
+
* `global.defaultTitle`, which is already the complete brand title —
|
|
295
|
+
* templating it produced `"Brand | Default Title | Brand"` doubling on any
|
|
296
|
+
* page without its own `seo.yaml` entry.
|
|
297
|
+
*
|
|
298
|
+
* @param fallbackTitle - a page-specific title to use when `seo.yaml` has no
|
|
299
|
+
* `title` for this page (e.g. the route `title` from `pages.yaml`). It IS run
|
|
300
|
+
* through `titleTemplate`; `global.defaultTitle` is the last resort and is not.
|
|
301
|
+
*/
|
|
302
|
+
declare function resolvePageSeo(pageSlug: string, pagePath: string | undefined, seoConfig: SeoConfiguration | undefined, fallbackTitle?: string): ResolvedPageSeo;
|
|
303
|
+
/**
|
|
304
|
+
* Build the complete, framework-agnostic set of `<head>` tags for a page.
|
|
305
|
+
*
|
|
306
|
+
* This is the function both the `useSEO` runtime and the build-time emitter
|
|
307
|
+
* call, guaranteeing identical output. Pass `overrides` to mirror the
|
|
308
|
+
* composable's `applyHead(overrides)` behaviour, or to force `robots` (used by
|
|
309
|
+
* the emitter's `noindex` option).
|
|
310
|
+
*/
|
|
311
|
+
declare function buildHeadTags(pageSlug: string, pagePath: string | undefined, seoConfig: SeoConfiguration | undefined, overrides?: HeadTagOverrides): ResolvedHeadTags;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Pure, framework-free `<head>` splicing for the build-time SEO emitter.
|
|
315
|
+
*
|
|
316
|
+
* Given a built `index.html` shell and a set of resolved head tags (from
|
|
317
|
+
* `buildHeadTags`), this produces a new HTML string where the SEO-managed
|
|
318
|
+
* tags — `<title>`, `description`, `keywords`, `robots`, `canonical`,
|
|
319
|
+
* verification, all `og:*` / `article:*` properties, all `twitter:*` names,
|
|
320
|
+
* and `application/ld+json` scripts — have been **replaced** (not duplicated)
|
|
321
|
+
* with the resolved set.
|
|
322
|
+
*
|
|
323
|
+
* Design goals:
|
|
324
|
+
* - **Idempotent**: running it twice yields the same output (it strips the
|
|
325
|
+
* managed tags first, then re-inserts the canonical set).
|
|
326
|
+
* - **Deterministic**: tag order is fixed by `renderHeadTags`.
|
|
327
|
+
* - **Conservative**: only tags we own are touched. Charset, viewport, CSP,
|
|
328
|
+
* theme-color, favicons, stylesheets, and the app script are left intact.
|
|
329
|
+
*
|
|
330
|
+
* This is intentionally regex-based (no DOM dependency) to mirror the existing
|
|
331
|
+
* `dcsCdnImagePlugin` post-build HTML rewriting and to keep the emitter free of
|
|
332
|
+
* heavy parser deps at build time.
|
|
333
|
+
*/
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Render the resolved head tags to a deterministic HTML fragment.
|
|
337
|
+
* Order: title, meta (in the order produced by buildHeadTags), link, script.
|
|
338
|
+
*/
|
|
339
|
+
declare function renderHeadTags(tags: ResolvedHeadTags, indent?: string): string;
|
|
340
|
+
/**
|
|
341
|
+
* Strip the SEO-managed tags from a `<head>` block so they can be re-inserted
|
|
342
|
+
* without duplication. Operates only within `<head>...</head>` to avoid
|
|
343
|
+
* touching body content.
|
|
344
|
+
*/
|
|
345
|
+
declare function stripManagedHeadTags(html: string): string;
|
|
346
|
+
/**
|
|
347
|
+
* Splice resolved SEO head tags into an HTML document.
|
|
348
|
+
*
|
|
349
|
+
* Strips the existing managed tags, then inserts the rendered canonical set
|
|
350
|
+
* immediately before `</head>`. If no `<head>` is present the HTML is returned
|
|
351
|
+
* unchanged (defensive — the emitter logs and no-ops in that case).
|
|
352
|
+
*
|
|
353
|
+
* Idempotent: applying twice produces identical output.
|
|
354
|
+
*/
|
|
355
|
+
declare function spliceHeadHtml(html: string, tags: ResolvedHeadTags): string;
|
|
356
|
+
|
|
421
357
|
/**
|
|
422
358
|
* Types for release notes API
|
|
423
359
|
*/
|
|
@@ -723,4 +659,4 @@ interface UseReviewContentReturn {
|
|
|
723
659
|
}
|
|
724
660
|
declare function useReviewContent(config: UseReviewContentConfig): UseReviewContentReturn;
|
|
725
661
|
|
|
726
|
-
export { type DcsContentFile,
|
|
662
|
+
export { type DcsContentFile, GlobalSeoConfig, type HeadLinkTag, type HeadMetaTag, type HeadScriptTag, type HeadTagOverrides, type MediaCarouselItem, type ReleaseNote, type ReleaseNotesReturn, type ResolvedHeadTags, ResolvedPageSeo, type ReviewItem, SeoConfiguration, SeoOpenGraphConfig, SeoSchemaConfig, SeoTwitterConfig, type SiteVersionReturn, type TextContentConfig, type TextContentReturn, type UseMediaCarouselConfig, type UseMediaCarouselReturn, type UseReviewContentConfig, type UseReviewContentReturn, UseSeoReturn, buildHeadTags, createSiteSEO, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, renderHeadTags, resolvePageSeo, spliceHeadHtml, stripManagedHeadTags, useMediaCarousel, useReleaseNotes, useResponsiveImage, useReviewContent, useSEO, useSiteVersion, useTextContent };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { resolvePageSeo, generateJsonLd, buildHeadTags } from './chunk-TIGZ7RKI.js';
|
|
2
|
+
export { buildHeadTags, buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, renderHeadTags, resolvePageSeo, spliceHeadHtml, stripManagedHeadTags } from './chunk-TIGZ7RKI.js';
|
|
1
3
|
import { shallowRef, ref, computed, onMounted, readonly, toValue, onUnmounted } from 'vue';
|
|
2
4
|
import { useHead } from '@unhead/vue';
|
|
3
5
|
import { isCdnAssetUrl, resolveResponsiveImage } from '@duffcloudservices/cms-core';
|
|
4
6
|
export { isCdnAssetUrl, resolveResponsiveImage } from '@duffcloudservices/cms-core';
|
|
5
7
|
|
|
6
|
-
// src/composables/useTextContent.ts
|
|
7
8
|
var fetchCache = /* @__PURE__ */ new Map();
|
|
8
9
|
function getBuildTimeContent() {
|
|
9
10
|
try {
|
|
@@ -161,128 +162,6 @@ function getBuildTimeSeo() {
|
|
|
161
162
|
}
|
|
162
163
|
return void 0;
|
|
163
164
|
}
|
|
164
|
-
function generateOpenGraphMeta(og, global, pageTitle, pageDescription, canonical) {
|
|
165
|
-
const tags = [];
|
|
166
|
-
tags.push({ property: "og:title", content: og.title || pageTitle });
|
|
167
|
-
tags.push({ property: "og:description", content: og.description || pageDescription });
|
|
168
|
-
tags.push({ property: "og:url", content: og.url || canonical });
|
|
169
|
-
tags.push({ property: "og:type", content: og.type || "website" });
|
|
170
|
-
const image = og.image || global.images?.ogDefault;
|
|
171
|
-
if (image) {
|
|
172
|
-
tags.push({ property: "og:image", content: image });
|
|
173
|
-
if (og.imageAlt || pageTitle) {
|
|
174
|
-
tags.push({ property: "og:image:alt", content: og.imageAlt || pageTitle });
|
|
175
|
-
}
|
|
176
|
-
if (og.imageWidth) {
|
|
177
|
-
tags.push({ property: "og:image:width", content: String(og.imageWidth) });
|
|
178
|
-
}
|
|
179
|
-
if (og.imageHeight) {
|
|
180
|
-
tags.push({ property: "og:image:height", content: String(og.imageHeight) });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (global.siteName) {
|
|
184
|
-
tags.push({ property: "og:site_name", content: global.siteName });
|
|
185
|
-
}
|
|
186
|
-
if (global.locale) {
|
|
187
|
-
tags.push({ property: "og:locale", content: global.locale });
|
|
188
|
-
}
|
|
189
|
-
if (og.type === "article") {
|
|
190
|
-
if (og.publishedTime) {
|
|
191
|
-
tags.push({ property: "article:published_time", content: og.publishedTime });
|
|
192
|
-
}
|
|
193
|
-
if (og.modifiedTime) {
|
|
194
|
-
tags.push({ property: "article:modified_time", content: og.modifiedTime });
|
|
195
|
-
}
|
|
196
|
-
if (og.author) {
|
|
197
|
-
tags.push({ property: "article:author", content: og.author });
|
|
198
|
-
}
|
|
199
|
-
if (og.section) {
|
|
200
|
-
tags.push({ property: "article:section", content: og.section });
|
|
201
|
-
}
|
|
202
|
-
if (og.tags) {
|
|
203
|
-
og.tags.forEach((tag) => {
|
|
204
|
-
tags.push({ property: "article:tag", content: tag });
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return tags;
|
|
209
|
-
}
|
|
210
|
-
function generateTwitterMeta(twitter, global, pageTitle, pageDescription) {
|
|
211
|
-
const tags = [];
|
|
212
|
-
tags.push({ name: "twitter:card", content: twitter.card || "summary_large_image" });
|
|
213
|
-
tags.push({ name: "twitter:title", content: twitter.title || pageTitle });
|
|
214
|
-
tags.push({ name: "twitter:description", content: twitter.description || pageDescription });
|
|
215
|
-
const image = twitter.image || global.images?.twitterDefault;
|
|
216
|
-
if (image) {
|
|
217
|
-
tags.push({ name: "twitter:image", content: image });
|
|
218
|
-
if (twitter.imageAlt || pageTitle) {
|
|
219
|
-
tags.push({ name: "twitter:image:alt", content: twitter.imageAlt || pageTitle });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
const site = twitter.site || global.social?.twitter;
|
|
223
|
-
if (site) {
|
|
224
|
-
tags.push({ name: "twitter:site", content: site.startsWith("@") ? site : `@${site}` });
|
|
225
|
-
}
|
|
226
|
-
if (twitter.creator) {
|
|
227
|
-
tags.push({
|
|
228
|
-
name: "twitter:creator",
|
|
229
|
-
content: twitter.creator.startsWith("@") ? twitter.creator : `@${twitter.creator}`
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
return tags;
|
|
233
|
-
}
|
|
234
|
-
function generateJsonLd(schemas, global) {
|
|
235
|
-
return schemas.map((schema) => {
|
|
236
|
-
const base = {
|
|
237
|
-
"@context": "https://schema.org",
|
|
238
|
-
"@type": schema.type
|
|
239
|
-
};
|
|
240
|
-
if (schema.properties) {
|
|
241
|
-
Object.assign(base, schema.properties);
|
|
242
|
-
}
|
|
243
|
-
if (schema.type === "WebSite" && global.siteUrl && !base.url) {
|
|
244
|
-
base.url = global.siteUrl;
|
|
245
|
-
}
|
|
246
|
-
if (schema.type === "WebSite" && global.siteName && !base.name) {
|
|
247
|
-
base.name = global.siteName;
|
|
248
|
-
}
|
|
249
|
-
return base;
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
function resolvePageSeo(pageSlug, pagePath, seoConfig) {
|
|
253
|
-
const global = seoConfig?.global ?? {};
|
|
254
|
-
const page = seoConfig?.pages?.[pageSlug] ?? {};
|
|
255
|
-
let canonical = page.canonical || "";
|
|
256
|
-
if (!canonical && global.siteUrl) {
|
|
257
|
-
const path = pagePath ?? (pageSlug === "home" ? "/" : `/${pageSlug}`);
|
|
258
|
-
canonical = `${global.siteUrl.replace(/\/$/, "")}${path}`;
|
|
259
|
-
}
|
|
260
|
-
let title = page.title || global.defaultTitle || pageSlug;
|
|
261
|
-
if (!page.noTitleTemplate && global.titleTemplate) {
|
|
262
|
-
title = global.titleTemplate.replace("%s", title);
|
|
263
|
-
}
|
|
264
|
-
const openGraph = {
|
|
265
|
-
type: page.openGraph?.type || "website",
|
|
266
|
-
title: page.openGraph?.title || page.title || global.defaultTitle || "",
|
|
267
|
-
description: page.openGraph?.description || page.description || global.defaultDescription || "",
|
|
268
|
-
...page.openGraph
|
|
269
|
-
};
|
|
270
|
-
const twitter = {
|
|
271
|
-
card: page.twitter?.card || "summary_large_image",
|
|
272
|
-
...page.twitter
|
|
273
|
-
};
|
|
274
|
-
const schemas = [...global.schemas ?? [], ...page.schemas ?? []];
|
|
275
|
-
return {
|
|
276
|
-
title,
|
|
277
|
-
description: page.description || global.defaultDescription || "",
|
|
278
|
-
canonical,
|
|
279
|
-
robots: page.robots || global.robots || "index, follow",
|
|
280
|
-
openGraph,
|
|
281
|
-
twitter,
|
|
282
|
-
schemas,
|
|
283
|
-
alternates: page.alternates ?? []
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
165
|
function useSEO(pageSlug, pagePath) {
|
|
287
166
|
const seoConfig = getBuildTimeSeo();
|
|
288
167
|
const hasBuildTimeSeo = seoConfig !== void 0;
|
|
@@ -296,46 +175,13 @@ function useSEO(pageSlug, pagePath) {
|
|
|
296
175
|
return config.value.canonical;
|
|
297
176
|
}
|
|
298
177
|
function applyHead(overrides) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (resolved.robots) {
|
|
306
|
-
meta.push({ name: "robots", content: resolved.robots });
|
|
307
|
-
}
|
|
308
|
-
if (global.verification?.google) {
|
|
309
|
-
meta.push({ name: "google-site-verification", content: global.verification.google });
|
|
310
|
-
}
|
|
311
|
-
if (global.verification?.bing) {
|
|
312
|
-
meta.push({ name: "msvalidate.01", content: global.verification.bing });
|
|
313
|
-
}
|
|
314
|
-
const ogMeta = generateOpenGraphMeta(
|
|
315
|
-
resolved.openGraph,
|
|
316
|
-
global,
|
|
317
|
-
title,
|
|
318
|
-
description,
|
|
319
|
-
resolved.canonical
|
|
320
|
-
);
|
|
321
|
-
meta.push(...ogMeta.map((t) => ({ property: t.property, content: t.content })));
|
|
322
|
-
const twitterMeta = generateTwitterMeta(resolved.twitter, global, title, description);
|
|
323
|
-
meta.push(...twitterMeta.map((t) => ({ name: t.name, content: t.content })));
|
|
324
|
-
if (overrides?.meta) {
|
|
325
|
-
meta.push(...overrides.meta);
|
|
326
|
-
}
|
|
327
|
-
const link = [];
|
|
328
|
-
if (resolved.canonical) {
|
|
329
|
-
link.push({ rel: "canonical", href: resolved.canonical });
|
|
330
|
-
}
|
|
331
|
-
resolved.alternates.forEach((alt) => {
|
|
332
|
-
link.push({ rel: "alternate", href: alt.href, hreflang: alt.hreflang });
|
|
178
|
+
const { title, meta, link, script } = buildHeadTags(pageSlug, pagePath, seoConfig, {
|
|
179
|
+
title: overrides?.title,
|
|
180
|
+
description: overrides?.description,
|
|
181
|
+
keywords: overrides?.keywords,
|
|
182
|
+
schemas: overrides?.schemas,
|
|
183
|
+
meta: overrides?.meta
|
|
333
184
|
});
|
|
334
|
-
const schemas = overrides?.schemas ?? getSchema();
|
|
335
|
-
const script = schemas.map((schema) => ({
|
|
336
|
-
type: "application/ld+json",
|
|
337
|
-
children: JSON.stringify(schema)
|
|
338
|
-
}));
|
|
339
185
|
useHead({
|
|
340
186
|
title,
|
|
341
187
|
meta,
|