@duffcloudservices/cms 0.10.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.
- package/README.md +59 -2
- package/dist/chunk-DAYLLSEE.js +3 -0
- package/dist/{chunk-KCWMS7P4.js.map → chunk-DAYLLSEE.js.map} +1 -1
- package/dist/{chunk-UPAMLKOQ.js → chunk-F3EIWEZD.js} +158 -11
- package/dist/chunk-F3EIWEZD.js.map +1 -0
- package/dist/index.d.ts +74 -6
- package/dist/index.js +88 -6
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +91 -3
- package/dist/plugins/index.js +350 -82
- package/dist/plugins/index.js.map +1 -1
- package/dist/seo/index.d.ts +132 -226
- package/dist/seo/index.js +2 -2
- package/dist/spliceHeadHtml-CsBEucGy.d.ts +254 -0
- package/dist/{vitepressTransform-DeEzgGWU.d.ts → vitepressTransform-DfmABXmK.d.ts} +53 -2
- package/package.json +14 -2
- package/src/components/DcsCallButton.test.ts +126 -0
- package/src/components/DcsCallButton.vue +185 -0
- package/src/components/DcsReviewShowcase.vue +13 -2
- package/src/components/LiteMediaEmbed.test.ts +229 -0
- package/src/components/LiteMediaEmbed.vue +399 -0
- package/src/composables/useMediaCarousel.ts +6 -1
- package/src/composables/useReviewContent.test.ts +86 -0
- package/src/composables/useReviewContent.ts +34 -2
- package/src/composables/useSiteVisitorSession.test.ts +120 -0
- package/src/composables/useSiteVisitorSession.ts +160 -0
- package/dist/chunk-KCWMS7P4.js +0 -3
- package/dist/chunk-UPAMLKOQ.js.map +0 -1
package/dist/plugins/index.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { buildSitemapXml, isHandAuthoredRobotsAcceptable, buildRobotsTxt, buildLlmsTxt, matchesExcludedGlob, breadcrumbTrailFromRoute, findReviewItemsForPage, buildHeadTags, spliceHeadHtml, loadPagesManifest } from '../chunk-
|
|
2
|
-
export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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) =>
|
|
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
|
|
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;
|
|
@@ -67,8 +68,50 @@ function dcsMotionTokensStyleTag() {
|
|
|
67
68
|
injectTo: "head-prepend"
|
|
68
69
|
};
|
|
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
|
+
}
|
|
70
112
|
|
|
71
113
|
// src/plugins/dcsContentPlugin.ts
|
|
114
|
+
var DCS_SSR_NO_EXTERNAL = ["@duffcloudservices/cms"];
|
|
72
115
|
function dcsContentPlugin(options = {}) {
|
|
73
116
|
const { contentPath = ".dcs/content.yaml", debug = false } = options;
|
|
74
117
|
let resolvedConfig;
|
|
@@ -84,18 +127,19 @@ function dcsContentPlugin(options = {}) {
|
|
|
84
127
|
return {
|
|
85
128
|
define: {
|
|
86
129
|
__DCS_CONTENT__: "globalThis.__DCS_CONTENT__"
|
|
87
|
-
}
|
|
130
|
+
},
|
|
131
|
+
ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
|
|
88
132
|
};
|
|
89
133
|
}
|
|
90
134
|
const projectRoot = config.root || process.cwd();
|
|
91
135
|
const possiblePaths = [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
136
|
+
path5.resolve(projectRoot, contentPath),
|
|
137
|
+
path5.resolve(projectRoot, "..", contentPath),
|
|
138
|
+
path5.resolve(process.cwd(), contentPath)
|
|
95
139
|
];
|
|
96
140
|
let foundPath;
|
|
97
141
|
for (const testPath of possiblePaths) {
|
|
98
|
-
if (
|
|
142
|
+
if (fs5.existsSync(testPath)) {
|
|
99
143
|
foundPath = testPath;
|
|
100
144
|
break;
|
|
101
145
|
}
|
|
@@ -109,12 +153,13 @@ function dcsContentPlugin(options = {}) {
|
|
|
109
153
|
return {
|
|
110
154
|
define: {
|
|
111
155
|
__DCS_CONTENT__: "undefined"
|
|
112
|
-
}
|
|
156
|
+
},
|
|
157
|
+
ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
|
|
113
158
|
};
|
|
114
159
|
}
|
|
115
160
|
try {
|
|
116
|
-
const fileContent =
|
|
117
|
-
const content =
|
|
161
|
+
const fileContent = fs5.readFileSync(foundPath, "utf8");
|
|
162
|
+
const content = yaml4.load(fileContent);
|
|
118
163
|
if (debug) {
|
|
119
164
|
console.log(`[dcs-content] Loaded ${foundPath}`);
|
|
120
165
|
console.log(`[dcs-content] Version: ${content.version}`);
|
|
@@ -124,14 +169,16 @@ function dcsContentPlugin(options = {}) {
|
|
|
124
169
|
return {
|
|
125
170
|
define: {
|
|
126
171
|
__DCS_CONTENT__: JSON.stringify(content)
|
|
127
|
-
}
|
|
172
|
+
},
|
|
173
|
+
ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
|
|
128
174
|
};
|
|
129
175
|
} catch (error) {
|
|
130
176
|
console.warn("[dcs-content] Failed to parse content.yaml:", error);
|
|
131
177
|
return {
|
|
132
178
|
define: {
|
|
133
179
|
__DCS_CONTENT__: "undefined"
|
|
134
|
-
}
|
|
180
|
+
},
|
|
181
|
+
ssr: { noExternal: DCS_SSR_NO_EXTERNAL }
|
|
135
182
|
};
|
|
136
183
|
}
|
|
137
184
|
},
|
|
@@ -157,6 +204,14 @@ function dcsContentPlugin(options = {}) {
|
|
|
157
204
|
order: "pre",
|
|
158
205
|
handler(html) {
|
|
159
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);
|
|
214
|
+
}
|
|
160
215
|
if (isDev) {
|
|
161
216
|
const projectRoot = resolvedConfig?.root || process.cwd();
|
|
162
217
|
const content = readDcsYamlFresh(
|
|
@@ -190,17 +245,17 @@ function dcsContentPlugin(options = {}) {
|
|
|
190
245
|
configureServer(server) {
|
|
191
246
|
const projectRoot = resolvedConfig?.root || process.cwd();
|
|
192
247
|
const watchPaths = [
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
].map((p) =>
|
|
248
|
+
path5.resolve(projectRoot, contentPath),
|
|
249
|
+
path5.resolve(projectRoot, "..", contentPath)
|
|
250
|
+
].map((p) => path5.normalize(p));
|
|
196
251
|
watchPaths.forEach((watchPath) => {
|
|
197
|
-
if (
|
|
252
|
+
if (fs5.existsSync(watchPath)) {
|
|
198
253
|
server.watcher.add(watchPath);
|
|
199
254
|
}
|
|
200
255
|
});
|
|
201
256
|
let reloadTimer;
|
|
202
257
|
server.watcher.on("change", (changedPath) => {
|
|
203
|
-
if (!watchPaths.includes(
|
|
258
|
+
if (!watchPaths.includes(path5.normalize(changedPath))) return;
|
|
204
259
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
205
260
|
reloadTimer = setTimeout(() => {
|
|
206
261
|
if (debug) {
|
|
@@ -212,19 +267,191 @@ function dcsContentPlugin(options = {}) {
|
|
|
212
267
|
}
|
|
213
268
|
};
|
|
214
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
|
|
215
442
|
function resolveOutDir(config) {
|
|
216
443
|
const out = config.build?.outDir || "dist";
|
|
217
|
-
return
|
|
444
|
+
return path5.isAbsolute(out) ? out : path5.resolve(config.root || process.cwd(), out);
|
|
218
445
|
}
|
|
219
446
|
function loadSeoConfig(projectRoot, seoPath, debug) {
|
|
220
447
|
const possiblePaths = [
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
448
|
+
path5.resolve(projectRoot, seoPath),
|
|
449
|
+
path5.resolve(projectRoot, "..", seoPath),
|
|
450
|
+
path5.resolve(process.cwd(), seoPath)
|
|
224
451
|
];
|
|
225
452
|
let foundPath;
|
|
226
453
|
for (const testPath of possiblePaths) {
|
|
227
|
-
if (
|
|
454
|
+
if (fs5.existsSync(testPath)) {
|
|
228
455
|
foundPath = testPath;
|
|
229
456
|
break;
|
|
230
457
|
}
|
|
@@ -238,8 +465,8 @@ function loadSeoConfig(projectRoot, seoPath, debug) {
|
|
|
238
465
|
return null;
|
|
239
466
|
}
|
|
240
467
|
try {
|
|
241
|
-
const fileContent =
|
|
242
|
-
const config =
|
|
468
|
+
const fileContent = fs5.readFileSync(foundPath, "utf8");
|
|
469
|
+
const config = yaml4.load(fileContent);
|
|
243
470
|
return { config, foundPath };
|
|
244
471
|
} catch (error) {
|
|
245
472
|
console.warn("[dcs-seo] Failed to parse seo.yaml:", error);
|
|
@@ -248,17 +475,17 @@ function loadSeoConfig(projectRoot, seoPath, debug) {
|
|
|
248
475
|
}
|
|
249
476
|
function loadContentConfig(projectRoot, contentRelPath, debug) {
|
|
250
477
|
const possiblePaths = [
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
478
|
+
path5.resolve(projectRoot, contentRelPath),
|
|
479
|
+
path5.resolve(projectRoot, "..", contentRelPath),
|
|
480
|
+
path5.resolve(process.cwd(), contentRelPath)
|
|
254
481
|
];
|
|
255
|
-
const foundPath = possiblePaths.find((p) =>
|
|
482
|
+
const foundPath = possiblePaths.find((p) => fs5.existsSync(p));
|
|
256
483
|
if (!foundPath) {
|
|
257
484
|
if (debug) console.log(`[dcs-seo] No content.yaml found (reviews disabled)`);
|
|
258
485
|
return void 0;
|
|
259
486
|
}
|
|
260
487
|
try {
|
|
261
|
-
const parsed =
|
|
488
|
+
const parsed = yaml4.load(fs5.readFileSync(foundPath, "utf8"));
|
|
262
489
|
if (!parsed || typeof parsed !== "object") return void 0;
|
|
263
490
|
return parsed;
|
|
264
491
|
} catch (error) {
|
|
@@ -266,21 +493,16 @@ function loadContentConfig(projectRoot, contentRelPath, debug) {
|
|
|
266
493
|
return void 0;
|
|
267
494
|
}
|
|
268
495
|
}
|
|
269
|
-
function routeToOutputFile(outDir, routePath) {
|
|
270
|
-
const trimmed = routePath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
271
|
-
if (trimmed === "") return path3.join(outDir, "index.html");
|
|
272
|
-
return path3.join(outDir, ...trimmed.split("/"), "index.html");
|
|
273
|
-
}
|
|
274
496
|
function loadExcludedGlobs(projectRoot, pagesPath) {
|
|
275
497
|
const possiblePaths = [
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
498
|
+
path5.resolve(projectRoot, pagesPath),
|
|
499
|
+
path5.resolve(projectRoot, "..", pagesPath),
|
|
500
|
+
path5.resolve(process.cwd(), pagesPath)
|
|
279
501
|
];
|
|
280
|
-
const found = possiblePaths.find((p) =>
|
|
502
|
+
const found = possiblePaths.find((p) => fs5.existsSync(p));
|
|
281
503
|
if (!found) return [];
|
|
282
504
|
try {
|
|
283
|
-
const raw =
|
|
505
|
+
const raw = yaml4.load(fs5.readFileSync(found, "utf8"));
|
|
284
506
|
const excluded = raw?.excluded;
|
|
285
507
|
if (!Array.isArray(excluded)) return [];
|
|
286
508
|
return excluded.filter((e) => typeof e === "string");
|
|
@@ -292,14 +514,14 @@ function deriveLastmod(seoConfig, projectRoot, pagesPath) {
|
|
|
292
514
|
const fromSeo = seoConfig?.lastUpdated;
|
|
293
515
|
if (typeof fromSeo === "string" && fromSeo.length > 0) return fromSeo;
|
|
294
516
|
const possiblePaths = [
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
517
|
+
path5.resolve(projectRoot, pagesPath),
|
|
518
|
+
path5.resolve(projectRoot, "..", pagesPath),
|
|
519
|
+
path5.resolve(process.cwd(), pagesPath)
|
|
298
520
|
];
|
|
299
|
-
const found = possiblePaths.find((p) =>
|
|
521
|
+
const found = possiblePaths.find((p) => fs5.existsSync(p));
|
|
300
522
|
if (!found) return void 0;
|
|
301
523
|
try {
|
|
302
|
-
const raw =
|
|
524
|
+
const raw = yaml4.load(fs5.readFileSync(found, "utf8"));
|
|
303
525
|
const fromPages = raw?.lastUpdated;
|
|
304
526
|
if (typeof fromPages === "string" && fromPages.length > 0) return fromPages;
|
|
305
527
|
} catch {
|
|
@@ -337,7 +559,7 @@ function emitSiteFiles(params) {
|
|
|
337
559
|
lastmod
|
|
338
560
|
});
|
|
339
561
|
if (xml) {
|
|
340
|
-
|
|
562
|
+
fs5.writeFileSync(path5.join(outDir, "sitemap.xml"), xml, "utf8");
|
|
341
563
|
wroteSitemap = true;
|
|
342
564
|
written.push("sitemap.xml");
|
|
343
565
|
if (debug) console.log("[dcs-seo] wrote sitemap.xml");
|
|
@@ -346,8 +568,8 @@ function emitSiteFiles(params) {
|
|
|
346
568
|
}
|
|
347
569
|
}
|
|
348
570
|
if (robots.enabled ?? true) {
|
|
349
|
-
const robotsPath =
|
|
350
|
-
const existing =
|
|
571
|
+
const robotsPath = path5.join(outDir, "robots.txt");
|
|
572
|
+
const existing = fs5.existsSync(robotsPath) && !robots.force ? fs5.readFileSync(robotsPath, "utf8") : null;
|
|
351
573
|
const keepHandAuthored = existing !== null && isHandAuthoredRobotsAcceptable(existing);
|
|
352
574
|
if (keepHandAuthored) {
|
|
353
575
|
if (debug)
|
|
@@ -359,7 +581,7 @@ function emitSiteFiles(params) {
|
|
|
359
581
|
robots,
|
|
360
582
|
hasSitemap: wroteSitemap
|
|
361
583
|
});
|
|
362
|
-
|
|
584
|
+
fs5.writeFileSync(robotsPath, txt, "utf8");
|
|
363
585
|
written.push("robots.txt");
|
|
364
586
|
if (debug) {
|
|
365
587
|
console.log(
|
|
@@ -378,7 +600,7 @@ function emitSiteFiles(params) {
|
|
|
378
600
|
excludedGlobs
|
|
379
601
|
});
|
|
380
602
|
if (txt) {
|
|
381
|
-
|
|
603
|
+
fs5.writeFileSync(path5.join(outDir, "llms.txt"), txt, "utf8");
|
|
382
604
|
written.push("llms.txt");
|
|
383
605
|
if (debug) console.log("[dcs-seo] wrote llms.txt");
|
|
384
606
|
} else if (debug) {
|
|
@@ -428,6 +650,7 @@ function emitStaticSeoHtml(params) {
|
|
|
428
650
|
const forceNoindex = noindexSet.has(route.path) || route.slug && noindexSet.has(route.slug);
|
|
429
651
|
const breadcrumbTrail = siteUrl ? breadcrumbTrailFromRoute(route.path, siteUrl, routeTitles) : void 0;
|
|
430
652
|
const reviews = contentConfig ? findReviewItemsForPage(contentConfig, route.slug) : void 0;
|
|
653
|
+
const license = contentConfig ? findBusinessLicense(contentConfig, route.slug) : void 0;
|
|
431
654
|
const isBlogPost = /^\/blog\/.+/.test(route.path) && !!route.title;
|
|
432
655
|
const blogMeta = isBlogPost ? { headline: route.title, url: breadcrumbTrail?.[breadcrumbTrail.length - 1]?.item } : void 0;
|
|
433
656
|
const faq = pickPageFaq(contentConfig, route.slug);
|
|
@@ -438,27 +661,50 @@ function emitStaticSeoHtml(params) {
|
|
|
438
661
|
emitGraph: true,
|
|
439
662
|
breadcrumbTrail,
|
|
440
663
|
reviews,
|
|
664
|
+
license,
|
|
441
665
|
blogMeta,
|
|
442
666
|
faq
|
|
443
667
|
});
|
|
444
668
|
const html = spliceHeadHtml(shellHtml, tags);
|
|
445
669
|
const outFile = routeToOutputFile(outDir, route.path);
|
|
446
|
-
|
|
447
|
-
|
|
670
|
+
fs5.mkdirSync(path5.dirname(outFile), { recursive: true });
|
|
671
|
+
fs5.writeFileSync(outFile, html, "utf8");
|
|
448
672
|
written++;
|
|
449
673
|
if (debug) {
|
|
450
674
|
console.log(
|
|
451
|
-
`[dcs-seo] wrote ${
|
|
675
|
+
`[dcs-seo] wrote ${path5.relative(outDir, outFile)} (title="${tags.title}"${forceNoindex ? ", noindex" : ""})`
|
|
452
676
|
);
|
|
453
677
|
}
|
|
454
678
|
}
|
|
455
679
|
return written;
|
|
456
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
|
+
}
|
|
457
702
|
function dcsSeoPlugin(options = {}) {
|
|
458
703
|
const {
|
|
459
704
|
seoPath = ".dcs/seo.yaml",
|
|
460
705
|
debug = false,
|
|
461
706
|
emitStaticHtml = false,
|
|
707
|
+
prerenderBody,
|
|
462
708
|
pagesPath = ".dcs/pages.yaml",
|
|
463
709
|
contentPath = ".dcs/content.yaml",
|
|
464
710
|
exclude = [],
|
|
@@ -468,7 +714,9 @@ function dcsSeoPlugin(options = {}) {
|
|
|
468
714
|
emitSiteFiles: emitSiteFilesOpt = emitStaticHtml,
|
|
469
715
|
siteUrl,
|
|
470
716
|
robots = {},
|
|
471
|
-
preview
|
|
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",
|
|
472
720
|
llms = true
|
|
473
721
|
} = options;
|
|
474
722
|
let resolvedConfig;
|
|
@@ -520,7 +768,7 @@ function dcsSeoPlugin(options = {}) {
|
|
|
520
768
|
* Fully defensive: any missing/unparseable input logs a warning and
|
|
521
769
|
* no-ops. Never throws (so it can never break a production build).
|
|
522
770
|
*/
|
|
523
|
-
writeBundle() {
|
|
771
|
+
async writeBundle() {
|
|
524
772
|
if (!emitStaticHtml) {
|
|
525
773
|
if (options.emitSiteFiles === true) {
|
|
526
774
|
console.warn(
|
|
@@ -529,15 +777,16 @@ function dcsSeoPlugin(options = {}) {
|
|
|
529
777
|
}
|
|
530
778
|
return;
|
|
531
779
|
}
|
|
780
|
+
let bodyPrerenderCtx = null;
|
|
532
781
|
try {
|
|
533
782
|
const projectRoot = resolvedConfig?.root || process.cwd();
|
|
534
783
|
const outDir = resolveOutDir(resolvedConfig);
|
|
535
|
-
if (!
|
|
784
|
+
if (!fs5.existsSync(outDir)) {
|
|
536
785
|
console.warn(`[dcs-seo] emitStaticHtml: outDir not found (${outDir}); skipping`);
|
|
537
786
|
return;
|
|
538
787
|
}
|
|
539
|
-
const shellPath =
|
|
540
|
-
if (!
|
|
788
|
+
const shellPath = path5.join(outDir, "index.html");
|
|
789
|
+
if (!fs5.existsSync(shellPath)) {
|
|
541
790
|
console.warn(
|
|
542
791
|
`[dcs-seo] emitStaticHtml: no index.html in ${outDir}; nothing to emit`
|
|
543
792
|
);
|
|
@@ -556,7 +805,7 @@ function dcsSeoPlugin(options = {}) {
|
|
|
556
805
|
`[dcs-seo] emitStaticHtml: ${seoPath} missing or unparseable; emitting with defaults only`
|
|
557
806
|
);
|
|
558
807
|
}
|
|
559
|
-
const shellHtml =
|
|
808
|
+
const shellHtml = fs5.readFileSync(shellPath, "utf8");
|
|
560
809
|
const excludedGlobs = loadExcludedGlobs(projectRoot, pagesPath);
|
|
561
810
|
const contentConfig = loadContentConfig(projectRoot, contentPath, debug);
|
|
562
811
|
const written = emitStaticSeoHtml({
|
|
@@ -594,9 +843,28 @@ function dcsSeoPlugin(options = {}) {
|
|
|
594
843
|
);
|
|
595
844
|
}
|
|
596
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
|
+
}
|
|
597
862
|
} catch (error) {
|
|
598
863
|
console.warn("[dcs-seo] emitStaticHtml failed; build output left unchanged:", error);
|
|
599
864
|
}
|
|
865
|
+
if (bodyPrerenderCtx) {
|
|
866
|
+
await runBodyPrerender(bodyPrerenderCtx, { exclude, noindex, debug });
|
|
867
|
+
}
|
|
600
868
|
},
|
|
601
869
|
/**
|
|
602
870
|
* Dev-only: inject the FRESH on-disk `seo.yaml` as an inline
|
|
@@ -637,17 +905,17 @@ function dcsSeoPlugin(options = {}) {
|
|
|
637
905
|
configureServer(server) {
|
|
638
906
|
const projectRoot = resolvedConfig?.root || process.cwd();
|
|
639
907
|
const watchPaths = [
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
].map((p) =>
|
|
908
|
+
path5.resolve(projectRoot, seoPath),
|
|
909
|
+
path5.resolve(projectRoot, "..", seoPath)
|
|
910
|
+
].map((p) => path5.normalize(p));
|
|
643
911
|
watchPaths.forEach((watchPath) => {
|
|
644
|
-
if (
|
|
912
|
+
if (fs5.existsSync(watchPath)) {
|
|
645
913
|
server.watcher.add(watchPath);
|
|
646
914
|
}
|
|
647
915
|
});
|
|
648
916
|
let reloadTimer;
|
|
649
917
|
server.watcher.on("change", (changedPath) => {
|
|
650
|
-
if (!watchPaths.includes(
|
|
918
|
+
if (!watchPaths.includes(path5.normalize(changedPath))) return;
|
|
651
919
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
652
920
|
reloadTimer = setTimeout(() => {
|
|
653
921
|
if (debug) {
|
|
@@ -804,14 +1072,14 @@ function buildPictureElement(entry, alt, extraAttrs, sizes) {
|
|
|
804
1072
|
}
|
|
805
1073
|
function loadCdnImageMap(projectRoot, relativeMapPath, debug) {
|
|
806
1074
|
const possiblePaths = [
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1075
|
+
path5.resolve(projectRoot, relativeMapPath),
|
|
1076
|
+
path5.resolve(projectRoot, "..", relativeMapPath),
|
|
1077
|
+
path5.resolve(process.cwd(), relativeMapPath)
|
|
810
1078
|
];
|
|
811
1079
|
for (const testPath of possiblePaths) {
|
|
812
|
-
if (
|
|
1080
|
+
if (fs5.existsSync(testPath)) {
|
|
813
1081
|
try {
|
|
814
|
-
const raw =
|
|
1082
|
+
const raw = fs5.readFileSync(testPath, "utf8");
|
|
815
1083
|
const data = JSON.parse(raw);
|
|
816
1084
|
const map = /* @__PURE__ */ new Map();
|
|
817
1085
|
for (const entry of data.images) {
|
|
@@ -972,13 +1240,13 @@ function dcsCdnBuildEnd(options = {}) {
|
|
|
972
1240
|
return;
|
|
973
1241
|
}
|
|
974
1242
|
const outDir = siteConfig.outDir;
|
|
975
|
-
if (!
|
|
1243
|
+
if (!fs5.existsSync(outDir)) return;
|
|
976
1244
|
let filesProcessed = 0;
|
|
977
1245
|
let refsReplaced = 0;
|
|
978
1246
|
function walkDir(dir) {
|
|
979
|
-
const entries =
|
|
1247
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
980
1248
|
for (const entry of entries) {
|
|
981
|
-
const fullPath =
|
|
1249
|
+
const fullPath = path5.join(dir, entry.name);
|
|
982
1250
|
if (entry.isDirectory()) {
|
|
983
1251
|
walkDir(fullPath);
|
|
984
1252
|
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
@@ -987,7 +1255,7 @@ function dcsCdnBuildEnd(options = {}) {
|
|
|
987
1255
|
}
|
|
988
1256
|
}
|
|
989
1257
|
function processFile(filePath) {
|
|
990
|
-
let content =
|
|
1258
|
+
let content = fs5.readFileSync(filePath, "utf8");
|
|
991
1259
|
if (!pathPrefixes.some((p) => content.includes(p))) return;
|
|
992
1260
|
let changed = false;
|
|
993
1261
|
if (filePath.endsWith(".html")) {
|
|
@@ -1022,7 +1290,7 @@ function dcsCdnBuildEnd(options = {}) {
|
|
|
1022
1290
|
}
|
|
1023
1291
|
}
|
|
1024
1292
|
if (changed) {
|
|
1025
|
-
|
|
1293
|
+
fs5.writeFileSync(filePath, content, "utf8");
|
|
1026
1294
|
filesProcessed++;
|
|
1027
1295
|
}
|
|
1028
1296
|
}
|
|
@@ -1071,6 +1339,6 @@ function responsiveImagePlugin(md) {
|
|
|
1071
1339
|
};
|
|
1072
1340
|
}
|
|
1073
1341
|
|
|
1074
|
-
export { DCS_MOTION_TOKENS_CSS, dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsMotionTokensStyleTag, 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 };
|
|
1075
1343
|
//# sourceMappingURL=index.js.map
|
|
1076
1344
|
//# sourceMappingURL=index.js.map
|