@duffcloudservices/cms 0.6.0 → 0.7.1

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, buildRobotsTxt, buildLlmsTxt, matchesExcludedGlob, breadcrumbTrailFromRoute, findReviewItemsForPage, buildHeadTags, spliceHeadHtml, loadPagesManifest } from '../chunk-FUNIALH6.js';
2
+ export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-FUNIALH6.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,200 @@ 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
+ if (fs2.existsSync(robotsPath) && !robots.force) {
224
+ if (debug) console.log("[dcs-seo] dist/robots.txt exists and force not set; leaving it");
225
+ } else {
226
+ const txt = buildRobotsTxt({
227
+ siteUrl: effectiveSiteUrl,
228
+ preview,
229
+ robots,
230
+ hasSitemap: wroteSitemap
231
+ });
232
+ fs2.writeFileSync(robotsPath, txt, "utf8");
233
+ written.push("robots.txt");
234
+ if (debug) console.log("[dcs-seo] wrote robots.txt");
235
+ }
236
+ }
237
+ if (llms && !preview) {
238
+ const txt = buildLlmsTxt({
239
+ routes,
240
+ siteUrl: effectiveSiteUrl,
241
+ seoConfig,
242
+ exclude,
243
+ noindex,
244
+ excludedGlobs
245
+ });
246
+ if (txt) {
247
+ fs2.writeFileSync(path2.join(outDir, "llms.txt"), txt, "utf8");
248
+ written.push("llms.txt");
249
+ if (debug) console.log("[dcs-seo] wrote llms.txt");
250
+ } else if (debug) {
251
+ console.log("[dcs-seo] nothing indexable; llms.txt not written");
252
+ }
253
+ }
254
+ return written;
255
+ }
256
+ function pickPageFaq(content, pageSlug) {
257
+ if (!content) return void 0;
258
+ const fromBlock = (block) => {
259
+ if (!block) return void 0;
260
+ const v = block["faq"];
261
+ return Array.isArray(v) ? v : void 0;
262
+ };
263
+ return fromBlock(content.pages?.[pageSlug]) ?? fromBlock(content.global);
264
+ }
127
265
  function emitStaticSeoHtml(params) {
128
- const { outDir, shellHtml, routes, seoConfig, exclude = [], noindex = [], debug = false } = params;
266
+ const {
267
+ outDir,
268
+ shellHtml,
269
+ routes,
270
+ seoConfig,
271
+ contentConfig,
272
+ exclude = [],
273
+ noindex = [],
274
+ excludedGlobs = [],
275
+ debug = false
276
+ } = params;
129
277
  const excludeSet = new Set(exclude);
130
278
  const noindexSet = new Set(noindex);
279
+ const siteUrl = seoConfig?.global?.siteUrl ?? "";
280
+ const routeTitles = { "/": "Home" };
281
+ for (const r of routes) {
282
+ if (r.title && r.path) routeTitles[r.path.replace(/\/+$/, "") || "/"] = r.title;
283
+ }
131
284
  let written = 0;
132
285
  for (const route of routes) {
133
286
  if (excludeSet.has(route.path) || route.slug && excludeSet.has(route.slug)) {
134
287
  if (debug) console.log(`[dcs-seo] skip (excluded): ${route.path}`);
135
288
  continue;
136
289
  }
290
+ if (matchesExcludedGlob(route.path, excludedGlobs)) {
291
+ if (debug) console.log(`[dcs-seo] skip (excluded glob): ${route.path}`);
292
+ continue;
293
+ }
137
294
  const forceNoindex = noindexSet.has(route.path) || route.slug && noindexSet.has(route.slug);
295
+ const breadcrumbTrail = siteUrl ? breadcrumbTrailFromRoute(route.path, siteUrl, routeTitles) : void 0;
296
+ const reviews = contentConfig ? findReviewItemsForPage(contentConfig, route.slug) : void 0;
297
+ const isBlogPost = /^\/blog\/.+/.test(route.path) && !!route.title;
298
+ const blogMeta = isBlogPost ? { headline: route.title, url: breadcrumbTrail?.[breadcrumbTrail.length - 1]?.item } : void 0;
299
+ const faq = pickPageFaq(contentConfig, route.slug);
138
300
  const tags = buildHeadTags(route.slug, route.path, seoConfig, {
139
301
  includeKeywords: true,
140
302
  fallbackTitle: route.title,
141
- robots: forceNoindex ? "noindex, nofollow" : void 0
303
+ robots: forceNoindex ? "noindex, nofollow" : void 0,
304
+ emitGraph: true,
305
+ breadcrumbTrail,
306
+ reviews,
307
+ blogMeta,
308
+ faq
142
309
  });
143
310
  const html = spliceHeadHtml(shellHtml, tags);
144
311
  const outFile = routeToOutputFile(outDir, route.path);
@@ -159,8 +326,16 @@ function dcsSeoPlugin(options = {}) {
159
326
  debug = false,
160
327
  emitStaticHtml = false,
161
328
  pagesPath = ".dcs/pages.yaml",
329
+ contentPath = ".dcs/content.yaml",
162
330
  exclude = [],
163
- noindex = []
331
+ noindex = [],
332
+ // Site files default ON whenever per-route HTML emission is on, so the
333
+ // sites already using emitStaticHtml gain sitemap/robots/llms with no edit.
334
+ emitSiteFiles: emitSiteFilesOpt = emitStaticHtml,
335
+ siteUrl,
336
+ robots = {},
337
+ preview = false,
338
+ llms = true
164
339
  } = options;
165
340
  let resolvedConfig;
166
341
  return {
@@ -203,7 +378,14 @@ function dcsSeoPlugin(options = {}) {
203
378
  * no-ops. Never throws (so it can never break a production build).
204
379
  */
205
380
  writeBundle() {
206
- if (!emitStaticHtml) return;
381
+ if (!emitStaticHtml) {
382
+ if (options.emitSiteFiles === true) {
383
+ console.warn(
384
+ "[dcs-seo] emitSiteFiles:true is ignored because emitStaticHtml is false; set emitStaticHtml:true to emit sitemap.xml/robots.txt/llms.txt"
385
+ );
386
+ }
387
+ return;
388
+ }
207
389
  try {
208
390
  const projectRoot = resolvedConfig?.root || process.cwd();
209
391
  const outDir = resolveOutDir(resolvedConfig);
@@ -232,18 +414,43 @@ function dcsSeoPlugin(options = {}) {
232
414
  );
233
415
  }
234
416
  const shellHtml = fs2.readFileSync(shellPath, "utf8");
417
+ const excludedGlobs = loadExcludedGlobs(projectRoot, pagesPath);
418
+ const contentConfig = loadContentConfig(projectRoot, contentPath, debug);
235
419
  const written = emitStaticSeoHtml({
236
420
  outDir,
237
421
  shellHtml,
238
422
  routes,
239
423
  seoConfig: loaded?.config,
424
+ contentConfig,
240
425
  exclude,
241
426
  noindex,
427
+ excludedGlobs,
242
428
  debug
243
429
  });
244
430
  if (debug) {
245
431
  console.log(`[dcs-seo] emitStaticHtml: wrote ${written} per-route HTML file(s)`);
246
432
  }
433
+ if (emitSiteFilesOpt) {
434
+ const siteFiles = emitSiteFiles({
435
+ outDir,
436
+ projectRoot,
437
+ pagesPath,
438
+ routes,
439
+ seoConfig: loaded?.config,
440
+ siteUrl,
441
+ exclude,
442
+ noindex,
443
+ preview,
444
+ robots,
445
+ llms,
446
+ debug
447
+ });
448
+ if (debug) {
449
+ console.log(
450
+ `[dcs-seo] emitSiteFiles: wrote ${siteFiles.length} site file(s): ${siteFiles.join(", ") || "(none)"}`
451
+ );
452
+ }
453
+ }
247
454
  } catch (error) {
248
455
  console.warn("[dcs-seo] emitStaticHtml failed; build output left unchanged:", error);
249
456
  }
@@ -664,6 +871,6 @@ function responsiveImagePlugin(md) {
664
871
  };
665
872
  }
666
873
 
667
- export { dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsPreviewPlugin, dcsSeoPlugin, emitStaticSeoHtml, responsiveImagePlugin };
874
+ export { dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsPreviewPlugin, dcsSeoPlugin, emitSiteFiles, emitStaticSeoHtml, responsiveImagePlugin };
668
875
  //# sourceMappingURL=index.js.map
669
876
  //# sourceMappingURL=index.js.map