dogsbay 0.2.0-beta.33 → 0.2.0-beta.35
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.
|
@@ -123,7 +123,7 @@ export const sitemapComplete = {
|
|
|
123
123
|
severity: "warning",
|
|
124
124
|
description: "Every built HTML page is listed in the sitemap.",
|
|
125
125
|
run(ctx) {
|
|
126
|
-
const { file, distRoot, allFiles } = ctx;
|
|
126
|
+
const { file, distRoot, allFiles, config } = ctx;
|
|
127
127
|
const state = getState(distRoot);
|
|
128
128
|
// Emit once per audit run — we have access to allFiles here.
|
|
129
129
|
if (state.emitted.has("seo/sitemap-complete"))
|
|
@@ -134,13 +134,22 @@ export const sitemapComplete = {
|
|
|
134
134
|
// to add here.
|
|
135
135
|
return [];
|
|
136
136
|
}
|
|
137
|
+
// urlBase comes from the path component of site.url. When the
|
|
138
|
+
// site is mounted at a subpath (typical GH Pages project deploy:
|
|
139
|
+
// `site.url: https://user.github.io/repo`), sitemap entries
|
|
140
|
+
// carry the urlBase prefix (the emitter writes absolute URLs),
|
|
141
|
+
// but the filesystem-derived urlPath below doesn't — files
|
|
142
|
+
// live at `dist/<page>/index.html` regardless of urlBase. Pass
|
|
143
|
+
// urlBase into the comparison so the suffix-strip step
|
|
144
|
+
// accounts for it. See plans/sitemap-audit-urlbase.md.
|
|
145
|
+
const urlBase = urlBaseFromSiteUrl(config?.siteUrl);
|
|
137
146
|
// Map each html file to its likely "page URL" form. Astro
|
|
138
147
|
// emits `docs/intro/index.html` for the URL `/docs/intro/`,
|
|
139
148
|
// so we strip `/index.html` and ensure a leading slash.
|
|
140
149
|
const issues = [];
|
|
141
150
|
for (const f of allFiles) {
|
|
142
151
|
const expectedPath = htmlPathToUrlPath(f.path);
|
|
143
|
-
const found = pathListedInSitemap(expectedPath, state.sitemapLocs);
|
|
152
|
+
const found = pathListedInSitemap(expectedPath, state.sitemapLocs, urlBase);
|
|
144
153
|
if (!found) {
|
|
145
154
|
issues.push({
|
|
146
155
|
ruleId: "seo/sitemap-complete",
|
|
@@ -216,9 +225,18 @@ function htmlPathToUrlPath(htmlPath) {
|
|
|
216
225
|
* Sitemap entries can be absolute (`https://example.com/foo/`)
|
|
217
226
|
* or relative (`/foo/`); we match either by suffix.
|
|
218
227
|
*
|
|
228
|
+
* `urlBase` is the path component of `site.url` — e.g. `/repo` for
|
|
229
|
+
* a GH Pages project deploy at `https://user.github.io/repo`. When
|
|
230
|
+
* non-empty, the platform's sitemap emitter writes absolute URLs
|
|
231
|
+
* including that segment (`https://user.github.io/repo/intro/`),
|
|
232
|
+
* but the filesystem-derived `urlPath` doesn't have it (files live
|
|
233
|
+
* at `dist/intro/index.html`). Strip the urlBase off the sitemap
|
|
234
|
+
* suffix before comparing so the audit doesn't flag every page as
|
|
235
|
+
* missing on every subpath-mounted deploy.
|
|
236
|
+
*
|
|
219
237
|
* Trailing slash tolerant: `/foo/` matches `/foo` too.
|
|
220
238
|
*/
|
|
221
|
-
function pathListedInSitemap(urlPath, locs) {
|
|
239
|
+
function pathListedInSitemap(urlPath, locs, urlBase) {
|
|
222
240
|
for (const loc of locs) {
|
|
223
241
|
if (loc === urlPath)
|
|
224
242
|
return true;
|
|
@@ -230,6 +248,16 @@ function pathListedInSitemap(urlPath, locs) {
|
|
|
230
248
|
if (afterHost >= 0)
|
|
231
249
|
suffix = loc.slice(afterHost);
|
|
232
250
|
}
|
|
251
|
+
// Strip the urlBase prefix when the sitemap entry carries one
|
|
252
|
+
// and the comparison target doesn't.
|
|
253
|
+
if (urlBase) {
|
|
254
|
+
if (suffix === urlBase) {
|
|
255
|
+
suffix = "/";
|
|
256
|
+
}
|
|
257
|
+
else if (suffix.startsWith(`${urlBase}/`)) {
|
|
258
|
+
suffix = suffix.slice(urlBase.length);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
233
261
|
if (suffix === urlPath)
|
|
234
262
|
return true;
|
|
235
263
|
// Trailing-slash flexibility
|
|
@@ -238,3 +266,21 @@ function pathListedInSitemap(urlPath, locs) {
|
|
|
238
266
|
}
|
|
239
267
|
return false;
|
|
240
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Extract the path component of `site.url` for use as the urlBase
|
|
271
|
+
* stripping prefix. Returns `""` when site.url is missing, has no
|
|
272
|
+
* path, or doesn't parse — comparison falls back to today's
|
|
273
|
+
* behaviour.
|
|
274
|
+
*/
|
|
275
|
+
function urlBaseFromSiteUrl(siteUrl) {
|
|
276
|
+
if (!siteUrl)
|
|
277
|
+
return "";
|
|
278
|
+
try {
|
|
279
|
+
const u = new URL(siteUrl);
|
|
280
|
+
const path = u.pathname.replace(/\/$/, "");
|
|
281
|
+
return path === "/" ? "" : path;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { registerRule } from "../../registry.js";
|
|
14
14
|
import { localeCoherence } from "./locale-coherence.js";
|
|
15
15
|
import { namespaceCoherence } from "./namespace-coherence.js";
|
|
16
|
+
import { navTargetExists } from "./nav-target-exists.js";
|
|
16
17
|
import { versionCoherence } from "./version-coherence.js";
|
|
17
18
|
let registered = false;
|
|
18
19
|
/**
|
|
@@ -26,6 +27,7 @@ export function registerStructureRules() {
|
|
|
26
27
|
registerRule(namespaceCoherence);
|
|
27
28
|
registerRule(versionCoherence);
|
|
28
29
|
registerRule(localeCoherence);
|
|
30
|
+
registerRule(navTargetExists);
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Test-only: reset the "registered" flag so unit tests can
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `structure/nav-target-exists` — every `file:` reference in a
|
|
3
|
+
* declared nav file resolves to a content file on disk.
|
|
4
|
+
*
|
|
5
|
+
* Re-runs the same resolution `loadNavFile` does at build time,
|
|
6
|
+
* but in `collect` mode: instead of `console.warn` + label-only
|
|
7
|
+
* fallback (the build-pipeline behaviour), the audit surfaces
|
|
8
|
+
* every miss as a structured Issue. This is what makes nav-file
|
|
9
|
+
* typos:
|
|
10
|
+
*
|
|
11
|
+
* - count in the `dogsbay site check` summary
|
|
12
|
+
* - bump the exit code (error severity)
|
|
13
|
+
* - get categorised under `structure` for selective opt-out
|
|
14
|
+
*
|
|
15
|
+
* Without this rule, a misnamed file in nav.yml emitted a
|
|
16
|
+
* `[dogsbay] Nav file references missing file: ...` line to
|
|
17
|
+
* stderr during build but never entered the audit catalog, so
|
|
18
|
+
* CI couldn't catch it. Reported by the docs team against
|
|
19
|
+
* dogsbay-docs-platform.
|
|
20
|
+
*
|
|
21
|
+
* Source content + nav heuristic: matches `buildNavFromDirectory`
|
|
22
|
+
* — an explicit `source.nav` path wins, otherwise we look for
|
|
23
|
+
* `nav.yml` / `nav.yaml` / `nav.json` in the resolved content
|
|
24
|
+
* dir. If no nav file is present, the rule is a no-op (the site
|
|
25
|
+
* uses directory-scan nav, which has its own validation in the
|
|
26
|
+
* importer).
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync } from "node:fs";
|
|
29
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
30
|
+
import { loadNavFile } from "@dogsbay/format-dogsbay-md";
|
|
31
|
+
const HEURISTIC_NAV_FILES = ["nav.yml", "nav.yaml", "nav.json"];
|
|
32
|
+
export const navTargetExists = {
|
|
33
|
+
id: "structure/nav-target-exists",
|
|
34
|
+
category: "structure",
|
|
35
|
+
stage: "source-corpus",
|
|
36
|
+
severity: "error",
|
|
37
|
+
description: "Every `file:` reference in nav files (nav.yml / nav.yaml / nav.json) resolves to a content file that exists on disk.",
|
|
38
|
+
run(rawCtx) {
|
|
39
|
+
const ctx = rawCtx;
|
|
40
|
+
const auditSources = ctx.auditSources;
|
|
41
|
+
const siteRoot = ctx.siteRoot;
|
|
42
|
+
if (!auditSources || auditSources.length === 0 || !siteRoot) {
|
|
43
|
+
// No way to find nav files — typically a unit-test run with
|
|
44
|
+
// pre-imported nav.
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const issues = [];
|
|
48
|
+
for (const src of auditSources) {
|
|
49
|
+
const { contentDir, navOverride } = src;
|
|
50
|
+
const navPath = resolveNavFile(navOverride, contentDir, siteRoot);
|
|
51
|
+
if (!navPath)
|
|
52
|
+
continue; // directory-scan source → skip
|
|
53
|
+
// Use `collect` mode so the loader pushes structured
|
|
54
|
+
// findings to onMissingTarget instead of console.warn'ing.
|
|
55
|
+
const misses = [];
|
|
56
|
+
try {
|
|
57
|
+
loadNavFile(navPath, contentDir, {
|
|
58
|
+
missingFile: "collect",
|
|
59
|
+
onMissingTarget: (m) => misses.push(m),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
// Nav file itself is unreadable / malformed — emit as a
|
|
64
|
+
// distinct issue. The build's heuristic-loader warns and
|
|
65
|
+
// falls back to directory scan; the audit makes the
|
|
66
|
+
// condition visible.
|
|
67
|
+
issues.push({
|
|
68
|
+
ruleId: "structure/nav-target-exists",
|
|
69
|
+
severity: "error",
|
|
70
|
+
file: navPath,
|
|
71
|
+
message: `Nav file ${navPath} failed to load: ${err.message}`,
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const miss of misses) {
|
|
76
|
+
issues.push({
|
|
77
|
+
ruleId: "structure/nav-target-exists",
|
|
78
|
+
severity: "error",
|
|
79
|
+
file: navPath,
|
|
80
|
+
message: `Nav references missing file: ${miss.file} ` +
|
|
81
|
+
`(resolved to ${miss.resolvedAbs}). Fix the path or remove the entry.`,
|
|
82
|
+
context: miss.file,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return issues;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Mirror the resolution `buildNavFromDirectory` uses:
|
|
91
|
+
* 1. Explicit `source.nav` wins (relative to siteRoot)
|
|
92
|
+
* 2. Auto-detect nav.{yml,yaml,json} in the resolved content dir
|
|
93
|
+
* 3. Otherwise undefined (directory-scan nav — no nav file to audit)
|
|
94
|
+
*/
|
|
95
|
+
function resolveNavFile(navOverride, contentDir, siteRoot) {
|
|
96
|
+
if (navOverride) {
|
|
97
|
+
return isAbsolute(navOverride) ? navOverride : resolve(siteRoot, navOverride);
|
|
98
|
+
}
|
|
99
|
+
for (const fileName of HEURISTIC_NAV_FILES) {
|
|
100
|
+
const candidate = join(contentDir, fileName);
|
|
101
|
+
if (existsSync(candidate))
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
package/dist/audit/run.js
CHANGED
|
@@ -28,6 +28,8 @@ export function runAudit(inputs) {
|
|
|
28
28
|
namespaces: inputs.namespaces,
|
|
29
29
|
versions: inputs.versions,
|
|
30
30
|
locales: inputs.locales,
|
|
31
|
+
auditSources: inputs.auditSources,
|
|
32
|
+
siteRoot: inputs.siteRoot,
|
|
31
33
|
};
|
|
32
34
|
for (const issue of rule.run(ctx)) {
|
|
33
35
|
issues.push(issue);
|
|
@@ -179,6 +179,16 @@ export async function siteCheck(cwd, options) {
|
|
|
179
179
|
.map((s) => effectiveLocale(s, config.content.defaultLocale))
|
|
180
180
|
.filter((l) => l !== undefined))
|
|
181
181
|
: undefined;
|
|
182
|
+
// Re-resolve content sources for `structure/nav-target-exists`.
|
|
183
|
+
// The rule walks each source's nav file and re-runs the
|
|
184
|
+
// `file:` resolution that `loadNavFile` does at build time,
|
|
185
|
+
// surfacing misses as structured audit issues (the build-time
|
|
186
|
+
// `console.warn` doesn't enter the summary or exit code).
|
|
187
|
+
const auditSources = [];
|
|
188
|
+
for (const src of config.content.sources) {
|
|
189
|
+
const r = await resolveSource(src, siteRoot);
|
|
190
|
+
auditSources.push({ contentDir: r.path, navOverride: src.nav });
|
|
191
|
+
}
|
|
182
192
|
const report = runAudit({
|
|
183
193
|
rules,
|
|
184
194
|
pages,
|
|
@@ -189,6 +199,8 @@ export async function siteCheck(cwd, options) {
|
|
|
189
199
|
namespaces,
|
|
190
200
|
versions,
|
|
191
201
|
locales,
|
|
202
|
+
auditSources,
|
|
203
|
+
siteRoot,
|
|
192
204
|
});
|
|
193
205
|
// ── Output ─────────────────────────────────────────────────────
|
|
194
206
|
if (!options.quiet) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dogsbay",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.35",
|
|
4
4
|
"description": "CLI for Dogsbay — scaffold, build, and serve documentation sites with markdown / MkDocs / Obsidian / OpenAPI sources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"picocolors": "^1.1.0",
|
|
33
33
|
"prompts": "^2.4.2",
|
|
34
34
|
"yaml": "^2.8.3",
|
|
35
|
-
"@dogsbay/format-
|
|
36
|
-
"@dogsbay/format-astro": "0.2.0-beta.
|
|
37
|
-
"@dogsbay/format-
|
|
38
|
-
"@dogsbay/format-mdx": "0.2.0-beta.
|
|
39
|
-
"@dogsbay/format-starlight": "0.2.0-beta.
|
|
40
|
-
"@dogsbay/format-
|
|
41
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
42
|
-
"@dogsbay/format-
|
|
35
|
+
"@dogsbay/format-obsidian": "0.2.0-beta.35",
|
|
36
|
+
"@dogsbay/format-astro": "0.2.0-beta.35",
|
|
37
|
+
"@dogsbay/format-mkdocs": "0.2.0-beta.35",
|
|
38
|
+
"@dogsbay/format-mdx": "0.2.0-beta.35",
|
|
39
|
+
"@dogsbay/format-starlight": "0.2.0-beta.35",
|
|
40
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.35",
|
|
41
|
+
"@dogsbay/types": "0.2.0-beta.35",
|
|
42
|
+
"@dogsbay/format-openapi": "0.2.0-beta.35"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.0.0",
|