create-zudo-doc 0.2.0-next.3 → 0.2.0-next.5

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.
Files changed (57) hide show
  1. package/dist/features/versioning.d.ts +1 -5
  2. package/dist/features/versioning.js +4 -8
  3. package/dist/scaffold.js +15 -5
  4. package/dist/settings-gen.js +2 -0
  5. package/package.json +2 -1
  6. package/templates/base/pages/_data.ts +8 -31
  7. package/templates/base/pages/docs/{[...slug].tsx → [[...slug]].tsx} +48 -193
  8. package/templates/base/pages/index.tsx +5 -12
  9. package/templates/base/pages/lib/_category-nav.tsx +7 -40
  10. package/templates/base/pages/lib/_category-tree-nav.tsx +6 -38
  11. package/templates/base/pages/lib/_doc-body-end.tsx +34 -0
  12. package/templates/base/pages/lib/_doc-content-header.tsx +98 -0
  13. package/templates/base/pages/lib/_doc-history-area.tsx +14 -2
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +12 -2
  15. package/templates/base/pages/lib/_doc-pager.tsx +79 -0
  16. package/templates/base/pages/lib/_footer-with-defaults.tsx +13 -15
  17. package/templates/base/pages/lib/_header-with-defaults.tsx +1 -3
  18. package/templates/base/pages/lib/_nav-source-cache.ts +145 -0
  19. package/templates/base/pages/lib/_nav-source-docs.ts +207 -42
  20. package/templates/base/pages/lib/_sidebar-prepaint.tsx +52 -0
  21. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +1 -3
  22. package/templates/base/pages/lib/_site-tree-nav.tsx +7 -42
  23. package/templates/base/pages/lib/doc-page-props.ts +48 -0
  24. package/templates/base/pages/lib/locale-merge.ts +149 -41
  25. package/templates/base/pages/lib/route-enumerators.ts +47 -78
  26. package/templates/base/src/components/content/heading-h3.tsx +1 -7
  27. package/templates/base/src/components/site-tree-nav.tsx +2 -14
  28. package/templates/base/src/config/i18n.ts +1 -3
  29. package/templates/base/src/utils/base.ts +18 -7
  30. package/templates/base/src/utils/docs.ts +51 -1
  31. package/templates/base/src/utils/slug.ts +43 -0
  32. package/templates/base/src/utils/smart-break.tsx +1 -7
  33. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +26 -16
  34. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -2
  35. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -1
  36. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -1
  37. package/templates/features/i18n/files/pages/[locale]/docs/{[...slug].tsx → [[...slug]].tsx} +72 -198
  38. package/templates/features/i18n/files/pages/[locale]/index.tsx +11 -32
  39. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +18 -10
  40. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +3 -1
  41. package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +356 -0
  42. package/templates/features/versioning/files/pages/v/[version]/docs/{[...slug].tsx → [[...slug]].tsx} +50 -171
  43. package/templates/base/src/components/content/component-map.ts +0 -25
  44. package/templates/base/src/components/content/content-blockquote.tsx +0 -16
  45. package/templates/base/src/components/content/content-code.tsx +0 -117
  46. package/templates/base/src/components/content/content-link.tsx +0 -83
  47. package/templates/base/src/components/content/content-ol.tsx +0 -19
  48. package/templates/base/src/components/content/content-paragraph.tsx +0 -10
  49. package/templates/base/src/components/content/content-strong.tsx +0 -16
  50. package/templates/base/src/components/content/content-table.tsx +0 -18
  51. package/templates/base/src/components/content/content-ul.tsx +0 -18
  52. package/templates/base/src/components/content/heading-h2.tsx +0 -26
  53. package/templates/base/src/components/content/heading-h4.tsx +0 -26
  54. package/templates/base/src/plugins/rehype-strip-md-extension.ts +0 -58
  55. package/templates/base/src/plugins/remark-admonitions.ts +0 -99
  56. package/templates/base/src/plugins/remark-resolve-markdown-links.ts +0 -127
  57. package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +0 -490
@@ -13,15 +13,11 @@ import type { FeatureModule } from "../compose.js";
13
13
  *
14
14
  * docs/versions.tsx (always — versioning gate only)
15
15
  * v/[version]/docs/[...slug].tsx (always — versioning gate only)
16
- * v/[version]/ja/docs/[...slug].tsx (i18n + versioning — see below)
16
+ * v/[version]/[locale]/docs/[...slug].tsx (i18n + versioning — see below)
17
17
  * [locale]/docs/versions.tsx (i18n + versioning)
18
18
  *
19
19
  * `copyFeatureFiles` (compose.ts) auto-copies everything under `files/`.
20
20
  * postProcess removes the i18n-gated subset when i18n is OFF so single-
21
21
  * locale projects don't ship orphan routes.
22
- *
23
- * Note: `v/[version]/ja/docs/[...slug].tsx` hardcodes `ja` per W2 spec-lock
24
- * Decision 9 / §6.4 — matches main verbatim. Future generalization to
25
- * `[locale]` is deferred (maintainer-question follow-up).
26
22
  */
27
23
  export declare const versioningFeature: FeatureModule;
@@ -14,16 +14,12 @@ import path from "path";
14
14
  *
15
15
  * docs/versions.tsx (always — versioning gate only)
16
16
  * v/[version]/docs/[...slug].tsx (always — versioning gate only)
17
- * v/[version]/ja/docs/[...slug].tsx (i18n + versioning — see below)
17
+ * v/[version]/[locale]/docs/[...slug].tsx (i18n + versioning — see below)
18
18
  * [locale]/docs/versions.tsx (i18n + versioning)
19
19
  *
20
20
  * `copyFeatureFiles` (compose.ts) auto-copies everything under `files/`.
21
21
  * postProcess removes the i18n-gated subset when i18n is OFF so single-
22
22
  * locale projects don't ship orphan routes.
23
- *
24
- * Note: `v/[version]/ja/docs/[...slug].tsx` hardcodes `ja` per W2 spec-lock
25
- * Decision 9 / §6.4 — matches main verbatim. Future generalization to
26
- * `[locale]` is deferred (maintainer-question follow-up).
27
23
  */
28
24
  export const versioningFeature = (choices) => ({
29
25
  name: "versioning",
@@ -33,9 +29,9 @@ export const versioningFeature = (choices) => ({
33
29
  if (await fs.pathExists(localeVersions)) {
34
30
  await fs.remove(localeVersions);
35
31
  }
36
- const jaVersionedDocs = path.join(targetDir, "pages", "v", "[version]", "ja");
37
- if (await fs.pathExists(jaVersionedDocs)) {
38
- await fs.remove(jaVersionedDocs);
32
+ const localeVersionedDocs = path.join(targetDir, "pages", "v", "[version]", "[locale]");
33
+ if (await fs.pathExists(localeVersionedDocs)) {
34
+ await fs.remove(localeVersionedDocs);
39
35
  }
40
36
  }
41
37
  },
package/dist/scaffold.js CHANGED
@@ -248,16 +248,26 @@ function generatePackageJson(choices) {
248
248
  // so render failures surface `console.*` output. next.27 is unusable
249
249
  // for adapter consumers — its tarball omitted emit-worker.mjs
250
250
  // (Takazudo/zudo-front-builder#794, fixed in next.28) — so never pin 27.
251
- "@takazudo/zfb": "0.1.0-next.28",
252
- "@takazudo/zfb-runtime": "0.1.0-next.28",
251
+ // Bumped to next.30 — adds Next.js-style `[[...slug]]` optional-catchall
252
+ // route syntax (Takazudo/zudo-front-builder#812) and raises the zfb-runtime
253
+ // hono floor to ^4.12.23, clearing 9 advisories (#813); also two router
254
+ // hardening fixes (overlapping-sibling rejection #816, per-segment rank
255
+ // sort for dev/prod parity). No consumer-facing breaking change.
256
+ // Bumped to next.31 — CSS-pipeline and islands-scanner fixes (no
257
+ // consumer-facing breaking change): authored-CSS path when Tailwind is
258
+ // disabled, reproducible CSS-Modules scoped names (project-relative paths),
259
+ // dev-mode git-restore detection, Tailwind temp-file cleanup, and a
260
+ // near-miss `"use client"` directive scanner.
261
+ "@takazudo/zfb": "0.1.0-next.31",
262
+ "@takazudo/zfb-runtime": "0.1.0-next.31",
253
263
  // zfb-adapter-cloudflare — required for any route with `prerender = false`.
254
264
  // Pinned in lockstep with @takazudo/zfb.
255
- "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.28",
265
+ "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.31",
256
266
  // @takazudo/zudo-doc — published from this monorepo via
257
267
  // .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
258
268
  // lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
259
269
  // version moves, so a fresh scaffold pulls the version we just published.
260
- "@takazudo/zudo-doc": "^0.2.0-next.3",
270
+ "@takazudo/zudo-doc": "^0.2.0-next.5",
261
271
  // zod — used by the generated zfb.config.ts. zfb-config-gen emits
262
272
  // `import { z } from "zod"` for the content-collection schema +
263
273
  // `z.toJSONSchema(...)` conversion. Without this dep, the consumer
@@ -315,7 +325,7 @@ function generatePackageJson(choices) {
315
325
  // @takazudo/zudo-doc/integrations/doc-history which in turn imports
316
326
  // @takazudo/zudo-doc-history-server/git-history. Without this dep the
317
327
  // plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
318
- deps["@takazudo/zudo-doc-history-server"] = "^0.2.0-next.3";
328
+ deps["@takazudo/zudo-doc-history-server"] = "^0.2.0-next.5";
319
329
  // W7A (#1736): doc-history-plugin.mjs spawns `tsx -e <inline-script>` to
320
330
  // run the v2 runtime in a TS-aware Node subprocess; without tsx the
321
331
  // plugin's preBuild step exits with ENOENT before zfb finishes config
@@ -107,6 +107,8 @@ export function generateSettingsFile(choices) {
107
107
  // the chat once they're wiring up a real `ANTHROPIC_API_KEY`. Defaulting
108
108
  // demo mode off avoids silently disabling chat for them.
109
109
  lines.push(` aiChatDemoMode: false as boolean,`);
110
+ lines.push(` aiChatAllowedOrigins: [] as string[],`);
111
+ lines.push(` aiChatGlobalDailyLimit: false as number | false,`);
110
112
  if (choices.features.includes("docHistory")) {
111
113
  lines.push(` docHistory: true,`);
112
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "0.2.0-next.3",
3
+ "version": "0.2.0-next.5",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -46,6 +46,7 @@
46
46
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
47
47
  "build": "pnpm clean && tsc",
48
48
  "dev": "tsc --watch",
49
+ "typecheck": "tsc --noEmit",
49
50
  "test": "vitest run",
50
51
  "test:slow": "vitest run --config vitest.slow.config.ts",
51
52
  "prepublishOnly": "pnpm build && pnpm test"
@@ -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 { toRouteSlug } from "@/utils/slug";
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // Types
@@ -92,9 +93,14 @@ export function getDocs(collectionName: string): ZfbDocsEntry[] {
92
93
  }));
93
94
  }
94
95
 
96
+ // The `id` field bridged onto every entry is the canonical route slug, so it
97
+ // routes through the one shared rule (`toRouteSlug` in @/utils/slug) — bare
98
+ // root `index` → "" (URL /docs/), nested `x/index` → "x". Previously this was
99
+ // a standalone copy of the strip logic (the lone "" dissenter of the five
100
+ // index-stripping sites); consolidating it here means there is one source of
101
+ // truth. See @/utils/slug for the canonical-root rationale (#1891 / #1873).
95
102
  function stripIndexSuffix(slug: string): string {
96
- if (slug === "index") return "";
97
- return slug.endsWith("/index") ? slug.slice(0, -"/index".length) : slug;
103
+ return toRouteSlug(slug);
98
104
  }
99
105
 
100
106
  /**
@@ -148,32 +154,3 @@ export function filterDrafts(entries: ZfbDocsEntry[]): ZfbDocsEntry[] {
148
154
  return entries.filter((e) => !e.data.draft);
149
155
  }
150
156
 
151
- /**
152
- * Merge locale docs with base (EN) fallbacks.
153
- *
154
- * Strategy (mirrors src/utils/locale-docs.ts):
155
- * 1. Load locale docs (e.g. "docs-ja")
156
- * 2. Load base docs ("docs")
157
- * 3. Locale docs take priority; base docs fill in missing slugs.
158
- * 4. Track which slugs came from base (fallbackSlugs).
159
- *
160
- * Returns { allDocs, fallbackSlugs }.
161
- * categoryMeta is not merged here — callers use loadCategoryMeta() directly.
162
- */
163
- export function mergeLocaleDocs(
164
- locale: string,
165
- ): { allDocs: ZfbDocsEntry[]; fallbackSlugs: Set<string> } {
166
- const localeDocs = filterDrafts(getDocs(`docs-${locale}`));
167
- const baseDocs = filterDrafts(getDocs("docs"));
168
-
169
- const localeSlugSet = new Set(localeDocs.map((d) => d.data.slug ?? d.id));
170
-
171
- const fallbackDocs = baseDocs.filter(
172
- (d) => !localeSlugSet.has(d.data.slug ?? d.id),
173
- );
174
-
175
- return {
176
- allDocs: [...localeDocs, ...fallbackDocs],
177
- fallbackSlugs: new Set(fallbackDocs.map((d) => d.data.slug ?? d.id)),
178
- };
179
- }
@@ -11,36 +11,34 @@
11
11
  // params: { slug: string[] } — e.g. ["getting-started", "intro"]
12
12
  // props: { entry, autoIndex, breadcrumbs, prev, next }
13
13
  //
14
+ // Route is the OPTIONAL catchall `[[...slug]]` so a bare root index.mdx can
15
+ // build at `/docs/` (canonical root URL — #1891). The root entry emits
16
+ // `params.slug = []` (zero segments) via `toSlugParams`; a required `[...slug]`
17
+ // catchall rejects an empty array and would drop the whole route.
18
+ //
14
19
  // The catchall slug is an array per zfb spec — the component joins it when
15
20
  // deriving the string form (e.g. for Content lookups, breadcrumbs, etc.).
16
21
  //
17
22
  // Locale: defaultLocale (EN). Non-default locales are handled by
18
- // pages/[locale]/docs/[...slug].tsx.
23
+ // pages/[locale]/docs/[[...slug]].tsx.
19
24
 
20
- import { getCollection } from "zfb/content";
21
- import type { CollectionEntry } from "zfb/content";
22
25
  import type { DocsEntry } from "@/types/docs-entry";
23
26
  import { settings } from "@/config/settings";
24
- import { defaultLocale, t } from "@/config/i18n";
27
+ import { defaultLocale } from "@/config/i18n";
25
28
  import { docsUrl } from "@/utils/base";
26
29
  import {
27
30
  buildNavTree,
28
31
  buildBreadcrumbs,
29
32
  flattenTree,
30
33
  findNode,
31
- loadCategoryMeta,
32
34
  collectAutoIndexNodes,
33
- isNavVisible,
34
35
  type NavNode,
35
- type BreadcrumbItem,
36
36
  } from "@/utils/docs";
37
37
  import { getNavSectionForSlug, getNavSubtree } from "@/utils/nav-scope";
38
- import { toRouteSlug } from "@/utils/slug";
38
+ import { toRouteSlug, toSlugParams } from "@/utils/slug";
39
39
  import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
40
40
  import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
41
41
  import { NavCardGrid } from "@takazudo/zudo-doc/nav-indexing";
42
- import { FrontmatterPreview } from "@takazudo/zudo-doc/metainfo";
43
- import { frontmatterRenderers } from "@/config/frontmatter-preview-renderers";
44
42
  // Shared MDX-tag → Preact-component bag. Includes htmlOverrides
45
43
  // (native typography), HtmlPreviewWrapper (Island), and stub bindings
46
44
  // for every other custom tag the MDX corpus references — see
@@ -49,21 +47,19 @@ import { createMdxComponents } from "../_mdx-components";
49
47
  import { FooterWithDefaults } from "../lib/_footer-with-defaults";
50
48
  import { DocHistoryArea } from "../lib/_doc-history-area";
51
49
  import { DocMetainfoArea } from "../lib/_doc-metainfo-area";
52
- import { DocTagsArea } from "../lib/_doc-tags-area";
53
- import { BodyEndIslands } from "../lib/_body-end-islands";
54
50
  import { SidebarWithDefaults } from "../lib/_sidebar-with-defaults";
55
51
  import { HeaderWithDefaults } from "../lib/_header-with-defaults";
56
52
  import { HeadWithDefaults } from "../lib/_head-with-defaults";
57
- import { buildFrontmatterPreviewEntries } from "../lib/_frontmatter-preview-data";
58
53
  import { composeMetaTitle } from "../lib/_compose-meta-title";
59
54
  import { buildInlineVersionSwitcher } from "../lib/_inline-version-switcher";
60
55
  import type { JSX } from "preact";
61
- import { bridgeEntries } from "../_data";
56
+ import { resolveNavSource } from "../lib/_nav-source-docs";
62
57
  import { extractHeadings } from "../lib/_extract-headings";
63
- import DesktopSidebarToggle from "@/components/desktop-sidebar-toggle";
64
- import { SidebarResizerInit } from "@takazudo/zudo-doc/sidebar-resizer";
65
- import type { VNode } from "preact";
66
- import { Island } from "@takazudo/zfb";
58
+ import type { DocPageEntry, AutoIndexNode, DocPageEntryProps, DocPageAutoIndexProps } from "../lib/doc-page-props";
59
+ import { DocPager } from "../lib/_doc-pager";
60
+ import { DocContentHeader } from "../lib/_doc-content-header";
61
+ import { SidebarPrepaint } from "../lib/_sidebar-prepaint";
62
+ import { DocBodyEnd } from "../lib/_doc-body-end";
67
63
 
68
64
  export const frontmatter = { title: "Docs" };
69
65
 
@@ -71,31 +67,9 @@ export const frontmatter = { title: "Docs" };
71
67
  // Props contract
72
68
  // ---------------------------------------------------------------------------
73
69
 
74
- interface DocPageEntry extends DocsEntry {
75
- /** zfb content renderer. */
76
- Content: CollectionEntry<unknown>["Content"];
77
- /** zfb module specifier (for Content bridge). */
78
- module_specifier: string;
79
- }
80
-
81
- interface AutoIndexNode extends NavNode {
82
- children: NavNode[];
83
- }
70
+ // DocPageEntry, AutoIndexNode imported from pages/lib/doc-page-props.ts
84
71
 
85
- interface DocPageProps {
86
- /** The docs entry to render, or null for auto-index pages. */
87
- entry: DocPageEntry | null;
88
- /** Pre-built auto-index node (categories without index.mdx). */
89
- autoIndex?: AutoIndexNode;
90
- /** Breadcrumb trail, first item is home. */
91
- breadcrumbs: BreadcrumbItem[];
92
- /** Preceding page in the nav tree. */
93
- prev: NavNode | null;
94
- /** Following page in the nav tree. */
95
- next: NavNode | null;
96
- /** Depth-2/3/4 headings extracted from the MDX body, for SSG TOC links. */
97
- headings: ReturnType<typeof extractHeadings>;
98
- }
72
+ type DocPageProps = DocPageEntryProps | DocPageAutoIndexProps;
99
73
 
100
74
  // ---------------------------------------------------------------------------
101
75
  // paths() — synchronous route enumeration (ADR-004)
@@ -113,13 +87,13 @@ export function paths(): Array<{
113
87
  props: DocPageProps;
114
88
  }> {
115
89
  const locale = defaultLocale;
116
- const allDocs = (bridgeEntries(getCollection("docs"), "docs") as unknown as DocPageEntry[]);
117
- // In static builds, always exclude drafts.
118
- const docs = allDocs.filter((doc) => !doc.data.draft);
119
- const categoryMeta = loadCategoryMeta(settings.docsDir);
90
+ // Identity-stable nav source (draft-filtered, unlisted retained). The same
91
+ // instances are returned across this route's many per-page paths()
92
+ // invocations, so buildNavTree's identity fast-path skips the key
93
+ // recomputation see pages/lib/_nav-source-docs.ts (#1902).
94
+ const { docs, navDocs, categoryMeta } = resolveNavSource(locale, undefined);
120
95
 
121
96
  // Nav docs: exclude unlisted (for sidebar/prev-next) but keep for breadcrumbs
122
- const navDocs = docs.filter(isNavVisible);
123
97
  const tree = buildNavTree(navDocs as unknown as DocsEntry[], locale, categoryMeta);
124
98
  // Full tree (including unlisted) for accurate breadcrumbs
125
99
  const fullTree = buildNavTree(docs as unknown as DocsEntry[], locale, categoryMeta);
@@ -156,8 +130,9 @@ export function paths(): Array<{
156
130
  }
157
131
 
158
132
  result.push({
159
- params: { slug: slug.split("/") },
133
+ params: { slug: toSlugParams(slug) },
160
134
  props: {
135
+ kind: "entry",
161
136
  entry,
162
137
  breadcrumbs: buildBreadcrumbs(fullTree, slug, locale),
163
138
  prev: prevNode,
@@ -170,9 +145,9 @@ export function paths(): Array<{
170
145
  // Auto-generated index pages for categories without index.mdx
171
146
  for (const node of collectAutoIndexNodes(tree)) {
172
147
  result.push({
173
- params: { slug: node.slug.split("/") },
148
+ params: { slug: toSlugParams(node.slug) },
174
149
  props: {
175
- entry: null,
150
+ kind: "autoIndex",
176
151
  autoIndex: node as AutoIndexNode,
177
152
  breadcrumbs: buildBreadcrumbs(fullTree, node.slug, locale),
178
153
  prev: null,
@@ -189,33 +164,26 @@ export function paths(): Array<{
189
164
  // Page component
190
165
  // ---------------------------------------------------------------------------
191
166
 
192
- interface PageArgs {
193
- params: { slug: string[] };
194
- entry: DocPageProps["entry"];
195
- autoIndex?: DocPageProps["autoIndex"];
196
- breadcrumbs: DocPageProps["breadcrumbs"];
197
- prev: DocPageProps["prev"];
198
- next: DocPageProps["next"];
199
- headings: DocPageProps["headings"];
200
- }
167
+ type PageArgs = DocPageProps & { params: { slug: string[] } };
201
168
 
202
- export default function DocsPage({ entry, autoIndex, breadcrumbs, prev, next, headings }: PageArgs): JSX.Element {
169
+ export default function DocsPage(props: PageArgs): JSX.Element {
170
+ const { breadcrumbs, prev, next, headings } = props;
203
171
  const locale = defaultLocale;
204
172
 
205
- const slug = autoIndex
206
- ? autoIndex.slug
207
- : (entry!.data.slug ?? toRouteSlug(entry!.slug));
173
+ const slug = props.kind === "autoIndex"
174
+ ? props.autoIndex.slug
175
+ : (props.entry.data.slug ?? toRouteSlug(props.entry.slug));
208
176
 
209
- const title = autoIndex ? autoIndex.label : entry!.data.title;
210
- const description = autoIndex ? autoIndex.description : entry!.data.description;
177
+ const title = props.kind === "autoIndex" ? props.autoIndex.label : props.entry.data.title;
178
+ const description = props.kind === "autoIndex" ? props.autoIndex.description : props.entry.data.description;
211
179
 
212
180
  // Locale-aware components bag — creates nav wrappers bound to the active
213
181
  // locale so CategoryNav/CategoryTreeNav/SiteTreeNav query the right collection.
214
182
  const components = createMdxComponents(locale);
215
183
 
216
184
  // Resolve child hrefs for auto-index pages
217
- const autoIndexChildren = autoIndex
218
- ? autoIndex.children
185
+ const autoIndexChildren = props.kind === "autoIndex"
186
+ ? props.autoIndex.children
219
187
  .filter((c: NavNode) => c.hasPage || c.children.length > 0)
220
188
  .map((c: NavNode) => ({
221
189
  ...c,
@@ -235,7 +203,7 @@ export default function DocsPage({ entry, autoIndex, breadcrumbs, prev, next, he
235
203
  // both lang (BCP-47 locale string) and navSection (filesystem-derived
236
204
  // kebab-case slug) come from controlled, trusted sources.
237
205
  const navSection = getNavSectionForSlug(slug);
238
- const hideSidebar = entry?.data?.hide_sidebar;
206
+ const hideSidebar = props.kind === "entry" ? props.entry.data.hide_sidebar : undefined;
239
207
  const sidebarPersistKey = hideSidebar
240
208
  ? undefined
241
209
  : `sidebar-${locale}-${navSection ?? "default"}`;
@@ -248,7 +216,7 @@ export default function DocsPage({ entry, autoIndex, breadcrumbs, prev, next, he
248
216
  lang={locale}
249
217
  noindex={settings.noindex}
250
218
  hideSidebar={hideSidebar}
251
- hideToc={entry?.data?.hide_toc}
219
+ hideToc={props.kind === "entry" ? props.entry.data.hide_toc : undefined}
252
220
  headings={headings}
253
221
  canonical={canonical}
254
222
  sidebarPersistKey={sidebarPersistKey}
@@ -276,36 +244,11 @@ export default function DocsPage({ entry, autoIndex, breadcrumbs, prev, next, he
276
244
  currentPath={docsUrl(slug, locale)}
277
245
  />
278
246
  }
279
- afterSidebar={
280
- // Pre-paint inline script: restore persisted sidebar visibility to
281
- // <html data-sidebar-hidden> before first paint to avoid flash.
282
- // Runs unconditionally when sidebarToggle is enabled; the attribute
283
- // is only set when localStorage says "false" so the default (visible)
284
- // needs no attribute and causes no layout shift.
285
- settings.sidebarToggle ? (
286
- <>
287
- <script dangerouslySetInnerHTML={{
288
- __html: `(function(){try{if(localStorage.getItem('zudo-doc-sidebar-visible')==='false'){document.documentElement.setAttribute('data-sidebar-hidden','');}}catch(e){}})();`,
289
- }} />
290
- {Island({
291
- when: "load",
292
- children: <DesktopSidebarToggle />,
293
- }) as unknown as VNode}
294
- </>
295
- ) : undefined
296
- }
247
+ afterSidebar={<SidebarPrepaint />}
297
248
  footerOverride={<FooterWithDefaults lang={locale} />}
298
- bodyEndComponents={
299
- <>
300
- <BodyEndIslands basePath={settings.base ?? "/"} />
301
- {/* SidebarResizerInit: attach drag handle to #desktop-sidebar on load
302
- and on AFTER_NAVIGATE_EVENT (zfb:after-swap under the Strategy B
303
- SPA navigation model). Idempotent — safe on every page. */}
304
- {settings.sidebarResizer && <SidebarResizerInit />}
305
- </>
306
- }
249
+ bodyEndComponents={<DocBodyEnd />}
307
250
  >
308
- {autoIndex ? (
251
+ {props.kind === "autoIndex" ? (
309
252
  /* Auto-index page: category without an index.mdx.
310
253
  Fragment (not <div>) so children become direct children of
311
254
  <article class="zd-content">, picking up the flow-space rule
@@ -313,7 +256,7 @@ export default function DocsPage({ entry, autoIndex, breadcrumbs, prev, next, he
313
256
  Wrapping in <div> would make h1/description p children-of-children
314
257
  and the flow gap (~24px) would never apply — see #1460. */
315
258
  <>
316
- <h1 class="text-heading font-bold mb-vsp-xs">{autoIndex.label}</h1>
259
+ <h1 class="text-heading font-bold mb-vsp-xs">{props.autoIndex.label}</h1>
317
260
 
318
261
  {/* Build-time date block — chrome parity (#1461). Auto-index pages
319
262
  previously rendered without doc-meta; reference site shows it on
@@ -321,9 +264,9 @@ export default function DocsPage({ entry, autoIndex, breadcrumbs, prev, next, he
321
264
  entry exists for this slug. */}
322
265
  <DocMetainfoArea slug={slug} locale={locale} />
323
266
 
324
- {autoIndex.description && (
267
+ {props.autoIndex.description && (
325
268
  <p class="mb-vsp-lg text-title text-muted">
326
- {autoIndex.description}
269
+ {props.autoIndex.description}
327
270
  </p>
328
271
  )}
329
272
  <NavCardGrid children={autoIndexChildren} />
@@ -332,112 +275,24 @@ export default function DocsPage({ entry, autoIndex, breadcrumbs, prev, next, he
332
275
  /* Regular doc page. Fragment (not <div>) for the same reason as
333
276
  the auto-index branch above — see #1460. */
334
277
  <>
335
- <h1 class="text-heading font-bold mb-vsp-xs">{entry!.data.title}</h1>
336
-
337
- {/* Build-time date block (Created / Updated / Author).
338
- doc-metainfo placement — between <h1> and description.
339
- Data from `.zfb/doc-history-meta.json` (esbuild-inlined, no fs). */}
340
- <DocMetainfoArea slug={slug} locale={locale} />
341
-
342
- {/* Page-level tag chips — matching doc-tags placement (#1658). */}
343
- <DocTagsArea slug={slug} locale={locale} tags={entry!.data.tags} />
344
-
345
- {entry!.data.description && (
346
- <p class="mb-vsp-lg text-title text-muted">
347
- {entry!.data.description}
348
- </p>
349
- )}
350
-
351
- {/* Frontmatter preview — non-system, custom keys only. Returns
352
- null when the entries array is empty, so pages without
353
- custom frontmatter emit nothing. Custom per-key renderers
354
- from frontmatter-preview-renderers.tsx produce styled cells
355
- (pills, badges, etc.) instead of plain text. */}
356
- <FrontmatterPreview
357
- entries={buildFrontmatterPreviewEntries(entry!.data)}
358
- title={t("frontmatter.preview.title", locale)}
359
- keyColLabel={t("frontmatter.preview.keyCol", locale)}
360
- valueColLabel={t("frontmatter.preview.valueCol", locale)}
361
- renderers={frontmatterRenderers}
362
- data={entry!.data as Record<string, unknown>}
363
- locale={locale}
364
- />
278
+ <DocContentHeader entry={props.entry} slug={slug} locale={locale} />
365
279
 
366
280
  {/* MDX content rendered via zfb's Content bridge */}
367
- {entry && <entry.Content components={components} />}
281
+ <props.entry.Content components={components} />
368
282
 
369
283
  {/* Prev / Next pagination — placed before the document utilities
370
284
  section to match the Astro reference order: content → pager →
371
285
  view-source / history. In the Astro layout, BodyFootUtilArea was
372
286
  rendered by the doc-layout wrapper after the <slot /> content,
373
287
  so the pager (inside the slot) came first. Fixes #1535. */}
374
- <nav class="mt-vsp-2xl grid grid-cols-2 gap-hsp-xl">
375
- {prev ? (
376
- <a
377
- href={prev.href}
378
- class="group border border-muted rounded-lg p-hsp-lg hover:border-accent"
379
- >
380
- <div class="flex items-center gap-hsp-xs text-caption text-muted mb-vsp-2xs">
381
- <svg
382
- xmlns="http://www.w3.org/2000/svg"
383
- class="h-[1.125rem] w-[1.125rem]"
384
- fill="none"
385
- viewBox="0 0 24 24"
386
- stroke="currentColor"
387
- stroke-width="2"
388
- >
389
- <path
390
- stroke-linecap="round"
391
- stroke-linejoin="round"
392
- d="M15 19l-7-7 7-7"
393
- />
394
- </svg>
395
- <span class="no-underline">{t("nav.previous", locale)}</span>
396
- </div>
397
- <p class="text-small font-semibold underline group-hover:text-accent">
398
- {prev.label}
399
- </p>
400
- </a>
401
- ) : (
402
- <div />
403
- )}
404
- {next ? (
405
- <a
406
- href={next.href}
407
- class="group border border-muted rounded-lg p-hsp-lg hover:border-accent text-right"
408
- >
409
- <div class="flex items-center justify-end gap-hsp-xs text-caption text-muted mb-vsp-2xs">
410
- <span class="no-underline">{t("nav.next", locale)}</span>
411
- <svg
412
- xmlns="http://www.w3.org/2000/svg"
413
- class="h-[1.125rem] w-[1.125rem]"
414
- fill="none"
415
- viewBox="0 0 24 24"
416
- stroke="currentColor"
417
- stroke-width="2"
418
- >
419
- <path
420
- stroke-linecap="round"
421
- stroke-linejoin="round"
422
- d="M9 5l7 7-7 7"
423
- />
424
- </svg>
425
- </div>
426
- <p class="text-small font-semibold underline group-hover:text-accent">
427
- {next.label}
428
- </p>
429
- </a>
430
- ) : (
431
- <div />
432
- )}
433
- </nav>
288
+ <DocPager prev={prev} next={next} locale={locale} />
434
289
 
435
290
  {/* Document utilities (revision history + view-source link) — skipped for unlisted pages */}
436
- {!entry!.data.unlisted && (
291
+ {!props.entry.data.unlisted && (
437
292
  <DocHistoryArea
438
293
  slug={slug}
439
294
  locale={locale}
440
- entrySlug={entry!.slug}
295
+ entrySlug={props.entry.slug}
441
296
  contentDir={settings.docsDir}
442
297
  />
443
298
  )}
@@ -12,16 +12,14 @@
12
12
  // → collectTags() counts unique tags for the tag section header
13
13
  // → DocLayoutWithDefaults renders the page with no sidebar/TOC
14
14
 
15
- import { loadDocs } from "./_data";
16
15
  import { settings } from "@/config/settings";
17
16
  import { defaultLocale, t } from "@/config/i18n";
18
17
  import { withBase } from "@/utils/base";
19
18
  import {
20
19
  buildNavTree,
21
20
  groupSatelliteNodes,
22
- isNavVisible,
23
21
  } from "@/utils/docs";
24
- import { loadCategoryMeta } from "@/utils/docs";
22
+ import { resolveNavSource } from "./lib/_nav-source-docs";
25
23
  import { getCategoryOrder } from "@/utils/nav-scope";
26
24
  import { collectTags } from "@/utils/tags";
27
25
  import { toRouteSlug } from "@/utils/slug";
@@ -41,20 +39,15 @@ export const frontmatter = { title: "Home" };
41
39
  export default function IndexPage(): JSX.Element {
42
40
  const locale = defaultLocale;
43
41
 
44
- // `loadDocs` bridges zfb's CollectionEntry Astro-style DocsEntry
45
- // (adds `id`/`collection`) so `@/utils/docs` helpers see the shape
46
- // they expect.
47
- const allDocs = loadDocs("docs");
48
- const docs = allDocs.filter((doc) => !doc.data.draft);
49
- const categoryMeta = loadCategoryMeta(settings.docsDir);
50
- const navDocs = docs.filter(isNavVisible);
42
+ // Identity-stable nav source (draft-filtered, unlisted retained). navDocs is
43
+ // pre-filtered (isNavVisible) and shared with the nav-tree fast-path.
44
+ const { navDocs, categoryMeta } = resolveNavSource(locale, undefined);
51
45
  const tree = buildNavTree(navDocs, locale, categoryMeta);
52
46
  const categoryOrder = getCategoryOrder();
53
47
  const groupedTree = groupSatelliteNodes(tree, categoryOrder);
54
48
 
55
- const tagDocs = docs.filter(isNavVisible);
56
49
  const tagCount = collectTags(
57
- tagDocs,
50
+ navDocs,
58
51
  (id, data) => data.slug ?? toRouteSlug(id),
59
52
  ).size;
60
53