@duffcloudservices/cms 0.3.16 → 0.4.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/README.md +39 -4
- package/dist/chunk-RDYVYYTC.js +311 -0
- package/dist/chunk-RDYVYYTC.js.map +1 -0
- package/dist/index.d.ts +199 -263
- package/dist/index.js +14 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +93 -16
- package/dist/plugins/index.js +167 -61
- package/dist/plugins/index.js.map +1 -1
- package/dist/seo-DsJjfI1p.d.ts +267 -0
- package/package.json +4 -1
- package/src/components/ManagedImage.vue +66 -0
- package/src/components/PreviewRibbon.vue +4 -5
- package/src/components/ResponsiveImage.vue +11 -2
- package/src/composables/useReleaseNotes.ts +6 -7
- package/src/composables/useSEO.ts +21 -259
- package/src/composables/useSiteVersion.ts +5 -6
- package/src/composables/useTextContent.test.ts +46 -0
- package/src/composables/useTextContent.ts +13 -5
package/README.md
CHANGED
|
@@ -61,11 +61,15 @@ export default defineConfig({
|
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
# .env
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Optional: for runtime overrides (premium tier)
|
|
64
|
+
# For runtime overrides (premium tier): the API base URL only.
|
|
67
65
|
VITE_API_BASE_URL=https://portal.duffcloudservices.com
|
|
68
66
|
VITE_TEXT_OVERRIDE_MODE=commit # 'commit' (default) or 'runtime'
|
|
67
|
+
|
|
68
|
+
# Deprecated: the site is now resolved server-side from the request Host
|
|
69
|
+
# (or the dedicated Container App's DCS_SITE_SLUG). VITE_SITE_SLUG is no
|
|
70
|
+
# longer placed in request URLs; it is optional and used only as a local
|
|
71
|
+
# cache-key hint. Safe to omit.
|
|
72
|
+
# VITE_SITE_SLUG=your-site-slug
|
|
69
73
|
```
|
|
70
74
|
|
|
71
75
|
### 3. Use Composables
|
|
@@ -312,7 +316,38 @@ entry points.
|
|
|
312
316
|
|
|
313
317
|
### Managed background images
|
|
314
318
|
|
|
315
|
-
For customer-site hero, CTA, footer, and
|
|
319
|
+
For customer-site hero, CTA, footer, card, staff, and gallery images that need editor image replacement, prefer the exported `ManagedImage` component over CSS-only `background-image` or hardcoded image arrays.
|
|
320
|
+
|
|
321
|
+
```vue
|
|
322
|
+
<script setup lang="ts">
|
|
323
|
+
import ManagedImage from '@duffcloudservices/cms/managed-image'
|
|
324
|
+
</script>
|
|
325
|
+
|
|
326
|
+
<template>
|
|
327
|
+
<ManagedImage
|
|
328
|
+
page-slug="home"
|
|
329
|
+
image-key="hero.image.url"
|
|
330
|
+
alt-key="hero.image.alt"
|
|
331
|
+
fallback-src="/images/hero.jpg"
|
|
332
|
+
fallback-alt="Clinic hero"
|
|
333
|
+
context="hero"
|
|
334
|
+
class="h-full w-full object-cover"
|
|
335
|
+
/>
|
|
336
|
+
</template>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Add the URL and alt keys to `.dcs/content.yaml` using CDN URLs for committed content:
|
|
340
|
+
|
|
341
|
+
```yaml
|
|
342
|
+
pages:
|
|
343
|
+
home:
|
|
344
|
+
hero.image.url: https://files.duffcloudservices.com/content/site-slug/assets/example.jpg
|
|
345
|
+
hero.image.alt: Clinic hero
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`ManagedImage` resolves build-time/runtime content with `useTextContent`, renders a real `<picture>/<img>` target, applies responsive CDN variants through `useResponsiveImage`, and emits `data-dcs-image-key`, `data-dcs-image-url`, and `data-dcs-image-alt` on the actual `<img>`. This lets the visual editor open image management for the asset and lets snapshot capture wait on an actual image before `full-page.png`.
|
|
349
|
+
|
|
350
|
+
For repeated galleries or cards, use indexed keys such as `gallery.item-0.image.url` and `gallery.item-0.image.alt`, then pass those keys through the rendered item. Do not make editable images CSS-only backgrounds; if a design needs background behavior, render a real managed image layer behind the content.
|
|
316
351
|
|
|
317
352
|
Current first-party surfaces:
|
|
318
353
|
|
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
function loadPagesManifest(projectRoot, relativePagesPath, debug = false) {
|
|
253
|
+
const possiblePaths = [
|
|
254
|
+
path.resolve(projectRoot, relativePagesPath),
|
|
255
|
+
path.resolve(projectRoot, "..", relativePagesPath),
|
|
256
|
+
path.resolve(process.cwd(), relativePagesPath)
|
|
257
|
+
];
|
|
258
|
+
let foundPath;
|
|
259
|
+
for (const testPath of possiblePaths) {
|
|
260
|
+
if (fs.existsSync(testPath)) {
|
|
261
|
+
foundPath = testPath;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!foundPath) {
|
|
266
|
+
if (debug) {
|
|
267
|
+
console.warn("[dcs-seo] No pages.yaml found at:");
|
|
268
|
+
possiblePaths.forEach((p) => console.warn(` - ${p}`));
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
let raw;
|
|
273
|
+
try {
|
|
274
|
+
raw = yaml.load(fs.readFileSync(foundPath, "utf8"));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.warn(`[dcs-seo] Failed to parse ${foundPath}:`, error);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const entries = parsePagesManifest(raw);
|
|
280
|
+
if (!entries) {
|
|
281
|
+
console.warn(`[dcs-seo] pages.yaml at ${foundPath} has no usable page entries`);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (debug) {
|
|
285
|
+
console.log(`[dcs-seo] Loaded ${entries.length} routes from ${foundPath}`);
|
|
286
|
+
}
|
|
287
|
+
return entries;
|
|
288
|
+
}
|
|
289
|
+
function parsePagesManifest(raw) {
|
|
290
|
+
if (!raw || typeof raw !== "object") return null;
|
|
291
|
+
const pages = raw.pages;
|
|
292
|
+
if (!Array.isArray(pages)) return null;
|
|
293
|
+
const routes = [];
|
|
294
|
+
for (const entry of pages) {
|
|
295
|
+
if (!entry || typeof entry !== "object") continue;
|
|
296
|
+
const p = entry.path;
|
|
297
|
+
if (typeof p !== "string" || p.length === 0) continue;
|
|
298
|
+
const slug = entry.slug;
|
|
299
|
+
const title = entry.title;
|
|
300
|
+
routes.push({
|
|
301
|
+
slug: typeof slug === "string" ? slug : "",
|
|
302
|
+
path: p,
|
|
303
|
+
...typeof title === "string" && title.length > 0 ? { title } : {}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return routes.length > 0 ? routes : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export { buildHeadTags, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, loadPagesManifest, renderHeadTags, resolvePageSeo, spliceHeadHtml, stripManagedHeadTags };
|
|
310
|
+
//# sourceMappingURL=chunk-RDYVYYTC.js.map
|
|
311
|
+
//# sourceMappingURL=chunk-RDYVYYTC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/seo/headTags.ts","../src/seo/spliceHeadHtml.ts","../src/seo/pagesManifest.ts"],"names":["path"],"mappings":"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","file":"chunk-RDYVYYTC.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 * 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"]}
|