@duffcloudservices/cms 0.6.0 → 0.8.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,8 +1,8 @@
1
- import { buildHeadTags, spliceHeadHtml, loadPagesManifest } from '../chunk-JJK7OGC2.js';
2
- export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-JJK7OGC2.js';
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
3
  import fs2 from 'fs';
4
4
  import path2 from 'path';
5
- import yaml from 'js-yaml';
5
+ import yaml2 from 'js-yaml';
6
6
  import { defineComponent, h } from 'vue';
7
7
 
8
8
  function dcsContentPlugin(options = {}) {
@@ -41,7 +41,7 @@ function dcsContentPlugin(options = {}) {
41
41
  }
42
42
  try {
43
43
  const fileContent = fs2.readFileSync(foundPath, "utf8");
44
- const content = yaml.load(fileContent);
44
+ const content = yaml2.load(fileContent);
45
45
  if (debug) {
46
46
  console.log(`[dcs-content] Loaded ${foundPath}`);
47
47
  console.log(`[dcs-content] Version: ${content.version}`);
@@ -112,33 +112,207 @@ function loadSeoConfig(projectRoot, seoPath, debug) {
112
112
  }
113
113
  try {
114
114
  const fileContent = fs2.readFileSync(foundPath, "utf8");
115
- const config = yaml.load(fileContent);
115
+ const config = yaml2.load(fileContent);
116
116
  return { config, foundPath };
117
117
  } catch (error) {
118
118
  console.warn("[dcs-seo] Failed to parse seo.yaml:", error);
119
119
  return null;
120
120
  }
121
121
  }
122
+ function loadContentConfig(projectRoot, contentRelPath, debug) {
123
+ const possiblePaths = [
124
+ path2.resolve(projectRoot, contentRelPath),
125
+ path2.resolve(projectRoot, "..", contentRelPath),
126
+ path2.resolve(process.cwd(), contentRelPath)
127
+ ];
128
+ const foundPath = possiblePaths.find((p) => fs2.existsSync(p));
129
+ if (!foundPath) {
130
+ if (debug) console.log(`[dcs-seo] No content.yaml found (reviews disabled)`);
131
+ return void 0;
132
+ }
133
+ try {
134
+ const parsed = yaml2.load(fs2.readFileSync(foundPath, "utf8"));
135
+ if (!parsed || typeof parsed !== "object") return void 0;
136
+ return parsed;
137
+ } catch (error) {
138
+ console.warn("[dcs-seo] Failed to parse content.yaml (reviews disabled):", error);
139
+ return void 0;
140
+ }
141
+ }
122
142
  function routeToOutputFile(outDir, routePath) {
123
143
  const trimmed = routePath.replace(/^\/+/, "").replace(/\/+$/, "");
124
144
  if (trimmed === "") return path2.join(outDir, "index.html");
125
145
  return path2.join(outDir, ...trimmed.split("/"), "index.html");
126
146
  }
147
+ function loadExcludedGlobs(projectRoot, pagesPath) {
148
+ const possiblePaths = [
149
+ path2.resolve(projectRoot, pagesPath),
150
+ path2.resolve(projectRoot, "..", pagesPath),
151
+ path2.resolve(process.cwd(), pagesPath)
152
+ ];
153
+ const found = possiblePaths.find((p) => fs2.existsSync(p));
154
+ if (!found) return [];
155
+ try {
156
+ const raw = yaml2.load(fs2.readFileSync(found, "utf8"));
157
+ const excluded = raw?.excluded;
158
+ if (!Array.isArray(excluded)) return [];
159
+ return excluded.filter((e) => typeof e === "string");
160
+ } catch {
161
+ return [];
162
+ }
163
+ }
164
+ function deriveLastmod(seoConfig, projectRoot, pagesPath) {
165
+ const fromSeo = seoConfig?.lastUpdated;
166
+ if (typeof fromSeo === "string" && fromSeo.length > 0) return fromSeo;
167
+ const possiblePaths = [
168
+ path2.resolve(projectRoot, pagesPath),
169
+ path2.resolve(projectRoot, "..", pagesPath),
170
+ path2.resolve(process.cwd(), pagesPath)
171
+ ];
172
+ const found = possiblePaths.find((p) => fs2.existsSync(p));
173
+ if (!found) return void 0;
174
+ try {
175
+ const raw = yaml2.load(fs2.readFileSync(found, "utf8"));
176
+ const fromPages = raw?.lastUpdated;
177
+ if (typeof fromPages === "string" && fromPages.length > 0) return fromPages;
178
+ } catch {
179
+ }
180
+ return void 0;
181
+ }
182
+ function emitSiteFiles(params) {
183
+ const {
184
+ outDir,
185
+ projectRoot,
186
+ pagesPath,
187
+ routes,
188
+ seoConfig,
189
+ siteUrl,
190
+ exclude = [],
191
+ noindex = [],
192
+ preview = false,
193
+ robots = {},
194
+ llms = true,
195
+ debug = false
196
+ } = params;
197
+ const written = [];
198
+ const effectiveSiteUrl = siteUrl || seoConfig?.global?.siteUrl;
199
+ const excludedGlobs = loadExcludedGlobs(projectRoot, pagesPath);
200
+ const lastmod = deriveLastmod(seoConfig, projectRoot, pagesPath);
201
+ let wroteSitemap = false;
202
+ if (!preview) {
203
+ const xml = buildSitemapXml({
204
+ routes,
205
+ siteUrl: effectiveSiteUrl,
206
+ seoConfig,
207
+ exclude,
208
+ noindex,
209
+ excludedGlobs,
210
+ lastmod
211
+ });
212
+ if (xml) {
213
+ fs2.writeFileSync(path2.join(outDir, "sitemap.xml"), xml, "utf8");
214
+ wroteSitemap = true;
215
+ written.push("sitemap.xml");
216
+ if (debug) console.log("[dcs-seo] wrote sitemap.xml");
217
+ } else if (debug) {
218
+ console.log("[dcs-seo] no absolute <loc> derivable; sitemap.xml not written");
219
+ }
220
+ }
221
+ if (robots.enabled ?? true) {
222
+ const robotsPath = path2.join(outDir, "robots.txt");
223
+ const existing = fs2.existsSync(robotsPath) && !robots.force ? fs2.readFileSync(robotsPath, "utf8") : null;
224
+ const keepHandAuthored = existing !== null && isHandAuthoredRobotsAcceptable(existing);
225
+ if (keepHandAuthored) {
226
+ if (debug)
227
+ console.log("[dcs-seo] dist/robots.txt is hand-authored and meets the floor; leaving it");
228
+ } else {
229
+ const txt = buildRobotsTxt({
230
+ siteUrl: effectiveSiteUrl,
231
+ preview,
232
+ robots,
233
+ hasSitemap: wroteSitemap
234
+ });
235
+ fs2.writeFileSync(robotsPath, txt, "utf8");
236
+ written.push("robots.txt");
237
+ if (debug) {
238
+ console.log(
239
+ existing !== null ? "[dcs-seo] dist/robots.txt failed the factory floor; overwrote with emitted robots" : "[dcs-seo] wrote robots.txt"
240
+ );
241
+ }
242
+ }
243
+ }
244
+ if (llms && !preview) {
245
+ const txt = buildLlmsTxt({
246
+ routes,
247
+ siteUrl: effectiveSiteUrl,
248
+ seoConfig,
249
+ exclude,
250
+ noindex,
251
+ excludedGlobs
252
+ });
253
+ if (txt) {
254
+ fs2.writeFileSync(path2.join(outDir, "llms.txt"), txt, "utf8");
255
+ written.push("llms.txt");
256
+ if (debug) console.log("[dcs-seo] wrote llms.txt");
257
+ } else if (debug) {
258
+ console.log("[dcs-seo] nothing indexable; llms.txt not written");
259
+ }
260
+ }
261
+ return written;
262
+ }
263
+ function pickPageFaq(content, pageSlug) {
264
+ if (!content) return void 0;
265
+ const fromBlock = (block) => {
266
+ if (!block) return void 0;
267
+ const v = block["faq"];
268
+ return Array.isArray(v) ? v : void 0;
269
+ };
270
+ return fromBlock(content.pages?.[pageSlug]) ?? fromBlock(content.global);
271
+ }
127
272
  function emitStaticSeoHtml(params) {
128
- const { outDir, shellHtml, routes, seoConfig, exclude = [], noindex = [], debug = false } = params;
273
+ const {
274
+ outDir,
275
+ shellHtml,
276
+ routes,
277
+ seoConfig,
278
+ contentConfig,
279
+ exclude = [],
280
+ noindex = [],
281
+ excludedGlobs = [],
282
+ debug = false
283
+ } = params;
129
284
  const excludeSet = new Set(exclude);
130
285
  const noindexSet = new Set(noindex);
286
+ const siteUrl = seoConfig?.global?.siteUrl ?? "";
287
+ const routeTitles = { "/": "Home" };
288
+ for (const r of routes) {
289
+ if (r.title && r.path) routeTitles[r.path.replace(/\/+$/, "") || "/"] = r.title;
290
+ }
131
291
  let written = 0;
132
292
  for (const route of routes) {
133
293
  if (excludeSet.has(route.path) || route.slug && excludeSet.has(route.slug)) {
134
294
  if (debug) console.log(`[dcs-seo] skip (excluded): ${route.path}`);
135
295
  continue;
136
296
  }
297
+ if (matchesExcludedGlob(route.path, excludedGlobs)) {
298
+ if (debug) console.log(`[dcs-seo] skip (excluded glob): ${route.path}`);
299
+ continue;
300
+ }
137
301
  const forceNoindex = noindexSet.has(route.path) || route.slug && noindexSet.has(route.slug);
302
+ const breadcrumbTrail = siteUrl ? breadcrumbTrailFromRoute(route.path, siteUrl, routeTitles) : void 0;
303
+ const reviews = contentConfig ? findReviewItemsForPage(contentConfig, route.slug) : void 0;
304
+ const isBlogPost = /^\/blog\/.+/.test(route.path) && !!route.title;
305
+ const blogMeta = isBlogPost ? { headline: route.title, url: breadcrumbTrail?.[breadcrumbTrail.length - 1]?.item } : void 0;
306
+ const faq = pickPageFaq(contentConfig, route.slug);
138
307
  const tags = buildHeadTags(route.slug, route.path, seoConfig, {
139
308
  includeKeywords: true,
140
309
  fallbackTitle: route.title,
141
- robots: forceNoindex ? "noindex, nofollow" : void 0
310
+ robots: forceNoindex ? "noindex, nofollow" : void 0,
311
+ emitGraph: true,
312
+ breadcrumbTrail,
313
+ reviews,
314
+ blogMeta,
315
+ faq
142
316
  });
143
317
  const html = spliceHeadHtml(shellHtml, tags);
144
318
  const outFile = routeToOutputFile(outDir, route.path);
@@ -159,8 +333,16 @@ function dcsSeoPlugin(options = {}) {
159
333
  debug = false,
160
334
  emitStaticHtml = false,
161
335
  pagesPath = ".dcs/pages.yaml",
336
+ contentPath = ".dcs/content.yaml",
162
337
  exclude = [],
163
- noindex = []
338
+ noindex = [],
339
+ // Site files default ON whenever per-route HTML emission is on, so the
340
+ // sites already using emitStaticHtml gain sitemap/robots/llms with no edit.
341
+ emitSiteFiles: emitSiteFilesOpt = emitStaticHtml,
342
+ siteUrl,
343
+ robots = {},
344
+ preview = false,
345
+ llms = true
164
346
  } = options;
165
347
  let resolvedConfig;
166
348
  return {
@@ -203,7 +385,14 @@ function dcsSeoPlugin(options = {}) {
203
385
  * no-ops. Never throws (so it can never break a production build).
204
386
  */
205
387
  writeBundle() {
206
- if (!emitStaticHtml) return;
388
+ if (!emitStaticHtml) {
389
+ if (options.emitSiteFiles === true) {
390
+ console.warn(
391
+ "[dcs-seo] emitSiteFiles:true is ignored because emitStaticHtml is false; set emitStaticHtml:true to emit sitemap.xml/robots.txt/llms.txt"
392
+ );
393
+ }
394
+ return;
395
+ }
207
396
  try {
208
397
  const projectRoot = resolvedConfig?.root || process.cwd();
209
398
  const outDir = resolveOutDir(resolvedConfig);
@@ -232,18 +421,43 @@ function dcsSeoPlugin(options = {}) {
232
421
  );
233
422
  }
234
423
  const shellHtml = fs2.readFileSync(shellPath, "utf8");
424
+ const excludedGlobs = loadExcludedGlobs(projectRoot, pagesPath);
425
+ const contentConfig = loadContentConfig(projectRoot, contentPath, debug);
235
426
  const written = emitStaticSeoHtml({
236
427
  outDir,
237
428
  shellHtml,
238
429
  routes,
239
430
  seoConfig: loaded?.config,
431
+ contentConfig,
240
432
  exclude,
241
433
  noindex,
434
+ excludedGlobs,
242
435
  debug
243
436
  });
244
437
  if (debug) {
245
438
  console.log(`[dcs-seo] emitStaticHtml: wrote ${written} per-route HTML file(s)`);
246
439
  }
440
+ if (emitSiteFilesOpt) {
441
+ const siteFiles = emitSiteFiles({
442
+ outDir,
443
+ projectRoot,
444
+ pagesPath,
445
+ routes,
446
+ seoConfig: loaded?.config,
447
+ siteUrl,
448
+ exclude,
449
+ noindex,
450
+ preview,
451
+ robots,
452
+ llms,
453
+ debug
454
+ });
455
+ if (debug) {
456
+ console.log(
457
+ `[dcs-seo] emitSiteFiles: wrote ${siteFiles.length} site file(s): ${siteFiles.join(", ") || "(none)"}`
458
+ );
459
+ }
460
+ }
247
461
  } catch (error) {
248
462
  console.warn("[dcs-seo] emitStaticHtml failed; build output left unchanged:", error);
249
463
  }
@@ -664,6 +878,6 @@ function responsiveImagePlugin(md) {
664
878
  };
665
879
  }
666
880
 
667
- export { dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsPreviewPlugin, dcsSeoPlugin, emitStaticSeoHtml, responsiveImagePlugin };
881
+ export { dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsPreviewPlugin, dcsSeoPlugin, emitSiteFiles, emitStaticSeoHtml, responsiveImagePlugin };
668
882
  //# sourceMappingURL=index.js.map
669
883
  //# sourceMappingURL=index.js.map