create-zudo-doc 0.2.0-next.6 → 0.2.0-next.8
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/dist/constants.d.ts +2 -0
- package/dist/constants.js +26 -0
- package/dist/scaffold.js +11 -5
- package/dist/settings-gen.js +9 -0
- package/dist/zfb-config-gen.js +17 -0
- package/package.json +1 -1
- package/templates/base/pages/_data.ts +28 -11
- package/templates/base/pages/docs/[[...slug]].tsx +57 -129
- package/templates/base/pages/index.tsx +3 -1
- package/templates/base/pages/lib/_category-nav.tsx +13 -9
- package/templates/base/pages/lib/_doc-history-area.tsx +22 -2
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +6 -6
- package/templates/base/pages/lib/_doc-page-shell.tsx +229 -0
- package/templates/base/pages/lib/_doc-route-paths.ts +101 -0
- package/templates/base/pages/lib/_extract-headings.ts +263 -33
- package/templates/base/pages/lib/_footer-with-defaults.tsx +9 -3
- package/templates/base/pages/lib/_head-with-defaults.tsx +15 -14
- package/templates/base/pages/lib/_nav-source-cache.ts +3 -3
- package/templates/base/pages/lib/_nav-source-docs.ts +9 -9
- package/templates/base/pages/lib/locale-merge.ts +15 -8
- package/templates/base/pages/lib/route-enumerators.ts +18 -3
- package/templates/base/src/components/client-router-bootstrap.tsx +55 -5
- package/templates/base/src/components/sidebar-toggle.tsx +106 -48
- package/templates/base/src/components/theme-toggle.tsx +15 -1
- package/templates/base/src/config/color-scheme-utils.ts +5 -3
- package/templates/base/src/config/frontmatter-preview-defaults.ts +2 -0
- package/templates/base/src/styles/global.css +38 -11
- package/templates/base/src/types/docs-entry.ts +7 -0
- package/templates/base/src/utils/base.ts +13 -1
- package/templates/base/src/utils/dedent.ts +1 -1
- package/templates/base/src/utils/docs.ts +62 -9
- package/templates/base/src/utils/smart-break.tsx +2 -2
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +172 -13
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +34 -12
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +1 -0
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +4 -2
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +7 -1
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +7 -1
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +8 -4
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +8 -4
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +57 -126
- package/templates/features/i18n/files/pages/[locale]/index.tsx +3 -1
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +4 -1
- package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +65 -125
- package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +66 -131
- package/templates/base/src/components/html-preview/highlighted-code.tsx +0 -74
- package/templates/base/src/components/html-preview/html-preview.tsx +0 -108
- package/templates/base/src/components/html-preview/preflight.ts +0 -112
- package/templates/base/src/components/html-preview/preview-base.tsx +0 -159
- package/templates/base/src/components/mobile-toc.tsx +0 -94
- package/templates/base/src/components/toc.tsx +0 -63
package/dist/constants.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface LightDarkPairing {
|
|
|
5
5
|
}
|
|
6
6
|
export declare const LIGHT_DARK_PAIRINGS: LightDarkPairing[];
|
|
7
7
|
export declare const SINGLE_SCHEMES: string[];
|
|
8
|
+
export declare const LIGHT_SCHEMES: string[];
|
|
8
9
|
export interface SupportedLang {
|
|
9
10
|
value: string;
|
|
10
11
|
label: string;
|
|
@@ -18,3 +19,4 @@ export interface Feature {
|
|
|
18
19
|
cliFlag: string;
|
|
19
20
|
}
|
|
20
21
|
export declare const FEATURES: Feature[];
|
|
22
|
+
export declare const HEADER_RIGHT_LABELS: Record<string, string>;
|
package/dist/constants.js
CHANGED
|
@@ -55,6 +55,19 @@ export const SINGLE_SCHEMES = [
|
|
|
55
55
|
"Gruvbox Light",
|
|
56
56
|
"Ayu Light",
|
|
57
57
|
];
|
|
58
|
+
// Light-only subset of SINGLE_SCHEMES. Used by the preset generator to populate
|
|
59
|
+
// the "Light scheme" dropdown (dark schemes are derived as SINGLE_SCHEMES minus these).
|
|
60
|
+
export const LIGHT_SCHEMES = [
|
|
61
|
+
"Default Light",
|
|
62
|
+
"GitHub Light",
|
|
63
|
+
"Catppuccin Latte",
|
|
64
|
+
"Solarized Light",
|
|
65
|
+
"Rose Pine Dawn",
|
|
66
|
+
"Atom One Light",
|
|
67
|
+
"Everforest Light",
|
|
68
|
+
"Gruvbox Light",
|
|
69
|
+
"Ayu Light",
|
|
70
|
+
];
|
|
58
71
|
export const SUPPORTED_LANGS = [
|
|
59
72
|
{ value: "en", label: "English" },
|
|
60
73
|
{ value: "ja", label: "Japanese" },
|
|
@@ -222,3 +235,16 @@ export const FEATURES = [
|
|
|
222
235
|
cliFlag: "footer-taglist",
|
|
223
236
|
},
|
|
224
237
|
];
|
|
238
|
+
// Display labels for header-right items. Keys are canonical component/trigger
|
|
239
|
+
// names from HeaderRightComponentName / HeaderRightTriggerName
|
|
240
|
+
// (src/config/settings-types.ts in the host); they are deliberately not imported
|
|
241
|
+
// here so constants.ts stays pure data with no cross-package dependencies.
|
|
242
|
+
export const HEADER_RIGHT_LABELS = {
|
|
243
|
+
"version-switcher": "Version switcher",
|
|
244
|
+
"design-token-panel": "Design token panel (trigger)",
|
|
245
|
+
"ai-chat": "AI chat (trigger)",
|
|
246
|
+
"github-link": "GitHub link",
|
|
247
|
+
"theme-toggle": "Theme toggle",
|
|
248
|
+
search: "Search",
|
|
249
|
+
"language-switcher": "Language switcher",
|
|
250
|
+
};
|
package/dist/scaffold.js
CHANGED
|
@@ -258,16 +258,22 @@ function generatePackageJson(choices) {
|
|
|
258
258
|
// disabled, reproducible CSS-Modules scoped names (project-relative paths),
|
|
259
259
|
// dev-mode git-restore detection, Tailwind temp-file cleanup, and a
|
|
260
260
|
// near-miss `"use client"` directive scanner.
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
// next.33 added the opt-in hierarchical heading-ID strategy
|
|
262
|
+
// (Takazudo/zudo-front-builder#871): `markdown.features.headingIds.strategy`.
|
|
263
|
+
// The generated config + TOC builder use it via settings.headingIdStrategy.
|
|
264
|
+
// next.35 fixes resolve_links rewriting bare same-page `[text](#anchor)` /
|
|
265
|
+
// `[text](?query)` links to `/<parent-dir>/#anchor` (zudolab/zudo-doc#1948,
|
|
266
|
+
// upstream Takazudo/zudo-front-builder#875).
|
|
267
|
+
"@takazudo/zfb": "0.1.0-next.35",
|
|
268
|
+
"@takazudo/zfb-runtime": "0.1.0-next.35",
|
|
263
269
|
// zfb-adapter-cloudflare — required for any route with `prerender = false`.
|
|
264
270
|
// Pinned in lockstep with @takazudo/zfb.
|
|
265
|
-
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.
|
|
271
|
+
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.35",
|
|
266
272
|
// @takazudo/zudo-doc — published from this monorepo via
|
|
267
273
|
// .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
|
|
268
274
|
// lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
|
|
269
275
|
// version moves, so a fresh scaffold pulls the version we just published.
|
|
270
|
-
"@takazudo/zudo-doc": "^0.2.0-next.
|
|
276
|
+
"@takazudo/zudo-doc": "^0.2.0-next.8",
|
|
271
277
|
// zod — used by the generated zfb.config.ts. zfb-config-gen emits
|
|
272
278
|
// `import { z } from "zod"` for the content-collection schema +
|
|
273
279
|
// `z.toJSONSchema(...)` conversion. Without this dep, the consumer
|
|
@@ -325,7 +331,7 @@ function generatePackageJson(choices) {
|
|
|
325
331
|
// @takazudo/zudo-doc/integrations/doc-history which in turn imports
|
|
326
332
|
// @takazudo/zudo-doc-history-server/git-history. Without this dep the
|
|
327
333
|
// plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
|
|
328
|
-
deps["@takazudo/zudo-doc-history-server"] = "^0.2.0-next.
|
|
334
|
+
deps["@takazudo/zudo-doc-history-server"] = "^0.2.0-next.8";
|
|
329
335
|
// W7A (#1736): doc-history-plugin.mjs spawns `tsx -e <inline-script>` to
|
|
330
336
|
// run the v2 runtime in a TS-aware Node subprocess; without tsx the
|
|
331
337
|
// plugin's preBuild step exits with ENOENT before zfb finishes config
|
package/dist/settings-gen.js
CHANGED
|
@@ -130,6 +130,15 @@ export function generateSettingsFile(choices) {
|
|
|
130
130
|
else {
|
|
131
131
|
lines.push(` designTokenPanel: false as boolean,`);
|
|
132
132
|
}
|
|
133
|
+
lines.push(` tocMinDepth: 2 as number,`);
|
|
134
|
+
lines.push(` tocMaxDepth: 4 as number,`);
|
|
135
|
+
// Heading-ID (anchor) strategy — single source of truth shared by
|
|
136
|
+
// zfb.config.ts (markdown.features.headingIds) and the host TOC builder
|
|
137
|
+
// (pages/lib/_extract-headings.ts). "hierarchical" emits ancestor-prefixed
|
|
138
|
+
// anchors (foo / foo-moo / foo-moo-mew); "flat" is zfb's legacy scheme.
|
|
139
|
+
// Default to "hierarchical": safe for greenfield (no existing deep links to
|
|
140
|
+
// break) and the recommended scheme (upstream zfb#871).
|
|
141
|
+
lines.push(` headingIdStrategy: "hierarchical" as "flat" | "hierarchical",`);
|
|
133
142
|
if (choices.features.includes("sidebarResizer")) {
|
|
134
143
|
lines.push(` sidebarResizer: true as boolean,`);
|
|
135
144
|
}
|
package/dist/zfb-config-gen.js
CHANGED
|
@@ -71,6 +71,8 @@ export function generateZfbConfig(choices) {
|
|
|
71
71
|
lines.push(` standalone: z.boolean().optional(),`);
|
|
72
72
|
lines.push(` slug: z.string().optional(),`);
|
|
73
73
|
lines.push(` generated: z.boolean().optional(),`);
|
|
74
|
+
lines.push(` category_no_page: z.boolean().optional(),`);
|
|
75
|
+
lines.push(` category_sort_order: z.enum(["asc", "desc"]).optional(),`);
|
|
74
76
|
lines.push(` })`);
|
|
75
77
|
lines.push(` .passthrough();`);
|
|
76
78
|
lines.push(``);
|
|
@@ -152,6 +154,7 @@ export function generateZfbConfig(choices) {
|
|
|
152
154
|
lines.push(` options: {`);
|
|
153
155
|
lines.push(` docsDir: settings.docsDir,`);
|
|
154
156
|
lines.push(` locales: localeRecord,`);
|
|
157
|
+
lines.push(` base: settings.base,`);
|
|
155
158
|
lines.push(` },`);
|
|
156
159
|
lines.push(` },`);
|
|
157
160
|
lines.push(` ]`);
|
|
@@ -211,6 +214,16 @@ export function generateZfbConfig(choices) {
|
|
|
211
214
|
lines.push(` dir: locale.dir,`);
|
|
212
215
|
lines.push(` routePrefix: \`/\${code}/docs/\`,`);
|
|
213
216
|
lines.push(` })),`);
|
|
217
|
+
lines.push(` // Versioned collections: each version's EN dir + per-locale dirs.`);
|
|
218
|
+
lines.push(` ...(settings.versions`);
|
|
219
|
+
lines.push(` ? settings.versions.flatMap((version) => [`);
|
|
220
|
+
lines.push(` { dir: version.docsDir, routePrefix: \`/v/\${version.slug}/docs/\` },`);
|
|
221
|
+
lines.push(` ...Object.entries(version.locales ?? {}).map(([code, locale]) => ({`);
|
|
222
|
+
lines.push(` dir: locale.dir,`);
|
|
223
|
+
lines.push(` routePrefix: \`/v/\${version.slug}/\${code}/docs/\`,`);
|
|
224
|
+
lines.push(` })),`);
|
|
225
|
+
lines.push(` ])`);
|
|
226
|
+
lines.push(` : []),`);
|
|
214
227
|
lines.push(` ],`);
|
|
215
228
|
lines.push(` onBrokenLinks: "warn",`);
|
|
216
229
|
lines.push(` },`);
|
|
@@ -271,6 +284,10 @@ export function generateZfbConfig(choices) {
|
|
|
271
284
|
lines.push(` imageDimensions: {},`);
|
|
272
285
|
lines.push(` // warn-only link validation — failOnBroken: false never fails the build.`);
|
|
273
286
|
lines.push(` linkValidation: { failOnBroken: false },`);
|
|
287
|
+
lines.push(` // Heading-ID (anchor) strategy — single source of truth in`);
|
|
288
|
+
lines.push(` // settings.headingIdStrategy, also mirrored by the host TOC builder`);
|
|
289
|
+
lines.push(` // (pages/lib/_extract-headings.ts) so TOC anchors match rendered ids.`);
|
|
290
|
+
lines.push(` headingIds: { strategy: settings.headingIdStrategy },`);
|
|
274
291
|
lines.push(` },`);
|
|
275
292
|
lines.push(` },`);
|
|
276
293
|
lines.push(` plugins: integrationPlugins,`);
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { getCollection } from "zfb/content";
|
|
14
14
|
import type { CollectionEntry } from "zfb/content";
|
|
15
15
|
import type { DocsEntry } from "@/types/docs-entry";
|
|
16
|
+
import type { DocPageEntry } from "./lib/doc-page-props";
|
|
16
17
|
import { toRouteSlug } from "@/utils/slug";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -79,7 +80,7 @@ export type ZfbDocsEntry = CollectionEntry<ZfbDocsData> & {
|
|
|
79
80
|
* - `collection` — the collection name, for DocsEntry compat
|
|
80
81
|
*/
|
|
81
82
|
export function getDocs(collectionName: string): ZfbDocsEntry[] {
|
|
82
|
-
const entries = getCollection(collectionName)
|
|
83
|
+
const entries = getCollection<ZfbDocsData>(collectionName);
|
|
83
84
|
return entries.map((e) => ({
|
|
84
85
|
...e,
|
|
85
86
|
// Astro-compat: strip a trailing `/index` from the entry id so
|
|
@@ -123,27 +124,43 @@ export function bridgeEntries<T = ZfbDocsData>(
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
/**
|
|
126
|
-
*
|
|
127
|
+
* Typed bridge from a raw zfb collection result to `DocPageEntry[]`.
|
|
127
128
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* `
|
|
129
|
+
* This is the **single, justified** cast at the zfb/DocsEntry boundary.
|
|
130
|
+
* `CollectionEntry<ZfbDocsData> & { id, collection }` structurally satisfies
|
|
131
|
+
* `DocPageEntry` because:
|
|
132
|
+
* - `id` and `collection` are added by `bridgeEntries`
|
|
133
|
+
* - `data` (ZfbDocsData) structurally satisfies `DocsEntry.data` (all
|
|
134
|
+
* required/optional fields are present; the index signature is wider)
|
|
135
|
+
* - `body`, `slug`, `module_specifier`, `Content` are provided by
|
|
136
|
+
* `CollectionEntry<ZfbDocsData>`
|
|
137
|
+
* The plain `as DocPageEntry[]` (not `as unknown as`) is intentional — it
|
|
138
|
+
* expresses that this is a well-understood structural subtype relationship,
|
|
139
|
+
* not an escape from the type system. The zfb type is the source of truth;
|
|
140
|
+
* DocsEntry/DocPageEntry are local compatibility shapes for @/utils/docs.
|
|
131
141
|
*/
|
|
132
|
-
export function
|
|
133
|
-
|
|
142
|
+
export function bridgeDocsEntries(
|
|
143
|
+
entries: ReadonlyArray<CollectionEntry<ZfbDocsData>>,
|
|
144
|
+
collectionName: string,
|
|
145
|
+
): DocPageEntry[] {
|
|
146
|
+
return bridgeEntries(entries, collectionName) as DocPageEntry[];
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
/**
|
|
137
150
|
* One-shot helper for paths()/render-time pages that just need a
|
|
138
|
-
* `DocsEntry[]` for `@/utils/docs` consumption — wraps `getDocs`
|
|
139
|
-
*
|
|
140
|
-
* any page that previously did
|
|
151
|
+
* `DocsEntry[]` for `@/utils/docs` consumption — wraps `getDocs` so
|
|
152
|
+
* call sites stay one-line. Use this from any page that previously did
|
|
141
153
|
* `getCollection("docs") as unknown as DocsEntry[]` — that idiom
|
|
142
154
|
* silently dropped the `id`/`collection` fields the utility helpers
|
|
143
155
|
* read, which threw `Cannot read properties of undefined` at runtime.
|
|
156
|
+
*
|
|
157
|
+
* `ZfbDocsEntry` structurally satisfies `DocsEntry`: it carries `id`,
|
|
158
|
+
* `collection`, `data` (ZfbDocsData satisfies DocsEntry.data field-for-
|
|
159
|
+
* field), `body`, plus the zfb-specific extras (`slug`, `Content`, etc.)
|
|
160
|
+
* that DocsEntry does not require.
|
|
144
161
|
*/
|
|
145
162
|
export function loadDocs(collectionName: string): DocsEntry[] {
|
|
146
|
-
return
|
|
163
|
+
return getDocs(collectionName);
|
|
147
164
|
}
|
|
148
165
|
|
|
149
166
|
/**
|
|
@@ -22,44 +22,32 @@
|
|
|
22
22
|
// Locale: defaultLocale (EN). Non-default locales are handled by
|
|
23
23
|
// pages/[locale]/docs/[[...slug]].tsx.
|
|
24
24
|
|
|
25
|
-
import type { DocsEntry } from "@/types/docs-entry";
|
|
26
25
|
import { settings } from "@/config/settings";
|
|
27
26
|
import { defaultLocale } from "@/config/i18n";
|
|
28
|
-
import { docsUrl } from "@/utils/base";
|
|
27
|
+
import { docsUrl, absoluteUrl } from "@/utils/base";
|
|
29
28
|
import {
|
|
30
29
|
buildNavTree,
|
|
31
30
|
buildBreadcrumbs,
|
|
32
|
-
flattenTree,
|
|
33
|
-
findNode,
|
|
34
31
|
collectAutoIndexNodes,
|
|
35
32
|
type NavNode,
|
|
36
33
|
} from "@/utils/docs";
|
|
37
34
|
import { getNavSectionForSlug, getNavSubtree } from "@/utils/nav-scope";
|
|
38
35
|
import { toRouteSlug, toSlugParams } from "@/utils/slug";
|
|
39
|
-
import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
|
|
40
|
-
import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
|
|
41
|
-
import { NavCardGrid } from "@takazudo/zudo-doc/nav-indexing";
|
|
42
36
|
// Shared MDX-tag → Preact-component bag. Includes htmlOverrides
|
|
43
37
|
// (native typography), HtmlPreviewWrapper (Island), and stub bindings
|
|
44
38
|
// for every other custom tag the MDX corpus references — see
|
|
45
39
|
// `pages/_mdx-components.ts` for the full list and rationale.
|
|
46
40
|
import { createMdxComponents } from "../_mdx-components";
|
|
47
|
-
import { FooterWithDefaults } from "../lib/_footer-with-defaults";
|
|
48
41
|
import { DocHistoryArea } from "../lib/_doc-history-area";
|
|
49
42
|
import { DocMetainfoArea } from "../lib/_doc-metainfo-area";
|
|
50
|
-
import { SidebarWithDefaults } from "../lib/_sidebar-with-defaults";
|
|
51
|
-
import { HeaderWithDefaults } from "../lib/_header-with-defaults";
|
|
52
|
-
import { HeadWithDefaults } from "../lib/_head-with-defaults";
|
|
53
|
-
import { composeMetaTitle } from "../lib/_compose-meta-title";
|
|
54
43
|
import { buildInlineVersionSwitcher } from "../lib/_inline-version-switcher";
|
|
55
44
|
import type { JSX } from "preact";
|
|
56
45
|
import { resolveNavSource } from "../lib/_nav-source-docs";
|
|
57
46
|
import { extractHeadings } from "../lib/_extract-headings";
|
|
58
47
|
import type { DocPageEntry, AutoIndexNode, DocPageEntryProps, DocPageAutoIndexProps } from "../lib/doc-page-props";
|
|
59
|
-
import { DocPager } from "../lib/_doc-pager";
|
|
60
48
|
import { DocContentHeader } from "../lib/_doc-content-header";
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
49
|
+
import { DocPageShell } from "../lib/_doc-page-shell";
|
|
50
|
+
import { resolveDocPrevNext, flattenSubtree } from "../lib/_doc-route-paths";
|
|
63
51
|
|
|
64
52
|
export const frontmatter = { title: "Docs" };
|
|
65
53
|
|
|
@@ -94,40 +82,31 @@ export function paths(): Array<{
|
|
|
94
82
|
const { docs, navDocs, categoryMeta } = resolveNavSource(locale, undefined);
|
|
95
83
|
|
|
96
84
|
// Nav docs: exclude unlisted (for sidebar/prev-next) but keep for breadcrumbs
|
|
97
|
-
const tree = buildNavTree(navDocs
|
|
85
|
+
const tree = buildNavTree(navDocs, locale, categoryMeta);
|
|
98
86
|
// Full tree (including unlisted) for accurate breadcrumbs
|
|
99
|
-
const fullTree = buildNavTree(docs
|
|
87
|
+
const fullTree = buildNavTree(docs, locale, categoryMeta);
|
|
100
88
|
|
|
101
89
|
const result: Array<{ params: { slug: string[] }; props: DocPageProps }> = [];
|
|
102
90
|
|
|
103
91
|
// Regular doc pages
|
|
104
92
|
for (const entry of docs) {
|
|
93
|
+
// A `category_no_page` index.mdx carries category metadata only — keep it
|
|
94
|
+
// in the nav tree (built above, used for breadcrumbs) but emit NO route for
|
|
95
|
+
// it. zfb's walker retains every .mdx as a collection entry, so without
|
|
96
|
+
// this explicit skip the metadata file would silently add a route.
|
|
97
|
+
if (entry.data.category_no_page === true) continue;
|
|
105
98
|
const slug = entry.data.slug ?? toRouteSlug(entry.slug);
|
|
106
99
|
const navSection = getNavSectionForSlug(slug);
|
|
107
100
|
const subtree = getNavSubtree(tree, navSection);
|
|
108
|
-
const flat = flattenTree(subtree);
|
|
109
|
-
const idx = flat.findIndex((n) => n.slug === slug);
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const found = findNode(tree, entry.data.pagination_prev);
|
|
120
|
-
prevNode = found ?? prevNode;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (entry.data.pagination_next !== undefined) {
|
|
124
|
-
if (entry.data.pagination_next === null) {
|
|
125
|
-
nextNode = null;
|
|
126
|
-
} else {
|
|
127
|
-
const found = findNode(tree, entry.data.pagination_next);
|
|
128
|
-
nextNode = found ?? nextNode;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
102
|
+
// Prev/next + frontmatter pagination overrides resolved against THIS
|
|
103
|
+
// route's own `tree`. Latest route — hrefs stay unversioned (no rewrite).
|
|
104
|
+
const { prev: prevNode, next: nextNode } = resolveDocPrevNext(
|
|
105
|
+
tree,
|
|
106
|
+
flattenSubtree(subtree),
|
|
107
|
+
slug,
|
|
108
|
+
entry.data,
|
|
109
|
+
);
|
|
131
110
|
|
|
132
111
|
result.push({
|
|
133
112
|
params: { slug: toSlugParams(slug) },
|
|
@@ -181,7 +160,8 @@ export default function DocsPage(props: PageArgs): JSX.Element {
|
|
|
181
160
|
// locale so CategoryNav/CategoryTreeNav/SiteTreeNav query the right collection.
|
|
182
161
|
const components = createMdxComponents(locale);
|
|
183
162
|
|
|
184
|
-
// Resolve child hrefs for auto-index pages
|
|
163
|
+
// Resolve child hrefs for auto-index pages — latest route keeps the nav
|
|
164
|
+
// node's own docsUrl href (fallback for a noPage parent without an href).
|
|
185
165
|
const autoIndexChildren = props.kind === "autoIndex"
|
|
186
166
|
? props.autoIndex.children
|
|
187
167
|
.filter((c: NavNode) => c.hasPage || c.children.length > 0)
|
|
@@ -191,12 +171,9 @@ export default function DocsPage(props: PageArgs): JSX.Element {
|
|
|
191
171
|
}))
|
|
192
172
|
: [];
|
|
193
173
|
|
|
194
|
-
// Canonical URL —
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const canonical = settings.siteUrl
|
|
198
|
-
? settings.siteUrl.replace(/\/$/, "") + pageUrl
|
|
199
|
-
: undefined;
|
|
174
|
+
// Canonical URL — base-prefixed page path, absolutized against siteUrl.
|
|
175
|
+
const currentPath = docsUrl(slug, locale);
|
|
176
|
+
const canonical = absoluteUrl(currentPath);
|
|
200
177
|
|
|
201
178
|
// Persist key: locale + nav-section so the sidebar DOM node is reused
|
|
202
179
|
// across same-locale + same-section navigations only. No sanitizer needed —
|
|
@@ -209,95 +186,46 @@ export default function DocsPage(props: PageArgs): JSX.Element {
|
|
|
209
186
|
: `sidebar-${locale}-${navSection ?? "default"}`;
|
|
210
187
|
|
|
211
188
|
return (
|
|
212
|
-
<
|
|
213
|
-
|
|
189
|
+
<DocPageShell
|
|
190
|
+
kind={props.kind}
|
|
191
|
+
locale={locale}
|
|
192
|
+
slug={slug}
|
|
193
|
+
title={title}
|
|
214
194
|
description={description}
|
|
215
|
-
head={<HeadWithDefaults title={title} description={description} canonical={canonical} />}
|
|
216
|
-
lang={locale}
|
|
217
|
-
noindex={settings.noindex}
|
|
218
|
-
hideSidebar={hideSidebar}
|
|
219
|
-
hideToc={props.kind === "entry" ? props.entry.data.hide_toc : undefined}
|
|
220
|
-
headings={headings}
|
|
221
195
|
canonical={canonical}
|
|
196
|
+
breadcrumbs={breadcrumbs}
|
|
197
|
+
prev={prev}
|
|
198
|
+
next={next}
|
|
199
|
+
headings={headings}
|
|
200
|
+
navSection={navSection}
|
|
222
201
|
sidebarPersistKey={sidebarPersistKey}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
202
|
+
hideSidebar={hideSidebar}
|
|
203
|
+
hideToc={props.kind === "entry" ? props.entry.data.hide_toc : undefined}
|
|
204
|
+
currentPath={currentPath}
|
|
205
|
+
versionSwitcher={buildInlineVersionSwitcher(slug, locale)}
|
|
206
|
+
autoIndexLabel={props.kind === "autoIndex" ? props.autoIndex.label : undefined}
|
|
207
|
+
autoIndexChildren={autoIndexChildren}
|
|
208
|
+
metainfoSlot={
|
|
209
|
+
props.kind === "autoIndex" ? <DocMetainfoArea slug={slug} locale={locale} /> : null
|
|
230
210
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<
|
|
234
|
-
items={breadcrumbs}
|
|
235
|
-
rightSlot={buildInlineVersionSwitcher(slug, locale)}
|
|
236
|
-
/>
|
|
211
|
+
contentHeaderSlot={
|
|
212
|
+
props.kind === "entry" ? (
|
|
213
|
+
<DocContentHeader entry={props.entry} slug={slug} locale={locale} />
|
|
237
214
|
) : undefined
|
|
238
215
|
}
|
|
239
|
-
|
|
240
|
-
<
|
|
241
|
-
currentSlug={slug}
|
|
242
|
-
lang={locale}
|
|
243
|
-
navSection={getNavSectionForSlug(slug)}
|
|
244
|
-
currentPath={docsUrl(slug, locale)}
|
|
245
|
-
/>
|
|
216
|
+
contentSlot={
|
|
217
|
+
props.kind === "entry" ? <props.entry.Content components={components} /> : undefined
|
|
246
218
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<>
|
|
259
|
-
<h1 class="text-heading font-bold mb-vsp-xs">{props.autoIndex.label}</h1>
|
|
260
|
-
|
|
261
|
-
{/* Build-time date block — chrome parity (#1461). Auto-index pages
|
|
262
|
-
previously rendered without doc-meta; reference site shows it on
|
|
263
|
-
every docs page. The component returns null when no manifest
|
|
264
|
-
entry exists for this slug. */}
|
|
265
|
-
<DocMetainfoArea slug={slug} locale={locale} />
|
|
266
|
-
|
|
267
|
-
{props.autoIndex.description && (
|
|
268
|
-
<p class="mb-vsp-lg text-title text-muted">
|
|
269
|
-
{props.autoIndex.description}
|
|
270
|
-
</p>
|
|
271
|
-
)}
|
|
272
|
-
<NavCardGrid children={autoIndexChildren} />
|
|
273
|
-
</>
|
|
274
|
-
) : (
|
|
275
|
-
/* Regular doc page. Fragment (not <div>) for the same reason as
|
|
276
|
-
the auto-index branch above — see #1460. */
|
|
277
|
-
<>
|
|
278
|
-
<DocContentHeader entry={props.entry} slug={slug} locale={locale} />
|
|
279
|
-
|
|
280
|
-
{/* MDX content rendered via zfb's Content bridge */}
|
|
281
|
-
<props.entry.Content components={components} />
|
|
282
|
-
|
|
283
|
-
{/* Prev / Next pagination — placed before the document utilities
|
|
284
|
-
section to match the Astro reference order: content → pager →
|
|
285
|
-
view-source / history. In the Astro layout, BodyFootUtilArea was
|
|
286
|
-
rendered by the doc-layout wrapper after the <slot /> content,
|
|
287
|
-
so the pager (inside the slot) came first. Fixes #1535. */}
|
|
288
|
-
<DocPager prev={prev} next={next} locale={locale} />
|
|
289
|
-
|
|
290
|
-
{/* Document utilities (revision history + view-source link) — skipped for unlisted pages */}
|
|
291
|
-
{!props.entry.data.unlisted && (
|
|
292
|
-
<DocHistoryArea
|
|
293
|
-
slug={slug}
|
|
294
|
-
locale={locale}
|
|
295
|
-
entrySlug={props.entry.slug}
|
|
296
|
-
contentDir={settings.docsDir}
|
|
297
|
-
/>
|
|
298
|
-
)}
|
|
299
|
-
</>
|
|
300
|
-
)}
|
|
301
|
-
</DocLayoutWithDefaults>
|
|
219
|
+
docHistorySlot={
|
|
220
|
+
props.kind === "entry" && !props.entry.data.unlisted ? (
|
|
221
|
+
<DocHistoryArea
|
|
222
|
+
slug={slug}
|
|
223
|
+
locale={locale}
|
|
224
|
+
entrySlug={props.entry.slug}
|
|
225
|
+
contentDir={settings.docsDir}
|
|
226
|
+
/>
|
|
227
|
+
) : null
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
302
230
|
);
|
|
303
231
|
}
|
|
@@ -46,8 +46,10 @@ export default function IndexPage(): JSX.Element {
|
|
|
46
46
|
const categoryOrder = getCategoryOrder();
|
|
47
47
|
const groupedTree = groupSatelliteNodes(tree, categoryOrder);
|
|
48
48
|
|
|
49
|
+
// Drop category_no_page index files so the count matches the number of tag
|
|
50
|
+
// pages actually built (the tag routes exclude them too).
|
|
49
51
|
const tagCount = collectTags(
|
|
50
|
-
navDocs,
|
|
52
|
+
navDocs.filter((d) => !d.data.category_no_page),
|
|
51
53
|
(id, data) => data.slug ?? toRouteSlug(id),
|
|
52
54
|
).size;
|
|
53
55
|
|
|
@@ -21,9 +21,9 @@ import type { NavNode as V2NavNode } from "@takazudo/zudo-doc/nav-indexing/types
|
|
|
21
21
|
import {
|
|
22
22
|
buildNavTree,
|
|
23
23
|
findNode,
|
|
24
|
+
firstRoutedHref,
|
|
24
25
|
} from "@/utils/docs";
|
|
25
26
|
import { defaultLocale, type Locale } from "@/config/i18n";
|
|
26
|
-
import { docsUrl } from "@/utils/base";
|
|
27
27
|
import { resolveNavSource } from "./_nav-source-docs";
|
|
28
28
|
|
|
29
29
|
export interface CategoryNavWrapperProps {
|
|
@@ -39,8 +39,9 @@ export interface CategoryNavWrapperProps {
|
|
|
39
39
|
* of "claude", not children). Each slug is resolved to its nav node; nodes
|
|
40
40
|
* not found in the tree are silently skipped.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* A `category_no_page` category has no route of its own, so its card links to
|
|
43
|
+
* the first routed descendant page (via firstRoutedHref); categories with no
|
|
44
|
+
* reachable page are skipped rather than emitting a dead link.
|
|
44
45
|
*/
|
|
45
46
|
categories?: string[];
|
|
46
47
|
/**
|
|
@@ -61,8 +62,8 @@ export interface CategoryNavWrapperProps {
|
|
|
61
62
|
* - `categories`: resolves an explicit list of top-level slugs as cards.
|
|
62
63
|
* Use this when the target categories are siblings in the nav tree rather
|
|
63
64
|
* than children of a common parent (e.g. claude-md / claude-skills are
|
|
64
|
-
* top-level peers of claude, not children of it).
|
|
65
|
-
*
|
|
65
|
+
* top-level peers of claude, not children of it). A noPage category card
|
|
66
|
+
* links to its first routed descendant page (it has no route of its own).
|
|
66
67
|
*
|
|
67
68
|
* Returns null when no visible children are resolved.
|
|
68
69
|
*/
|
|
@@ -85,14 +86,17 @@ export function CategoryNavWrapper({
|
|
|
85
86
|
let children: V2NavNode[];
|
|
86
87
|
|
|
87
88
|
if (categories !== undefined) {
|
|
88
|
-
// Explicit slug list mode: resolve each slug to its nav node and build
|
|
89
|
-
//
|
|
90
|
-
//
|
|
89
|
+
// Explicit slug list mode: resolve each slug to its nav node and build a
|
|
90
|
+
// card for it. A `category_no_page` category has no route of its own
|
|
91
|
+
// (collectAutoIndexNodes skips noPage nodes), so its card links to the
|
|
92
|
+
// first routed descendant page; categories with no reachable page are
|
|
93
|
+
// skipped rather than emitting a dead link.
|
|
91
94
|
children = categories
|
|
92
95
|
.map((slug): V2NavNode | null => {
|
|
93
96
|
const node = findNode(tree, slug);
|
|
94
97
|
if (!node) return null;
|
|
95
|
-
const href = node.href ??
|
|
98
|
+
const href = node.href ?? firstRoutedHref(node);
|
|
99
|
+
if (!href) return null;
|
|
96
100
|
return {
|
|
97
101
|
label: node.label,
|
|
98
102
|
description: node.description,
|
|
@@ -67,6 +67,17 @@ interface DocHistoryAreaProps {
|
|
|
67
67
|
* view-source GitHub URL. Omit to suppress the view-source link.
|
|
68
68
|
*/
|
|
69
69
|
contentDir?: string;
|
|
70
|
+
/**
|
|
71
|
+
* True when this locale page falls back to the base EN collection
|
|
72
|
+
* (i.e. the slug has no translation for the active locale). When true,
|
|
73
|
+
* the history data-path derivations use defaultLocale so the island
|
|
74
|
+
* fetches the correct bare-slug JSON and the SSR manifest lookup hits
|
|
75
|
+
* the bare key — both of which only exist for EN-origin files.
|
|
76
|
+
* Display labels (t() calls) still use the active locale so JA users
|
|
77
|
+
* see JA labels on fallback pages. Omit (or false) for translated pages
|
|
78
|
+
* and all other call sites (EN route, tag pages) — behavior unchanged.
|
|
79
|
+
*/
|
|
80
|
+
isFallback?: boolean;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
/**
|
|
@@ -93,6 +104,7 @@ export function DocHistoryArea({
|
|
|
93
104
|
locale,
|
|
94
105
|
entrySlug,
|
|
95
106
|
contentDir,
|
|
107
|
+
isFallback,
|
|
96
108
|
}: DocHistoryAreaProps): VNode | null {
|
|
97
109
|
if (!settings.docHistory) return null;
|
|
98
110
|
|
|
@@ -106,11 +118,18 @@ export function DocHistoryArea({
|
|
|
106
118
|
// collectContentFiles walk in packages/doc-history-server. (#1891)
|
|
107
119
|
const historySlug = toHistorySlug(slug);
|
|
108
120
|
|
|
121
|
+
// On EN-fallback locale pages the history data exists only at the bare
|
|
122
|
+
// (non-locale-prefixed) path — the prebuild/server writes locale-prefixed
|
|
123
|
+
// keys/paths only for files physically present in the locale collection.
|
|
124
|
+
// Use defaultLocale for data lookups when isFallback is true; keep locale
|
|
125
|
+
// for all display label calls (t()) so JA users see JA labels.
|
|
126
|
+
const effectiveHistoryLocale = isFallback ? defaultLocale : locale;
|
|
127
|
+
|
|
109
128
|
// Look up the build-time manifest entry for this page. The composedSlug
|
|
110
129
|
// matches the key written by the prebuild step: bare slug for the default
|
|
111
130
|
// locale, "<localeKey>/<slug>" for non-default locales.
|
|
112
131
|
const composedSlug =
|
|
113
|
-
|
|
132
|
+
effectiveHistoryLocale === defaultLocale ? historySlug : `${effectiveHistoryLocale}/${historySlug}`;
|
|
114
133
|
type MetaEntry = { author: string; createdDate: string; updatedDate: string };
|
|
115
134
|
const meta = (docHistoryMeta as Record<string, MetaEntry>)[composedSlug];
|
|
116
135
|
|
|
@@ -120,7 +139,8 @@ export function DocHistoryArea({
|
|
|
120
139
|
const historyLabel = t("doc.history", locale);
|
|
121
140
|
|
|
122
141
|
// Real-component props — locale omitted for the default locale.
|
|
123
|
-
|
|
142
|
+
// Use effectiveHistoryLocale so fallback pages fetch the bare (non-ja/) path.
|
|
143
|
+
const docHistoryLocale = effectiveHistoryLocale === defaultLocale ? undefined : effectiveHistoryLocale;
|
|
124
144
|
const docHistoryBasePath = settings.base ?? "/";
|
|
125
145
|
|
|
126
146
|
// Build the SSR fallback with only the sr-only metadata block so the
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
// (b11-2 pattern).
|
|
14
14
|
//
|
|
15
15
|
// Date formatting uses Intl.DateTimeFormat (browser-safe). We do NOT
|
|
16
|
-
// import `formatDate` from `src/utils/git-info.ts`
|
|
17
|
-
//
|
|
18
|
-
// would be dragged into the client bundle
|
|
16
|
+
// import the old `formatDate` from `src/utils/git-info.ts` — that module
|
|
17
|
+
// carried top-level Node.js imports (`execFileSync`, `existsSync`) that
|
|
18
|
+
// would be dragged into the client bundle (the B-11 lesson). That file
|
|
19
|
+
// was removed in S1 cleanup (#1928); the mirror below is the canonical copy.
|
|
19
20
|
//
|
|
20
21
|
// Labels are resolved from the project's i18n table so non-default
|
|
21
22
|
// locales (e.g. /ja/) get translated "作成" / "更新" strings.
|
|
@@ -36,9 +37,8 @@ import { toHistorySlug } from "@/utils/slug";
|
|
|
36
37
|
import docHistoryMeta from "#doc-history-meta";
|
|
37
38
|
|
|
38
39
|
// BCP-47 locale tag mapping used by Intl.DateTimeFormat.
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// (`execFileSync`, `existsSync`) — the B-11 lesson applies here too.
|
|
40
|
+
// Originally mirrored from `src/utils/git-info.ts` (removed in S1 #1928).
|
|
41
|
+
// The formatDate function below is the stable copy; kept in sync manually.
|
|
42
42
|
const LOCALE_TO_BCP47: Record<string, string> = {
|
|
43
43
|
en: "en-US",
|
|
44
44
|
ja: "ja-JP",
|