create-zudo-doc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/create-zudo-doc.js +2 -0
  4. package/dist/api.d.ts +20 -0
  5. package/dist/api.js +13 -0
  6. package/dist/claude-md-gen.d.ts +2 -0
  7. package/dist/claude-md-gen.js +113 -0
  8. package/dist/cli.d.ts +39 -0
  9. package/dist/cli.js +157 -0
  10. package/dist/compose.d.ts +95 -0
  11. package/dist/compose.js +206 -0
  12. package/dist/constants.d.ts +20 -0
  13. package/dist/constants.js +224 -0
  14. package/dist/features/body-foot-util.d.ts +10 -0
  15. package/dist/features/body-foot-util.js +12 -0
  16. package/dist/features/claude-resources.d.ts +2 -0
  17. package/dist/features/claude-resources.js +6 -0
  18. package/dist/features/design-token-panel.d.ts +14 -0
  19. package/dist/features/design-token-panel.js +27 -0
  20. package/dist/features/doc-history.d.ts +9 -0
  21. package/dist/features/doc-history.js +11 -0
  22. package/dist/features/doc-tags.d.ts +19 -0
  23. package/dist/features/doc-tags.js +33 -0
  24. package/dist/features/footer-taglist.d.ts +14 -0
  25. package/dist/features/footer-taglist.js +17 -0
  26. package/dist/features/footer.d.ts +8 -0
  27. package/dist/features/footer.js +10 -0
  28. package/dist/features/i18n.d.ts +22 -0
  29. package/dist/features/i18n.js +41 -0
  30. package/dist/features/image-enlarge.d.ts +11 -0
  31. package/dist/features/image-enlarge.js +13 -0
  32. package/dist/features/index.d.ts +15 -0
  33. package/dist/features/index.js +53 -0
  34. package/dist/features/llms-txt.d.ts +11 -0
  35. package/dist/features/llms-txt.js +13 -0
  36. package/dist/features/search.d.ts +9 -0
  37. package/dist/features/search.js +11 -0
  38. package/dist/features/sidebar-resizer.d.ts +14 -0
  39. package/dist/features/sidebar-resizer.js +16 -0
  40. package/dist/features/sidebar-toggle.d.ts +13 -0
  41. package/dist/features/sidebar-toggle.js +15 -0
  42. package/dist/features/tag-governance.d.ts +14 -0
  43. package/dist/features/tag-governance.js +16 -0
  44. package/dist/features/tauri-dev.d.ts +2 -0
  45. package/dist/features/tauri-dev.js +25 -0
  46. package/dist/features/tauri.d.ts +11 -0
  47. package/dist/features/tauri.js +52 -0
  48. package/dist/features/versioning.d.ts +27 -0
  49. package/dist/features/versioning.js +43 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +150 -0
  52. package/dist/preset.d.ts +37 -0
  53. package/dist/preset.js +156 -0
  54. package/dist/prompts.d.ts +32 -0
  55. package/dist/prompts.js +248 -0
  56. package/dist/scaffold.d.ts +4 -0
  57. package/dist/scaffold.js +344 -0
  58. package/dist/settings-gen.d.ts +2 -0
  59. package/dist/settings-gen.js +237 -0
  60. package/dist/utils.d.ts +8 -0
  61. package/dist/utils.js +34 -0
  62. package/dist/zfb-config-gen.d.ts +19 -0
  63. package/dist/zfb-config-gen.js +222 -0
  64. package/package.json +65 -0
  65. package/templates/base/.htmlvalidate.json +5 -0
  66. package/templates/base/.zfb/doc-history-meta.json +1 -0
  67. package/templates/base/pages/404.tsx +55 -0
  68. package/templates/base/pages/_data.ts +179 -0
  69. package/templates/base/pages/_mdx-components.ts +249 -0
  70. package/templates/base/pages/docs/[...slug].tsx +448 -0
  71. package/templates/base/pages/index.tsx +158 -0
  72. package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
  73. package/templates/base/pages/lib/_category-nav.tsx +148 -0
  74. package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
  75. package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
  76. package/templates/base/pages/lib/_details.tsx +30 -0
  77. package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
  78. package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
  79. package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
  80. package/templates/base/pages/lib/_extract-headings.ts +81 -0
  81. package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
  82. package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
  83. package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
  84. package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
  85. package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
  86. package/templates/base/pages/lib/_math-block.tsx +63 -0
  87. package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
  88. package/templates/base/pages/lib/_preset-generator.tsx +81 -0
  89. package/templates/base/pages/lib/_search-widget-script.ts +388 -0
  90. package/templates/base/pages/lib/_search-widget.tsx +196 -0
  91. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
  92. package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
  93. package/templates/base/pages/lib/locale-merge.ts +58 -0
  94. package/templates/base/pages/lib/route-enumerators.ts +302 -0
  95. package/templates/base/pages/sitemap.xml.tsx +51 -0
  96. package/templates/base/plugins/connect-adapter.mjs +144 -0
  97. package/templates/base/plugins/copy-public-plugin.mjs +50 -0
  98. package/templates/base/plugins/search-index-plugin.mjs +54 -0
  99. package/templates/base/scripts/run-b4push.sh +102 -0
  100. package/templates/base/src/components/ai-chat-modal.tsx +15 -0
  101. package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
  102. package/templates/base/src/components/content/component-map.ts +25 -0
  103. package/templates/base/src/components/content/content-blockquote.tsx +16 -0
  104. package/templates/base/src/components/content/content-code.tsx +117 -0
  105. package/templates/base/src/components/content/content-link.tsx +83 -0
  106. package/templates/base/src/components/content/content-ol.tsx +19 -0
  107. package/templates/base/src/components/content/content-paragraph.tsx +10 -0
  108. package/templates/base/src/components/content/content-strong.tsx +16 -0
  109. package/templates/base/src/components/content/content-table.tsx +18 -0
  110. package/templates/base/src/components/content/content-ul.tsx +18 -0
  111. package/templates/base/src/components/content/heading-h2.tsx +26 -0
  112. package/templates/base/src/components/content/heading-h3.tsx +26 -0
  113. package/templates/base/src/components/content/heading-h4.tsx +26 -0
  114. package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
  115. package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
  116. package/templates/base/src/components/doc-history.tsx +18 -0
  117. package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
  118. package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
  119. package/templates/base/src/components/html-preview/preflight.ts +112 -0
  120. package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
  121. package/templates/base/src/components/image-enlarge.tsx +19 -0
  122. package/templates/base/src/components/mobile-toc.tsx +94 -0
  123. package/templates/base/src/components/preset-generator.tsx +14 -0
  124. package/templates/base/src/components/sidebar-toggle.tsx +98 -0
  125. package/templates/base/src/components/sidebar-tree.tsx +543 -0
  126. package/templates/base/src/components/site-tree-nav.tsx +233 -0
  127. package/templates/base/src/components/theme-toggle.tsx +93 -0
  128. package/templates/base/src/components/toc.tsx +63 -0
  129. package/templates/base/src/components/tree-nav-shared.tsx +71 -0
  130. package/templates/base/src/config/color-scheme-utils.ts +182 -0
  131. package/templates/base/src/config/color-schemes.ts +128 -0
  132. package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
  133. package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
  134. package/templates/base/src/config/i18n.ts +225 -0
  135. package/templates/base/src/config/settings-types.ts +162 -0
  136. package/templates/base/src/config/sidebars.ts +66 -0
  137. package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
  138. package/templates/base/src/config/tag-vocabulary.ts +20 -0
  139. package/templates/base/src/hooks/use-active-heading.ts +133 -0
  140. package/templates/base/src/plugins/docs-source-map.ts +103 -0
  141. package/templates/base/src/plugins/hast-utils.ts +10 -0
  142. package/templates/base/src/plugins/rehype-code-title.ts +50 -0
  143. package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
  144. package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
  145. package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
  146. package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
  147. package/templates/base/src/plugins/remark-admonitions.ts +99 -0
  148. package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
  149. package/templates/base/src/plugins/url-utils.ts +4 -0
  150. package/templates/base/src/styles/global.css +1066 -0
  151. package/templates/base/src/types/docs-entry.ts +39 -0
  152. package/templates/base/src/types/heading.ts +5 -0
  153. package/templates/base/src/types/locale.ts +10 -0
  154. package/templates/base/src/utils/base.ts +139 -0
  155. package/templates/base/src/utils/content-files.ts +106 -0
  156. package/templates/base/src/utils/dedent.ts +24 -0
  157. package/templates/base/src/utils/docs.ts +335 -0
  158. package/templates/base/src/utils/git-info.ts +70 -0
  159. package/templates/base/src/utils/github.ts +19 -0
  160. package/templates/base/src/utils/header-right-items.ts +38 -0
  161. package/templates/base/src/utils/nav-scope.ts +63 -0
  162. package/templates/base/src/utils/sidebar.ts +104 -0
  163. package/templates/base/src/utils/slug.ts +10 -0
  164. package/templates/base/src/utils/smart-break.tsx +126 -0
  165. package/templates/base/src/utils/tags.ts +126 -0
  166. package/templates/base/tsconfig.json +36 -0
  167. package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
  168. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
  169. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
  170. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
  171. package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
  172. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
  173. package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
  174. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
  175. package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
  176. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
  177. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
  178. package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
  179. package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
  180. package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
  181. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
  182. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
  183. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
  184. package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
  185. package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
  186. package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
  187. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
  188. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
  189. package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
  190. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
  191. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
  192. package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
  193. package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
  194. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
  195. package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
  196. package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
  197. package/templates/features/tauri/files/src-tauri/build.rs +3 -0
  198. package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
  199. package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
  200. package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
  201. package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
  202. package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
  203. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
  204. package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
  205. package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
  206. package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
  207. package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
  208. package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
  209. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
  210. package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
  211. package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
  212. package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
@@ -0,0 +1,178 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Locale-aware DocHistory area wrapper for the zfb doc pages.
4
+ //
5
+ // Mirrors the Phase B-1 pattern used by _footer-with-defaults.tsx: this
6
+ // wrapper lives in pages/lib/ with a leading underscore so the zfb router
7
+ // skips it as a page module, while the two doc-page modules
8
+ // (docs/[...slug].tsx and [locale]/docs/[...slug].tsx) import it directly.
9
+ // It gates on settings.docHistory, resolves the correct locale prop
10
+ // (omitted for the default locale, matching the doc-history fetch-path
11
+ // branch in src/components/doc-history.tsx), and passes the assembled
12
+ // island into BodyFootUtilArea — restoring the
13
+ // `<section aria-label="Document utilities">` landmark and its Revision
14
+ // History heading in the SSG output for all zfb doc routes.
15
+ //
16
+ // Wave 8 (Path A — super-epic #1333 / child epic #1355): the doc-history
17
+ // island is now built right here using zfb's native `<Island ssrFallback>`
18
+ // API with the real DocHistory component imported from
19
+ // `@/components/doc-history`. Previously this file passed
20
+ // DocHistoryIslandProps to BodyFootUtilArea, which fed an SSR-skip
21
+ // wrapper that did not import the real component — the orphan-component
22
+ // bug that left the marker un-bundled. The host-side import here is the
23
+ // page → real-component chain zfb's island scanner walks.
24
+
25
+ import type { VNode } from "preact";
26
+ import { Island } from "@takazudo/zfb";
27
+ import { settings } from "@/config/settings";
28
+ import { defaultLocale, t } from "@/config/i18n";
29
+ import { BodyFootUtilArea } from "@takazudo/zudo-doc/body-foot-util";
30
+ import { buildGitHubSourceUrl } from "@/utils/github";
31
+ import { DocHistory } from "@/components/doc-history";
32
+ // SSR author + date metadata comes from `.zfb/doc-history-meta.json`, a
33
+ // build-time manifest emitted by `scripts/zfb-prebuild.mjs` (step 2:
34
+ // doc-history-meta) before `zfb build` runs. esbuild inlines the JSON
35
+ // statically so no Node-only `fs` code reaches the client bundle.
36
+ // The `#doc-history-meta` alias is defined in tsconfig.json and resolves
37
+ // to the absolute path of `.zfb/doc-history-meta.json` — this is needed
38
+ // because the zfb bundler builds pages from a shadow tree; relative paths
39
+ // across the shadow boundary would resolve to the wrong location.
40
+ import docHistoryMeta from "#doc-history-meta";
41
+
42
+ // Set explicit `displayName` on the named-export DocHistory so zfb's
43
+ // `captureComponentName` produces a stable marker even after the SSR
44
+ // pipeline runs the component through a function-name-rewriting layer.
45
+ // (DocHistory is `export function DocHistory(...)` — `name` is already
46
+ // "DocHistory" but the explicit assignment is a guard for production
47
+ // minification regressions, mirroring the BodyEndIslands helper.)
48
+ (DocHistory as { displayName?: string }).displayName = "DocHistory";
49
+
50
+ interface DocHistoryAreaProps {
51
+ /** Page slug, e.g. "getting-started/intro". */
52
+ slug: string;
53
+ /** Active locale string, e.g. "en", "ja". */
54
+ locale: string;
55
+ /**
56
+ * Raw zfb entry slug (relative path without extension), e.g.
57
+ * "getting-started/intro" or "getting-started/index". Appended with
58
+ * ".mdx" to form the file path passed to buildGitHubSourceUrl.
59
+ * Omit for auto-index pages (no underlying MDX file) — sourceUrl
60
+ * will be suppressed automatically.
61
+ */
62
+ entrySlug?: string;
63
+ /**
64
+ * Content directory for the active locale, e.g. "src/content/docs"
65
+ * or "src/content/docs-ja". Combined with entrySlug to build the
66
+ * view-source GitHub URL. Omit to suppress the view-source link.
67
+ */
68
+ contentDir?: string;
69
+ }
70
+
71
+ /**
72
+ * Renders the `<BodyFootUtilArea>` shell with a doc-history island when
73
+ * `settings.docHistory` is enabled. Returns null otherwise so no empty
74
+ * landmark appears on pages where history is disabled.
75
+ *
76
+ * The locale prop is forwarded to the real DocHistory component only for
77
+ * non-default locales — the history JSON server stores default-locale
78
+ * files without a locale path segment (matching the fetch-path branch in
79
+ * doc-history.tsx).
80
+ *
81
+ * When entrySlug + contentDir are both provided and settings.bodyFootUtilArea
82
+ * has viewSourceLink enabled, computes sourceUrl via buildGitHubSourceUrl and
83
+ * resolves the i18n label for the active locale — keeping the v2 component
84
+ * oblivious to project settings (host-side computation, B-8-2).
85
+ *
86
+ * The SSR fallback for the doc-history island is built from git metadata
87
+ * (author name, created/updated dates) so that static HTML contains the
88
+ * author marker before JS hydration, visible to screen readers and crawlers.
89
+ */
90
+ export function DocHistoryArea({
91
+ slug,
92
+ locale,
93
+ entrySlug,
94
+ contentDir,
95
+ }: DocHistoryAreaProps): VNode | null {
96
+ if (!settings.docHistory) return null;
97
+
98
+ // Look up the build-time manifest entry for this page. The composedSlug
99
+ // matches the key written by the prebuild step: bare slug for the default
100
+ // locale, "<localeKey>/<slug>" for non-default locales.
101
+ const composedSlug = locale === defaultLocale ? slug : `${locale}/${slug}`;
102
+ type MetaEntry = { author: string; createdDate: string; updatedDate: string };
103
+ const meta = (docHistoryMeta as Record<string, MetaEntry>)[composedSlug];
104
+
105
+ // Locale-aware labels for the SSR fallback.
106
+ const createdLabel = t("doc.created", locale);
107
+ const updatedLabel = t("doc.updated", locale);
108
+ const historyLabel = t("doc.history", locale);
109
+
110
+ // Real-component props — locale omitted for the default locale.
111
+ const docHistoryLocale = locale === defaultLocale ? undefined : locale;
112
+ const docHistoryBasePath = settings.base ?? "/";
113
+
114
+ // Build the SSR fallback with only the sr-only metadata block so the
115
+ // author marker and Created/Updated labels are present in SSG output
116
+ // before JS hydration, discoverable by screen readers and crawlers.
117
+ // The visible "History" trigger button is NOT included here — DocHistory
118
+ // renders its own trigger after hydration, and including one in the
119
+ // ssrFallback as well caused a duplicate button in the DOM because
120
+ // Preact's render() does not reliably remove static ssrFallback HTML
121
+ // before mounting the new component output (same wrapper-self-Island
122
+ // pattern fixed for Toc/Sidebar in commit 4014cdc).
123
+ const author = meta?.author;
124
+ const createdDate = meta?.createdDate;
125
+ const updatedDate = meta?.updatedDate;
126
+
127
+ const fallback: VNode = (
128
+ <div class="sr-only">
129
+ {author && <span>{author}</span>}
130
+ <span>
131
+ {createdLabel}
132
+ {createdDate ? `: ${createdDate}` : ""}
133
+ </span>
134
+ <span>
135
+ {updatedLabel}
136
+ {updatedDate ? `: ${updatedDate}` : ""}
137
+ </span>
138
+ </div>
139
+ );
140
+
141
+ // Compose the SSR-skip island with zfb's native `<Island ssrFallback>` API.
142
+ // The page → this file → real DocHistory import chain is what the scanner
143
+ // walks; the marker emitted is "DocHistory" via captureComponentName.
144
+ const docHistoryIsland = Island({
145
+ when: "idle",
146
+ ssrFallback: fallback,
147
+ children: (
148
+ <DocHistory
149
+ slug={slug}
150
+ locale={docHistoryLocale}
151
+ basePath={docHistoryBasePath}
152
+ />
153
+ ),
154
+ }) as unknown as VNode;
155
+
156
+ // Compute the view-source GitHub URL host-side so the v2 BodyFootUtilArea
157
+ // component stays oblivious to project settings. Guards mirror the legacy
158
+ // body-foot-util-area.astro: gate on bodyFootUtilArea.viewSourceLink, and
159
+ // require both entrySlug and contentDir (auto-index pages pass neither).
160
+ const utilSettings = settings.bodyFootUtilArea;
161
+ const sourceUrl =
162
+ utilSettings && utilSettings.viewSourceLink && entrySlug && contentDir
163
+ ? buildGitHubSourceUrl(contentDir, entrySlug + ".mdx")
164
+ : null;
165
+
166
+ // Resolve the i18n label host-side; pass the result so the v2 component
167
+ // stays framework-agnostic. Falls back to the EN default when locale has
168
+ // no translation (see DEFAULT_VIEW_SOURCE_LABEL in the v2 package).
169
+ const viewSourceLabel = t("doc.viewSource", locale);
170
+
171
+ return (
172
+ <BodyFootUtilArea
173
+ docHistoryIsland={docHistoryIsland}
174
+ sourceUrl={sourceUrl}
175
+ viewSourceLabel={viewSourceLabel}
176
+ />
177
+ );
178
+ }
@@ -0,0 +1,100 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Locale-aware DocMetainfo area wrapper for the zfb doc pages.
4
+ //
5
+ // Renders the visible date block (created / updated dates + author)
6
+ // between the article <h1> and the description paragraph, mirroring the
7
+ // position of the legacy `doc-metainfo.astro` in the Astro layout.
8
+ //
9
+ // Data source: `.zfb/doc-history-meta.json`, a build-time manifest
10
+ // emitted by `scripts/zfb-prebuild.mjs` before `zfb build` runs.
11
+ // esbuild inlines the JSON statically so no Node.js `fs` code reaches
12
+ // the client bundle — the same approach used by `_doc-history-area.tsx`
13
+ // (b11-2 pattern).
14
+ //
15
+ // Date formatting uses Intl.DateTimeFormat (browser-safe). We do NOT
16
+ // import `formatDate` from `src/utils/git-info.ts` because that module
17
+ // has top-level Node.js imports (`execFileSync`, `existsSync`) that
18
+ // would be dragged into the client bundle — the B-11 lesson.
19
+ //
20
+ // Labels are resolved from the project's i18n table so non-default
21
+ // locales (e.g. /ja/) get translated "作成" / "更新" strings.
22
+
23
+ import type { VNode } from "preact";
24
+ import { settings } from "@/config/settings";
25
+ import { defaultLocale, t } from "@/config/i18n";
26
+ import { DocMetainfo } from "@takazudo/zudo-doc/metainfo";
27
+ // SSR author + date metadata comes from `.zfb/doc-history-meta.json`, a
28
+ // build-time manifest emitted by `scripts/zfb-prebuild.mjs` (step 2:
29
+ // doc-history-meta) before `zfb build` runs. esbuild inlines the JSON
30
+ // statically so no Node-only `fs` code reaches the client bundle.
31
+ // The `#doc-history-meta` alias is defined in tsconfig.json and resolves
32
+ // to the absolute path of `.zfb/doc-history-meta.json` — this is needed
33
+ // because the zfb bundler builds pages from a shadow tree; relative paths
34
+ // across the shadow boundary would resolve to the wrong location.
35
+ import docHistoryMeta from "#doc-history-meta";
36
+
37
+ // BCP-47 locale tag mapping used by Intl.DateTimeFormat.
38
+ // Kept in sync with `src/utils/git-info.ts` manually; we cannot import
39
+ // that module here because it carries top-level Node.js imports
40
+ // (`execFileSync`, `existsSync`) — the B-11 lesson applies here too.
41
+ const LOCALE_TO_BCP47: Record<string, string> = {
42
+ en: "en-US",
43
+ ja: "ja-JP",
44
+ de: "de-DE",
45
+ };
46
+
47
+ /** Format an ISO date string for display, respecting the active locale. */
48
+ function formatDate(isoDate: string, locale: string): string {
49
+ const d = new Date(isoDate);
50
+ if (isNaN(d.getTime())) return isoDate;
51
+ return d.toLocaleDateString(LOCALE_TO_BCP47[locale] ?? "en-US", {
52
+ year: "numeric",
53
+ month: "short",
54
+ day: "numeric",
55
+ });
56
+ }
57
+
58
+ interface DocMetainfoAreaProps {
59
+ /** Page slug, e.g. "getting-started/intro". */
60
+ slug: string;
61
+ /** Active locale string, e.g. "en", "ja". */
62
+ locale: string;
63
+ }
64
+
65
+ /**
66
+ * Renders the visible date block (Created / Updated / Author) when
67
+ * `settings.docMetainfo` is enabled and the build-time manifest has an
68
+ * entry for the active page.
69
+ *
70
+ * Returns null when `docMetainfo` is disabled, the page is untracked
71
+ * (no manifest entry), or the manifest was generated in a shallow clone
72
+ * (`SKIP_DOC_HISTORY=1` → empty JSON).
73
+ *
74
+ * The component is intentionally server-render-only: it emits static
75
+ * HTML from build-time data and has no client JS footprint. It sits
76
+ * between `<h1>` and the description `<p>`, mirroring the legacy Astro
77
+ * `doc-metainfo.astro` placement.
78
+ */
79
+ export function DocMetainfoArea({ slug, locale }: DocMetainfoAreaProps): VNode | null {
80
+ if (!settings.docMetainfo) return null;
81
+
82
+ // Key format: bare slug for default locale, "<locale>/<slug>" for others.
83
+ // Matches the prebuild step's composedSlug logic in scripts/zfb-prebuild.mjs.
84
+ const composedSlug = locale === defaultLocale ? slug : `${locale}/${slug}`;
85
+
86
+ type MetaEntry = { author: string; createdDate: string; updatedDate: string };
87
+ const meta = (docHistoryMeta as Record<string, MetaEntry>)[composedSlug];
88
+
89
+ if (!meta) return null;
90
+
91
+ return (
92
+ <DocMetainfo
93
+ createdAt={meta.createdDate ? formatDate(meta.createdDate, locale) : null}
94
+ updatedAt={meta.updatedDate ? formatDate(meta.updatedDate, locale) : null}
95
+ author={meta.author || null}
96
+ createdLabel={t("doc.created", locale)}
97
+ updatedLabel={t("doc.updated", locale)}
98
+ />
99
+ );
100
+ }
@@ -0,0 +1,89 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Locale-aware DocTags area wrapper for the zfb doc pages.
4
+ //
5
+ // Renders the page-level tag chips (e.g. "Tags: #customization") between
6
+ // the DocMetainfo block and the description paragraph, mirroring the
7
+ // position of the legacy `doc-tags.astro` in the Astro layout.
8
+ //
9
+ // Restoration of a Astro→zfb migration regression: the DocTags component
10
+ // was correctly ported into @takazudo/zudo-doc/metainfo/doc-tags.tsx
11
+ // but no page template wired it up (#1658, closes #1508).
12
+ //
13
+ // tagHref logic: inlined from _footer-with-defaults.tsx (the `tagHref`
14
+ // helper there). Extraction was considered but would cause ripple in the
15
+ // footer file and its callers — per the spec's "no opportunistic refactor"
16
+ // rule, a local copy is used here instead.
17
+ //
18
+ // i18n: both `doc.tags` and `doc.taggedWith` are confirmed present for all
19
+ // project locales (en, ja, de) in src/config/i18n.ts — no fallback needed.
20
+
21
+ import type { VNode } from "preact";
22
+ import { settings } from "@/config/settings";
23
+ import { defaultLocale, t } from "@/config/i18n";
24
+ import { withBase } from "@/utils/base";
25
+ import { resolvePageTags } from "@/utils/tags";
26
+ import { DocTags } from "@takazudo/zudo-doc/metainfo";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Internal helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Build the base-prefixed tag detail page href for the given locale.
34
+ *
35
+ * Inlined from _footer-with-defaults.tsx `tagHref` — not extracted to avoid
36
+ * ripple (spec rule: no opportunistic refactor on tagHref extraction).
37
+ */
38
+ function tagHref(tag: string, locale: string): string {
39
+ const path =
40
+ locale === defaultLocale
41
+ ? `/docs/tags/${tag}`
42
+ : `/${locale}/docs/tags/${tag}`;
43
+ return withBase(path);
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Component
48
+ // ---------------------------------------------------------------------------
49
+
50
+ interface DocTagsAreaProps {
51
+ /** Page slug, e.g. "guides/sidebar". */
52
+ slug: string;
53
+ /** Active locale string, e.g. "en", "ja". */
54
+ locale: string;
55
+ /** Raw tag strings from the page frontmatter (entry.data.tags). */
56
+ tags: readonly string[] | undefined;
57
+ }
58
+
59
+ /**
60
+ * Renders the page-level tag chip block when `settings.docTags` is enabled
61
+ * and the page has at least one resolved (non-deprecated) tag.
62
+ *
63
+ * Returns null when `docTags` is disabled, the page has no tags, or all
64
+ * raw tags resolve to deprecated entries.
65
+ *
66
+ * Placement is "after-title" to match the legacy `doc-tags.astro` position
67
+ * (between the date block and description paragraph).
68
+ */
69
+ export function DocTagsArea({ locale, tags }: DocTagsAreaProps): VNode | null {
70
+ if (!settings.docTags) return null;
71
+
72
+ const rawTags = tags ?? [];
73
+ const canonicalTags = resolvePageTags(rawTags);
74
+ if (canonicalTags.length === 0) return null;
75
+
76
+ const resolvedTags = canonicalTags.map((tag) => ({
77
+ tag,
78
+ href: tagHref(tag, locale),
79
+ }));
80
+
81
+ return (
82
+ <DocTags
83
+ placement="after-title"
84
+ tags={resolvedTags}
85
+ tagsLabel={t("doc.tags", locale)}
86
+ taggedWithLabel={t("doc.taggedWith", locale)}
87
+ />
88
+ );
89
+ }
@@ -0,0 +1,81 @@
1
+ // pages/lib/_extract-headings.ts — extract TOC headings from a raw MDX body.
2
+ //
3
+ // Shared helper called by all four catch-all `paths()` functions so each page
4
+ // passes real heading data to `DocLayoutWithDefaults` rather than an empty
5
+ // array. The result drops directly into the `headings` prop of `Toc` /
6
+ // `MobileToc` — the shape is byte-aligned with `HeadingItem` in
7
+ // `packages/zudo-doc/src/toc/types.ts`.
8
+ //
9
+ // Algorithm:
10
+ // 1. Walk the body line-by-line looking for ATX-style markdown headings
11
+ // (`## Text`, `### Text`, `#### Text`).
12
+ // 2. Compute a GitHub-compatible slug using the same `GithubSlugger` that
13
+ // the `rehype-heading-links` plugin uses at render time, so the TOC
14
+ // anchor hrefs match the rendered heading IDs in the HTML.
15
+ // 3. Return only depth 2–4 headings — depth 1 is the page title (rendered
16
+ // separately as an <h1>); depth 5–6 are too granular for the TOC.
17
+ //
18
+ // Caveats:
19
+ // - This is a regex walk over raw text, not an AST parse. MDX JSX expressions
20
+ // or code fences that contain `##` on their own line are matched. In
21
+ // practice this is rare; Astro's `entry.render()` returned the same shape
22
+ // and relied on the same assumption (headings extracted pre-render).
23
+ // - Lines inside code fences (``` … ```) are skipped to avoid treating
24
+ // literal `## code` examples as real headings.
25
+
26
+ import GithubSlugger from "github-slugger";
27
+
28
+ export interface HeadingItem {
29
+ readonly depth: number;
30
+ readonly slug: string;
31
+ readonly text: string;
32
+ }
33
+
34
+ /**
35
+ * Extract depth-2/3/4 headings from a raw MDX/markdown body.
36
+ *
37
+ * Uses the same slugging algorithm as `rehype-heading-links` so the
38
+ * `href="#slug"` values in the TOC match the rendered heading element IDs.
39
+ *
40
+ * @param body - Raw markdown body string (frontmatter already stripped).
41
+ * @returns Array of `{ depth, slug, text }` items in document order.
42
+ */
43
+ export function extractHeadings(body: string): HeadingItem[] {
44
+ const slugger = new GithubSlugger();
45
+ const headings: HeadingItem[] = [];
46
+
47
+ // Track the opening fence string (`` ``` `` or ```` ```` ````) so we match the
48
+ // correct closing fence — Markdown allows longer fences to nest shorter ones.
49
+ let codeFenceOpener: string | null = null;
50
+ for (const line of body.split("\n")) {
51
+ // Detect code fence open/close. A fence is 3+ backticks optionally followed
52
+ // by a language specifier. The closing fence must match the opener's length.
53
+ const fenceMatch = /^(`{3,})/.exec(line);
54
+ if (fenceMatch) {
55
+ const fence = fenceMatch[1] as string;
56
+ if (codeFenceOpener === null) {
57
+ codeFenceOpener = fence;
58
+ } else if (fence.length >= codeFenceOpener.length) {
59
+ codeFenceOpener = null;
60
+ }
61
+ continue;
62
+ }
63
+ if (codeFenceOpener !== null) continue;
64
+
65
+ // Match ATX headings at depth 2, 3, or 4. Allow one or more spaces/tabs
66
+ // after the hash characters (both are valid per the CommonMark spec).
67
+ const match = /^(#{2,4})[ \t]+(.+)$/.exec(line.trim());
68
+ if (!match) continue;
69
+
70
+ const depth = (match[1] as string).length;
71
+ const raw = (match[2] as string).trim();
72
+
73
+ headings.push({
74
+ depth,
75
+ slug: slugger.slug(raw),
76
+ text: raw,
77
+ });
78
+ }
79
+
80
+ return headings;
81
+ }
@@ -0,0 +1,234 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Locale-aware Footer wrapper for the zfb doc pages.
4
+ //
5
+ // Mirrors the data-prep logic that lived in src/components/footer.astro
6
+ // (deleted in commit a4d9956) — reading settings.footer, localizing link
7
+ // hrefs and titles, and optionally collecting tag columns when taglist is
8
+ // enabled — then feeds the result into the presentational <Footer> shell
9
+ // from @takazudo/zudo-doc/footer.
10
+ //
11
+ // Callers pass a `lang` prop (the active locale string, e.g. "en", "ja").
12
+ // The component returns a fully populated <Footer> when settings.footer is
13
+ // configured, or a bare <Footer /> shell when it is not (the shell still
14
+ // emits the contentinfo ARIA landmark).
15
+ //
16
+ // Data-prep helpers used:
17
+ // settings.footer — link columns, copyright, taglist config
18
+ // isExternal / resolveHref / withBase — href normalization
19
+ // defaultLocale — determines when locale prefix is needed
20
+ // tagVocabulary — group-by ordering for grouped taglist mode
21
+ // collectTags — builds tag → { count, docs } map
22
+ // toRouteSlug — derives route slug from collection id
23
+ // loadDocs / filterDrafts — synchronous zfb collection helpers (ADR-004)
24
+
25
+ import type { VNode } from "preact";
26
+ import { settings } from "@/config/settings";
27
+ import { Footer } from "@takazudo/zudo-doc/footer";
28
+ import type { FooterLinkColumn, FooterTagColumn } from "@takazudo/zudo-doc/footer";
29
+ import { isExternal, resolveHref, withBase } from "@/utils/base";
30
+ import { defaultLocale } from "@/config/i18n";
31
+ import { tagVocabulary } from "@/config/tag-vocabulary";
32
+ import { collectTags } from "@/utils/tags";
33
+ import { toRouteSlug } from "@/utils/slug";
34
+ import { loadDocs } from "../_data";
35
+ import type { DocsEntry } from "@/types/docs-entry";
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Internal helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Prefix an internal href with the locale path for non-default locales,
43
+ * then apply the configured base prefix. External hrefs pass through.
44
+ *
45
+ * Mirrors the `localizeHref` function from the historical footer.astro.
46
+ */
47
+ function localizeHref(href: string, lang: string): string {
48
+ if (isExternal(href)) return href;
49
+ if (lang !== defaultLocale) {
50
+ const path = href.startsWith("/") ? href : `/${href}`;
51
+ return resolveHref(`/${lang}${path}`);
52
+ }
53
+ return resolveHref(href);
54
+ }
55
+
56
+ /** Build the base-prefixed tag detail page href for the given locale. */
57
+ function tagHref(tag: string, lang: string): string {
58
+ const path =
59
+ lang === defaultLocale
60
+ ? `/docs/tags/${tag}`
61
+ : `/${lang}/docs/tags/${tag}`;
62
+ return withBase(path);
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Component
67
+ // ---------------------------------------------------------------------------
68
+
69
+ interface FooterWithDefaultsProps {
70
+ /** Active locale string, e.g. "en", "ja". Defaults to defaultLocale. */
71
+ lang?: string;
72
+ }
73
+
74
+ /**
75
+ * Locale-aware Footer wrapper.
76
+ *
77
+ * Reads settings.footer and assembles the linkColumns / tagColumns /
78
+ * copyright props expected by the presentational <Footer> shell. When
79
+ * settings.footer is false, a bare <Footer /> shell is returned (the
80
+ * contentinfo ARIA landmark is still present).
81
+ */
82
+ export function FooterWithDefaults({
83
+ lang = defaultLocale,
84
+ }: FooterWithDefaultsProps): VNode {
85
+ const footer = settings.footer;
86
+
87
+ // Locale-keyed persist key: same-locale swaps preserve DOM identity;
88
+ // cross-locale swaps discard the stale footer and re-render. (#1546)
89
+ const persistKey = `footer-${lang}`;
90
+
91
+ // When footer is not configured, return the bare shell so the
92
+ // contentinfo ARIA landmark is present.
93
+ if (!footer) {
94
+ return <Footer persistKey={persistKey} />;
95
+ }
96
+
97
+ const { links, copyright, taglist } = footer;
98
+
99
+ // ── Link columns ────────────────────────────────────────────────────────
100
+
101
+ const linkColumns: FooterLinkColumn[] = links.map((column) => ({
102
+ title: (column.locales as Record<string, { title: string }> | undefined)?.[lang]?.title ?? column.title,
103
+ items: column.items.map((item) => ({
104
+ label: (item.locales as Record<string, { label: string }> | undefined)?.[lang]?.label ?? item.label,
105
+ href: localizeHref(item.href, lang),
106
+ isExternal: isExternal(item.href),
107
+ })),
108
+ }));
109
+
110
+ // ── Tag columns (optional) ───────────────────────────────────────────────
111
+
112
+ let tagColumns: FooterTagColumn[] = [];
113
+
114
+ if (taglist?.enabled) {
115
+ // Load docs synchronously (zfb ADR-004 — synchronous content snapshot).
116
+ let docs: DocsEntry[];
117
+ if (lang === defaultLocale) {
118
+ docs = loadDocs("docs").filter((d) => !d.data.draft && !d.data.unlisted);
119
+ } else {
120
+ const localeDocs = loadDocs(`docs-${lang}`).filter(
121
+ (d) => !d.data.draft && !d.data.unlisted,
122
+ );
123
+ const baseDocs = loadDocs("docs").filter(
124
+ (d) => !d.data.draft && !d.data.unlisted,
125
+ );
126
+ const localeSlugSet = new Set(
127
+ localeDocs.map((d) => d.data.slug ?? toRouteSlug(d.id)),
128
+ );
129
+ docs = [
130
+ ...localeDocs,
131
+ ...baseDocs.filter(
132
+ (d) => !localeSlugSet.has(d.data.slug ?? toRouteSlug(d.id)),
133
+ ),
134
+ ];
135
+ }
136
+
137
+ const tagMap = collectTags(
138
+ docs,
139
+ (id, data) => data.slug ?? toRouteSlug(id),
140
+ );
141
+ const allTags = [...tagMap.values()].sort((a, b) =>
142
+ a.tag.localeCompare(b.tag, lang),
143
+ );
144
+
145
+ const vocabularyActive =
146
+ Boolean(settings.tagVocabulary) && settings.tagGovernance !== "off";
147
+ const requestedGroupBy = taglist.groupBy ?? "group";
148
+ const effectiveGroupBy = vocabularyActive ? requestedGroupBy : "flat";
149
+
150
+ const localeOverrides = (taglist.locales as Record<string, { title?: string; groupTitles?: Record<string, string> }> | undefined)?.[lang];
151
+ const groupTitles: Record<string, string> = {
152
+ ...taglist.groupTitles,
153
+ ...localeOverrides?.groupTitles,
154
+ };
155
+ const flatTitle = localeOverrides?.title ?? taglist.title ?? "Tags";
156
+
157
+ if (effectiveGroupBy === "flat" || !vocabularyActive) {
158
+ if (allTags.length > 0) {
159
+ tagColumns = [
160
+ {
161
+ group: "__flat__",
162
+ title: flatTitle,
163
+ tags: allTags.map(({ tag, count }) => ({
164
+ tag,
165
+ count,
166
+ href: tagHref(tag, lang),
167
+ })),
168
+ },
169
+ ];
170
+ }
171
+ } else {
172
+ // Grouped mode: one column per vocabulary group, in declaration order.
173
+ const groupByCanonical = new Map<string, string>();
174
+ const groupOrder: string[] = [];
175
+ const seenGroups = new Set<string>();
176
+ for (const entry of tagVocabulary) {
177
+ if (!entry.group) continue;
178
+ groupByCanonical.set(entry.id, entry.group);
179
+ if (!seenGroups.has(entry.group)) {
180
+ seenGroups.add(entry.group);
181
+ groupOrder.push(entry.group);
182
+ }
183
+ }
184
+
185
+ const buckets = new Map<string, typeof allTags>();
186
+ for (const group of groupOrder) buckets.set(group, []);
187
+ const ungrouped: typeof allTags = [];
188
+
189
+ for (const info of allTags) {
190
+ const group = groupByCanonical.get(info.tag);
191
+ if (group && buckets.has(group)) {
192
+ buckets.get(group)!.push(info);
193
+ } else {
194
+ ungrouped.push(info);
195
+ }
196
+ }
197
+
198
+ tagColumns = groupOrder
199
+ .filter((g) => (buckets.get(g)?.length ?? 0) > 0)
200
+ .map((g) => ({
201
+ group: g,
202
+ title:
203
+ groupTitles[g] ??
204
+ g.charAt(0).toUpperCase() + g.slice(1),
205
+ tags: buckets.get(g)!.map(({ tag, count }) => ({
206
+ tag,
207
+ count,
208
+ href: tagHref(tag, lang),
209
+ })),
210
+ }));
211
+
212
+ if (ungrouped.length > 0) {
213
+ tagColumns.push({
214
+ group: "__flat__",
215
+ title: flatTitle,
216
+ tags: ungrouped.map(({ tag, count }) => ({
217
+ tag,
218
+ count,
219
+ href: tagHref(tag, lang),
220
+ })),
221
+ });
222
+ }
223
+ }
224
+ }
225
+
226
+ return (
227
+ <Footer
228
+ linkColumns={linkColumns}
229
+ tagColumns={tagColumns}
230
+ copyright={copyright}
231
+ persistKey={persistKey}
232
+ />
233
+ );
234
+ }