@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
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
// src/seo/headTags.ts
|
|
6
|
+
function generateOpenGraphMeta(og, global, resolvedTitle, pageDescription, canonical) {
|
|
7
|
+
const tags = [];
|
|
8
|
+
tags.push({ property: "og:title", content: og.title || resolvedTitle });
|
|
9
|
+
tags.push({ property: "og:description", content: og.description || pageDescription });
|
|
10
|
+
tags.push({ property: "og:url", content: og.url || canonical });
|
|
11
|
+
tags.push({ property: "og:type", content: og.type || "website" });
|
|
12
|
+
const image = og.image || global.images?.ogDefault;
|
|
13
|
+
if (image) {
|
|
14
|
+
tags.push({ property: "og:image", content: image });
|
|
15
|
+
if (og.imageAlt || resolvedTitle) {
|
|
16
|
+
tags.push({ property: "og:image:alt", content: og.imageAlt || resolvedTitle });
|
|
17
|
+
}
|
|
18
|
+
if (og.imageWidth) {
|
|
19
|
+
tags.push({ property: "og:image:width", content: String(og.imageWidth) });
|
|
20
|
+
}
|
|
21
|
+
if (og.imageHeight) {
|
|
22
|
+
tags.push({ property: "og:image:height", content: String(og.imageHeight) });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (global.siteName) {
|
|
26
|
+
tags.push({ property: "og:site_name", content: global.siteName });
|
|
27
|
+
}
|
|
28
|
+
if (global.locale) {
|
|
29
|
+
tags.push({ property: "og:locale", content: global.locale });
|
|
30
|
+
}
|
|
31
|
+
if (og.type === "article") {
|
|
32
|
+
if (og.publishedTime) {
|
|
33
|
+
tags.push({ property: "article:published_time", content: og.publishedTime });
|
|
34
|
+
}
|
|
35
|
+
if (og.modifiedTime) {
|
|
36
|
+
tags.push({ property: "article:modified_time", content: og.modifiedTime });
|
|
37
|
+
}
|
|
38
|
+
if (og.author) {
|
|
39
|
+
tags.push({ property: "article:author", content: og.author });
|
|
40
|
+
}
|
|
41
|
+
if (og.section) {
|
|
42
|
+
tags.push({ property: "article:section", content: og.section });
|
|
43
|
+
}
|
|
44
|
+
if (og.tags) {
|
|
45
|
+
og.tags.forEach((tag) => {
|
|
46
|
+
tags.push({ property: "article:tag", content: tag });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return tags;
|
|
51
|
+
}
|
|
52
|
+
function generateTwitterMeta(twitter, global, resolvedTitle, pageDescription) {
|
|
53
|
+
const tags = [];
|
|
54
|
+
tags.push({ name: "twitter:card", content: twitter.card || "summary_large_image" });
|
|
55
|
+
tags.push({ name: "twitter:title", content: twitter.title || resolvedTitle });
|
|
56
|
+
tags.push({ name: "twitter:description", content: twitter.description || pageDescription });
|
|
57
|
+
const image = twitter.image || global.images?.twitterDefault;
|
|
58
|
+
if (image) {
|
|
59
|
+
tags.push({ name: "twitter:image", content: image });
|
|
60
|
+
if (twitter.imageAlt || resolvedTitle) {
|
|
61
|
+
tags.push({ name: "twitter:image:alt", content: twitter.imageAlt || resolvedTitle });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const site = twitter.site || global.social?.twitter;
|
|
65
|
+
if (site) {
|
|
66
|
+
tags.push({ name: "twitter:site", content: site.startsWith("@") ? site : `@${site}` });
|
|
67
|
+
}
|
|
68
|
+
if (twitter.creator) {
|
|
69
|
+
tags.push({
|
|
70
|
+
name: "twitter:creator",
|
|
71
|
+
content: twitter.creator.startsWith("@") ? twitter.creator : `@${twitter.creator}`
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return tags;
|
|
75
|
+
}
|
|
76
|
+
function generateJsonLd(schemas, global) {
|
|
77
|
+
return schemas.map((schema) => {
|
|
78
|
+
const base = {
|
|
79
|
+
"@context": "https://schema.org",
|
|
80
|
+
"@type": schema.type
|
|
81
|
+
};
|
|
82
|
+
if (schema.properties) {
|
|
83
|
+
Object.assign(base, schema.properties);
|
|
84
|
+
}
|
|
85
|
+
if (schema.type === "WebSite" && global.siteUrl && !base.url) {
|
|
86
|
+
base.url = global.siteUrl;
|
|
87
|
+
}
|
|
88
|
+
if (schema.type === "WebSite" && global.siteName && !base.name) {
|
|
89
|
+
base.name = global.siteName;
|
|
90
|
+
}
|
|
91
|
+
return base;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function resolvePageSeo(pageSlug, pagePath, seoConfig, fallbackTitle) {
|
|
95
|
+
const global = seoConfig?.global ?? {};
|
|
96
|
+
const page = seoConfig?.pages?.[pageSlug] ?? {};
|
|
97
|
+
let canonical = page.canonical || "";
|
|
98
|
+
if (!canonical && global.siteUrl) {
|
|
99
|
+
const path2 = pagePath ?? (pageSlug === "home" ? "/" : `/${pageSlug}`);
|
|
100
|
+
canonical = `${global.siteUrl.replace(/\/$/, "")}${path2}`;
|
|
101
|
+
}
|
|
102
|
+
const pageSpecificTitle = page.title || fallbackTitle;
|
|
103
|
+
let title;
|
|
104
|
+
if (pageSpecificTitle) {
|
|
105
|
+
title = page.noTitleTemplate || !global.titleTemplate ? pageSpecificTitle : global.titleTemplate.replace("%s", pageSpecificTitle);
|
|
106
|
+
} else {
|
|
107
|
+
title = global.defaultTitle || pageSlug;
|
|
108
|
+
}
|
|
109
|
+
const openGraph = {
|
|
110
|
+
type: page.openGraph?.type || "website",
|
|
111
|
+
title: page.openGraph?.title || title,
|
|
112
|
+
description: page.openGraph?.description || page.description || global.defaultDescription || "",
|
|
113
|
+
...page.openGraph
|
|
114
|
+
};
|
|
115
|
+
const twitter = {
|
|
116
|
+
card: page.twitter?.card || "summary_large_image",
|
|
117
|
+
...page.twitter
|
|
118
|
+
};
|
|
119
|
+
const schemas = [...global.schemas ?? [], ...page.schemas ?? []];
|
|
120
|
+
return {
|
|
121
|
+
title,
|
|
122
|
+
description: page.description || global.defaultDescription || "",
|
|
123
|
+
canonical,
|
|
124
|
+
robots: page.robots || global.robots || "index, follow",
|
|
125
|
+
openGraph,
|
|
126
|
+
twitter,
|
|
127
|
+
schemas,
|
|
128
|
+
alternates: page.alternates ?? [],
|
|
129
|
+
// Surface keywords on the resolved object so both runtime and emitter can
|
|
130
|
+
// emit the meta tag without re-reading the raw page config.
|
|
131
|
+
keywords: page.keywords
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function buildHeadTags(pageSlug, pagePath, seoConfig, overrides) {
|
|
135
|
+
const resolved = resolvePageSeo(pageSlug, pagePath, seoConfig, overrides?.fallbackTitle);
|
|
136
|
+
const global = seoConfig?.global ?? {};
|
|
137
|
+
const title = overrides?.title ?? resolved.title;
|
|
138
|
+
const description = overrides?.description ?? resolved.description;
|
|
139
|
+
const robots = overrides?.robots ?? resolved.robots;
|
|
140
|
+
const keywords = overrides?.keywords ?? resolved.keywords;
|
|
141
|
+
const meta = [];
|
|
142
|
+
meta.push({ name: "description", content: description });
|
|
143
|
+
if (keywords && overrides?.includeKeywords) {
|
|
144
|
+
meta.push({ name: "keywords", content: keywords });
|
|
145
|
+
}
|
|
146
|
+
if (robots) {
|
|
147
|
+
meta.push({ name: "robots", content: robots });
|
|
148
|
+
}
|
|
149
|
+
if (global.verification?.google) {
|
|
150
|
+
meta.push({ name: "google-site-verification", content: global.verification.google });
|
|
151
|
+
}
|
|
152
|
+
if (global.verification?.bing) {
|
|
153
|
+
meta.push({ name: "msvalidate.01", content: global.verification.bing });
|
|
154
|
+
}
|
|
155
|
+
const ogMeta = generateOpenGraphMeta(resolved.openGraph, global, title, description, resolved.canonical);
|
|
156
|
+
meta.push(...ogMeta.map((t) => ({ property: t.property, content: t.content })));
|
|
157
|
+
const twitterMeta = generateTwitterMeta(resolved.twitter, global, title, description);
|
|
158
|
+
meta.push(...twitterMeta.map((t) => ({ name: t.name, content: t.content })));
|
|
159
|
+
if (overrides?.meta) {
|
|
160
|
+
meta.push(...overrides.meta);
|
|
161
|
+
}
|
|
162
|
+
const link = [];
|
|
163
|
+
if (resolved.canonical) {
|
|
164
|
+
link.push({ rel: "canonical", href: resolved.canonical });
|
|
165
|
+
}
|
|
166
|
+
resolved.alternates.forEach((alt) => {
|
|
167
|
+
link.push({ rel: "alternate", href: alt.href, hreflang: alt.hreflang });
|
|
168
|
+
});
|
|
169
|
+
const jsonLd = overrides?.schemas ?? generateJsonLd(resolved.schemas, global);
|
|
170
|
+
const script = jsonLd.map((schema) => ({
|
|
171
|
+
type: "application/ld+json",
|
|
172
|
+
children: JSON.stringify(schema)
|
|
173
|
+
}));
|
|
174
|
+
return { title, meta, link, script, jsonLd, resolved };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/seo/spliceHeadHtml.ts
|
|
178
|
+
function escapeAttr(value) {
|
|
179
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
180
|
+
}
|
|
181
|
+
function escapeJsonLd(json) {
|
|
182
|
+
return json.replace(/<\//g, "<\\/");
|
|
183
|
+
}
|
|
184
|
+
var MANAGED_META_NAMES = /* @__PURE__ */ new Set([
|
|
185
|
+
"description",
|
|
186
|
+
"keywords",
|
|
187
|
+
"robots",
|
|
188
|
+
"google-site-verification",
|
|
189
|
+
"msvalidate.01"
|
|
190
|
+
]);
|
|
191
|
+
var MANAGED_PROPERTY_PREFIXES = ["og:", "article:"];
|
|
192
|
+
var MANAGED_NAME_PREFIXES = ["twitter:"];
|
|
193
|
+
function renderHeadTags(tags, indent = " ") {
|
|
194
|
+
const lines = [];
|
|
195
|
+
lines.push(`${indent}<title>${escapeAttr(tags.title)}</title>`);
|
|
196
|
+
for (const m of tags.meta) {
|
|
197
|
+
if (m.property !== void 0) {
|
|
198
|
+
lines.push(`${indent}<meta property="${escapeAttr(m.property)}" content="${escapeAttr(m.content)}" />`);
|
|
199
|
+
} else if (m.name !== void 0) {
|
|
200
|
+
lines.push(`${indent}<meta name="${escapeAttr(m.name)}" content="${escapeAttr(m.content)}" />`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const l of tags.link) {
|
|
204
|
+
const hreflang = l.hreflang ? ` hreflang="${escapeAttr(l.hreflang)}"` : "";
|
|
205
|
+
lines.push(`${indent}<link rel="${escapeAttr(l.rel)}" href="${escapeAttr(l.href)}"${hreflang} />`);
|
|
206
|
+
}
|
|
207
|
+
for (const s of tags.script) {
|
|
208
|
+
lines.push(`${indent}<script type="${escapeAttr(s.type)}">${escapeJsonLd(s.children)}</script>`);
|
|
209
|
+
}
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
}
|
|
212
|
+
function stripManagedHeadTags(html) {
|
|
213
|
+
const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
214
|
+
if (!headMatch) return html;
|
|
215
|
+
let head = headMatch[1];
|
|
216
|
+
head = head.replace(/[ \t]*<title>[\s\S]*?<\/title>[ \t]*\r?\n?/gi, "");
|
|
217
|
+
head = head.replace(
|
|
218
|
+
/[ \t]*<script[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>[ \t]*\r?\n?/gi,
|
|
219
|
+
""
|
|
220
|
+
);
|
|
221
|
+
head = head.replace(/[ \t]*<meta\b[^>]*>[ \t]*\r?\n?/gi, (tag) => {
|
|
222
|
+
const nameMatch = tag.match(/\bname=["']([^"']*)["']/i);
|
|
223
|
+
const propMatch = tag.match(/\bproperty=["']([^"']*)["']/i);
|
|
224
|
+
const name = nameMatch?.[1]?.toLowerCase();
|
|
225
|
+
const property = propMatch?.[1]?.toLowerCase();
|
|
226
|
+
if (name) {
|
|
227
|
+
if (MANAGED_META_NAMES.has(name)) return "";
|
|
228
|
+
if (MANAGED_NAME_PREFIXES.some((p) => name.startsWith(p))) return "";
|
|
229
|
+
}
|
|
230
|
+
if (property) {
|
|
231
|
+
if (MANAGED_PROPERTY_PREFIXES.some((p) => property.startsWith(p))) return "";
|
|
232
|
+
}
|
|
233
|
+
return tag;
|
|
234
|
+
});
|
|
235
|
+
head = head.replace(
|
|
236
|
+
/[ \t]*<link\b[^>]*\brel=["'](?:canonical|alternate)["'][^>]*>[ \t]*\r?\n?/gi,
|
|
237
|
+
""
|
|
238
|
+
);
|
|
239
|
+
return html.slice(0, headMatch.index + headMatch[0].indexOf(headMatch[1])) + head + html.slice(headMatch.index + headMatch[0].indexOf(headMatch[1]) + headMatch[1].length);
|
|
240
|
+
}
|
|
241
|
+
function spliceHeadHtml(html, tags) {
|
|
242
|
+
if (!/<\/head>/i.test(html)) {
|
|
243
|
+
return html;
|
|
244
|
+
}
|
|
245
|
+
const stripped = stripManagedHeadTags(html);
|
|
246
|
+
const fragment = renderHeadTags(tags);
|
|
247
|
+
return stripped.replace(/([ \t]*)<\/head>/i, (_m, indent) => {
|
|
248
|
+
return `${fragment}
|
|
249
|
+
${indent}</head>`;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/seo/vitepressTransform.ts
|
|
254
|
+
function defaultRelativePathToRoute(relativePath, params) {
|
|
255
|
+
let stem = (relativePath ?? "").replace(/\.md$/i, "");
|
|
256
|
+
if (params) {
|
|
257
|
+
for (const [key, value] of Object.entries(params)) {
|
|
258
|
+
if (value == null) continue;
|
|
259
|
+
stem = stem.replace(`[${key}]`, String(value));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (stem === "index") return "/";
|
|
263
|
+
if (stem.endsWith("/index")) stem = stem.slice(0, -"/index".length);
|
|
264
|
+
return `/${stem}`;
|
|
265
|
+
}
|
|
266
|
+
function normaliseSiteUrl(siteUrl) {
|
|
267
|
+
return (siteUrl ?? "").replace(/\/$/, "");
|
|
268
|
+
}
|
|
269
|
+
function ldScript(obj) {
|
|
270
|
+
return ["script", { type: "application/ld+json" }, JSON.stringify(obj)];
|
|
271
|
+
}
|
|
272
|
+
function buildVitePressSeoHead(pageData, options) {
|
|
273
|
+
const {
|
|
274
|
+
seoConfig,
|
|
275
|
+
pageTypeRules = [],
|
|
276
|
+
resolvePage,
|
|
277
|
+
relativePathToRoute = defaultRelativePathToRoute,
|
|
278
|
+
includeKeywords = true
|
|
279
|
+
} = options;
|
|
280
|
+
const global = seoConfig?.global ?? {};
|
|
281
|
+
const siteUrl = normaliseSiteUrl(global.siteUrl);
|
|
282
|
+
const fm = pageData.frontmatter ?? {};
|
|
283
|
+
const route = relativePathToRoute(pageData.relativePath, pageData.params);
|
|
284
|
+
const slug = route === "/" ? "home" : route.slice(1);
|
|
285
|
+
const canonical = route === "/" ? `${siteUrl}/` : `${siteUrl}${route}`;
|
|
286
|
+
const ctx = {
|
|
287
|
+
route,
|
|
288
|
+
slug,
|
|
289
|
+
canonical,
|
|
290
|
+
siteUrl,
|
|
291
|
+
frontmatter: fm,
|
|
292
|
+
global,
|
|
293
|
+
pageData
|
|
294
|
+
};
|
|
295
|
+
const resolved = resolvePageSeo(slug, route, seoConfig, fm.title);
|
|
296
|
+
const overrides = resolvePage?.(ctx) ?? {};
|
|
297
|
+
const rawTitle = overrides.title ?? pageSpecificSeoTitle(slug, seoConfig) ?? fm.title ?? resolved.title;
|
|
298
|
+
const templated = applyTitleTemplate(rawTitle, global, resolved);
|
|
299
|
+
const pageSeoDescription = seoConfig?.pages?.[slug]?.description;
|
|
300
|
+
const description = overrides.description || pageSeoDescription || fm.description || global.defaultDescription || "";
|
|
301
|
+
const keywords = overrides.keywords || seoConfig?.pages?.[slug]?.keywords || (Array.isArray(fm.tags) ? fm.tags.join(", ") : "");
|
|
302
|
+
const ogType = overrides.ogType || resolved.openGraph.type;
|
|
303
|
+
const ogImage = overrides.ogImage || resolved.openGraph.image || fm.headerImage || fm.image || global.images?.ogDefault;
|
|
304
|
+
const head = [];
|
|
305
|
+
if (includeKeywords && keywords) {
|
|
306
|
+
head.push(["meta", { name: "keywords", content: keywords }]);
|
|
307
|
+
}
|
|
308
|
+
head.push(["meta", { name: "robots", content: resolved.robots }]);
|
|
309
|
+
head.push(["link", { rel: "canonical", href: canonical }]);
|
|
310
|
+
if (global.verification?.google) {
|
|
311
|
+
head.push(["meta", { name: "google-site-verification", content: global.verification.google }]);
|
|
312
|
+
}
|
|
313
|
+
if (global.verification?.bing) {
|
|
314
|
+
head.push(["meta", { name: "msvalidate.01", content: global.verification.bing }]);
|
|
315
|
+
}
|
|
316
|
+
const pageOg = seoConfig?.pages?.[slug]?.openGraph;
|
|
317
|
+
const ogConfig = {
|
|
318
|
+
...pageOg,
|
|
319
|
+
type: ogType,
|
|
320
|
+
title: overrides.ogTitle || templated,
|
|
321
|
+
description: overrides.ogDescription || pageOg?.description || description,
|
|
322
|
+
...ogImage ? { image: ogImage } : {},
|
|
323
|
+
url: canonical
|
|
324
|
+
};
|
|
325
|
+
for (const t of generateOpenGraphMeta(ogConfig, global, templated, description, canonical)) {
|
|
326
|
+
head.push(["meta", { property: t.property, content: t.content }]);
|
|
327
|
+
}
|
|
328
|
+
for (const t of generateTwitterMeta(resolved.twitter, global, templated, description)) {
|
|
329
|
+
head.push(["meta", { name: t.name, content: t.content }]);
|
|
330
|
+
}
|
|
331
|
+
for (const obj of generateJsonLd(resolved.schemas, global)) {
|
|
332
|
+
head.push(ldScript(obj));
|
|
333
|
+
}
|
|
334
|
+
for (const rule of pageTypeRules) {
|
|
335
|
+
let matched = false;
|
|
336
|
+
try {
|
|
337
|
+
matched = rule.match(ctx);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.warn("[dcs-seo] pageTypeRule.match threw; skipping rule:", err);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!matched) continue;
|
|
343
|
+
let objects = [];
|
|
344
|
+
try {
|
|
345
|
+
objects = rule.build(ctx) ?? [];
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.warn("[dcs-seo] pageTypeRule.build threw; skipping rule output:", err);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
for (const obj of objects) {
|
|
351
|
+
if (obj) head.push(ldScript(obj));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
// Hand back the RAW title for `pageData.title`; VitePress applies its own
|
|
356
|
+
// `titleTemplate` to produce the templated <title> element.
|
|
357
|
+
title: rawTitle,
|
|
358
|
+
head,
|
|
359
|
+
description: description || void 0,
|
|
360
|
+
setPageTitle: overrides.setPageTitle ?? false
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function pageSpecificSeoTitle(slug, seoConfig) {
|
|
364
|
+
return seoConfig?.pages?.[slug]?.title || void 0;
|
|
365
|
+
}
|
|
366
|
+
function applyTitleTemplate(rawTitle, global, resolved) {
|
|
367
|
+
if (rawTitle === resolved.title) return rawTitle;
|
|
368
|
+
if (!global.titleTemplate) return rawTitle;
|
|
369
|
+
return global.titleTemplate.replace("%s", rawTitle);
|
|
370
|
+
}
|
|
371
|
+
function createSeoTransformPageData(options) {
|
|
372
|
+
return function transformPageData(pageData) {
|
|
373
|
+
try {
|
|
374
|
+
const { head, title, description, setPageTitle } = buildVitePressSeoHead(pageData, options);
|
|
375
|
+
pageData.frontmatter = pageData.frontmatter ?? {};
|
|
376
|
+
const existing = pageData.frontmatter.head ?? [];
|
|
377
|
+
pageData.frontmatter.head = [...existing, ...head];
|
|
378
|
+
if (description) {
|
|
379
|
+
pageData.description = description;
|
|
380
|
+
}
|
|
381
|
+
if (setPageTitle && title) {
|
|
382
|
+
pageData.title = title;
|
|
383
|
+
}
|
|
384
|
+
if (options.debug) {
|
|
385
|
+
console.log(
|
|
386
|
+
`[dcs-seo] transformPageData ${pageData.relativePath} \u2192 +${head.length} head tag(s)` + (setPageTitle && title ? ` (title="${title}")` : "")
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.warn(
|
|
391
|
+
`[dcs-seo] createSeoTransformPageData failed for ${pageData?.relativePath}; head left unchanged:`,
|
|
392
|
+
err
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function loadPagesManifest(projectRoot, relativePagesPath, debug = false) {
|
|
398
|
+
const possiblePaths = [
|
|
399
|
+
path.resolve(projectRoot, relativePagesPath),
|
|
400
|
+
path.resolve(projectRoot, "..", relativePagesPath),
|
|
401
|
+
path.resolve(process.cwd(), relativePagesPath)
|
|
402
|
+
];
|
|
403
|
+
let foundPath;
|
|
404
|
+
for (const testPath of possiblePaths) {
|
|
405
|
+
if (fs.existsSync(testPath)) {
|
|
406
|
+
foundPath = testPath;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!foundPath) {
|
|
411
|
+
if (debug) {
|
|
412
|
+
console.warn("[dcs-seo] No pages.yaml found at:");
|
|
413
|
+
possiblePaths.forEach((p) => console.warn(` - ${p}`));
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
let raw;
|
|
418
|
+
try {
|
|
419
|
+
raw = yaml.load(fs.readFileSync(foundPath, "utf8"));
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.warn(`[dcs-seo] Failed to parse ${foundPath}:`, error);
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const entries = parsePagesManifest(raw);
|
|
425
|
+
if (!entries) {
|
|
426
|
+
console.warn(`[dcs-seo] pages.yaml at ${foundPath} has no usable page entries`);
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
if (debug) {
|
|
430
|
+
console.log(`[dcs-seo] Loaded ${entries.length} routes from ${foundPath}`);
|
|
431
|
+
}
|
|
432
|
+
return entries;
|
|
433
|
+
}
|
|
434
|
+
function parsePagesManifest(raw) {
|
|
435
|
+
if (!raw || typeof raw !== "object") return null;
|
|
436
|
+
const pages = raw.pages;
|
|
437
|
+
if (!Array.isArray(pages)) return null;
|
|
438
|
+
const routes = [];
|
|
439
|
+
for (const entry of pages) {
|
|
440
|
+
if (!entry || typeof entry !== "object") continue;
|
|
441
|
+
const p = entry.path;
|
|
442
|
+
if (typeof p !== "string" || p.length === 0) continue;
|
|
443
|
+
const slug = entry.slug;
|
|
444
|
+
const title = entry.title;
|
|
445
|
+
routes.push({
|
|
446
|
+
slug: typeof slug === "string" ? slug : "",
|
|
447
|
+
path: p,
|
|
448
|
+
...typeof title === "string" && title.length > 0 ? { title } : {}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return routes.length > 0 ? routes : null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export { buildHeadTags, buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, loadPagesManifest, renderHeadTags, resolvePageSeo, spliceHeadHtml, stripManagedHeadTags };
|
|
455
|
+
//# sourceMappingURL=chunk-TIGZ7RKI.js.map
|
|
456
|
+
//# sourceMappingURL=chunk-TIGZ7RKI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/seo/headTags.ts","../src/seo/spliceHeadHtml.ts","../src/seo/vitepressTransform.ts","../src/seo/pagesManifest.ts"],"names":["path"],"mappings":"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","file":"chunk-TIGZ7RKI.js","sourcesContent":["/**\n * Framework-agnostic SEO head-tag resolution.\n *\n * This module is the single source of truth for turning a\n * (`pageSlug`, `pagePath`, `SeoConfiguration`) triple into a plain,\n * serialisable description of the `<head>` tags a page should carry:\n * resolved title, meta[], link[], and JSON-LD script[].\n *\n * It is consumed by:\n * - `useSEO` (runtime, via `@unhead/vue`) — see `../composables/useSEO.ts`\n * - `dcsSeoPlugin`'s build-time static-HTML emitter — see\n * `../plugins/dcsSeoPlugin.ts`\n *\n * Keeping the resolution here (rather than inside the Vue composable) means\n * the runtime and the build-time emitter produce byte-identical tags from the\n * same `seo.yaml`, with no Vue/unhead dependency required at build time.\n *\n * Runtime behaviour is intentionally identical to the previous in-composable\n * logic **except** for one corrected bug: `og:title` now falls back to the\n * fully-resolved (template-applied) page title instead of the raw, untemplated\n * `page.title`. Previously `og:title` could diverge from the `<title>` element\n * (e.g. `<title>Iron Oak Contractors | Our Services</title>` but\n * `og:title = \"Our Services\"`).\n */\n\nimport type {\n SeoConfiguration,\n GlobalSeoConfig,\n SeoOpenGraphConfig,\n SeoTwitterConfig,\n SeoSchemaConfig,\n ResolvedPageSeo,\n} from '../types/seo'\n\n// =============================================================================\n// Plain head-tag structures (no framework types)\n// =============================================================================\n\n/** A `<meta>` tag — either a `name=`/`content=` or `property=`/`content=` pair. */\nexport interface HeadMetaTag {\n name?: string\n property?: string\n content: string\n}\n\n/** A `<link>` tag (canonical, alternate, etc.). */\nexport interface HeadLinkTag {\n rel: string\n href: string\n hreflang?: string\n}\n\n/** A `<script type=\"application/ld+json\">` tag carrying serialised JSON-LD. */\nexport interface HeadScriptTag {\n type: string\n /** Pre-serialised JSON-LD string (already `JSON.stringify`-ed). */\n children: string\n}\n\n/**\n * The complete, framework-agnostic set of resolved `<head>` tags for a page.\n *\n * - `title` is the final, template-applied title (what goes in `<title>`).\n * - `meta` covers description, keywords, robots, verification, OG, Twitter.\n * - `link` covers canonical + hreflang alternates.\n * - `script` covers JSON-LD (global + page schemas).\n * - `jsonLd` is the same JSON-LD as parsed objects, for callers that want the\n * structured form (e.g. `useSEO().getSchema()`).\n */\nexport interface ResolvedHeadTags {\n title: string\n meta: HeadMetaTag[]\n link: HeadLinkTag[]\n script: HeadScriptTag[]\n jsonLd: object[]\n /** The fully-resolved page SEO (merged global + page) used to build tags. */\n resolved: ResolvedPageSeo\n}\n\n/** Optional overrides applied on top of the resolved config when building tags. */\nexport interface HeadTagOverrides {\n /** Override the resolved `<title>`. */\n title?: string\n /**\n * Page-specific title fallback used when `seo.yaml` has no `title` for this\n * page (e.g. the route `title` from `pages.yaml`). Unlike `global.defaultTitle`\n * this IS run through `titleTemplate`, so un-configured routes (blog posts,\n * etc.) get unique titles rather than the global default.\n */\n fallbackTitle?: string\n /** Override the resolved meta description. */\n description?: string\n /** Override the meta keywords value. */\n keywords?: string\n /**\n * Force the robots directive (e.g. `'noindex, nofollow'`). When supplied this\n * wins over both page- and global-level robots.\n */\n robots?: string\n /** Replace the JSON-LD schema objects entirely (already-built objects). */\n schemas?: object[]\n /** Extra meta tags appended after the generated ones. */\n meta?: HeadMetaTag[]\n /**\n * Emit a `<meta name=\"keywords\">` tag from the page's `keywords` field.\n *\n * Defaults to `false` so the `useSEO` runtime path stays byte-identical to\n * its historical output (which never emitted keywords). The static-HTML\n * emitter opts in (`true`) to surface page keywords in the baked `<head>`.\n */\n includeKeywords?: boolean\n}\n\n// =============================================================================\n// Open Graph / Twitter / JSON-LD generation\n// =============================================================================\n\n/**\n * Generate Open Graph meta tags from config.\n *\n * @param resolvedTitle - the final, template-applied page title. Used as the\n * `og:title` fallback so OG stays consistent with `<title>`.\n */\nexport function generateOpenGraphMeta(\n og: SeoOpenGraphConfig,\n global: GlobalSeoConfig,\n resolvedTitle: string,\n pageDescription: string,\n canonical: string\n): Array<{ property: string; content: string }> {\n const tags: Array<{ property: string; content: string }> = []\n\n // og:title falls back to the *resolved* (templated) title — see module\n // docblock. `og.title` is already pre-resolved in `resolvePageSeo` to\n // `page.openGraph.title || resolvedTitle`, so this is consistent either way.\n tags.push({ property: 'og:title', content: og.title || resolvedTitle })\n tags.push({ property: 'og:description', content: og.description || pageDescription })\n tags.push({ property: 'og:url', content: og.url || canonical })\n tags.push({ property: 'og:type', content: og.type || 'website' })\n\n const image = og.image || global.images?.ogDefault\n if (image) {\n tags.push({ property: 'og:image', content: image })\n if (og.imageAlt || resolvedTitle) {\n tags.push({ property: 'og:image:alt', content: og.imageAlt || resolvedTitle })\n }\n if (og.imageWidth) {\n tags.push({ property: 'og:image:width', content: String(og.imageWidth) })\n }\n if (og.imageHeight) {\n tags.push({ property: 'og:image:height', content: String(og.imageHeight) })\n }\n }\n\n if (global.siteName) {\n tags.push({ property: 'og:site_name', content: global.siteName })\n }\n\n if (global.locale) {\n tags.push({ property: 'og:locale', content: global.locale })\n }\n\n // Article-specific tags\n if (og.type === 'article') {\n if (og.publishedTime) {\n tags.push({ property: 'article:published_time', content: og.publishedTime })\n }\n if (og.modifiedTime) {\n tags.push({ property: 'article:modified_time', content: og.modifiedTime })\n }\n if (og.author) {\n tags.push({ property: 'article:author', content: og.author })\n }\n if (og.section) {\n tags.push({ property: 'article:section', content: og.section })\n }\n if (og.tags) {\n og.tags.forEach((tag) => {\n tags.push({ property: 'article:tag', content: tag })\n })\n }\n }\n\n return tags\n}\n\n/**\n * Generate Twitter Card meta tags from config.\n *\n * @param resolvedTitle - the final, template-applied page title (Twitter title\n * fallback), mirroring the OG behaviour.\n */\nexport function generateTwitterMeta(\n twitter: SeoTwitterConfig,\n global: GlobalSeoConfig,\n resolvedTitle: string,\n pageDescription: string\n): Array<{ name: string; content: string }> {\n const tags: Array<{ name: string; content: string }> = []\n\n tags.push({ name: 'twitter:card', content: twitter.card || 'summary_large_image' })\n tags.push({ name: 'twitter:title', content: twitter.title || resolvedTitle })\n tags.push({ name: 'twitter:description', content: twitter.description || pageDescription })\n\n const image = twitter.image || global.images?.twitterDefault\n if (image) {\n tags.push({ name: 'twitter:image', content: image })\n if (twitter.imageAlt || resolvedTitle) {\n tags.push({ name: 'twitter:image:alt', content: twitter.imageAlt || resolvedTitle })\n }\n }\n\n const site = twitter.site || global.social?.twitter\n if (site) {\n tags.push({ name: 'twitter:site', content: site.startsWith('@') ? site : `@${site}` })\n }\n\n if (twitter.creator) {\n tags.push({\n name: 'twitter:creator',\n content: twitter.creator.startsWith('@') ? twitter.creator : `@${twitter.creator}`,\n })\n }\n\n return tags\n}\n\n/**\n * Generate JSON-LD schema objects from schema configs, auto-populating common\n * WebSite properties from global config.\n */\nexport function generateJsonLd(schemas: SeoSchemaConfig[], global: GlobalSeoConfig): object[] {\n return schemas.map((schema) => {\n const base: Record<string, unknown> = {\n '@context': 'https://schema.org',\n '@type': schema.type,\n }\n\n // Merge properties\n if (schema.properties) {\n Object.assign(base, schema.properties)\n }\n\n // Auto-populate common properties from global config\n if (schema.type === 'WebSite' && global.siteUrl && !base.url) {\n base.url = global.siteUrl\n }\n if (schema.type === 'WebSite' && global.siteName && !base.name) {\n base.name = global.siteName\n }\n\n return base\n })\n}\n\n/**\n * Resolve page SEO by merging global defaults with page-specific config.\n *\n * Behavioural changes from the historical in-composable version (both bug\n * fixes):\n * 1. `openGraph.title` falls back to the **template-applied** page title, not\n * the raw `page.title`, so `og:title` matches `<title>`.\n * 2. The `titleTemplate` is applied **only** to a page-specific title\n * (`page.title`, or the `fallbackTitle` arg). It is no longer applied to\n * `global.defaultTitle`, which is already the complete brand title —\n * templating it produced `\"Brand | Default Title | Brand\"` doubling on any\n * page without its own `seo.yaml` entry.\n *\n * @param fallbackTitle - a page-specific title to use when `seo.yaml` has no\n * `title` for this page (e.g. the route `title` from `pages.yaml`). It IS run\n * through `titleTemplate`; `global.defaultTitle` is the last resort and is not.\n */\nexport function resolvePageSeo(\n pageSlug: string,\n pagePath: string | undefined,\n seoConfig: SeoConfiguration | undefined,\n fallbackTitle?: string\n): ResolvedPageSeo {\n const global = seoConfig?.global ?? {}\n const page = seoConfig?.pages?.[pageSlug] ?? {}\n\n // Build canonical URL\n let canonical = page.canonical || ''\n if (!canonical && global.siteUrl) {\n const path = pagePath ?? (pageSlug === 'home' ? '/' : `/${pageSlug}`)\n canonical = `${global.siteUrl.replace(/\\/$/, '')}${path}`\n }\n\n // Build title. The `%s` slot is for a PAGE-SPECIFIC title only (a seo.yaml\n // `page.title` or a `pages.yaml` route title). `global.defaultTitle` is\n // already brand-complete, so it is used verbatim — never re-templated.\n const pageSpecificTitle = page.title || fallbackTitle\n let title: string\n if (pageSpecificTitle) {\n title =\n page.noTitleTemplate || !global.titleTemplate\n ? pageSpecificTitle\n : global.titleTemplate.replace('%s', pageSpecificTitle)\n } else {\n title = global.defaultTitle || pageSlug\n }\n\n // Merge Open Graph. og:title now falls back to the *resolved* (templated)\n // title rather than the raw page.title, keeping it consistent with <title>.\n const openGraph: ResolvedPageSeo['openGraph'] = {\n type: page.openGraph?.type || 'website',\n title: page.openGraph?.title || title,\n description: page.openGraph?.description || page.description || global.defaultDescription || '',\n ...page.openGraph,\n }\n\n // Merge Twitter\n const twitter: ResolvedPageSeo['twitter'] = {\n card: page.twitter?.card || 'summary_large_image',\n ...page.twitter,\n }\n\n // Combine schemas (global + page)\n const schemas = [...(global.schemas ?? []), ...(page.schemas ?? [])]\n\n return {\n title,\n description: page.description || global.defaultDescription || '',\n canonical,\n robots: page.robots || global.robots || 'index, follow',\n openGraph,\n twitter,\n schemas,\n alternates: page.alternates ?? [],\n // Surface keywords on the resolved object so both runtime and emitter can\n // emit the meta tag without re-reading the raw page config.\n keywords: page.keywords,\n }\n}\n\n// =============================================================================\n// Unified head-tag builder (consumed by runtime + emitter)\n// =============================================================================\n\n/**\n * Build the complete, framework-agnostic set of `<head>` tags for a page.\n *\n * This is the function both the `useSEO` runtime and the build-time emitter\n * call, guaranteeing identical output. Pass `overrides` to mirror the\n * composable's `applyHead(overrides)` behaviour, or to force `robots` (used by\n * the emitter's `noindex` option).\n */\nexport function buildHeadTags(\n pageSlug: string,\n pagePath: string | undefined,\n seoConfig: SeoConfiguration | undefined,\n overrides?: HeadTagOverrides\n): ResolvedHeadTags {\n const resolved = resolvePageSeo(pageSlug, pagePath, seoConfig, overrides?.fallbackTitle)\n const global = seoConfig?.global ?? {}\n\n const title = overrides?.title ?? resolved.title\n const description = overrides?.description ?? resolved.description\n const robots = overrides?.robots ?? resolved.robots\n const keywords = overrides?.keywords ?? resolved.keywords\n\n // ── meta ────────────────────────────────────────────────────────────────\n const meta: HeadMetaTag[] = []\n\n meta.push({ name: 'description', content: description })\n\n // Keywords are opt-in (default off) so the runtime composable output is\n // unchanged; the static-HTML emitter passes includeKeywords: true.\n if (keywords && overrides?.includeKeywords) {\n meta.push({ name: 'keywords', content: keywords })\n }\n\n if (robots) {\n meta.push({ name: 'robots', content: robots })\n }\n\n // Verification codes\n if (global.verification?.google) {\n meta.push({ name: 'google-site-verification', content: global.verification.google })\n }\n if (global.verification?.bing) {\n meta.push({ name: 'msvalidate.01', content: global.verification.bing })\n }\n\n // Open Graph\n const ogMeta = generateOpenGraphMeta(resolved.openGraph, global, title, description, resolved.canonical)\n meta.push(...ogMeta.map((t) => ({ property: t.property, content: t.content })))\n\n // Twitter\n const twitterMeta = generateTwitterMeta(resolved.twitter, global, title, description)\n meta.push(...twitterMeta.map((t) => ({ name: t.name, content: t.content })))\n\n // Caller-supplied extra meta\n if (overrides?.meta) {\n meta.push(...overrides.meta)\n }\n\n // ── link ──────────────────────────────────────────────────────────────\n const link: HeadLinkTag[] = []\n\n if (resolved.canonical) {\n link.push({ rel: 'canonical', href: resolved.canonical })\n }\n\n resolved.alternates.forEach((alt) => {\n link.push({ rel: 'alternate', href: alt.href, hreflang: alt.hreflang })\n })\n\n // ── script (JSON-LD) ──────────────────────────────────────────────────\n const jsonLd = overrides?.schemas ?? generateJsonLd(resolved.schemas, global)\n const script: HeadScriptTag[] = jsonLd.map((schema) => ({\n type: 'application/ld+json',\n children: JSON.stringify(schema),\n }))\n\n return { title, meta, link, script, jsonLd, resolved }\n}\n","/**\n * Pure, framework-free `<head>` splicing for the build-time SEO emitter.\n *\n * Given a built `index.html` shell and a set of resolved head tags (from\n * `buildHeadTags`), this produces a new HTML string where the SEO-managed\n * tags — `<title>`, `description`, `keywords`, `robots`, `canonical`,\n * verification, all `og:*` / `article:*` properties, all `twitter:*` names,\n * and `application/ld+json` scripts — have been **replaced** (not duplicated)\n * with the resolved set.\n *\n * Design goals:\n * - **Idempotent**: running it twice yields the same output (it strips the\n * managed tags first, then re-inserts the canonical set).\n * - **Deterministic**: tag order is fixed by `renderHeadTags`.\n * - **Conservative**: only tags we own are touched. Charset, viewport, CSP,\n * theme-color, favicons, stylesheets, and the app script are left intact.\n *\n * This is intentionally regex-based (no DOM dependency) to mirror the existing\n * `dcsCdnImagePlugin` post-build HTML rewriting and to keep the emitter free of\n * heavy parser deps at build time.\n */\n\nimport type { ResolvedHeadTags } from './headTags'\n\n/** Escape a string for safe inclusion in a double-quoted HTML attribute. */\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\n/**\n * Escape JSON-LD content for safe inclusion inside a `<script>` element.\n * Only `</` sequences are dangerous (they can prematurely close the script);\n * the rest of the JSON is already a valid string literal.\n */\nfunction escapeJsonLd(json: string): string {\n return json.replace(/<\\//g, '<\\\\/')\n}\n\n/** `name=` meta tags that the emitter owns and will replace. */\nconst MANAGED_META_NAMES = new Set([\n 'description',\n 'keywords',\n 'robots',\n 'google-site-verification',\n 'msvalidate.01',\n])\n\n/**\n * Property/name prefixes the emitter owns. Any meta whose `property` starts\n * with one of these (og:, article:) or whose `name` starts with `twitter:` is\n * considered managed and stripped before re-insertion.\n */\nconst MANAGED_PROPERTY_PREFIXES = ['og:', 'article:']\nconst MANAGED_NAME_PREFIXES = ['twitter:']\n\n/**\n * Render the resolved head tags to a deterministic HTML fragment.\n * Order: title, meta (in the order produced by buildHeadTags), link, script.\n */\nexport function renderHeadTags(tags: ResolvedHeadTags, indent = ' '): string {\n const lines: string[] = []\n\n lines.push(`${indent}<title>${escapeAttr(tags.title)}</title>`)\n\n for (const m of tags.meta) {\n if (m.property !== undefined) {\n lines.push(`${indent}<meta property=\"${escapeAttr(m.property)}\" content=\"${escapeAttr(m.content)}\" />`)\n } else if (m.name !== undefined) {\n lines.push(`${indent}<meta name=\"${escapeAttr(m.name)}\" content=\"${escapeAttr(m.content)}\" />`)\n }\n }\n\n for (const l of tags.link) {\n const hreflang = l.hreflang ? ` hreflang=\"${escapeAttr(l.hreflang)}\"` : ''\n lines.push(`${indent}<link rel=\"${escapeAttr(l.rel)}\" href=\"${escapeAttr(l.href)}\"${hreflang} />`)\n }\n\n for (const s of tags.script) {\n lines.push(`${indent}<script type=\"${escapeAttr(s.type)}\">${escapeJsonLd(s.children)}</script>`)\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Strip the SEO-managed tags from a `<head>` block so they can be re-inserted\n * without duplication. Operates only within `<head>...</head>` to avoid\n * touching body content.\n */\nexport function stripManagedHeadTags(html: string): string {\n const headMatch = html.match(/<head[^>]*>([\\s\\S]*?)<\\/head>/i)\n if (!headMatch) return html\n\n let head = headMatch[1]\n\n // Remove existing <title>…</title>\n head = head.replace(/[ \\t]*<title>[\\s\\S]*?<\\/title>[ \\t]*\\r?\\n?/gi, '')\n\n // Remove existing JSON-LD scripts\n head = head.replace(\n /[ \\t]*<script[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>[\\s\\S]*?<\\/script>[ \\t]*\\r?\\n?/gi,\n ''\n )\n\n // Remove managed <meta name=\"…\"> and <meta property=\"…\"> tags.\n // Matches any <meta ...> tag, inspects its name/property, drops it if managed.\n head = head.replace(/[ \\t]*<meta\\b[^>]*>[ \\t]*\\r?\\n?/gi, (tag) => {\n const nameMatch = tag.match(/\\bname=[\"']([^\"']*)[\"']/i)\n const propMatch = tag.match(/\\bproperty=[\"']([^\"']*)[\"']/i)\n const name = nameMatch?.[1]?.toLowerCase()\n const property = propMatch?.[1]?.toLowerCase()\n\n if (name) {\n if (MANAGED_META_NAMES.has(name)) return ''\n if (MANAGED_NAME_PREFIXES.some((p) => name.startsWith(p))) return ''\n }\n if (property) {\n if (MANAGED_PROPERTY_PREFIXES.some((p) => property.startsWith(p))) return ''\n }\n return tag\n })\n\n // Remove existing canonical link (alternates are also managed)\n head = head.replace(\n /[ \\t]*<link\\b[^>]*\\brel=[\"'](?:canonical|alternate)[\"'][^>]*>[ \\t]*\\r?\\n?/gi,\n ''\n )\n\n return html.slice(0, headMatch.index! + headMatch[0].indexOf(headMatch[1])) +\n head +\n html.slice(headMatch.index! + headMatch[0].indexOf(headMatch[1]) + headMatch[1].length)\n}\n\n/**\n * Splice resolved SEO head tags into an HTML document.\n *\n * Strips the existing managed tags, then inserts the rendered canonical set\n * immediately before `</head>`. If no `<head>` is present the HTML is returned\n * unchanged (defensive — the emitter logs and no-ops in that case).\n *\n * Idempotent: applying twice produces identical output.\n */\nexport function spliceHeadHtml(html: string, tags: ResolvedHeadTags): string {\n if (!/<\\/head>/i.test(html)) {\n return html\n }\n\n const stripped = stripManagedHeadTags(html)\n const fragment = renderHeadTags(tags)\n\n // Insert before </head>, preserving the indentation of the closing tag.\n return stripped.replace(/([ \\t]*)<\\/head>/i, (_m, indent) => {\n return `${fragment}\\n${indent}</head>`\n })\n}\n","/**\n * Build-time SEO for VitePress static-site generation.\n *\n * VitePress 1.6 does **not** use `unhead`, so the runtime `useSEO`/`applyHead`\n * composable is a no-op against the SSG HTML. The SSG-correct sink is the\n * `transformPageData(pageData)` build hook: writing `<meta>`/`<link>`/JSON-LD\n * into `pageData.frontmatter.head` (VitePress bakes those into the rendered\n * `<head>`) and overwriting `pageData.title` / `pageData.description` (VitePress\n * renders the `<title>` — via `titleTemplate` — and the `description` meta from\n * those two fields).\n *\n * This factory generalises the bespoke `buildSeoHead`/`transformPageData` that\n * shipped inline in a site's `.vitepress/config.ts`. It reuses the shared,\n * framework-agnostic resolver (`resolvePageSeo`, `generateOpenGraphMeta`,\n * `generateTwitterMeta`, `generateJsonLd`) for global + page meta / OG / Twitter\n * / canonical and the global JSON-LD knowledge graph, and delegates **page-type\n * JSON-LD** (e.g. Article / Place / CollectionPage / Service / FAQPage +\n * BreadcrumbList) to a *pluggable* rule set the site supplies. None of the\n * real-estate (or any other vertical's) schema logic lives in this package — it\n * is all site CONFIG.\n *\n * It is the VitePress counterpart to the Vue-SPA per-route emitter in\n * `dcsSeoPlugin({ emitStaticHtml: true })`; both produce identical global\n * meta/OG/Twitter/JSON-LD from the same `seo.yaml` via the shared resolver.\n *\n * @example\n * ```ts\n * // docs/.vitepress/config.ts\n * import { createSeoTransformPageData } from '@duffcloudservices/cms/plugins'\n * import seoConfig from '../../.dcs/seo.yaml'\n *\n * export default defineConfig({\n * transformPageData: createSeoTransformPageData({\n * seoConfig,\n * pageTypeRules: [\n * { match: (ctx) => ctx.route.startsWith('/blogs/'), build: (ctx) => [ ... ] },\n * // ...Place / CollectionPage / Service / FAQPage rules\n * ],\n * }),\n * })\n * ```\n */\n\nimport type {\n SeoConfiguration,\n GlobalSeoConfig,\n SeoOpenGraphConfig,\n} from '../types/seo'\nimport {\n resolvePageSeo,\n generateOpenGraphMeta,\n generateTwitterMeta,\n generateJsonLd,\n} from './headTags'\n\n/**\n * A VitePress `head` entry. Mirrors VitePress's `HeadConfig` without taking a\n * dependency on the `vitepress` package (which is not a dependency of this\n * library). The tuple forms are:\n * ['meta', { name|property, content }]\n * ['link', { rel, href, ... }]\n * ['script', { type: 'application/ld+json' }, '<serialised json>']\n */\nexport type VitePressHeadConfig =\n | [string, Record<string, string>]\n | [string, Record<string, string>, string]\n\n/**\n * The minimal slice of VitePress's `PageData` this factory reads and mutates.\n * Typed structurally so callers can pass VitePress's real `PageData` without a\n * cast and without this package importing `vitepress`.\n */\nexport interface VitePressPageData {\n /** Source-relative path, e.g. `index.md`, `blogs/my-post.md`. */\n relativePath: string\n /** Dynamic-route params (e.g. `{ topic: 'home-buying' }`). */\n params?: Record<string, unknown>\n /** Page frontmatter; `head` is appended to here. */\n frontmatter: Record<string, any>\n /** VitePress page title (drives `<title>` via `titleTemplate`). */\n title?: string\n /** VitePress page description (drives the `description` meta). */\n description?: string\n [key: string]: unknown\n}\n\n/**\n * Context handed to the site's page-type rules and resolver hooks. Everything a\n * site needs to derive its title/description/og/schemas for one page, computed\n * once per page by the factory.\n */\nexport interface SeoPageContext {\n /** Route path, e.g. `/`, `/blogs/my-post`, `/locations/birmingham`. */\n route: string\n /** Slug: `'home'` for `/`, otherwise the route without its leading slash. */\n slug: string\n /** Absolute canonical URL for this route. */\n canonical: string\n /** Normalised site base URL (no trailing slash), e.g. `https://example.com`. */\n siteUrl: string\n /** The page's frontmatter (read-only convenience; same object as pageData). */\n frontmatter: Record<string, any>\n /** The resolved global SEO config block. */\n global: GlobalSeoConfig\n /** The full VitePress page data (for rules that need more than the above). */\n pageData: VitePressPageData\n}\n\n/**\n * The resolved per-page title / description / OG type a site may override.\n * Returned by the optional `resolvePage` hook so a site can apply its own\n * per-page-type title precedence (e.g. \"{City} Luxury Real Estate\") and decide\n * whether that title should win over VitePress's `titleTemplate`.\n */\nexport interface ResolvedPageOverrides {\n /**\n * The page title. When `setPageTitle` is true this is written to\n * `pageData.title` (VitePress then applies `titleTemplate`).\n */\n title?: string\n /**\n * When true, `title` is written back to `pageData.title`. Leave false for\n * pages whose frontmatter title should remain authoritative (e.g. blog posts\n * that already carry a good `<h1>`/title).\n */\n setPageTitle?: boolean\n /** The meta description. Written to `pageData.description` when truthy. */\n description?: string\n /** Open Graph type override (e.g. `'article'`, `'profile'`). */\n ogType?: SeoOpenGraphConfig['type']\n /** Open Graph image URL override (e.g. a post's header image). */\n ogImage?: string\n /** Keywords override (comma-separated) for the `keywords` meta. */\n keywords?: string\n /**\n * Open Graph title override. Defaults to `title` so og:title tracks <title>.\n */\n ogTitle?: string\n /**\n * Open Graph description override. Defaults to `description`.\n */\n ogDescription?: string\n}\n\n/** A single pluggable page-type rule: when `match` is true, emit `build`. */\nexport interface SeoPageTypeRule {\n /** Return true when this rule applies to the page (by route/slug/etc.). */\n match: (ctx: SeoPageContext) => boolean\n /** Build the page-type JSON-LD objects to emit (already plain objects). */\n build: (ctx: SeoPageContext) => Array<Record<string, unknown>>\n}\n\nexport interface CreateSeoTransformPageDataOptions {\n /** The parsed `.dcs/seo.yaml` (global graph + per-page meta). */\n seoConfig: SeoConfiguration | undefined\n /**\n * Pluggable page-type rules. Evaluated in order; **every** matching rule's\n * `build` output is emitted (so a route can contribute both a primary schema\n * and a BreadcrumbList from one rule, or be matched by several). The\n * real-estate BlogPosting / Place / CollectionPage / Service / FAQPage logic\n * is supplied here by the site — never hardcoded in this package.\n */\n pageTypeRules?: SeoPageTypeRule[]\n /**\n * Optional hook to override per-page title / description / OG before tags are\n * built — the site's title precedence and per-type description fallbacks.\n * Receives the same context as the rules. Anything it omits falls back to the\n * resolver / frontmatter defaults.\n */\n resolvePage?: (ctx: SeoPageContext) => ResolvedPageOverrides | undefined\n /**\n * Map a `relativePath` (+ params) to a route. Defaults to a VitePress-correct\n * implementation: `index` becomes `/`, a trailing `/index` is dropped, `.md`\n * is stripped, and dynamic `[name]` segments are substituted from\n * `pageData.params`. Override only for unusual routing.\n */\n relativePathToRoute?: (relativePath: string, params?: Record<string, unknown>) => string\n /**\n * Emit a `<meta name=\"keywords\">` from the resolved/overridden keywords.\n * Default true (parity with the bespoke KDH emitter, which emitted keywords).\n */\n includeKeywords?: boolean\n /** Enable debug logging of the emitted head per page. */\n debug?: boolean\n}\n\n/** Default VitePress route derivation (matches the bespoke KDH helper). */\nexport function defaultRelativePathToRoute(\n relativePath: string,\n params?: Record<string, unknown>\n): string {\n let stem = (relativePath ?? '').replace(/\\.md$/i, '')\n // Substitute dynamic [param] segments (e.g. topics/[topic] → topics/home-buying).\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue\n stem = stem.replace(`[${key}]`, String(value))\n }\n }\n if (stem === 'index') return '/'\n if (stem.endsWith('/index')) stem = stem.slice(0, -'/index'.length)\n return `/${stem}`\n}\n\nfunction normaliseSiteUrl(siteUrl: string | undefined): string {\n return (siteUrl ?? '').replace(/\\/$/, '')\n}\n\n/** A `<script type=\"application/ld+json\">` VitePress head tuple. */\nfunction ldScript(obj: Record<string, unknown>): VitePressHeadConfig {\n return ['script', { type: 'application/ld+json' }, JSON.stringify(obj)]\n}\n\n/**\n * Build just the SEO head tuples for a page (no `pageData` mutation). Exposed\n * separately so it is unit-testable without a VitePress `pageData` round-trip\n * and reusable by callers that manage the `head`/`title` sinks themselves.\n *\n * @returns `{ head, title, description }` — the head tuples to append, and the\n * final title/description (already overridden) the caller should write to\n * `pageData` when `applyTitle`/`applyDescription` are appropriate.\n */\nexport function buildVitePressSeoHead(\n pageData: VitePressPageData,\n options: CreateSeoTransformPageDataOptions\n): { head: VitePressHeadConfig[]; title?: string; description?: string; setPageTitle: boolean } {\n const {\n seoConfig,\n pageTypeRules = [],\n resolvePage,\n relativePathToRoute = defaultRelativePathToRoute,\n includeKeywords = true,\n } = options\n\n const global = seoConfig?.global ?? {}\n const siteUrl = normaliseSiteUrl(global.siteUrl)\n\n const fm = pageData.frontmatter ?? {}\n const route = relativePathToRoute(pageData.relativePath, pageData.params)\n const slug = route === '/' ? 'home' : route.slice(1)\n const canonical = route === '/' ? `${siteUrl}/` : `${siteUrl}${route}`\n\n const ctx: SeoPageContext = {\n route,\n slug,\n canonical,\n siteUrl,\n frontmatter: fm,\n global,\n pageData,\n }\n\n // Resolve global + per-page SEO from the shared resolver. We pass the page's\n // frontmatter title as the fallback so un-configured pages still get a\n // sensible (templated) title; seo.yaml `pages.<slug>.title` still wins.\n const resolved = resolvePageSeo(slug, route, seoConfig, fm.title)\n\n // Site-provided per-page overrides (title precedence + per-type description).\n const overrides = resolvePage?.(ctx) ?? {}\n\n // ── Title handling ────────────────────────────────────────────────────────\n // Two titles matter for VitePress:\n // • `rawTitle` → written to `pageData.title` (VitePress applies its OWN\n // `titleTemplate`, so the <title> element is templated once).\n // • `templated` → used for og:title / twitter:title so they match the final\n // <title> element (the shared resolver's og:title fix).\n // A site override (`overrides.title`) is the RAW per-type title; we template\n // it ourselves for OG/Twitter using the same precedence as the resolver.\n const rawTitle =\n overrides.title ?? pageSpecificSeoTitle(slug, seoConfig) ?? fm.title ?? resolved.title\n const templated = applyTitleTemplate(rawTitle, global, resolved)\n\n // Description precedence mirrors the bespoke emitter: a site per-type override\n // → the explicit seo.yaml `page.description` → the page frontmatter → the\n // global default. (The shared resolver collapses frontmatter and the global\n // default, so we read the explicit page description directly to keep\n // frontmatter ahead of the global fallback.)\n const pageSeoDescription = seoConfig?.pages?.[slug]?.description\n const description =\n overrides.description ||\n pageSeoDescription ||\n fm.description ||\n global.defaultDescription ||\n ''\n const keywords =\n overrides.keywords ||\n seoConfig?.pages?.[slug]?.keywords ||\n (Array.isArray(fm.tags) ? fm.tags.join(', ') : '')\n\n // Open Graph type/image: site override → seo.yaml → frontmatter → default.\n const ogType =\n overrides.ogType || resolved.openGraph.type\n const ogImage =\n overrides.ogImage || resolved.openGraph.image || fm.headerImage || fm.image || global.images?.ogDefault\n\n const head: VitePressHeadConfig[] = []\n\n // Keywords (parity: the bespoke emitter pushed keywords when present).\n if (includeKeywords && keywords) {\n head.push(['meta', { name: 'keywords', content: keywords }])\n }\n\n // Robots.\n head.push(['meta', { name: 'robots', content: resolved.robots }])\n\n // Canonical.\n head.push(['link', { rel: 'canonical', href: canonical }])\n\n // Verification.\n if (global.verification?.google) {\n head.push(['meta', { name: 'google-site-verification', content: global.verification.google }])\n }\n if (global.verification?.bing) {\n head.push(['meta', { name: 'msvalidate.01', content: global.verification.bing }])\n }\n\n // Open Graph — reuse the shared generator, with our resolved title/desc/og\n // overrides applied via a synthetic OG config so the output matches the SPA\n // emitter byte-for-byte (article:* fields included when type === 'article').\n // og:title tracks the TEMPLATED title (the resolver's og:title fix), so it\n // matches the final <title> element rather than the raw per-type title.\n //\n // We DON'T spread `resolved.openGraph` wholesale: for an un-configured page\n // its `description` is the global default, which would wrongly win over this\n // page's actual `description`. We instead keep only the page's *explicit* OG\n // fields (from seo.yaml) and let `description`/`title` fall through correctly.\n const pageOg = seoConfig?.pages?.[slug]?.openGraph\n const ogConfig: SeoOpenGraphConfig = {\n ...pageOg,\n type: ogType,\n title: overrides.ogTitle || templated,\n description: overrides.ogDescription || pageOg?.description || description,\n ...(ogImage ? { image: ogImage } : {}),\n url: canonical,\n }\n for (const t of generateOpenGraphMeta(ogConfig, global, templated, description, canonical)) {\n head.push(['meta', { property: t.property, content: t.content }])\n }\n\n // Twitter — shared generator (twitter:title also tracks the templated title).\n for (const t of generateTwitterMeta(resolved.twitter, global, templated, description)) {\n head.push(['meta', { name: t.name, content: t.content }])\n }\n\n // JSON-LD: the global knowledge graph (RealEstateAgent / Person / WebSite /\n // Organization / …) on every page, via the shared generator.\n for (const obj of generateJsonLd(resolved.schemas, global)) {\n head.push(ldScript(obj as Record<string, unknown>))\n }\n\n // Page-type JSON-LD: every matching site rule contributes its objects.\n for (const rule of pageTypeRules) {\n let matched = false\n try {\n matched = rule.match(ctx)\n } catch (err) {\n console.warn('[dcs-seo] pageTypeRule.match threw; skipping rule:', err)\n continue\n }\n if (!matched) continue\n let objects: Array<Record<string, unknown>> = []\n try {\n objects = rule.build(ctx) ?? []\n } catch (err) {\n console.warn('[dcs-seo] pageTypeRule.build threw; skipping rule output:', err)\n continue\n }\n for (const obj of objects) {\n if (obj) head.push(ldScript(obj))\n }\n }\n\n return {\n // Hand back the RAW title for `pageData.title`; VitePress applies its own\n // `titleTemplate` to produce the templated <title> element.\n title: rawTitle,\n head,\n description: description || undefined,\n setPageTitle: overrides.setPageTitle ?? false,\n }\n}\n\n/**\n * Compute the page-specific (un-templated) `seo.yaml` title for a slug, if any.\n * This is the title VitePress should template — it must NOT come pre-templated.\n */\nfunction pageSpecificSeoTitle(\n slug: string,\n seoConfig: SeoConfiguration | undefined\n): string | undefined {\n return seoConfig?.pages?.[slug]?.title || undefined\n}\n\n/**\n * Apply the global `titleTemplate` to a raw page title, matching the shared\n * resolver's precedence: a page that opts out (`noTitleTemplate`) or a\n * brand-complete global default is used verbatim; otherwise `%s` is filled.\n *\n * When `rawTitle` is the resolver's already-final title (e.g. the global\n * `defaultTitle`, used when there is no page-specific title), we return it\n * unchanged to avoid double-templating.\n */\nfunction applyTitleTemplate(\n rawTitle: string,\n global: GlobalSeoConfig,\n resolved: ResolvedPageSeoLike\n): string {\n // If rawTitle is exactly the resolver's final title, it is either already\n // templated (page-specific) or a brand-complete default — use as-is.\n if (rawTitle === resolved.title) return rawTitle\n if (!global.titleTemplate) return rawTitle\n return global.titleTemplate.replace('%s', rawTitle)\n}\n\n/** Structural subset of `ResolvedPageSeo` used by the title helper. */\ninterface ResolvedPageSeoLike {\n title: string\n}\n\n/**\n * Create a VitePress `transformPageData(pageData)` function that bakes DCS SEO\n * (global meta/OG/Twitter/canonical + global JSON-LD graph + pluggable\n * page-type JSON-LD) into the SSG `<head>`.\n *\n * Mutations performed on `pageData`:\n * - **`frontmatter.head`** — the resolved tags are *appended* to any existing\n * `head` (so site-level `head` config is preserved).\n * - **`description`** — set to the resolved/overridden description so\n * VitePress emits exactly one `description` meta (no duplicate; we do not\n * push our own description meta).\n * - **`title`** — set only when the site's `resolvePage` hook returns\n * `setPageTitle: true` for this page, mirroring the bespoke behaviour where\n * seo.yaml/per-type titles are authoritative but a post's frontmatter title\n * is left intact.\n *\n * Defensive: never throws (a failure logs a warning and leaves `pageData`\n * untouched), so SEO can never break a production VitePress build.\n */\nexport function createSeoTransformPageData(\n options: CreateSeoTransformPageDataOptions\n): (pageData: VitePressPageData) => void {\n return function transformPageData(pageData: VitePressPageData): void {\n try {\n const { head, title, description, setPageTitle } = buildVitePressSeoHead(pageData, options)\n\n pageData.frontmatter = pageData.frontmatter ?? {}\n const existing = (pageData.frontmatter.head as VitePressHeadConfig[] | undefined) ?? []\n pageData.frontmatter.head = [...existing, ...head]\n\n // Drive VitePress's own description meta from our resolved value so it is\n // not duplicated by the site-level default. (VitePress reads\n // pageData.description BEFORE transformPageData and renders the meta from\n // it, so overwriting the field here is the correct sink.)\n if (description) {\n pageData.description = description\n }\n\n // Make seo.yaml / per-type titles authoritative for <title> when the site\n // asks for it; VitePress then appends its titleTemplate.\n if (setPageTitle && title) {\n pageData.title = title\n }\n\n if (options.debug) {\n console.log(\n `[dcs-seo] transformPageData ${pageData.relativePath} → +${head.length} head tag(s)` +\n (setPageTitle && title ? ` (title=\"${title}\")` : '')\n )\n }\n } catch (err) {\n console.warn(\n `[dcs-seo] createSeoTransformPageData failed for ${pageData?.relativePath}; head left unchanged:`,\n err\n )\n }\n }\n}\n","/**\n * Loader for the `.dcs/pages.yaml` route manifest.\n *\n * `pages.yaml` is the canonical page registry maintained by the DCS portal and\n * by Copilot when scaffolding pages. For the build-time SEO emitter we only\n * need each route's `slug` and `path` (e.g. `{ slug: 'home', path: '/' }`).\n *\n * The parser is intentionally defensive: any missing/unparseable file or\n * malformed entry yields `null` (caller logs + no-ops) so a bad manifest can\n * never break a production build.\n */\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport yaml from 'js-yaml'\n\n/** A single route extracted from `.dcs/pages.yaml`. */\nexport interface PageRouteEntry {\n /** Page slug, matching an entry in `seo.yaml` `pages.<slug>` (may be absent). */\n slug: string\n /** Route path, e.g. `/`, `/services`, `/blog/my-post`. */\n path: string\n /**\n * Human title from the manifest (e.g. \"Kitchen Cabinet Refresh\"). Used as the\n * per-route title fallback when `seo.yaml` has no entry for this page, so\n * un-configured routes (e.g. blog posts) get unique titles. Optional.\n */\n title?: string\n}\n\n/** Shape of the relevant slice of `.dcs/pages.yaml`. */\ninterface RawPagesManifest {\n pages?: Array<{ slug?: unknown; path?: unknown; title?: unknown }>\n}\n\n/**\n * Resolve and parse `.dcs/pages.yaml` from one of the usual locations.\n *\n * Mirrors the `.dcs` path resolution used elsewhere in the package (project\n * root, parent dir for VitePress-style nesting, and cwd).\n *\n * @returns the list of `{ slug, path }` routes, or `null` if the file is\n * absent, unreadable, unparseable, or contains no usable page entries.\n */\nexport function loadPagesManifest(\n projectRoot: string,\n relativePagesPath: string,\n debug = false\n): PageRouteEntry[] | null {\n const possiblePaths = [\n path.resolve(projectRoot, relativePagesPath),\n path.resolve(projectRoot, '..', relativePagesPath),\n path.resolve(process.cwd(), relativePagesPath),\n ]\n\n let foundPath: string | undefined\n for (const testPath of possiblePaths) {\n if (fs.existsSync(testPath)) {\n foundPath = testPath\n break\n }\n }\n\n if (!foundPath) {\n if (debug) {\n console.warn('[dcs-seo] No pages.yaml found at:')\n possiblePaths.forEach((p) => console.warn(` - ${p}`))\n }\n return null\n }\n\n let raw: RawPagesManifest\n try {\n raw = yaml.load(fs.readFileSync(foundPath, 'utf8')) as RawPagesManifest\n } catch (error) {\n console.warn(`[dcs-seo] Failed to parse ${foundPath}:`, error)\n return null\n }\n\n const entries = parsePagesManifest(raw)\n if (!entries) {\n console.warn(`[dcs-seo] pages.yaml at ${foundPath} has no usable page entries`)\n return null\n }\n\n if (debug) {\n console.log(`[dcs-seo] Loaded ${entries.length} routes from ${foundPath}`)\n }\n return entries\n}\n\n/**\n * Extract `{ slug, path }` routes from an already-parsed manifest object.\n * Exposed separately so tests can exercise it without touching the filesystem.\n *\n * Entries missing a string `path` are skipped; a missing slug falls back to the\n * empty string (so the route still gets baked, using global SEO defaults).\n */\nexport function parsePagesManifest(raw: unknown): PageRouteEntry[] | null {\n if (!raw || typeof raw !== 'object') return null\n const pages = (raw as RawPagesManifest).pages\n if (!Array.isArray(pages)) return null\n\n const routes: PageRouteEntry[] = []\n for (const entry of pages) {\n if (!entry || typeof entry !== 'object') continue\n const p = (entry as { path?: unknown }).path\n if (typeof p !== 'string' || p.length === 0) continue\n const slug = (entry as { slug?: unknown }).slug\n const title = (entry as { title?: unknown }).title\n routes.push({\n slug: typeof slug === 'string' ? slug : '',\n path: p,\n ...(typeof title === 'string' && title.length > 0 ? { title } : {}),\n })\n }\n\n return routes.length > 0 ? routes : null\n}\n"]}
|