create-zudo-doc 0.2.0 → 0.2.2

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 (83) hide show
  1. package/dist/api.js +4 -1
  2. package/dist/cli.js +4 -6
  3. package/dist/compose.d.ts +2 -3
  4. package/dist/compose.js +7 -4
  5. package/dist/features/tauri.d.ts +10 -5
  6. package/dist/features/tauri.js +49 -6
  7. package/dist/preset.js +11 -0
  8. package/dist/prompts.js +2 -6
  9. package/dist/scaffold.js +15 -9
  10. package/dist/settings-gen.js +9 -6
  11. package/dist/utils.d.ts +8 -0
  12. package/dist/utils.js +25 -0
  13. package/dist/zfb-config-gen.js +11 -50
  14. package/package.json +1 -1
  15. package/templates/base/pages/_data.ts +10 -23
  16. package/templates/base/pages/docs/[[...slug]].tsx +27 -168
  17. package/templates/base/pages/lib/_body-end-islands.tsx +3 -0
  18. package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
  19. package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
  20. package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
  21. package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
  22. package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
  23. package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
  24. package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
  25. package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
  26. package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
  27. package/templates/base/pages/lib/_header-with-defaults.tsx +54 -89
  28. package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
  29. package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
  30. package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
  31. package/templates/base/pages/lib/_search-widget-script.ts +32 -9
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
  33. package/templates/base/pages/lib/locale-merge.ts +1 -1
  34. package/templates/base/pages/lib/route-enumerators.ts +11 -7
  35. package/templates/base/plugins/connect-adapter.mjs +30 -1
  36. package/templates/base/plugins/copy-public-plugin.mjs +10 -2
  37. package/templates/base/plugins/search-index-plugin.mjs +20 -8
  38. package/templates/base/src/components/ai-chat-modal.tsx +2 -0
  39. package/templates/base/src/components/doc-history.tsx +2 -0
  40. package/templates/base/src/components/image-enlarge.tsx +2 -0
  41. package/templates/base/src/components/sidebar-toggle.tsx +1 -1
  42. package/templates/base/src/components/sidebar-tree.tsx +11 -5
  43. package/templates/base/src/components/theme-toggle.tsx +18 -102
  44. package/templates/base/src/config/color-schemes.ts +4 -0
  45. package/templates/base/src/config/docs-schema.ts +94 -0
  46. package/templates/base/src/config/i18n.ts +10 -3
  47. package/templates/base/src/styles/global.css +14 -0
  48. package/templates/base/src/types/docs-entry.ts +8 -26
  49. package/templates/base/src/utils/base.ts +5 -3
  50. package/templates/base/src/utils/docs.ts +144 -169
  51. package/templates/base/zfb-shim.d.ts +167 -0
  52. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
  53. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
  54. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
  55. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
  56. package/templates/features/docHistory/files/src/components/doc-history.tsx +30 -8
  57. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
  58. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
  59. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
  60. package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
  61. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
  62. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
  63. package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
  64. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +2 -0
  65. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
  66. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
  67. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +9 -3
  68. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
  69. package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
  70. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
  71. package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
  72. package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
  73. package/templates/base/src/components/content/heading-h3.tsx +0 -20
  74. package/templates/base/src/hooks/use-active-heading.ts +0 -133
  75. package/templates/base/src/plugins/docs-source-map.ts +0 -103
  76. package/templates/base/src/plugins/hast-utils.ts +0 -10
  77. package/templates/base/src/plugins/rehype-code-title.ts +0 -50
  78. package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
  79. package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
  80. package/templates/base/src/plugins/url-utils.ts +0 -4
  81. package/templates/base/src/utils/dedent.ts +0 -24
  82. package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
  83. package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
@@ -21,33 +21,19 @@
21
21
  //
22
22
  // Locale: defaultLocale (EN). Non-default locales are handled by
23
23
  // pages/[locale]/docs/[[...slug]].tsx.
24
+ //
25
+ // Enumeration + per-entry derived data (breadcrumbs, prev/next, headings) are
26
+ // built by the shared, memoized buildDocRouteEntries (#2010); rendering by the
27
+ // shared renderDocPage. This file owns only the route's nav source and the
28
+ // param/prop shapes.
24
29
 
25
30
  import { settings } from "@/config/settings";
26
31
  import { defaultLocale } from "@/config/i18n";
27
- import { docsUrl, absoluteUrl } from "@/utils/base";
28
- import {
29
- buildNavTree,
30
- buildBreadcrumbs,
31
- collectAutoIndexNodes,
32
- type NavNode,
33
- } from "@/utils/docs";
34
- import { getNavSectionForSlug, getNavSubtree } from "@/utils/nav-scope";
35
- import { toRouteSlug, toSlugParams } from "@/utils/slug";
36
- // Shared MDX-tag → Preact-component bag. Includes htmlOverrides
37
- // (native typography), HtmlPreviewWrapper (Island), and stub bindings
38
- // for every other custom tag the MDX corpus references — see
39
- // `pages/_mdx-components.ts` for the full list and rationale.
40
- import { createMdxComponents } from "../_mdx-components";
41
- import { DocHistoryArea } from "../lib/_doc-history-area";
42
- import { DocMetainfoArea } from "../lib/_doc-metainfo-area";
43
- import { buildInlineVersionSwitcher } from "../lib/_inline-version-switcher";
44
32
  import type { JSX } from "preact";
45
33
  import { resolveNavSource } from "../lib/_nav-source-docs";
46
- import { extractHeadings } from "../lib/_extract-headings";
47
- import type { DocPageEntry, AutoIndexNode, DocPageEntryProps, DocPageAutoIndexProps } from "../lib/doc-page-props";
48
- import { DocContentHeader } from "../lib/_doc-content-header";
49
- import { DocPageShell } from "../lib/_doc-page-shell";
50
- import { resolveDocPrevNext, flattenSubtree } from "../lib/_doc-route-paths";
34
+ import type { DocPageEntryProps, DocPageAutoIndexProps } from "../lib/doc-page-props";
35
+ import { buildDocRouteEntries } from "../lib/_doc-route-entries";
36
+ import { renderDocPage } from "../lib/_doc-page-renderer";
51
37
 
52
38
  export const frontmatter = { title: "Docs" };
53
39
 
@@ -55,8 +41,6 @@ export const frontmatter = { title: "Docs" };
55
41
  // Props contract
56
42
  // ---------------------------------------------------------------------------
57
43
 
58
- // DocPageEntry, AutoIndexNode imported from pages/lib/doc-page-props.ts
59
-
60
44
  type DocPageProps = DocPageEntryProps | DocPageAutoIndexProps;
61
45
 
62
46
  // ---------------------------------------------------------------------------
@@ -67,8 +51,8 @@ type DocPageProps = DocPageEntryProps | DocPageAutoIndexProps;
67
51
  * Enumerate all doc routes for the default locale (EN).
68
52
  *
69
53
  * Synchronous per ADR-004: getCollection() resolves from the pre-loaded
70
- * ContentSnapshot. All nav-tree and breadcrumb computation is done here
71
- * so the component is a pure renderer.
54
+ * ContentSnapshot. All nav-tree and breadcrumb computation is done in the
55
+ * shared builder so the component is a pure renderer.
72
56
  */
73
57
  export function paths(): Array<{
74
58
  params: { slug: string[] };
@@ -77,66 +61,19 @@ export function paths(): Array<{
77
61
  const locale = defaultLocale;
78
62
  // Identity-stable nav source (draft-filtered, unlisted retained). The same
79
63
  // instances are returned across this route's many per-page paths()
80
- // invocations, so buildNavTree's identity fast-path skips the key
81
- // recomputation — see pages/lib/_nav-source-docs.ts (#1902).
82
- const { docs, navDocs, categoryMeta } = resolveNavSource(locale, undefined);
83
-
84
- // Nav docs: exclude unlisted (for sidebar/prev-next) but keep for breadcrumbs
85
- const tree = buildNavTree(navDocs, locale, categoryMeta);
86
- // Full tree (including unlisted) for accurate breadcrumbs
87
- const fullTree = buildNavTree(docs, locale, categoryMeta);
88
-
89
- const result: Array<{ params: { slug: string[] }; props: DocPageProps }> = [];
90
-
91
- // Regular doc pages
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;
98
- const slug = entry.data.slug ?? toRouteSlug(entry.slug);
99
- const navSection = getNavSectionForSlug(slug);
100
- const subtree = getNavSubtree(tree, navSection);
101
-
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
- );
110
-
111
- result.push({
112
- params: { slug: toSlugParams(slug) },
113
- props: {
114
- kind: "entry",
115
- entry,
116
- breadcrumbs: buildBreadcrumbs(fullTree, slug, locale),
117
- prev: prevNode,
118
- next: nextNode,
119
- headings: extractHeadings(entry.body ?? ""),
120
- },
121
- });
122
- }
123
-
124
- // Auto-generated index pages for categories without index.mdx
125
- for (const node of collectAutoIndexNodes(tree)) {
126
- result.push({
127
- params: { slug: toSlugParams(node.slug) },
128
- props: {
129
- kind: "autoIndex",
130
- autoIndex: node as AutoIndexNode,
131
- breadcrumbs: buildBreadcrumbs(fullTree, node.slug, locale),
132
- prev: null,
133
- next: null,
134
- headings: [],
135
- },
136
- });
137
- }
138
-
139
- return result;
64
+ // invocations, so both buildNavTree's identity fast-path and the
65
+ // buildDocRouteEntries memo key on them — see pages/lib/_nav-source-docs.ts
66
+ // (#1902).
67
+ const source = resolveNavSource(locale, undefined);
68
+
69
+ return buildDocRouteEntries({
70
+ source,
71
+ locale,
72
+ routeSig: `docs;${locale}`,
73
+ }).map((item) => ({
74
+ params: { slug: item.slugParams },
75
+ props: item.props,
76
+ }));
140
77
  }
141
78
 
142
79
  // ---------------------------------------------------------------------------
@@ -146,86 +83,8 @@ export function paths(): Array<{
146
83
  type PageArgs = DocPageProps & { params: { slug: string[] } };
147
84
 
148
85
  export default function DocsPage(props: PageArgs): JSX.Element {
149
- const { breadcrumbs, prev, next, headings } = props;
150
- const locale = defaultLocale;
151
-
152
- const slug = props.kind === "autoIndex"
153
- ? props.autoIndex.slug
154
- : (props.entry.data.slug ?? toRouteSlug(props.entry.slug));
155
-
156
- const title = props.kind === "autoIndex" ? props.autoIndex.label : props.entry.data.title;
157
- const description = props.kind === "autoIndex" ? props.autoIndex.description : props.entry.data.description;
158
-
159
- // Locale-aware components bag — creates nav wrappers bound to the active
160
- // locale so CategoryNav/CategoryTreeNav/SiteTreeNav query the right collection.
161
- const components = createMdxComponents(locale);
162
-
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).
165
- const autoIndexChildren = props.kind === "autoIndex"
166
- ? props.autoIndex.children
167
- .filter((c: NavNode) => c.hasPage || c.children.length > 0)
168
- .map((c: NavNode) => ({
169
- ...c,
170
- href: c.href ?? docsUrl(c.slug, locale),
171
- }))
172
- : [];
173
-
174
- // Canonical URL — base-prefixed page path, absolutized against siteUrl.
175
- const currentPath = docsUrl(slug, locale);
176
- const canonical = absoluteUrl(currentPath);
177
-
178
- // Persist key: locale + nav-section so the sidebar DOM node is reused
179
- // across same-locale + same-section navigations only. No sanitizer needed —
180
- // both lang (BCP-47 locale string) and navSection (filesystem-derived
181
- // kebab-case slug) come from controlled, trusted sources.
182
- const navSection = getNavSectionForSlug(slug);
183
- const hideSidebar = props.kind === "entry" ? props.entry.data.hide_sidebar : undefined;
184
- const sidebarPersistKey = hideSidebar
185
- ? undefined
186
- : `sidebar-${locale}-${navSection ?? "default"}`;
187
-
188
- return (
189
- <DocPageShell
190
- kind={props.kind}
191
- locale={locale}
192
- slug={slug}
193
- title={title}
194
- description={description}
195
- canonical={canonical}
196
- breadcrumbs={breadcrumbs}
197
- prev={prev}
198
- next={next}
199
- headings={headings}
200
- navSection={navSection}
201
- sidebarPersistKey={sidebarPersistKey}
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
210
- }
211
- contentHeaderSlot={
212
- props.kind === "entry" ? (
213
- <DocContentHeader entry={props.entry} slug={slug} locale={locale} />
214
- ) : undefined
215
- }
216
- contentSlot={
217
- props.kind === "entry" ? <props.entry.Content components={components} /> : undefined
218
- }
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
- />
230
- );
86
+ return renderDocPage(props, {
87
+ locale: defaultLocale,
88
+ docHistoryContentDir: settings.docsDir,
89
+ });
231
90
  }
@@ -37,6 +37,7 @@ import ClientRouterBootstrap from "@/components/client-router-bootstrap";
37
37
  import DesignTokenPanelBootstrap from "@/components/design-token-panel-bootstrap";
38
38
  import ImageEnlarge, { ImageEnlargeSsrFallback } from "@/components/image-enlarge";
39
39
  import { PageLoadingOverlay } from "@takazudo/zudo-doc/page-loading";
40
+ // @slot:body-end-islands:imports
40
41
 
41
42
  // Set explicit `displayName` on each default-exported island so zfb's
42
43
  // `captureComponentName` produces a stable marker even after the SSR
@@ -51,6 +52,7 @@ import { PageLoadingOverlay } from "@takazudo/zudo-doc/page-loading";
51
52
  (DesignTokenPanelBootstrap as { displayName?: string }).displayName =
52
53
  "DesignTokenPanelBootstrap";
53
54
  (ImageEnlarge as { displayName?: string }).displayName = "ImageEnlarge";
55
+ // @slot:body-end-islands:display-names
54
56
 
55
57
  /**
56
58
  * Default sr-only label rendered as the AiChatModal SSR fallback. This
@@ -196,6 +198,7 @@ export function BodyEndIslands({
196
198
  <h2 class="sr-only">AI Assistant</h2>
197
199
  {aiChat}
198
200
  {imageEnlarge}
201
+ {/* @slot:body-end-islands:extra-islands */}
199
202
  </>
200
203
  );
201
204
  }
@@ -33,6 +33,17 @@ interface DocContentHeaderProps {
33
33
  * Only relevant for locale-prefixed and versioned-locale routes.
34
34
  */
35
35
  isFallback?: boolean;
36
+ /**
37
+ * Version slug when rendering a versioned route (e.g. "1.0"); undefined =
38
+ * latest. On versioned pages the date block and tag chips are hidden —
39
+ * the doc-history-meta manifest is built only from the LATEST content
40
+ * dirs, so a bare versioned slug would resolve to the latest file's
41
+ * Created/Updated/Author (wrong data), and tag chips would link to latest
42
+ * tag routes that may not exist for version-only tags (404). Mirrors the
43
+ * #1916 reduced-chrome stance that already hides doc history on
44
+ * versioned pages.
45
+ */
46
+ version?: string;
36
47
  }
37
48
 
38
49
  /**
@@ -50,6 +61,7 @@ export function DocContentHeader({
50
61
  slug,
51
62
  locale,
52
63
  isFallback = false,
64
+ version,
53
65
  }: DocContentHeaderProps): JSX.Element {
54
66
  return (
55
67
  <>
@@ -57,11 +69,19 @@ export function DocContentHeader({
57
69
 
58
70
  {/* Build-time date block (Created / Updated / Author).
59
71
  doc-metainfo placement — between <h1> and description.
60
- Data from `.zfb/doc-history-meta.json` (esbuild-inlined, no fs). */}
61
- <DocMetainfoArea slug={slug} locale={locale} />
72
+ Data from `.zfb/doc-history-meta.json` (esbuild-inlined, no fs).
73
+ Hidden on versioned pages — the manifest only covers latest
74
+ content dirs, so a versioned slug would show the LATEST file's
75
+ dates (see the `version` prop doc above). */}
76
+ {!version && (
77
+ <DocMetainfoArea slug={slug} locale={locale} isFallback={isFallback} />
78
+ )}
62
79
 
63
- {/* Page-level tag chips — matching doc-tags placement (#1658). */}
64
- <DocTagsArea slug={slug} locale={locale} tags={entry.data.tags} />
80
+ {/* Page-level tag chips — matching doc-tags placement (#1658).
81
+ Hidden on versioned pages: tag routes are built from latest
82
+ frontmatter only, so a version-only tag chip would 404 (see the
83
+ `version` prop doc above). */}
84
+ {!version && <DocTagsArea slug={slug} locale={locale} tags={entry.data.tags} />}
65
85
 
66
86
  {/* Fallback notice for non-translated pages */}
67
87
  {isFallback && !entry.data.generated && (
@@ -56,7 +56,8 @@ interface DocHistoryAreaProps {
56
56
  /**
57
57
  * Raw zfb entry slug (relative path without extension), e.g.
58
58
  * "getting-started/intro" or "getting-started/index". Appended with
59
- * ".mdx" to form the file path passed to buildGitHubSourceUrl.
59
+ * the source extension from the build-time manifest (".mdx" fallback)
60
+ * to form the file path passed to buildGitHubSourceUrl.
60
61
  * Omit for auto-index pages (no underlying MDX file) — sourceUrl
61
62
  * will be suppressed automatically.
62
63
  */
@@ -130,7 +131,13 @@ export function DocHistoryArea({
130
131
  // locale, "<localeKey>/<slug>" for non-default locales.
131
132
  const composedSlug =
132
133
  effectiveHistoryLocale === defaultLocale ? historySlug : `${effectiveHistoryLocale}/${historySlug}`;
133
- type MetaEntry = { author: string; createdDate: string; updatedDate: string };
134
+ type MetaEntry = {
135
+ author: string;
136
+ createdDate: string;
137
+ updatedDate: string;
138
+ /** Source file extension (".mdx" | ".md") — optional in older manifests. */
139
+ ext?: string;
140
+ };
134
141
  const meta = (docHistoryMeta as Record<string, MetaEntry>)[composedSlug];
135
142
 
136
143
  // Locale-aware labels for the SSR fallback.
@@ -156,7 +163,11 @@ export function DocHistoryArea({
156
163
  const createdDate = meta?.createdDate;
157
164
  const updatedDate = meta?.updatedDate;
158
165
 
159
- const fallback: VNode = (
166
+ // Explicit type annotation omitted: inferred JSX return is structurally
167
+ // compatible with zfb's VNode (the ssrFallback prop target). Preact's
168
+ // VNode<{}> generic form is not directly assignable to zfb's VNode at the
169
+ // type level even though the runtime shapes are identical.
170
+ const fallback = (
160
171
  <div class="sr-only">
161
172
  {author && <span>{author}</span>}
162
173
  <span>
@@ -188,11 +199,16 @@ export function DocHistoryArea({
188
199
  // Compute the view-source GitHub URL host-side so the v2 BodyFootUtilArea
189
200
  // component stays oblivious to project settings. Gate on
190
201
  // bodyFootUtilArea.viewSourceLink, and require both entrySlug and contentDir
191
- // (auto-index pages pass neither).
202
+ // (auto-index pages pass neither). The real source extension comes from the
203
+ // build-time manifest (`ext`, written by pre-build.ts) — the content walkers
204
+ // accept both .mdx and .md, so hardcoding ".mdx" produced broken view-source
205
+ // URLs for .md pages. ".mdx" remains the fallback for entries without a
206
+ // manifest record (untracked files, SKIP_DOC_HISTORY=1, stale manifests).
192
207
  const utilSettings = settings.bodyFootUtilArea;
208
+ const sourceExt = meta?.ext ?? ".mdx";
193
209
  const sourceUrl =
194
210
  utilSettings && utilSettings.viewSourceLink && entrySlug && contentDir
195
- ? buildGitHubSourceUrl(contentDir, entrySlug + ".mdx")
211
+ ? buildGitHubSourceUrl(contentDir, entrySlug + sourceExt)
196
212
  : null;
197
213
 
198
214
  // Resolve the i18n label host-side; pass the result so the v2 component
@@ -61,6 +61,16 @@ interface DocMetainfoAreaProps {
61
61
  slug: string;
62
62
  /** Active locale string, e.g. "en", "ja". */
63
63
  locale: string;
64
+ /**
65
+ * True when this locale page falls back to the base EN collection
66
+ * (i.e. the slug has no translation for the active locale). When true,
67
+ * the manifest lookup uses defaultLocale so the visible block resolves
68
+ * the bare-slug key — the only key that exists for EN-origin files —
69
+ * matching the dropdown's `effectiveHistoryLocale` derivation in
70
+ * _doc-history-area.tsx. Display formatting (dates + labels) still uses
71
+ * the active locale so JA users see JA formatting on fallback pages.
72
+ */
73
+ isFallback?: boolean;
64
74
  }
65
75
 
66
76
  /**
@@ -76,7 +86,7 @@ interface DocMetainfoAreaProps {
76
86
  * HTML from build-time data and has no client JS footprint. It sits
77
87
  * between `<h1>` and the description `<p>` (doc-metainfo placement).
78
88
  */
79
- export function DocMetainfoArea({ slug, locale }: DocMetainfoAreaProps): VNode | null {
89
+ export function DocMetainfoArea({ slug, locale, isFallback }: DocMetainfoAreaProps): VNode | null {
80
90
  if (!settings.docMetainfo) return null;
81
91
 
82
92
  // Doc-history storage sentinel ("" -> "index"): a root index page has the
@@ -87,10 +97,20 @@ export function DocMetainfoArea({ slug, locale }: DocMetainfoAreaProps): VNode |
87
97
  // @/utils/slug `toHistorySlug` and _doc-history-area.tsx. (#1891)
88
98
  const historySlug = toHistorySlug(slug);
89
99
 
100
+ // On EN-fallback locale pages the manifest only has the bare
101
+ // (non-locale-prefixed) key — the prebuild writes locale-prefixed keys
102
+ // only for files physically present in the locale collection. Use
103
+ // defaultLocale for the data lookup when isFallback is true, mirroring
104
+ // `effectiveHistoryLocale` in _doc-history-area.tsx so the visible block
105
+ // and the dropdown agree. Display formatting keeps the active locale.
106
+ const effectiveHistoryLocale = isFallback ? defaultLocale : locale;
107
+
90
108
  // Key format: bare slug for default locale, "<locale>/<slug>" for others.
91
109
  // Matches the prebuild step's composedSlug logic (pre-build.ts).
92
110
  const composedSlug =
93
- locale === defaultLocale ? historySlug : `${locale}/${historySlug}`;
111
+ effectiveHistoryLocale === defaultLocale
112
+ ? historySlug
113
+ : `${effectiveHistoryLocale}/${historySlug}`;
94
114
 
95
115
  type MetaEntry = { author: string; createdDate: string; updatedDate: string };
96
116
  const meta = (docHistoryMeta as Record<string, MetaEntry>)[composedSlug];
@@ -0,0 +1,192 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Shared page renderer for the 4 doc catch-all routes.
4
+ //
5
+ // Extracted (#2010) from the near-identical default components of:
6
+ // pages/docs/[[...slug]].tsx
7
+ // pages/[locale]/docs/[[...slug]].tsx
8
+ // pages/v/[version]/docs/[[...slug]].tsx
9
+ // pages/v/[version]/[locale]/docs/[[...slug]].tsx
10
+ //
11
+ // Each route's default export stays a thin adapter that reads its params /
12
+ // route-specific props (locale, version, contentDir, isFallback) and
13
+ // delegates here. Route-specific behavior is parameterized:
14
+ // - `version` present → versioned chrome: versioned canonical URL, version
15
+ // banner, version-aware switcher, auto-index child hrefs kept as the
16
+ // pre-remapped versioned hrefs from paths() (#1916 #2), and doc history
17
+ // hidden until versioned history is supported (#1916 #5).
18
+ // - `version` absent → latest chrome: docsUrl canonical, child hrefs fall
19
+ // back to the nav node's own docsUrl, doc history rendered for listed
20
+ // entries via `docHistoryContentDir`.
21
+
22
+ import { settings } from "@/config/settings";
23
+ import type { VersionConfig } from "@/config/settings";
24
+ import { t, type Locale } from "@/config/i18n";
25
+ import { docsUrl, versionedDocsUrl, absoluteUrl } from "@/utils/base";
26
+ import type { NavNode } from "@/utils/docs";
27
+ import { getNavSectionForSlug } from "@/utils/nav-scope";
28
+ import { toRouteSlug } from "@/utils/slug";
29
+ import type { JSX } from "preact";
30
+ // Shared MDX-tag → Preact-component bag. Includes htmlOverrides
31
+ // (native typography), HtmlPreviewWrapper (Island), and stub bindings
32
+ // for every other custom tag the MDX corpus references — see
33
+ // `pages/_mdx-components.ts` for the full list and rationale.
34
+ import { createMdxComponents } from "../_mdx-components";
35
+ import type { DocPageBaseProps } from "./doc-page-props";
36
+ import { DocHistoryArea } from "./_doc-history-area";
37
+ import { DocMetainfoArea } from "./_doc-metainfo-area";
38
+ import { buildInlineVersionSwitcher } from "./_inline-version-switcher";
39
+ import { DocContentHeader } from "./_doc-content-header";
40
+ import { DocPageShell } from "./_doc-page-shell";
41
+
42
+ export interface RenderDocPageOptions {
43
+ /** Active locale — drives nav wrappers, labels, and URL building. */
44
+ locale: Locale;
45
+ /** Version config when rendering a versioned route; undefined = latest. */
46
+ version?: VersionConfig;
47
+ /** True when this page falls back to the base EN collection (locale
48
+ * routes). Drives the fallback notice + history-area hint. */
49
+ isFallback?: boolean;
50
+ /**
51
+ * Content directory for the doc-history view-source link (e.g. the active
52
+ * locale's dir, or the base docsDir for EN/fallback pages). Latest routes
53
+ * pass it; versioned routes omit it — doc history is hidden on versioned
54
+ * pages regardless (#1916 #5).
55
+ */
56
+ docHistoryContentDir?: string;
57
+ }
58
+
59
+ export function renderDocPage(
60
+ props: DocPageBaseProps,
61
+ opts: RenderDocPageOptions,
62
+ ): JSX.Element {
63
+ const { breadcrumbs, prev, next, headings } = props;
64
+ const { locale, version, isFallback } = opts;
65
+
66
+ const slug = props.kind === "autoIndex"
67
+ ? props.autoIndex.slug
68
+ : (props.entry.data.slug ?? toRouteSlug(props.entry.slug));
69
+
70
+ const title = props.kind === "autoIndex" ? props.autoIndex.label : props.entry.data.title;
71
+ const description = props.kind === "autoIndex" ? props.autoIndex.description : props.entry.data.description;
72
+
73
+ // Locale-aware components bag — creates nav wrappers bound to the active
74
+ // locale so CategoryNav/CategoryTreeNav/SiteTreeNav query the right collection.
75
+ const components = createMdxComponents(locale);
76
+
77
+ // Resolve child hrefs for auto-index pages. Versioned routes: child cards
78
+ // already carry versioned hrefs from paths() (#1916 #2) — just filter to
79
+ // renderable nodes. Latest routes: keep the nav node's own docsUrl href
80
+ // (fallback for a noPage parent without an href).
81
+ const autoIndexChildren = props.kind === "autoIndex"
82
+ ? version
83
+ ? props.autoIndex.children.filter((c: NavNode) => c.hasPage || c.children.length > 0)
84
+ : props.autoIndex.children
85
+ .filter((c: NavNode) => c.hasPage || c.children.length > 0)
86
+ .map((c: NavNode) => ({
87
+ ...c,
88
+ href: c.href ?? docsUrl(c.slug, locale),
89
+ }))
90
+ : [];
91
+
92
+ // Version banner: drives the `<VersionBanner>` element inside
93
+ // DocLayoutWithDefaults when `version.banner` is "unmaintained" or
94
+ // "unreleased". The banner links out to the latest version of the
95
+ // current page (slug-preserving — strips the /v/{version}/ prefix,
96
+ // keeps the /{locale}/ locale prefix).
97
+ const versionBannerType = version?.banner ? version.banner : undefined;
98
+ const versionBannerLatestUrl = versionBannerType
99
+ ? docsUrl(slug, locale)
100
+ : undefined;
101
+ const versionBannerLabels = versionBannerType
102
+ ? {
103
+ message:
104
+ versionBannerType === "unmaintained"
105
+ ? t("version.banner.unmaintained", locale)
106
+ : t("version.banner.unreleased", locale),
107
+ latestLink: t("version.banner.latestLink", locale),
108
+ }
109
+ : undefined;
110
+
111
+ // Canonical URL — base-prefixed page path, absolutized against siteUrl.
112
+ // Versioned pages use the versioned URL as canonical.
113
+ const currentPath = version
114
+ ? versionedDocsUrl(slug, version.slug, locale)
115
+ : docsUrl(slug, locale);
116
+ const canonical = absoluteUrl(currentPath);
117
+
118
+ // Persist key: locale + nav-section so the sidebar DOM node is reused
119
+ // across same-locale + same-section navigations only. No sanitizer needed —
120
+ // both lang (BCP-47 locale string) and navSection (filesystem-derived
121
+ // kebab-case slug) come from controlled, trusted sources.
122
+ const navSection = getNavSectionForSlug(slug);
123
+ const hideSidebar = props.kind === "entry" ? props.entry.data.hide_sidebar : undefined;
124
+ const sidebarPersistKey = hideSidebar
125
+ ? undefined
126
+ : `sidebar-${locale}-${navSection ?? "default"}`;
127
+
128
+ return (
129
+ <DocPageShell
130
+ kind={props.kind}
131
+ locale={locale}
132
+ slug={slug}
133
+ title={title}
134
+ description={description}
135
+ canonical={canonical}
136
+ breadcrumbs={breadcrumbs}
137
+ prev={prev}
138
+ next={next}
139
+ headings={headings}
140
+ navSection={navSection}
141
+ sidebarPersistKey={sidebarPersistKey}
142
+ hideSidebar={hideSidebar}
143
+ hideToc={props.kind === "entry" ? props.entry.data.hide_toc : undefined}
144
+ currentPath={currentPath}
145
+ currentVersion={version?.slug}
146
+ versionSwitcher={buildInlineVersionSwitcher(slug, locale, version?.slug)}
147
+ versionBanner={versionBannerType}
148
+ versionBannerLatestUrl={versionBannerLatestUrl}
149
+ versionBannerLabels={versionBannerLabels}
150
+ autoIndexLabel={props.kind === "autoIndex" ? props.autoIndex.label : undefined}
151
+ autoIndexChildren={autoIndexChildren}
152
+ metainfoSlot={
153
+ // Versioned gate mirrors DocContentHeader: the doc-history-meta
154
+ // manifest is built from latest dirs only, so a bare versioned slug
155
+ // would surface the LATEST page's Created/Updated/Author.
156
+ !version && props.kind === "autoIndex" ? (
157
+ <DocMetainfoArea slug={slug} locale={locale} isFallback={isFallback} />
158
+ ) : null
159
+ }
160
+ contentHeaderSlot={
161
+ props.kind === "entry" ? (
162
+ <DocContentHeader
163
+ entry={props.entry}
164
+ slug={slug}
165
+ locale={locale}
166
+ isFallback={isFallback}
167
+ version={version?.slug}
168
+ />
169
+ ) : undefined
170
+ }
171
+ contentSlot={
172
+ props.kind === "entry" ? <props.entry.Content components={components} /> : undefined
173
+ }
174
+ docHistorySlot={
175
+ // #1916 #5: doc-history hidden on versioned pages until versioned
176
+ // history is supported.
177
+ !version &&
178
+ opts.docHistoryContentDir !== undefined &&
179
+ props.kind === "entry" &&
180
+ !props.entry.data.unlisted ? (
181
+ <DocHistoryArea
182
+ slug={slug}
183
+ locale={locale}
184
+ entrySlug={props.entry.slug}
185
+ contentDir={opts.docHistoryContentDir}
186
+ isFallback={isFallback}
187
+ />
188
+ ) : null
189
+ }
190
+ />
191
+ );
192
+ }
@@ -32,6 +32,7 @@ import { DocBodyEnd } from "./_doc-body-end";
32
32
  import { DocPager } from "./_doc-pager";
33
33
  import type { BreadcrumbItem } from "@/utils/docs";
34
34
  import type { VersionBannerLabels } from "@takazudo/zudo-doc/i18n-version";
35
+ import type { Locale } from "@/config/i18n";
35
36
  import type { extractHeadings } from "./_extract-headings";
36
37
 
37
38
  /** Slots and parameters that vary between the 4 doc routes. */
@@ -161,7 +162,7 @@ export function DocPageShell(props: DocPageShellProps): JSX.Element {
161
162
  versionBannerLabels={versionBannerLabels}
162
163
  headerOverride={
163
164
  <HeaderWithDefaults
164
- lang={locale}
165
+ lang={locale as Locale}
165
166
  currentSlug={props.slug}
166
167
  navSection={navSection}
167
168
  currentVersion={currentVersion}
@@ -176,7 +177,7 @@ export function DocPageShell(props: DocPageShellProps): JSX.Element {
176
177
  sidebarOverride={
177
178
  <SidebarWithDefaults
178
179
  currentSlug={props.slug}
179
- lang={locale}
180
+ lang={locale as Locale}
180
181
  navSection={navSection}
181
182
  currentVersion={currentVersion}
182
183
  currentPath={currentPath}