@conduction/docusaurus-preset 3.6.2 → 3.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.
@@ -87,25 +87,31 @@ check('sitemap.xml exists and has at least 1 URL', () => {
87
87
  return {ok: true, msg: `${n} URLs`};
88
88
  });
89
89
 
90
- /* sitemap.xml lastmod check is informational only. Google treats
91
- lastmod as the only sitemap-level signal that actually informs
92
- recrawl priority, but applying it requires per-site docusaurus.config
93
- changes (sites that override `presets:` don't pick up the preset's
94
- DEFAULT_SITEMAP_OPTIONS automatically). Warning here surfaces the
95
- gap without blocking deploy on fleet sites that haven't adopted
96
- the new sitemap config yet. Promote back to hard-fail in a future
97
- release once the preset wraps user presets to inject sitemap defaults
98
- automatically. */
99
- check('sitemap.xml emits <lastmod> on URLs (advisory, not fatal)', () => {
90
+ /* sitemap.xml should ship <lastmod> on the majority of URLs. Google
91
+ treats lastmod as the only sitemap-level signal that actually informs
92
+ recrawl priority, and only when it's trustworthy. Preset 3.7+ wraps
93
+ user-supplied opts.presets to inject DEFAULT_SITEMAP_OPTIONS
94
+ (lastmod: 'date') into any classic preset entry, so every site that
95
+ bumps should see lastmod automatically.
96
+
97
+ Hard-fail when lastmod is missing entirely (means the preset wrap
98
+ didn't kick in). Pass when at least half of URLs have lastmod —
99
+ Docusaurus' auto-generated routes (/docs/category/X/, the root path
100
+ without a source file, redirects, etc.) legitimately don't have a
101
+ git mtime to use, so 100% coverage is unrealistic. ~80% is typical
102
+ for a content-heavy docs site. */
103
+ check('sitemap.xml emits <lastmod> on URLs', () => {
100
104
  const body = readBuild('sitemap.xml');
101
105
  const locCount = (body.match(/<loc>/g) || []).length;
102
106
  const lastmodCount = (body.match(/<lastmod>/g) || []).length;
103
107
  if (locCount === 0) return {ok: false, msg: 'no <loc> entries to compare against'};
104
108
  if (lastmodCount === 0) {
105
- /* Surface as a "passed with note" rather than fail. */
106
- return {ok: true, msg: `WARN 0 / ${locCount} URLs have <lastmod> (enable sitemap.lastmod in docusaurus.config)`};
109
+ return {ok: false, msg: `0 / ${locCount} URLs have <lastmod>. Upgrade to @conduction/docusaurus-preset ^3.7.0 or set sitemap.lastmod in docusaurus.config.`};
107
110
  }
108
111
  const ratio = lastmodCount / locCount;
112
+ if (ratio < 0.5) {
113
+ return {ok: false, msg: `only ${lastmodCount} / ${locCount} URLs have <lastmod> (under 50%); investigate which routes are missing source files`};
114
+ }
109
115
  return {ok: true, msg: `${lastmodCount} / ${locCount} URLs (${Math.round(ratio * 100)}%)`};
110
116
  });
111
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "3.6.2",
3
+ "version": "3.7.1",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
package/src/index.js CHANGED
@@ -292,6 +292,41 @@ const I18N = {
292
292
  },
293
293
  };
294
294
 
295
+ /**
296
+ * Wrap a user-supplied `opts.presets` array so the classic preset's
297
+ * `sitemap` option inherits brand defaults (lastmod, ignorePatterns)
298
+ * when the site hasn't set them explicitly. Before this helper, sites
299
+ * that passed their own presets array silently lost the preset's
300
+ * DEFAULT_SITEMAP_OPTIONS, so the fleet sitemaps never shipped
301
+ * <lastmod> tags despite the preset claiming to add them. See
302
+ * MEMORY.md project_preset-4.0-wrap-user-presets for the back story.
303
+ *
304
+ * Recognises both 'classic' and '@docusaurus/preset-classic' entries.
305
+ * Leaves non-classic presets untouched. Explicit user values win:
306
+ * `sitemap: null` opts out, `sitemap: { lastmod: false }` opts out
307
+ * of lastmod specifically, and so on.
308
+ */
309
+ function wrapClassicPresetDefaults(userPresets) {
310
+ return userPresets.map(entry => {
311
+ if (!Array.isArray(entry)) return entry;
312
+ const [name, config] = entry;
313
+ const isClassic =
314
+ name === 'classic' || name === '@docusaurus/preset-classic';
315
+ if (!isClassic || typeof config !== 'object' || config === null) return entry;
316
+ if (config.sitemap === null) return entry; /* explicit opt-out */
317
+ return [
318
+ name,
319
+ {
320
+ ...config,
321
+ sitemap: {
322
+ ...DEFAULT_SITEMAP_OPTIONS,
323
+ ...(config.sitemap || {}),
324
+ },
325
+ },
326
+ ];
327
+ });
328
+ }
329
+
295
330
  /**
296
331
  * Brand-default navbar. Sites pass their own items[] and logo; the chrome
297
332
  * styling (cobalt-on-white, Plex-Mono caption) is locked.
@@ -476,30 +511,34 @@ function createConfig(opts) {
476
511
 
477
512
  i18n: opts.i18n || I18N,
478
513
 
479
- presets: opts.presets || [
480
- [
481
- 'classic',
482
- {
483
- docs: {
484
- sidebarPath: './sidebars.js',
485
- editUrl: opts.editUrl,
486
- },
487
- blog: opts.blog === false ? false : {
488
- showReadingTime: true,
489
- blogTitle: opts.title + ' blog',
490
- blogDescription: 'Updates from Conduction',
491
- },
492
- theme: {
493
- customCss,
494
- },
495
- /* AI-crawler-friendly defaults for @docusaurus/plugin-sitemap.
496
- Per-locale sitemap.xml emitted automatically; /academy/tags
497
- excluded across every locale prefix. Sites passing their own
498
- presets array must include their own sitemap config too. */
499
- sitemap: DEFAULT_SITEMAP_OPTIONS,
500
- },
501
- ],
502
- ],
514
+ /* Sites can pass `opts.presets` to override docs/blog/theme. When
515
+ they do, the preset wraps each classic preset entry to deep-merge
516
+ DEFAULT_SITEMAP_OPTIONS into the entry's sitemap key (lastmod
517
+ becomes automatic, ignorePatterns merge in). Without this wrap
518
+ sites would have to copy-paste the sitemap config in every
519
+ docusaurus.config.js, and the fleet would drift over time. */
520
+ presets: opts.presets
521
+ ? wrapClassicPresetDefaults(opts.presets)
522
+ : [
523
+ [
524
+ 'classic',
525
+ {
526
+ docs: {
527
+ sidebarPath: './sidebars.js',
528
+ editUrl: opts.editUrl,
529
+ },
530
+ blog: opts.blog === false ? false : {
531
+ showReadingTime: true,
532
+ blogTitle: opts.title + ' blog',
533
+ blogDescription: 'Updates from Conduction',
534
+ },
535
+ theme: {
536
+ customCss,
537
+ },
538
+ sitemap: DEFAULT_SITEMAP_OPTIONS,
539
+ },
540
+ ],
541
+ ],
503
542
 
504
543
  /* Brand theme: registers ./theme/* swizzles (Navbar, Footer, …)
505
544
  and auto-loads brand.css. Site-specific themes can be added by
@@ -581,15 +620,34 @@ function createConfig(opts) {
581
620
  opts.legalLinks || {}
582
621
  ),
583
622
  /* AI-friendly social-card defaults. `image` ships from the
584
- preset's static/img/og-conduction.png and gets served at every
585
- consuming site's /img/og-conduction.png; drop your own
586
- static/img/og-conduction.png to override per-site. `metadata`
587
- seeds twitter:site + twitter:card + og:type baselines; per-
588
- page MDX frontmatter still wins via Helmet de-dupe. */
589
- image: DEFAULT_OG_IMAGE,
590
- metadata: DEFAULT_METADATA,
623
+ preset's static/img/og-conduction.png and gets served at
624
+ every consuming site's /img/og-conduction.png; drop your
625
+ own static/img/og-conduction.png to override per-site, or
626
+ pass `themeConfig.image: 'img/og-my-app.png'` to use a
627
+ different file. `metadata` seeds twitter:site + twitter:card
628
+ + og:type baselines; per-page MDX frontmatter still wins
629
+ via Helmet de-dupe.
630
+
631
+ These two slots are handled below via explicit overrides
632
+ rather than the wholesale Object.assign so that user-set
633
+ metadata extends (rather than replaces) the brand defaults
634
+ and image falls back gracefully. */
635
+ image: opts.themeConfig?.image || DEFAULT_OG_IMAGE,
636
+ metadata: [
637
+ ...DEFAULT_METADATA,
638
+ ...(opts.themeConfig?.metadata || []),
639
+ ],
591
640
  },
592
- opts.themeConfig || {}
641
+ /* Object.assign last so opts.themeConfig overrides primitives
642
+ like colorMode and navbar, but the image + metadata keys
643
+ above pre-merged the user's values so the spread doesn't
644
+ clobber the brand defaults. */
645
+ (() => {
646
+ if (!opts.themeConfig) return {};
647
+ /* eslint-disable-next-line no-unused-vars */
648
+ const {image: _image, metadata: _metadata, ...rest} = opts.themeConfig;
649
+ return rest;
650
+ })()
593
651
  ),
594
652
 
595
653
  /* AI-crawler discovery: Organization + WebSite JSON-LD on every