@duffcloudservices/cms 0.9.0 → 0.11.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.
@@ -1,19 +1,20 @@
1
- import { buildSitemapXml, isHandAuthoredRobotsAcceptable, buildRobotsTxt, buildLlmsTxt, matchesExcludedGlob, breadcrumbTrailFromRoute, findReviewItemsForPage, buildHeadTags, spliceHeadHtml, loadPagesManifest } from '../chunk-UPAMLKOQ.js';
2
- export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-UPAMLKOQ.js';
3
- import fs3 from 'fs';
4
- import path3 from 'path';
5
- import yaml3 from 'js-yaml';
1
+ import { buildSitemapXml, isHandAuthoredRobotsAcceptable, buildRobotsTxt, buildLlmsTxt, matchesExcludedGlob, breadcrumbTrailFromRoute, findReviewItemsForPage, findBusinessLicense, buildHeadTags, spliceHeadHtml, routeToOutputFile, loadPagesManifest, isBodyPrerenderEnabled, prerenderBodies } from '../chunk-F3EIWEZD.js';
2
+ export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-F3EIWEZD.js';
3
+ import fs5 from 'fs';
4
+ import path5 from 'path';
5
+ import yaml4 from 'js-yaml';
6
+ import http from 'http';
6
7
  import { defineComponent, h } from 'vue';
7
8
 
8
9
  function dcsSearchPaths(projectRoot, relPath) {
9
10
  return [
10
- path3.resolve(projectRoot, relPath),
11
- path3.resolve(projectRoot, "..", relPath),
12
- path3.resolve(process.cwd(), relPath)
11
+ path5.resolve(projectRoot, relPath),
12
+ path5.resolve(projectRoot, "..", relPath),
13
+ path5.resolve(process.cwd(), relPath)
13
14
  ];
14
15
  }
15
16
  function resolveDcsFile(projectRoot, relPath) {
16
- return dcsSearchPaths(projectRoot, relPath).find((p) => fs3.existsSync(p));
17
+ return dcsSearchPaths(projectRoot, relPath).find((p) => fs5.existsSync(p));
17
18
  }
18
19
  function readDcsYamlFresh(projectRoot, relPath, label, debug = false) {
19
20
  const found = resolveDcsFile(projectRoot, relPath);
@@ -22,7 +23,7 @@ function readDcsYamlFresh(projectRoot, relPath, label, debug = false) {
22
23
  return void 0;
23
24
  }
24
25
  try {
25
- return yaml3.load(fs3.readFileSync(found, "utf8"));
26
+ return yaml4.load(fs5.readFileSync(found, "utf8"));
26
27
  } catch (error) {
27
28
  console.warn(`[${label}] serve: failed to parse ${relPath}:`, error);
28
29
  return void 0;
@@ -33,7 +34,84 @@ function buildGlobalAssignScript(globalName, value) {
33
34
  return `globalThis.${globalName} = ${json};`;
34
35
  }
35
36
 
37
+ // src/plugins/motionTokens.ts
38
+ var DCS_MOTION_TOKENS_CSS = `:root {
39
+ --motion-duration-stagger: 40ms;
40
+ --motion-duration-micro: 80ms;
41
+ --motion-duration-quick: 150ms;
42
+ --motion-duration-fast: 250ms;
43
+ --motion-duration-medium: 350ms;
44
+ --motion-duration-slow: 400ms;
45
+ --motion-duration-very-slow: 500ms;
46
+ --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
47
+ --motion-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
48
+ --motion-ease-linear: linear;
49
+ --motion-ease-bounce: cubic-bezier(0.34, 1.36, 0.64, 1);
50
+ --motion-ease-bounce-strong: cubic-bezier(0.34, 3.85, 0.64, 1);
51
+ --motion-distance-xs: 4px;
52
+ --motion-distance-sm: 8px;
53
+ --motion-distance-md: 12px;
54
+ --motion-distance-lg: 24px;
55
+ --motion-scale-lg: 0.96;
56
+ --motion-scale-md: 0.97;
57
+ --motion-scale-sm: 0.98;
58
+ --motion-scale-xs: 0.99;
59
+ --motion-blur-sm: 2px;
60
+ --motion-blur-md: 3px;
61
+ --motion-blur-lg: 8px;
62
+ }`;
63
+ function dcsMotionTokensStyleTag() {
64
+ return {
65
+ tag: "style",
66
+ attrs: { "data-dcs-motion-tokens": "" },
67
+ children: DCS_MOTION_TOKENS_CSS,
68
+ injectTo: "head-prepend"
69
+ };
70
+ }
71
+ var DEFAULT_AGENT_API_BASE_URL = "https://portal.duffcloudservices.com";
72
+ var DEFAULT_AGENT_WIDGET_SCRIPT_URL = "https://files.duffcloudservices.com/agent-widget/v1/dcs-agent-widget.js";
73
+ var trimTrailingSlash = (value) => value.replace(/\/+$/, "");
74
+ var encodeForInlineScript = (value) => JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
75
+ function buildAgentWidgetLoaderScript(options) {
76
+ const slug = encodeForInlineScript(options.siteSlug);
77
+ const api = encodeForInlineScript(trimTrailingSlash(options.apiBaseUrl || DEFAULT_AGENT_API_BASE_URL));
78
+ const cdn = encodeForInlineScript(options.cdnScriptUrl || DEFAULT_AGENT_WIDGET_SCRIPT_URL);
79
+ return "(function(){try{if(window.__DCS_AGENT_LOADER__)return;window.__DCS_AGENT_LOADER__=1;try{if(window.self!==window.top)return;}catch(e){return;}if(/[?&]dcs-hide-widget(?:[=&]|$)/.test(location.search))return;var S=" + slug + ",A=" + api + ",C=" + cdn + ';var go=function(){try{fetch(A+"/api/v1/sites/"+encodeURIComponent(S)+"/agent/config",{headers:{Accept:"application/json"}}).then(function(r){return r&&r.ok?r.json():null;}).then(function(c){if(!c||!c.enabled)return;window.__DCS_AGENT_BOOTSTRAP__={siteSlug:S,apiBaseUrl:A,config:c};var s=document.createElement("script");s.src=C;s.async=true;s.setAttribute("data-dcs-agent-slug",S);(document.head||document.documentElement).appendChild(s);}).catch(function(){});}catch(e){}};if("requestIdleCallback"in window){requestIdleCallback(go,{timeout:4000});}else{setTimeout(go,1200);}}catch(e){}})();';
80
+ }
81
+ var SITE_YAML_CANDIDATES = [".dcs/site.yaml", "../.dcs/site.yaml"];
82
+ function readSiteSlug(projectRoot) {
83
+ const candidates = [
84
+ ...SITE_YAML_CANDIDATES.map((rel) => path5.resolve(projectRoot, rel)),
85
+ path5.resolve(process.cwd(), ".dcs/site.yaml")
86
+ ];
87
+ for (const candidate of candidates) {
88
+ try {
89
+ if (!fs5.existsSync(candidate)) continue;
90
+ const parsed = yaml4.load(fs5.readFileSync(candidate, "utf8"));
91
+ const slug = parsed && typeof parsed.site_slug === "string" ? parsed.site_slug.trim() : "";
92
+ if (slug) return slug;
93
+ } catch {
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ function dcsAgentWidgetLoaderTag(options) {
99
+ const siteSlug = readSiteSlug(options.projectRoot);
100
+ if (!siteSlug) return null;
101
+ return {
102
+ tag: "script",
103
+ attrs: { "data-dcs-agent-loader": "" },
104
+ children: buildAgentWidgetLoaderScript({
105
+ siteSlug,
106
+ apiBaseUrl: options.apiBaseUrl,
107
+ cdnScriptUrl: options.cdnScriptUrl
108
+ }),
109
+ injectTo: "head"
110
+ };
111
+ }
112
+
36
113
  // src/plugins/dcsContentPlugin.ts
114
+ var DCS_SSR_NO_EXTERNAL = ["@duffcloudservices/cms"];
37
115
  function dcsContentPlugin(options = {}) {
38
116
  const { contentPath = ".dcs/content.yaml", debug = false } = options;
39
117
  let resolvedConfig;
@@ -49,18 +127,19 @@ function dcsContentPlugin(options = {}) {
49
127
  return {
50
128
  define: {
51
129
  __DCS_CONTENT__: "globalThis.__DCS_CONTENT__"
52
- }
130
+ },
131
+ ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
53
132
  };
54
133
  }
55
134
  const projectRoot = config.root || process.cwd();
56
135
  const possiblePaths = [
57
- path3.resolve(projectRoot, contentPath),
58
- path3.resolve(projectRoot, "..", contentPath),
59
- path3.resolve(process.cwd(), contentPath)
136
+ path5.resolve(projectRoot, contentPath),
137
+ path5.resolve(projectRoot, "..", contentPath),
138
+ path5.resolve(process.cwd(), contentPath)
60
139
  ];
61
140
  let foundPath;
62
141
  for (const testPath of possiblePaths) {
63
- if (fs3.existsSync(testPath)) {
142
+ if (fs5.existsSync(testPath)) {
64
143
  foundPath = testPath;
65
144
  break;
66
145
  }
@@ -74,12 +153,13 @@ function dcsContentPlugin(options = {}) {
74
153
  return {
75
154
  define: {
76
155
  __DCS_CONTENT__: "undefined"
77
- }
156
+ },
157
+ ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
78
158
  };
79
159
  }
80
160
  try {
81
- const fileContent = fs3.readFileSync(foundPath, "utf8");
82
- const content = yaml3.load(fileContent);
161
+ const fileContent = fs5.readFileSync(foundPath, "utf8");
162
+ const content = yaml4.load(fileContent);
83
163
  if (debug) {
84
164
  console.log(`[dcs-content] Loaded ${foundPath}`);
85
165
  console.log(`[dcs-content] Version: ${content.version}`);
@@ -89,50 +169,68 @@ function dcsContentPlugin(options = {}) {
89
169
  return {
90
170
  define: {
91
171
  __DCS_CONTENT__: JSON.stringify(content)
92
- }
172
+ },
173
+ ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
93
174
  };
94
175
  } catch (error) {
95
176
  console.warn("[dcs-content] Failed to parse content.yaml:", error);
96
177
  return {
97
178
  define: {
98
179
  __DCS_CONTENT__: "undefined"
99
- }
180
+ },
181
+ ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
100
182
  };
101
183
  }
102
184
  },
103
185
  /**
104
- * Dev-only: inject the FRESH on-disk `content.yaml` as an inline
105
- * `globalThis.__DCS_CONTENT__ = {…}` classic script at the top of `<head>`.
186
+ * Inject into `<head>` on every HTML request. Two concerns:
187
+ *
188
+ * 1. The constant DCS motion-token `:root` block (`DCS_MOTION_TOKENS_CSS`)
189
+ * — the fleet-wide seam that gives every DCS site the shared `--motion-*`
190
+ * vocabulary. It carries no per-site data, so (unlike the content inject)
191
+ * it is NOT gated on `isDev`: it runs in both `serve` and `build`.
192
+ * `head-prepend` keeps it ABOVE the bundled site CSS, so a site's own
193
+ * `:root` (theme `tailwind.css` / `style.css`) can still override a token.
106
194
  *
107
- * Runs on every dev HTML request, so the portal's `?_hc` hard reload (or any
108
- * full page reload) re-reads the YAML with no restart. A classic `<script>`
109
- * in `<head>` executes during HTML parsing, before the deferred module app
110
- * entry, so the global is set before the consumer's `useTextContent` setup
111
- * reads it. Gated on `isDev` so it never runs in `vite`/`vitepress build`
112
- * (the build path keeps the baked `define` literal).
195
+ * 2. Dev-only: the FRESH on-disk `content.yaml` as an inline
196
+ * `globalThis.__DCS_CONTENT__ = {…}` classic script. Runs on every dev
197
+ * HTML request, so the portal's `?_hc` hard reload re-reads the YAML with
198
+ * no restart. A classic `<script>` in `<head>` executes before the
199
+ * deferred module app entry, so the global is set before the consumer's
200
+ * `useTextContent` setup reads it. Gated on `isDev` so it never runs in
201
+ * `vite`/`vitepress build` (the build path keeps the baked `define`).
113
202
  */
114
203
  transformIndexHtml: {
115
204
  order: "pre",
116
205
  handler(html) {
117
- if (!isDev) return html;
118
- const projectRoot = resolvedConfig?.root || process.cwd();
119
- const content = readDcsYamlFresh(
120
- projectRoot,
121
- contentPath,
122
- "dcs-content",
123
- debug
124
- );
125
- if (content === void 0) return html;
126
- if (debug) {
127
- console.log("[dcs-content] serve: injecting fresh __DCS_CONTENT__ into <head>");
206
+ const tags = [dcsMotionTokensStyleTag()];
207
+ if (!isDev) {
208
+ const projectRoot = resolvedConfig?.root || process.cwd();
209
+ const agentTag = dcsAgentWidgetLoaderTag({
210
+ projectRoot,
211
+ apiBaseUrl: process.env.VITE_API_BASE_URL
212
+ });
213
+ if (agentTag) tags.push(agentTag);
128
214
  }
129
- const tags = [
130
- {
131
- tag: "script",
132
- children: buildGlobalAssignScript("__DCS_CONTENT__", content),
133
- injectTo: "head-prepend"
215
+ if (isDev) {
216
+ const projectRoot = resolvedConfig?.root || process.cwd();
217
+ const content = readDcsYamlFresh(
218
+ projectRoot,
219
+ contentPath,
220
+ "dcs-content",
221
+ debug
222
+ );
223
+ if (content !== void 0) {
224
+ if (debug) {
225
+ console.log("[dcs-content] serve: injecting fresh __DCS_CONTENT__ into <head>");
226
+ }
227
+ tags.push({
228
+ tag: "script",
229
+ children: buildGlobalAssignScript("__DCS_CONTENT__", content),
230
+ injectTo: "head-prepend"
231
+ });
134
232
  }
135
- ];
233
+ }
136
234
  return { html, tags };
137
235
  }
138
236
  },
@@ -147,17 +245,17 @@ function dcsContentPlugin(options = {}) {
147
245
  configureServer(server) {
148
246
  const projectRoot = resolvedConfig?.root || process.cwd();
149
247
  const watchPaths = [
150
- path3.resolve(projectRoot, contentPath),
151
- path3.resolve(projectRoot, "..", contentPath)
152
- ].map((p) => path3.normalize(p));
248
+ path5.resolve(projectRoot, contentPath),
249
+ path5.resolve(projectRoot, "..", contentPath)
250
+ ].map((p) => path5.normalize(p));
153
251
  watchPaths.forEach((watchPath) => {
154
- if (fs3.existsSync(watchPath)) {
252
+ if (fs5.existsSync(watchPath)) {
155
253
  server.watcher.add(watchPath);
156
254
  }
157
255
  });
158
256
  let reloadTimer;
159
257
  server.watcher.on("change", (changedPath) => {
160
- if (!watchPaths.includes(path3.normalize(changedPath))) return;
258
+ if (!watchPaths.includes(path5.normalize(changedPath))) return;
161
259
  if (reloadTimer) clearTimeout(reloadTimer);
162
260
  reloadTimer = setTimeout(() => {
163
261
  if (debug) {
@@ -169,19 +267,191 @@ function dcsContentPlugin(options = {}) {
169
267
  }
170
268
  };
171
269
  }
270
+ var CONTENT_TYPES = {
271
+ ".html": "text/html; charset=utf-8",
272
+ ".js": "text/javascript; charset=utf-8",
273
+ ".mjs": "text/javascript; charset=utf-8",
274
+ ".css": "text/css; charset=utf-8",
275
+ ".json": "application/json; charset=utf-8",
276
+ ".svg": "image/svg+xml",
277
+ ".png": "image/png",
278
+ ".jpg": "image/jpeg",
279
+ ".jpeg": "image/jpeg",
280
+ ".webp": "image/webp",
281
+ ".gif": "image/gif",
282
+ ".ico": "image/x-icon",
283
+ ".woff": "font/woff",
284
+ ".woff2": "font/woff2",
285
+ ".ttf": "font/ttf",
286
+ ".txt": "text/plain; charset=utf-8",
287
+ ".xml": "application/xml; charset=utf-8",
288
+ ".map": "application/json; charset=utf-8"
289
+ };
290
+ async function startStaticServer(outDir) {
291
+ const server = http.createServer((req, res) => {
292
+ let pathname = "/";
293
+ try {
294
+ pathname = decodeURIComponent(new URL(req.url ?? "/", "http://127.0.0.1").pathname);
295
+ } catch {
296
+ pathname = "/";
297
+ }
298
+ const resolved = path5.resolve(outDir, "." + pathname);
299
+ if (resolved !== outDir && !resolved.startsWith(outDir + path5.sep)) {
300
+ res.writeHead(403);
301
+ res.end();
302
+ return;
303
+ }
304
+ let filePath = resolved;
305
+ try {
306
+ const stat = fs5.statSync(filePath);
307
+ if (stat.isDirectory()) filePath = path5.join(filePath, "index.html");
308
+ } catch {
309
+ if (path5.extname(pathname)) {
310
+ res.writeHead(404);
311
+ res.end();
312
+ return;
313
+ }
314
+ filePath = path5.join(outDir, "index.html");
315
+ }
316
+ fs5.readFile(filePath, (err, buf) => {
317
+ if (err) {
318
+ res.writeHead(404);
319
+ res.end();
320
+ return;
321
+ }
322
+ res.writeHead(200, {
323
+ "Content-Type": CONTENT_TYPES[path5.extname(filePath).toLowerCase()] ?? "application/octet-stream"
324
+ });
325
+ res.end(buf);
326
+ });
327
+ });
328
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
329
+ const addr = server.address();
330
+ const origin = `http://127.0.0.1:${addr.port}`;
331
+ return {
332
+ origin,
333
+ close: () => new Promise((resolve) => {
334
+ server.close(() => resolve());
335
+ })
336
+ };
337
+ }
338
+ async function createPlaywrightRenderer(options) {
339
+ const {
340
+ outDir,
341
+ navTimeoutMs = 3e4,
342
+ mountTimeoutMs = 15e3,
343
+ settleMs = 250,
344
+ debug = false
345
+ } = options;
346
+ let chromium;
347
+ try {
348
+ const specifier = "playwright";
349
+ const pw = await import(
350
+ /* @vite-ignore */
351
+ specifier
352
+ );
353
+ chromium = pw.chromium;
354
+ } catch {
355
+ console.warn(
356
+ "[dcs-seo] body-prerender: `playwright` is not installed; skipping body prerender (head + JSON-LD still emitted). `pnpm add -D playwright` to enable."
357
+ );
358
+ return null;
359
+ }
360
+ if (!chromium) {
361
+ console.warn("[dcs-seo] body-prerender: playwright has no chromium export; skipping.");
362
+ return null;
363
+ }
364
+ let browser;
365
+ try {
366
+ browser = await chromium.launch({ headless: true });
367
+ } catch (err) {
368
+ console.warn(
369
+ "[dcs-seo] body-prerender: failed to launch Chromium (run `npx playwright install chromium`); skipping body prerender:",
370
+ err instanceof Error ? err.message : err
371
+ );
372
+ return null;
373
+ }
374
+ const staticServer = await startStaticServer(outDir);
375
+ const context = await browser.newContext({ reducedMotion: "reduce" });
376
+ const renderer = {
377
+ async renderRoute(routePath) {
378
+ const page = await context.newPage();
379
+ const errors = [];
380
+ page.on("pageerror", (err) => errors.push(`pageerror: ${err.message}`));
381
+ if (debug) {
382
+ page.on("console", (msg) => {
383
+ if (msg.type() === "error") console.log(`[dcs-seo] (console) ${msg.text()}`);
384
+ });
385
+ }
386
+ await page.route("**/*", (route) => {
387
+ let sameOrigin = false;
388
+ try {
389
+ sameOrigin = new URL(route.request().url()).origin === staticServer.origin;
390
+ } catch {
391
+ sameOrigin = false;
392
+ }
393
+ if (sameOrigin) {
394
+ route.continue().catch(() => {
395
+ });
396
+ } else {
397
+ route.abort().catch(() => {
398
+ });
399
+ }
400
+ });
401
+ try {
402
+ await page.goto(staticServer.origin + routePath, {
403
+ waitUntil: "load",
404
+ timeout: navTimeoutMs
405
+ });
406
+ await page.waitForFunction(
407
+ () => {
408
+ const el = document.querySelector("#app");
409
+ return !!el && el.children.length > 0;
410
+ },
411
+ void 0,
412
+ { timeout: mountTimeoutMs }
413
+ );
414
+ if (settleMs > 0) await page.waitForTimeout(settleMs);
415
+ const bodyHtml = await page.$eval("#app", (el) => el.innerHTML);
416
+ return { bodyHtml: bodyHtml ?? "", errors };
417
+ } catch (err) {
418
+ errors.push(err instanceof Error ? err.message : String(err));
419
+ return { bodyHtml: "", errors };
420
+ } finally {
421
+ await page.close();
422
+ }
423
+ }
424
+ };
425
+ return {
426
+ renderer,
427
+ async close() {
428
+ try {
429
+ await context.close();
430
+ } catch {
431
+ }
432
+ try {
433
+ await browser.close();
434
+ } catch {
435
+ }
436
+ await staticServer.close();
437
+ }
438
+ };
439
+ }
440
+
441
+ // src/plugins/dcsSeoPlugin.ts
172
442
  function resolveOutDir(config) {
173
443
  const out = config.build?.outDir || "dist";
174
- return path3.isAbsolute(out) ? out : path3.resolve(config.root || process.cwd(), out);
444
+ return path5.isAbsolute(out) ? out : path5.resolve(config.root || process.cwd(), out);
175
445
  }
176
446
  function loadSeoConfig(projectRoot, seoPath, debug) {
177
447
  const possiblePaths = [
178
- path3.resolve(projectRoot, seoPath),
179
- path3.resolve(projectRoot, "..", seoPath),
180
- path3.resolve(process.cwd(), seoPath)
448
+ path5.resolve(projectRoot, seoPath),
449
+ path5.resolve(projectRoot, "..", seoPath),
450
+ path5.resolve(process.cwd(), seoPath)
181
451
  ];
182
452
  let foundPath;
183
453
  for (const testPath of possiblePaths) {
184
- if (fs3.existsSync(testPath)) {
454
+ if (fs5.existsSync(testPath)) {
185
455
  foundPath = testPath;
186
456
  break;
187
457
  }
@@ -195,8 +465,8 @@ function loadSeoConfig(projectRoot, seoPath, debug) {
195
465
  return null;
196
466
  }
197
467
  try {
198
- const fileContent = fs3.readFileSync(foundPath, "utf8");
199
- const config = yaml3.load(fileContent);
468
+ const fileContent = fs5.readFileSync(foundPath, "utf8");
469
+ const config = yaml4.load(fileContent);
200
470
  return { config, foundPath };
201
471
  } catch (error) {
202
472
  console.warn("[dcs-seo] Failed to parse seo.yaml:", error);
@@ -205,17 +475,17 @@ function loadSeoConfig(projectRoot, seoPath, debug) {
205
475
  }
206
476
  function loadContentConfig(projectRoot, contentRelPath, debug) {
207
477
  const possiblePaths = [
208
- path3.resolve(projectRoot, contentRelPath),
209
- path3.resolve(projectRoot, "..", contentRelPath),
210
- path3.resolve(process.cwd(), contentRelPath)
478
+ path5.resolve(projectRoot, contentRelPath),
479
+ path5.resolve(projectRoot, "..", contentRelPath),
480
+ path5.resolve(process.cwd(), contentRelPath)
211
481
  ];
212
- const foundPath = possiblePaths.find((p) => fs3.existsSync(p));
482
+ const foundPath = possiblePaths.find((p) => fs5.existsSync(p));
213
483
  if (!foundPath) {
214
484
  if (debug) console.log(`[dcs-seo] No content.yaml found (reviews disabled)`);
215
485
  return void 0;
216
486
  }
217
487
  try {
218
- const parsed = yaml3.load(fs3.readFileSync(foundPath, "utf8"));
488
+ const parsed = yaml4.load(fs5.readFileSync(foundPath, "utf8"));
219
489
  if (!parsed || typeof parsed !== "object") return void 0;
220
490
  return parsed;
221
491
  } catch (error) {
@@ -223,21 +493,16 @@ function loadContentConfig(projectRoot, contentRelPath, debug) {
223
493
  return void 0;
224
494
  }
225
495
  }
226
- function routeToOutputFile(outDir, routePath) {
227
- const trimmed = routePath.replace(/^\/+/, "").replace(/\/+$/, "");
228
- if (trimmed === "") return path3.join(outDir, "index.html");
229
- return path3.join(outDir, ...trimmed.split("/"), "index.html");
230
- }
231
496
  function loadExcludedGlobs(projectRoot, pagesPath) {
232
497
  const possiblePaths = [
233
- path3.resolve(projectRoot, pagesPath),
234
- path3.resolve(projectRoot, "..", pagesPath),
235
- path3.resolve(process.cwd(), pagesPath)
498
+ path5.resolve(projectRoot, pagesPath),
499
+ path5.resolve(projectRoot, "..", pagesPath),
500
+ path5.resolve(process.cwd(), pagesPath)
236
501
  ];
237
- const found = possiblePaths.find((p) => fs3.existsSync(p));
502
+ const found = possiblePaths.find((p) => fs5.existsSync(p));
238
503
  if (!found) return [];
239
504
  try {
240
- const raw = yaml3.load(fs3.readFileSync(found, "utf8"));
505
+ const raw = yaml4.load(fs5.readFileSync(found, "utf8"));
241
506
  const excluded = raw?.excluded;
242
507
  if (!Array.isArray(excluded)) return [];
243
508
  return excluded.filter((e) => typeof e === "string");
@@ -249,14 +514,14 @@ function deriveLastmod(seoConfig, projectRoot, pagesPath) {
249
514
  const fromSeo = seoConfig?.lastUpdated;
250
515
  if (typeof fromSeo === "string" && fromSeo.length > 0) return fromSeo;
251
516
  const possiblePaths = [
252
- path3.resolve(projectRoot, pagesPath),
253
- path3.resolve(projectRoot, "..", pagesPath),
254
- path3.resolve(process.cwd(), pagesPath)
517
+ path5.resolve(projectRoot, pagesPath),
518
+ path5.resolve(projectRoot, "..", pagesPath),
519
+ path5.resolve(process.cwd(), pagesPath)
255
520
  ];
256
- const found = possiblePaths.find((p) => fs3.existsSync(p));
521
+ const found = possiblePaths.find((p) => fs5.existsSync(p));
257
522
  if (!found) return void 0;
258
523
  try {
259
- const raw = yaml3.load(fs3.readFileSync(found, "utf8"));
524
+ const raw = yaml4.load(fs5.readFileSync(found, "utf8"));
260
525
  const fromPages = raw?.lastUpdated;
261
526
  if (typeof fromPages === "string" && fromPages.length > 0) return fromPages;
262
527
  } catch {
@@ -294,7 +559,7 @@ function emitSiteFiles(params) {
294
559
  lastmod
295
560
  });
296
561
  if (xml) {
297
- fs3.writeFileSync(path3.join(outDir, "sitemap.xml"), xml, "utf8");
562
+ fs5.writeFileSync(path5.join(outDir, "sitemap.xml"), xml, "utf8");
298
563
  wroteSitemap = true;
299
564
  written.push("sitemap.xml");
300
565
  if (debug) console.log("[dcs-seo] wrote sitemap.xml");
@@ -303,8 +568,8 @@ function emitSiteFiles(params) {
303
568
  }
304
569
  }
305
570
  if (robots.enabled ?? true) {
306
- const robotsPath = path3.join(outDir, "robots.txt");
307
- const existing = fs3.existsSync(robotsPath) && !robots.force ? fs3.readFileSync(robotsPath, "utf8") : null;
571
+ const robotsPath = path5.join(outDir, "robots.txt");
572
+ const existing = fs5.existsSync(robotsPath) && !robots.force ? fs5.readFileSync(robotsPath, "utf8") : null;
308
573
  const keepHandAuthored = existing !== null && isHandAuthoredRobotsAcceptable(existing);
309
574
  if (keepHandAuthored) {
310
575
  if (debug)
@@ -316,7 +581,7 @@ function emitSiteFiles(params) {
316
581
  robots,
317
582
  hasSitemap: wroteSitemap
318
583
  });
319
- fs3.writeFileSync(robotsPath, txt, "utf8");
584
+ fs5.writeFileSync(robotsPath, txt, "utf8");
320
585
  written.push("robots.txt");
321
586
  if (debug) {
322
587
  console.log(
@@ -335,7 +600,7 @@ function emitSiteFiles(params) {
335
600
  excludedGlobs
336
601
  });
337
602
  if (txt) {
338
- fs3.writeFileSync(path3.join(outDir, "llms.txt"), txt, "utf8");
603
+ fs5.writeFileSync(path5.join(outDir, "llms.txt"), txt, "utf8");
339
604
  written.push("llms.txt");
340
605
  if (debug) console.log("[dcs-seo] wrote llms.txt");
341
606
  } else if (debug) {
@@ -385,6 +650,7 @@ function emitStaticSeoHtml(params) {
385
650
  const forceNoindex = noindexSet.has(route.path) || route.slug && noindexSet.has(route.slug);
386
651
  const breadcrumbTrail = siteUrl ? breadcrumbTrailFromRoute(route.path, siteUrl, routeTitles) : void 0;
387
652
  const reviews = contentConfig ? findReviewItemsForPage(contentConfig, route.slug) : void 0;
653
+ const license = contentConfig ? findBusinessLicense(contentConfig, route.slug) : void 0;
388
654
  const isBlogPost = /^\/blog\/.+/.test(route.path) && !!route.title;
389
655
  const blogMeta = isBlogPost ? { headline: route.title, url: breadcrumbTrail?.[breadcrumbTrail.length - 1]?.item } : void 0;
390
656
  const faq = pickPageFaq(contentConfig, route.slug);
@@ -395,27 +661,50 @@ function emitStaticSeoHtml(params) {
395
661
  emitGraph: true,
396
662
  breadcrumbTrail,
397
663
  reviews,
664
+ license,
398
665
  blogMeta,
399
666
  faq
400
667
  });
401
668
  const html = spliceHeadHtml(shellHtml, tags);
402
669
  const outFile = routeToOutputFile(outDir, route.path);
403
- fs3.mkdirSync(path3.dirname(outFile), { recursive: true });
404
- fs3.writeFileSync(outFile, html, "utf8");
670
+ fs5.mkdirSync(path5.dirname(outFile), { recursive: true });
671
+ fs5.writeFileSync(outFile, html, "utf8");
405
672
  written++;
406
673
  if (debug) {
407
674
  console.log(
408
- `[dcs-seo] wrote ${path3.relative(outDir, outFile)} (title="${tags.title}"${forceNoindex ? ", noindex" : ""})`
675
+ `[dcs-seo] wrote ${path5.relative(outDir, outFile)} (title="${tags.title}"${forceNoindex ? ", noindex" : ""})`
409
676
  );
410
677
  }
411
678
  }
412
679
  return written;
413
680
  }
681
+ async function runBodyPrerender(ctx, opts) {
682
+ const handle = await createPlaywrightRenderer({ outDir: ctx.outDir, debug: opts.debug });
683
+ if (!handle) return;
684
+ console.log("[dcs-seo] body-prerender: rendering routes via headless Chromium\u2026");
685
+ try {
686
+ const result = await prerenderBodies({
687
+ outDir: ctx.outDir,
688
+ routes: ctx.routes,
689
+ renderer: handle.renderer,
690
+ exclude: opts.exclude,
691
+ noindex: opts.noindex,
692
+ excludedGlobs: ctx.excludedGlobs,
693
+ debug: opts.debug
694
+ });
695
+ console.log(
696
+ `[dcs-seo] body-prerender: ${result.prerendered} route(s) now have crawler-visible body content (${result.skipped} skipped)`
697
+ );
698
+ } finally {
699
+ await handle.close();
700
+ }
701
+ }
414
702
  function dcsSeoPlugin(options = {}) {
415
703
  const {
416
704
  seoPath = ".dcs/seo.yaml",
417
705
  debug = false,
418
706
  emitStaticHtml = false,
707
+ prerenderBody,
419
708
  pagesPath = ".dcs/pages.yaml",
420
709
  contentPath = ".dcs/content.yaml",
421
710
  exclude = [],
@@ -425,7 +714,9 @@ function dcsSeoPlugin(options = {}) {
425
714
  emitSiteFiles: emitSiteFilesOpt = emitStaticHtml,
426
715
  siteUrl,
427
716
  robots = {},
428
- preview = false,
717
+ // Self-detect preview builds from DCS_PREVIEW (mirrors dcsEditorPlugin) so a
718
+ // release/** SWA preview build never body-prerenders; an explicit option wins.
719
+ preview = process.env.DCS_PREVIEW === "true",
429
720
  llms = true
430
721
  } = options;
431
722
  let resolvedConfig;
@@ -477,7 +768,7 @@ function dcsSeoPlugin(options = {}) {
477
768
  * Fully defensive: any missing/unparseable input logs a warning and
478
769
  * no-ops. Never throws (so it can never break a production build).
479
770
  */
480
- writeBundle() {
771
+ async writeBundle() {
481
772
  if (!emitStaticHtml) {
482
773
  if (options.emitSiteFiles === true) {
483
774
  console.warn(
@@ -486,15 +777,16 @@ function dcsSeoPlugin(options = {}) {
486
777
  }
487
778
  return;
488
779
  }
780
+ let bodyPrerenderCtx = null;
489
781
  try {
490
782
  const projectRoot = resolvedConfig?.root || process.cwd();
491
783
  const outDir = resolveOutDir(resolvedConfig);
492
- if (!fs3.existsSync(outDir)) {
784
+ if (!fs5.existsSync(outDir)) {
493
785
  console.warn(`[dcs-seo] emitStaticHtml: outDir not found (${outDir}); skipping`);
494
786
  return;
495
787
  }
496
- const shellPath = path3.join(outDir, "index.html");
497
- if (!fs3.existsSync(shellPath)) {
788
+ const shellPath = path5.join(outDir, "index.html");
789
+ if (!fs5.existsSync(shellPath)) {
498
790
  console.warn(
499
791
  `[dcs-seo] emitStaticHtml: no index.html in ${outDir}; nothing to emit`
500
792
  );
@@ -513,7 +805,7 @@ function dcsSeoPlugin(options = {}) {
513
805
  `[dcs-seo] emitStaticHtml: ${seoPath} missing or unparseable; emitting with defaults only`
514
806
  );
515
807
  }
516
- const shellHtml = fs3.readFileSync(shellPath, "utf8");
808
+ const shellHtml = fs5.readFileSync(shellPath, "utf8");
517
809
  const excludedGlobs = loadExcludedGlobs(projectRoot, pagesPath);
518
810
  const contentConfig = loadContentConfig(projectRoot, contentPath, debug);
519
811
  const written = emitStaticSeoHtml({
@@ -551,9 +843,28 @@ function dcsSeoPlugin(options = {}) {
551
843
  );
552
844
  }
553
845
  }
846
+ const buildBase = resolvedConfig?.base ?? "/";
847
+ const baseIsRoot = buildBase === "/" || buildBase === "./" || buildBase === "";
848
+ if (isBodyPrerenderEnabled({
849
+ emitStaticHtml,
850
+ prerenderBodyOption: prerenderBody,
851
+ preview,
852
+ seoPrerenderBody: loaded?.config?.prerenderBody
853
+ })) {
854
+ if (baseIsRoot) {
855
+ bodyPrerenderCtx = { outDir, routes, excludedGlobs };
856
+ } else {
857
+ console.warn(
858
+ `[dcs-seo] body prerender skipped: non-root base "${buildBase}" (assets would not resolve through the prerender server; head + JSON-LD still emitted)`
859
+ );
860
+ }
861
+ }
554
862
  } catch (error) {
555
863
  console.warn("[dcs-seo] emitStaticHtml failed; build output left unchanged:", error);
556
864
  }
865
+ if (bodyPrerenderCtx) {
866
+ await runBodyPrerender(bodyPrerenderCtx, { exclude, noindex, debug });
867
+ }
557
868
  },
558
869
  /**
559
870
  * Dev-only: inject the FRESH on-disk `seo.yaml` as an inline
@@ -594,17 +905,17 @@ function dcsSeoPlugin(options = {}) {
594
905
  configureServer(server) {
595
906
  const projectRoot = resolvedConfig?.root || process.cwd();
596
907
  const watchPaths = [
597
- path3.resolve(projectRoot, seoPath),
598
- path3.resolve(projectRoot, "..", seoPath)
599
- ].map((p) => path3.normalize(p));
908
+ path5.resolve(projectRoot, seoPath),
909
+ path5.resolve(projectRoot, "..", seoPath)
910
+ ].map((p) => path5.normalize(p));
600
911
  watchPaths.forEach((watchPath) => {
601
- if (fs3.existsSync(watchPath)) {
912
+ if (fs5.existsSync(watchPath)) {
602
913
  server.watcher.add(watchPath);
603
914
  }
604
915
  });
605
916
  let reloadTimer;
606
917
  server.watcher.on("change", (changedPath) => {
607
- if (!watchPaths.includes(path3.normalize(changedPath))) return;
918
+ if (!watchPaths.includes(path5.normalize(changedPath))) return;
608
919
  if (reloadTimer) clearTimeout(reloadTimer);
609
920
  reloadTimer = setTimeout(() => {
610
921
  if (debug) {
@@ -761,14 +1072,14 @@ function buildPictureElement(entry, alt, extraAttrs, sizes) {
761
1072
  }
762
1073
  function loadCdnImageMap(projectRoot, relativeMapPath, debug) {
763
1074
  const possiblePaths = [
764
- path3.resolve(projectRoot, relativeMapPath),
765
- path3.resolve(projectRoot, "..", relativeMapPath),
766
- path3.resolve(process.cwd(), relativeMapPath)
1075
+ path5.resolve(projectRoot, relativeMapPath),
1076
+ path5.resolve(projectRoot, "..", relativeMapPath),
1077
+ path5.resolve(process.cwd(), relativeMapPath)
767
1078
  ];
768
1079
  for (const testPath of possiblePaths) {
769
- if (fs3.existsSync(testPath)) {
1080
+ if (fs5.existsSync(testPath)) {
770
1081
  try {
771
- const raw = fs3.readFileSync(testPath, "utf8");
1082
+ const raw = fs5.readFileSync(testPath, "utf8");
772
1083
  const data = JSON.parse(raw);
773
1084
  const map = /* @__PURE__ */ new Map();
774
1085
  for (const entry of data.images) {
@@ -929,13 +1240,13 @@ function dcsCdnBuildEnd(options = {}) {
929
1240
  return;
930
1241
  }
931
1242
  const outDir = siteConfig.outDir;
932
- if (!fs3.existsSync(outDir)) return;
1243
+ if (!fs5.existsSync(outDir)) return;
933
1244
  let filesProcessed = 0;
934
1245
  let refsReplaced = 0;
935
1246
  function walkDir(dir) {
936
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
1247
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
937
1248
  for (const entry of entries) {
938
- const fullPath = path3.join(dir, entry.name);
1249
+ const fullPath = path5.join(dir, entry.name);
939
1250
  if (entry.isDirectory()) {
940
1251
  walkDir(fullPath);
941
1252
  } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -944,7 +1255,7 @@ function dcsCdnBuildEnd(options = {}) {
944
1255
  }
945
1256
  }
946
1257
  function processFile(filePath) {
947
- let content = fs3.readFileSync(filePath, "utf8");
1258
+ let content = fs5.readFileSync(filePath, "utf8");
948
1259
  if (!pathPrefixes.some((p) => content.includes(p))) return;
949
1260
  let changed = false;
950
1261
  if (filePath.endsWith(".html")) {
@@ -979,7 +1290,7 @@ function dcsCdnBuildEnd(options = {}) {
979
1290
  }
980
1291
  }
981
1292
  if (changed) {
982
- fs3.writeFileSync(filePath, content, "utf8");
1293
+ fs5.writeFileSync(filePath, content, "utf8");
983
1294
  filesProcessed++;
984
1295
  }
985
1296
  }
@@ -1028,6 +1339,6 @@ function responsiveImagePlugin(md) {
1028
1339
  };
1029
1340
  }
1030
1341
 
1031
- export { dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsPreviewPlugin, dcsSeoPlugin, emitSiteFiles, emitStaticSeoHtml, responsiveImagePlugin };
1342
+ export { DCS_MOTION_TOKENS_CSS, DEFAULT_AGENT_API_BASE_URL, DEFAULT_AGENT_WIDGET_SCRIPT_URL, buildAgentWidgetLoaderScript, dcsAgentWidgetLoaderTag, dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsMotionTokensStyleTag, dcsPreviewPlugin, dcsSeoPlugin, emitSiteFiles, emitStaticSeoHtml, readSiteSlug, responsiveImagePlugin };
1032
1343
  //# sourceMappingURL=index.js.map
1033
1344
  //# sourceMappingURL=index.js.map