@conduction/docusaurus-preset 3.5.0 → 3.6.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.
package/README.md CHANGED
@@ -213,6 +213,35 @@ createConfig({
213
213
  });
214
214
  ```
215
215
 
216
+ ## Traditional SEO baseline
217
+
218
+ The same `createConfig` call also wires the traditional-search baseline that pairs with the AI-crawler one. Google, Bing, DuckDuckGo and the AI surfaces those engines feed (Copilot, ChatGPT Search, Perplexity) all benefit.
219
+
220
+ **What's shipped automatically**
221
+
222
+ - **Sitemap with `lastmod`** from file mtime; `priority` and `changefreq` are dropped because Google ignores them. `/page/N/` pagination and `/academy/tags/` thin pages are excluded sitewide so they don't dilute crawl budget.
223
+ - **Footer legal links default to absolute URLs on `www.conduction.nl`** (`/privacy`, `/terms`, `/iso`). Earlier defaults used relative routes that 404'd on every per-app subdomain — the SEO audit found ~645 sitewide broken internal links across the fleet from this single mistake. Marketing sites that self-host these pages pass `legalLinks: { privacy: '/privacy', ... }` to opt back into relative routing.
224
+ - **Search Console / Bing Webmaster / Yandex / Facebook / Pinterest verification meta tags** via `opts.searchConsoleVerification`. Each present token becomes a `<meta>` tag in the global head, which lets a non-DNS-admin teammate verify the property via the console UI:
225
+
226
+ ```js
227
+ createConfig({
228
+ // ...
229
+ searchConsoleVerification: {
230
+ google: 'abc123...', // -> <meta name="google-site-verification">
231
+ bing: 'xyz...', // -> <meta name="msvalidate.01">
232
+ yandex: '...', // -> <meta name="yandex-verification">
233
+ facebook: '...', // -> <meta name="facebook-domain-verification">
234
+ pinterest: '...', // -> <meta name="p:domain_verify">
235
+ },
236
+ });
237
+ ```
238
+
239
+ **Known follow-ups (not yet automatic)**
240
+
241
+ - `BreadcrumbList` JSON-LD on every page. The DocBreadcrumbs DOM already renders; the schema needs a theme swizzle. Tracked as a 3.7+ candidate.
242
+ - `TechArticle` JSON-LD on docs pages with `dateModified` from git mtime. Same swizzle scope.
243
+ - Per-page title format. Docusaurus defaults to `{Page} | {Site}` which produces `OpenRegister | OpenRegister` on per-app homepages. Override per page via frontmatter `title:` for now; a `titleFormat` option may land later.
244
+
216
245
  ## Releasing
217
246
 
218
247
  Releases auto-publish on push to `main`, driven by [semantic-release](https://semantic-release.gitbook.io/) reading [conventional-commit](https://www.conventionalcommits.org/) messages. The [.github/workflows/publish-packages.yml](../.github/workflows/publish-packages.yml) workflow walks every commit since the last `@conduction/docusaurus-preset-v*` tag and decides what to ship:
@@ -87,6 +87,22 @@ 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 should ship <lastmod> on every URL. Google treats lastmod
91
+ as the only sitemap-level signal that actually informs recrawl
92
+ priority, and only when it's trustworthy. Sites that ship priority +
93
+ changefreq without lastmod (the Docusaurus default before preset
94
+ 3.6.0) get treated as having no freshness signal. */
95
+ check('sitemap.xml emits <lastmod> on URLs', () => {
96
+ const body = readBuild('sitemap.xml');
97
+ const locCount = (body.match(/<loc>/g) || []).length;
98
+ const lastmodCount = (body.match(/<lastmod>/g) || []).length;
99
+ if (locCount === 0) return {ok: false, msg: 'no <loc> entries to compare against'};
100
+ if (lastmodCount === 0) return {ok: false, msg: `0 / ${locCount} URLs have <lastmod> — enable sitemap.lastmod in docusaurus.config`};
101
+ const ratio = lastmodCount / locCount;
102
+ if (ratio < 0.5) return {ok: false, msg: `only ${lastmodCount} / ${locCount} URLs have <lastmod>`};
103
+ return {ok: true, msg: `${lastmodCount} / ${locCount} URLs (${Math.round(ratio * 100)}%)`};
104
+ });
105
+
90
106
  /* Helper for the JSON-LD checks below. Docusaurus emits ld+json
91
107
  tags via two paths with different attribute ordering: top-level
92
108
  headTags renders <script type="..."> first, while Helmet (used
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "3.5.0",
3
+ "version": "3.6.1",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
package/src/index.js CHANGED
@@ -160,7 +160,7 @@ function buildWebsiteJsonLd(opts) {
160
160
  * the site's tags after its own defaults.
161
161
  */
162
162
  function buildAiHeadTags(opts) {
163
- return [
163
+ const tags = [
164
164
  {
165
165
  tagName: 'script',
166
166
  attributes: {type: 'application/ld+json'},
@@ -172,6 +172,45 @@ function buildAiHeadTags(opts) {
172
172
  innerHTML: JSON.stringify(buildWebsiteJsonLd(opts)),
173
173
  },
174
174
  ];
175
+
176
+ /* Search Console verification meta tags. Sites pass tokens via
177
+ opts.searchConsoleVerification = { google: '...', bing: '...',
178
+ yandex: '...' }; each present token becomes a meta tag. Verifying
179
+ via meta (vs DNS TXT) lets a non-DNS-admin teammate access Search
180
+ Console / Bing Webmaster Tools. */
181
+ const verification = opts.searchConsoleVerification || {};
182
+ if (verification.google) {
183
+ tags.push({
184
+ tagName: 'meta',
185
+ attributes: {name: 'google-site-verification', content: verification.google},
186
+ });
187
+ }
188
+ if (verification.bing) {
189
+ tags.push({
190
+ tagName: 'meta',
191
+ attributes: {name: 'msvalidate.01', content: verification.bing},
192
+ });
193
+ }
194
+ if (verification.yandex) {
195
+ tags.push({
196
+ tagName: 'meta',
197
+ attributes: {name: 'yandex-verification', content: verification.yandex},
198
+ });
199
+ }
200
+ if (verification.facebook) {
201
+ tags.push({
202
+ tagName: 'meta',
203
+ attributes: {name: 'facebook-domain-verification', content: verification.facebook},
204
+ });
205
+ }
206
+ if (verification.pinterest) {
207
+ tags.push({
208
+ tagName: 'meta',
209
+ attributes: {name: 'p:domain_verify', content: verification.pinterest},
210
+ });
211
+ }
212
+
213
+ return tags;
175
214
  }
176
215
 
177
216
  /**
@@ -184,15 +223,36 @@ function buildAiHeadTags(opts) {
184
223
  * Sites passing their own classic preset config can override by
185
224
  * including a `sitemap` key alongside `docs`/`blog`/`theme`.
186
225
  */
226
+ /**
227
+ * Sitemap defaults. Google ignores `changefreq` and `priority` (and has
228
+ * for years; the @docusaurus/plugin-sitemap defaults are wrong on this
229
+ * point). `lastmod` is the only signal Google actually uses, and only
230
+ * if the dates are accurate, so we ship lastmod from file mtime. Bing
231
+ * still reads all three, harmless to omit.
232
+ *
233
+ * Sites with locale-specific tag pages and pagination should keep the
234
+ * exclude list in sync. Pagination (`/page/N/`) and tag pages
235
+ * (`/tags/{slug}/`) are documented Docusaurus duplicate-content traps;
236
+ * we exclude them by default so they neither dilute crawl budget nor
237
+ * confuse AI summarisers. (Do not write `/tags/*` followed by a slash
238
+ * in this comment: the literal asterisk-slash sequence would close
239
+ * the JSDoc block and break preset parsing for every consuming site.)
240
+ */
187
241
  const DEFAULT_SITEMAP_OPTIONS = {
188
- changefreq: 'weekly',
189
- priority: 0.5,
242
+ changefreq: null,
243
+ priority: null,
244
+ lastmod: 'date',
190
245
  ignorePatterns: [
191
246
  '/academy/tags/**',
192
247
  '/nl/academy/tags/**',
193
248
  '/en/academy/tags/**',
194
249
  '/de/academy/tags/**',
195
250
  '/fr/academy/tags/**',
251
+ '/page/**',
252
+ '/nl/page/**',
253
+ '/en/page/**',
254
+ '/de/page/**',
255
+ '/fr/page/**',
196
256
  ],
197
257
  filename: 'sitemap.xml',
198
258
  };
@@ -489,9 +549,15 @@ function createConfig(opts) {
489
549
  footerBrand: opts.footerBrand || null,
490
550
  /* Legal-bar links (Privacy / Terms / ISO) plus the two ISO
491
551
  9001 + 27001 certification badges on the right side of the
492
- canal-footer. Default keeps prior behaviour (pages live at
493
- /privacy, /terms, /iso on docs.conduction.nl + www.conduction.nl).
494
- Consumer sites that don't ship those pages can opt out per
552
+ canal-footer.
553
+
554
+ Defaults point at the canonical Conduction pages on
555
+ www.conduction.nl rather than relative routes. Earlier
556
+ defaults used /privacy, /terms, /iso which 404'd on every
557
+ per-app subdomain (openregister.conduction.nl/privacy etc.)
558
+ because those routes only exist on the marketing site. The
559
+ SEO audit found ~645 sitewide broken internal links across
560
+ the fleet from this single mistake. Sites can override per
495
561
  slot to silence broken-link warnings:
496
562
 
497
563
  legalLinks: {
@@ -499,12 +565,21 @@ function createConfig(opts) {
499
565
  terms: false, // hide the Terms link
500
566
  iso: false, // hide the ISO link AND the cert badges
501
567
  // (badges follow iso link by default)
502
- // any slot can also take a string for an external URL:
503
- privacy: 'https://docs.conduction.nl/privacy',
504
- // certs default-follow iso, override here:
505
- isoCertifications: true | false,
506
- } */
507
- legalLinks: opts.legalLinks || {},
568
+ privacy: '/privacy', // self-host: pass a relative route
569
+ certifications: true | false,
570
+ }
571
+
572
+ The marketing site at conduction-website passes legalLinks
573
+ explicitly with relative routes so its self-hosted Privacy /
574
+ Terms / ISO pages keep working as before. */
575
+ legalLinks: Object.assign(
576
+ {
577
+ privacy: 'https://www.conduction.nl/privacy',
578
+ terms: 'https://www.conduction.nl/terms',
579
+ iso: 'https://www.conduction.nl/iso',
580
+ },
581
+ opts.legalLinks || {}
582
+ ),
508
583
  /* AI-friendly social-card defaults. `image` ships from the
509
584
  preset's static/img/og-conduction.png and gets served at every
510
585
  consuming site's /img/og-conduction.png; drop your own