@conduction/docusaurus-preset 3.4.0 → 3.6.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 +79 -0
- package/bin/validate-ai-baseline.mjs +193 -0
- package/package.json +5 -1
- package/src/index.js +85 -12
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ A few non-negotiables encoded by the package CSS and worth knowing about:
|
|
|
21
21
|
- **Brand-default navbar** — locale-dropdown + GitHub link. Sites override `items[]` for site-specific navigation.
|
|
22
22
|
- **Brand-default footer** — three-column link grid + Conduction-tells (KvK, BTW, address). Per-property override: pass `footer: { links: [...] }` to swap columns and inherit the brand copyright unchanged. Spread `baseFooterLinks()` to keep one or two brand columns alongside site-specific ones.
|
|
23
23
|
- **Sensible defaults** — `trailingSlash`, `onBrokenLinks: 'warn'`, `respectPrefersColorScheme`, dark-mode brand mapping.
|
|
24
|
+
- **AI-crawler baseline** — Organization + WebSite JSON-LD on every page, `SoftwareApplication` JSON-LD from `<DetailHero>`, `FAQPage` JSON-LD from `<FAQ>`, default `og:image` + Twitter card meta, sitemap options, and a `postBuild` plugin that emits `robots.txt` when the site does not ship its own. See the AI baseline section below for the validator and content requirements.
|
|
24
25
|
|
|
25
26
|
## Usage
|
|
26
27
|
|
|
@@ -163,6 +164,84 @@ import '@conduction/docusaurus-preset/diagrams';
|
|
|
163
164
|
|
|
164
165
|
This is how product sites such as `mydash.conduction.nl/docs/...` adopt the brand without copying CSS or theme code, and stay in sync as the design-system evolves.
|
|
165
166
|
|
|
167
|
+
## AI-crawler baseline
|
|
168
|
+
|
|
169
|
+
Every site that consumes this preset inherits a contract that AI crawlers (GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot, Google AI Overviews) expect. The schemas, meta tags, and `robots.txt` ship automatically; sites only have to opt in to the content that surfaces them.
|
|
170
|
+
|
|
171
|
+
**What the preset ships**
|
|
172
|
+
|
|
173
|
+
| Surface | Source | How a site uses it |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| Organization + WebSite JSON-LD | `headTags` injected by `createConfig` | Automatic on every page |
|
|
176
|
+
| `og:image`, `twitter:site`, `twitter:card`, `og:type` | `themeConfig.image` + `themeConfig.metadata` defaults | Override per site by passing `themeConfig.image: 'img/og-my-app.png'` |
|
|
177
|
+
| Default `robots.txt` | `conduction-ai-crawling` postBuild plugin | Drop `static/robots.txt` to override |
|
|
178
|
+
| `SoftwareApplication` JSON-LD | `<DetailHero appId="my-app" .../>` | Pages that should advertise the app must render `<DetailHero>` with an `appId` that resolves in `src/data/apps-registry.js`. No DetailHero means no schema. |
|
|
179
|
+
| `FAQPage` JSON-LD | `<FAQ>` with `<FAQItem question=...>` children | Drop a `<FAQ>` block onto a page; the schema is auto-emitted from the children |
|
|
180
|
+
| Sitemap options | Default `sitemap` config on the classic preset | Sites that override `presets` must include their own `sitemap` block |
|
|
181
|
+
|
|
182
|
+
**Validating a site**
|
|
183
|
+
|
|
184
|
+
The preset ships a generic 8-check validator as a `bin`. Wire it into the site's build:
|
|
185
|
+
|
|
186
|
+
```jsonc
|
|
187
|
+
// docs/package.json
|
|
188
|
+
{
|
|
189
|
+
"scripts": {
|
|
190
|
+
"build": "docusaurus build",
|
|
191
|
+
"postbuild": "validate-ai-baseline",
|
|
192
|
+
"validate:ai-baseline": "validate-ai-baseline"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`npm run build` now exits non-zero if any of these regress: `robots.txt` exists with a Sitemap line and an AI-bot allow line, `sitemap.xml` has at least one URL, the homepage emits Organization + WebSite JSON-LD plus `og:image` / `og:type` / `twitter:site` / `twitter:card`, and the `og:image` URL resolves to a real file. Sites can extend the validator with extra checks (per-app SoftwareApplication, FAQPage on specific pages, etc.) by adding their own `scripts/validate-ai-baseline-site.mjs` and chaining it.
|
|
198
|
+
|
|
199
|
+
**Per-app docs site checklist**
|
|
200
|
+
|
|
201
|
+
For a per-app docs site to satisfy the full schema contract, the landing page must render `<DetailHero appId="my-app" .../>` with an `appId` that exists in `src/data/apps-registry.js`. That single render emits the `SoftwareApplication` JSON-LD with category mapping (Data and Processes -> BusinessApplication, Connectors -> DeveloperApplication, etc.), `operatingSystem: 'Nextcloud'`, and the EUPL-1.2 license URL. Sites that build a custom landing without `<DetailHero>` get only Organization + WebSite, not the per-app schema.
|
|
202
|
+
|
|
203
|
+
**Opting out**
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
createConfig({
|
|
207
|
+
title: '...',
|
|
208
|
+
url: '...',
|
|
209
|
+
baseUrl: '/',
|
|
210
|
+
aiCrawling: { disable: true }, // skip the whole postBuild plugin
|
|
211
|
+
// or, finer-grained:
|
|
212
|
+
aiCrawling: { disable: { robotsTxt: true } }, // ship our own static/robots.txt
|
|
213
|
+
});
|
|
214
|
+
```
|
|
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
|
+
|
|
166
245
|
## Releasing
|
|
167
246
|
|
|
168
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:
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/validate-ai-baseline.mjs
|
|
4
|
+
*
|
|
5
|
+
* Generic AI-crawler baseline validator. Runs as a postbuild step on
|
|
6
|
+
* every Conduction Docusaurus site that consumes
|
|
7
|
+
* @conduction/docusaurus-preset >= 3.4.0. Asserts the SSG output
|
|
8
|
+
* carries the contract AI crawlers (GPTBot, ClaudeBot, PerplexityBot,
|
|
9
|
+
* OAI-SearchBot, Claude-SearchBot, Google AI Overviews) expect.
|
|
10
|
+
*
|
|
11
|
+
* Universal checks only - no site-specific routes. Sites that want
|
|
12
|
+
* additional gates (per-app SoftwareApplication, FAQPage on specific
|
|
13
|
+
* pages, etc.) extend this script in place. See conduction-website's
|
|
14
|
+
* version for an example of additional checks.
|
|
15
|
+
*
|
|
16
|
+
* Exit codes:
|
|
17
|
+
* 0 all checks passed
|
|
18
|
+
* 1 one or more checks failed (CI should block)
|
|
19
|
+
* 2 build directory not found (script invoked before build)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {readFileSync, existsSync, statSync} from 'node:fs';
|
|
23
|
+
import {join, resolve} from 'node:path';
|
|
24
|
+
|
|
25
|
+
const buildDir = resolve(process.argv[2] || 'build');
|
|
26
|
+
|
|
27
|
+
if (!existsSync(buildDir)) {
|
|
28
|
+
console.error(`✗ build directory not found: ${buildDir}`);
|
|
29
|
+
console.error(` Run \`npx docusaurus build\` first.`);
|
|
30
|
+
process.exit(2);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const results = [];
|
|
34
|
+
|
|
35
|
+
function check(name, fn) {
|
|
36
|
+
try {
|
|
37
|
+
const r = fn();
|
|
38
|
+
results.push({name, ok: r.ok, msg: r.msg});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
results.push({name, ok: false, msg: `threw: ${e.message}`});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readBuild(p) {
|
|
45
|
+
return readFileSync(join(buildDir, p), 'utf8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* robots.txt - shipped by the preset's ai-crawling plugin (or the
|
|
49
|
+
site's own static/robots.txt). Either way, the file must exist
|
|
50
|
+
and name at least one AI search bot so a `grep` audit can confirm
|
|
51
|
+
the posture at a glance. */
|
|
52
|
+
check('robots.txt exists and is non-empty', () => {
|
|
53
|
+
const path = join(buildDir, 'robots.txt');
|
|
54
|
+
if (!existsSync(path)) return {ok: false, msg: 'missing'};
|
|
55
|
+
const size = statSync(path).size;
|
|
56
|
+
if (size < 50) return {ok: false, msg: `too small (${size} bytes)`};
|
|
57
|
+
return {ok: true, msg: `${size} bytes`};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
check('robots.txt names at least one AI search bot', () => {
|
|
61
|
+
const body = readBuild('robots.txt');
|
|
62
|
+
const candidates = ['OAI-SearchBot', 'Claude-SearchBot', 'PerplexityBot', 'ChatGPT-User', 'Claude-User'];
|
|
63
|
+
const found = candidates.filter(ua => body.includes(`User-agent: ${ua}`));
|
|
64
|
+
if (found.length === 0) {
|
|
65
|
+
return {ok: false, msg: `none of [${candidates.join(', ')}] referenced`};
|
|
66
|
+
}
|
|
67
|
+
return {ok: true, msg: `${found.length} bot(s): ${found.join(', ')}`};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
check('robots.txt has a Sitemap line', () => {
|
|
71
|
+
const body = readBuild('robots.txt');
|
|
72
|
+
const matches = body.match(/^Sitemap:\s+https?:\/\//gm) || [];
|
|
73
|
+
if (matches.length === 0) return {ok: false, msg: 'no Sitemap: line'};
|
|
74
|
+
return {ok: true, msg: `${matches.length} sitemap line(s)`};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/* sitemap.xml - emitted by @docusaurus/plugin-sitemap (loaded via
|
|
78
|
+
the classic preset). Locale-specific sitemaps (e.g. /nl/sitemap.xml)
|
|
79
|
+
are present for i18n builds; we only check the canonical one
|
|
80
|
+
because some sites are single-locale. */
|
|
81
|
+
check('sitemap.xml exists and has at least 1 URL', () => {
|
|
82
|
+
const path = join(buildDir, 'sitemap.xml');
|
|
83
|
+
if (!existsSync(path)) return {ok: false, msg: 'missing'};
|
|
84
|
+
const body = readBuild('sitemap.xml');
|
|
85
|
+
const n = (body.match(/<loc>/g) || []).length;
|
|
86
|
+
if (n < 1) return {ok: false, msg: 'no <loc> entries'};
|
|
87
|
+
return {ok: true, msg: `${n} URLs`};
|
|
88
|
+
});
|
|
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
|
+
|
|
106
|
+
/* Helper for the JSON-LD checks below. Docusaurus emits ld+json
|
|
107
|
+
tags via two paths with different attribute ordering: top-level
|
|
108
|
+
headTags renders <script type="..."> first, while Helmet (used
|
|
109
|
+
by <Head> from inside React components like <DetailHero>, <FAQ>)
|
|
110
|
+
prefixes data-rh="true". The regex matches either ordering. */
|
|
111
|
+
function extractJsonLdBlocks(html) {
|
|
112
|
+
const out = [];
|
|
113
|
+
const re = /<script\b[^>]*\btype="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g;
|
|
114
|
+
let m;
|
|
115
|
+
while ((m = re.exec(html)) !== null) {
|
|
116
|
+
out.push(m[1]);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
check('homepage emits >= 2 JSON-LD blocks, all valid JSON', () => {
|
|
122
|
+
if (!existsSync(join(buildDir, 'index.html'))) return {ok: false, msg: 'no index.html'};
|
|
123
|
+
const html = readBuild('index.html');
|
|
124
|
+
const blocks = extractJsonLdBlocks(html);
|
|
125
|
+
if (blocks.length < 2) return {ok: false, msg: `only ${blocks.length} block(s)`};
|
|
126
|
+
for (const [i, b] of blocks.entries()) {
|
|
127
|
+
try {JSON.parse(b);} catch (e) {
|
|
128
|
+
return {ok: false, msg: `block ${i} invalid JSON: ${e.message}`};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {ok: true, msg: `${blocks.length} blocks, all valid`};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
check('homepage JSON-LD includes Organization and WebSite', () => {
|
|
135
|
+
const html = readBuild('index.html');
|
|
136
|
+
const types = extractJsonLdBlocks(html).map(b => {
|
|
137
|
+
try {return JSON.parse(b)['@type'];} catch {return null;}
|
|
138
|
+
});
|
|
139
|
+
const want = ['Organization', 'WebSite'];
|
|
140
|
+
const missing = want.filter(t => !types.includes(t));
|
|
141
|
+
if (missing.length) return {ok: false, msg: `missing @type: ${missing.join(', ')}`};
|
|
142
|
+
return {ok: true, msg: types.filter(Boolean).join(' + ')};
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/* Social-card meta. og:image is the one that breaks LinkedIn /
|
|
146
|
+
Slack / AI previews when it 404s, so we also resolve the URL to
|
|
147
|
+
a local file in the build output. */
|
|
148
|
+
function metaTag(html, key) {
|
|
149
|
+
const re = new RegExp(`<meta[^>]+(?:name|property)="${key}"[^>]+content="([^"]+)"`, 'i');
|
|
150
|
+
const m = html.match(re);
|
|
151
|
+
return m ? m[1] : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
check('homepage has og:image, og:type, twitter:site, twitter:card', () => {
|
|
155
|
+
const html = readBuild('index.html');
|
|
156
|
+
const checks = {
|
|
157
|
+
'og:image': metaTag(html, 'og:image'),
|
|
158
|
+
'og:type': metaTag(html, 'og:type'),
|
|
159
|
+
'twitter:site': metaTag(html, 'twitter:site'),
|
|
160
|
+
'twitter:card': metaTag(html, 'twitter:card'),
|
|
161
|
+
};
|
|
162
|
+
const missing = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
|
|
163
|
+
if (missing.length) return {ok: false, msg: `missing: ${missing.join(', ')}`};
|
|
164
|
+
return {ok: true, msg: 'all four present'};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
check('og:image URL resolves to a file in the build', () => {
|
|
168
|
+
const html = readBuild('index.html');
|
|
169
|
+
const url = metaTag(html, 'og:image');
|
|
170
|
+
if (!url) return {ok: false, msg: 'no og:image meta'};
|
|
171
|
+
const path = url.replace(/^https?:\/\/[^/]+\//, '');
|
|
172
|
+
const local = join(buildDir, path);
|
|
173
|
+
if (!existsSync(local)) return {ok: false, msg: `og:image refers to ${url}, not found at ${local}`};
|
|
174
|
+
const size = statSync(local).size;
|
|
175
|
+
if (size < 1024) return {ok: false, msg: `og:image file suspiciously small (${size} bytes)`};
|
|
176
|
+
return {ok: true, msg: `${path} (${size} bytes)`};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/* Report */
|
|
180
|
+
let failed = 0;
|
|
181
|
+
for (const {name, ok, msg} of results) {
|
|
182
|
+
const icon = ok ? '✓' : '✗';
|
|
183
|
+
console.log(`${icon} ${name} - ${msg}`);
|
|
184
|
+
if (!ok) failed++;
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
if (failed) {
|
|
188
|
+
console.error(`${failed} of ${results.length} checks failed.`);
|
|
189
|
+
console.error('AI-crawler baseline regressed. Fix the failures above before merging.');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
} else {
|
|
192
|
+
console.log(`All ${results.length} AI-baseline checks passed.`);
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/docusaurus-preset",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"prepack": "node scripts/prepack-bundle-css.js"
|
|
6
6
|
},
|
|
7
|
+
"bin": {
|
|
8
|
+
"validate-ai-baseline": "./bin/validate-ai-baseline.mjs"
|
|
9
|
+
},
|
|
7
10
|
"description": "Conduction brand preset for Docusaurus 3. Tokens, theme, navbar, footer, i18n config for nl/en/de/fr, and the React component library that powers conduction.nl and the Conduction product sites.",
|
|
8
11
|
"main": "src/index.js",
|
|
9
12
|
"exports": {
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
"files": [
|
|
29
32
|
"src/",
|
|
30
33
|
"static/",
|
|
34
|
+
"bin/",
|
|
31
35
|
"README.md",
|
|
32
36
|
"MISSING_COMPONENTS.md"
|
|
33
37
|
],
|
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
|
-
|
|
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,34 @@ 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/*/`) are documented Docusaurus duplicate-content traps;
|
|
236
|
+
* we exclude them by default so they neither dilute crawl budget nor
|
|
237
|
+
* confuse AI summarisers.
|
|
238
|
+
*/
|
|
187
239
|
const DEFAULT_SITEMAP_OPTIONS = {
|
|
188
|
-
changefreq:
|
|
189
|
-
priority:
|
|
240
|
+
changefreq: null,
|
|
241
|
+
priority: null,
|
|
242
|
+
lastmod: 'date',
|
|
190
243
|
ignorePatterns: [
|
|
191
244
|
'/academy/tags/**',
|
|
192
245
|
'/nl/academy/tags/**',
|
|
193
246
|
'/en/academy/tags/**',
|
|
194
247
|
'/de/academy/tags/**',
|
|
195
248
|
'/fr/academy/tags/**',
|
|
249
|
+
'/page/**',
|
|
250
|
+
'/nl/page/**',
|
|
251
|
+
'/en/page/**',
|
|
252
|
+
'/de/page/**',
|
|
253
|
+
'/fr/page/**',
|
|
196
254
|
],
|
|
197
255
|
filename: 'sitemap.xml',
|
|
198
256
|
};
|
|
@@ -489,9 +547,15 @@ function createConfig(opts) {
|
|
|
489
547
|
footerBrand: opts.footerBrand || null,
|
|
490
548
|
/* Legal-bar links (Privacy / Terms / ISO) plus the two ISO
|
|
491
549
|
9001 + 27001 certification badges on the right side of the
|
|
492
|
-
canal-footer.
|
|
493
|
-
|
|
494
|
-
|
|
550
|
+
canal-footer.
|
|
551
|
+
|
|
552
|
+
Defaults point at the canonical Conduction pages on
|
|
553
|
+
www.conduction.nl rather than relative routes. Earlier
|
|
554
|
+
defaults used /privacy, /terms, /iso which 404'd on every
|
|
555
|
+
per-app subdomain (openregister.conduction.nl/privacy etc.)
|
|
556
|
+
because those routes only exist on the marketing site. The
|
|
557
|
+
SEO audit found ~645 sitewide broken internal links across
|
|
558
|
+
the fleet from this single mistake. Sites can override per
|
|
495
559
|
slot to silence broken-link warnings:
|
|
496
560
|
|
|
497
561
|
legalLinks: {
|
|
@@ -499,12 +563,21 @@ function createConfig(opts) {
|
|
|
499
563
|
terms: false, // hide the Terms link
|
|
500
564
|
iso: false, // hide the ISO link AND the cert badges
|
|
501
565
|
// (badges follow iso link by default)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
566
|
+
privacy: '/privacy', // self-host: pass a relative route
|
|
567
|
+
certifications: true | false,
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
The marketing site at conduction-website passes legalLinks
|
|
571
|
+
explicitly with relative routes so its self-hosted Privacy /
|
|
572
|
+
Terms / ISO pages keep working as before. */
|
|
573
|
+
legalLinks: Object.assign(
|
|
574
|
+
{
|
|
575
|
+
privacy: 'https://www.conduction.nl/privacy',
|
|
576
|
+
terms: 'https://www.conduction.nl/terms',
|
|
577
|
+
iso: 'https://www.conduction.nl/iso',
|
|
578
|
+
},
|
|
579
|
+
opts.legalLinks || {}
|
|
580
|
+
),
|
|
508
581
|
/* AI-friendly social-card defaults. `image` ships from the
|
|
509
582
|
preset's static/img/og-conduction.png and gets served at every
|
|
510
583
|
consuming site's /img/og-conduction.png; drop your own
|