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,302 @@
1
+ // Pure URL-enumeration helpers shared by both page paths() functions and the
2
+ // sitemap. Extracting these prevents the sitemap from drifting out of sync
3
+ // with the actual routes the page modules produce.
4
+ //
5
+ // Each enumerator returns absolute paths (with settings.base prefix and
6
+ // trailing slash applied) as expected by the sitemap and page modules.
7
+ // `enumerateAllRoutes()` composes the others and returns a deduped
8
+ // Map<url, lastmod> that the sitemap renderer wraps directly.
9
+ //
10
+ // Design principles:
11
+ // - Draft pages are always excluded (never built).
12
+ // - Unlisted pages ARE included — they have real HTML files and should
13
+ // appear in the sitemap even though they're hidden from nav.
14
+ // - toRouteSlug() is applied to all entry ids so category index pages
15
+ // (e.g. "getting-started/index" → "getting-started") get correct URLs.
16
+ // - Auto-generated category index pages (categories without index.mdx) are
17
+ // emitted by building the nav tree and calling collectAutoIndexNodes.
18
+
19
+ import { loadDocs } from "../_data";
20
+ import { mergeLocaleDocs } from "./locale-merge";
21
+ import { settings } from "@/config/settings";
22
+ import { defaultLocale } from "@/config/i18n";
23
+ import type { VersionConfig } from "@/config/settings";
24
+ import type { DocsEntry } from "@/types/docs-entry";
25
+ import { docsUrl, versionedDocsUrl, withBase, isDefaultLocaleOnlyPath } from "@/utils/base";
26
+ import { collectTags } from "@/utils/tags";
27
+ import { toRouteSlug } from "@/utils/slug";
28
+ import {
29
+ buildNavTree,
30
+ loadCategoryMeta,
31
+ collectAutoIndexNodes,
32
+ isNavVisible,
33
+ } from "@/utils/docs";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // enumerateDocsRoutes
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Enumerate all doc page URLs for a locale.
41
+ *
42
+ * For the default locale: loads the "docs" collection directly.
43
+ * For non-default locales: inlines a locale-first merge — locale docs take
44
+ * priority; base EN docs fill in slugs not covered by the locale collection,
45
+ * with default-locale-only paths excluded. A nav-tree pass then adds
46
+ * auto-generated category index pages.
47
+ *
48
+ * Applies toRouteSlug so "category/index" entries become "category/" URLs.
49
+ * Returns deduplicated URL strings with base prefix and trailing slash.
50
+ */
51
+ export function enumerateDocsRoutes(locale: string): string[] {
52
+ const urls: string[] = [];
53
+
54
+ if (locale === defaultLocale) {
55
+ const allDocs = loadDocs("docs").filter((d) => !d.data.draft);
56
+ const categoryMeta = loadCategoryMeta(settings.docsDir);
57
+ const navDocs = allDocs.filter(isNavVisible);
58
+ const tree = buildNavTree(navDocs, locale, categoryMeta);
59
+
60
+ for (const doc of allDocs) {
61
+ urls.push(docsUrl(doc.data.slug ?? toRouteSlug(doc.id), locale as string));
62
+ }
63
+ for (const node of collectAutoIndexNodes(tree)) {
64
+ urls.push(docsUrl(node.slug, locale as string));
65
+ }
66
+ } else {
67
+ const localeDocs = loadDocs(`docs-${locale}`).filter((d) => !d.data.draft);
68
+ const baseDocs = loadDocs("docs").filter((d) => !d.data.draft);
69
+ const localeSlugSet = new Set(localeDocs.map((d) => d.data.slug ?? d.id));
70
+ const fallbackDocs = baseDocs.filter(
71
+ (d) => !localeSlugSet.has(d.data.slug ?? d.id) && !isDefaultLocaleOnlyPath(`/docs/${d.data.slug ?? d.id}`),
72
+ );
73
+ const allDocs = [...localeDocs, ...fallbackDocs] as DocsEntry[];
74
+
75
+ for (const doc of allDocs) {
76
+ urls.push(docsUrl(doc.data.slug ?? doc.id, locale as string));
77
+ }
78
+
79
+ const localeConfig = (
80
+ settings.locales as Record<string, { dir: string }>
81
+ )[locale];
82
+ const contentDir = localeConfig?.dir ?? settings.docsDir;
83
+ const categoryMeta = new Map([
84
+ ...loadCategoryMeta(settings.docsDir),
85
+ ...loadCategoryMeta(contentDir),
86
+ ]);
87
+
88
+ const navDocs = allDocs.filter(isNavVisible);
89
+ const tree = buildNavTree(navDocs, locale, categoryMeta);
90
+ for (const node of collectAutoIndexNodes(tree)) {
91
+ urls.push(docsUrl(node.slug, locale as string));
92
+ }
93
+ }
94
+
95
+ return [...new Set(urls)];
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // enumerateTagsRoutes
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Enumerate tag-index and per-tag URLs for a locale.
104
+ *
105
+ * Uses the same tag map as the tag pages (unlisted + draft excluded) so the
106
+ * sitemap lists exactly the same tag pages that get built.
107
+ *
108
+ * Returns:
109
+ * - /docs/tags/ (or /{locale}/docs/tags/)
110
+ * - /docs/tags/{tag}/ (or /{locale}/docs/tags/{tag}/) for each unique tag
111
+ */
112
+ export function enumerateTagsRoutes(locale: string): string[] {
113
+ if (!settings.docTags) return [];
114
+
115
+ const urls: string[] = [];
116
+
117
+ const tagsBase =
118
+ locale === defaultLocale ? "/docs/tags" : `/${locale}/docs/tags`;
119
+ urls.push(withBase(tagsBase));
120
+
121
+ // Collect tags from the same merged doc set the tag pages use.
122
+ // mergeLocaleDocs (locale-merge.ts) filters unlisted + draft — mirrors
123
+ // the tag [tag].tsx pages which do the same filter.
124
+ let docs: DocsEntry[];
125
+ if (locale === defaultLocale) {
126
+ docs = loadDocs("docs").filter((d) => !d.data.unlisted && !d.data.draft);
127
+ } else {
128
+ docs = mergeLocaleDocs(locale);
129
+ }
130
+
131
+ const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
132
+
133
+ for (const tag of tagMap.keys()) {
134
+ const tagPath =
135
+ locale === defaultLocale
136
+ ? `/docs/tags/${tag}`
137
+ : `/${locale}/docs/tags/${tag}`;
138
+ urls.push(withBase(tagPath));
139
+ }
140
+
141
+ return urls;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // enumerateVersionedRoutes
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Enumerate doc URLs for a single (version, locale) combination.
150
+ *
151
+ * For the default locale: loads `docs-v-${version.slug}`.
152
+ * For non-default locales: locale-first merge — locale-specific collection
153
+ * takes priority; base EN collection fills in pages not yet translated.
154
+ * If the locale collection doesn't exist for this version, all pages fall
155
+ * back to the EN base (matching the page module's behaviour).
156
+ *
157
+ * Returns versioned URLs like /v/{version}/docs/{slug}/ or
158
+ * /v/{version}/{locale}/docs/{slug}/.
159
+ */
160
+ export function enumerateVersionedRoutes(
161
+ version: VersionConfig,
162
+ locale: string,
163
+ ): string[] {
164
+ const urls: string[] = [];
165
+
166
+ if (locale === defaultLocale) {
167
+ const collectionName = `docs-v-${version.slug}`;
168
+ const allDocs = loadDocs(collectionName).filter((d) => !d.data.draft);
169
+ const categoryMeta = loadCategoryMeta(version.docsDir);
170
+ const navDocs = allDocs.filter(isNavVisible);
171
+ const tree = buildNavTree(navDocs, "en", categoryMeta);
172
+
173
+ for (const doc of allDocs) {
174
+ const slug = doc.data.slug ?? toRouteSlug(doc.id);
175
+ urls.push(versionedDocsUrl(slug, version.slug));
176
+ }
177
+ for (const node of collectAutoIndexNodes(tree)) {
178
+ urls.push(versionedDocsUrl(node.slug, version.slug));
179
+ }
180
+ } else {
181
+ const baseCollectionName = `docs-v-${version.slug}`;
182
+ const localeDir = (
183
+ version.locales as Record<string, { dir: string }> | undefined
184
+ )?.[locale]?.dir;
185
+ const localeCollectionName = localeDir
186
+ ? `docs-v-${version.slug}-${locale}`
187
+ : null;
188
+
189
+ const baseDocs = loadDocs(baseCollectionName).filter((d) => !d.data.draft);
190
+ const localeDocs = localeCollectionName
191
+ ? loadDocs(localeCollectionName).filter((d) => !d.data.draft)
192
+ : [];
193
+
194
+ const localeSlugSet = new Set(localeDocs.map((d) => d.data.slug ?? d.id));
195
+ const fallbackDocs = baseDocs.filter(
196
+ (d) => !localeSlugSet.has(d.data.slug ?? d.id) && !isDefaultLocaleOnlyPath(`/docs/${d.data.slug ?? d.id}`),
197
+ );
198
+ const allDocs = [...localeDocs, ...fallbackDocs] as DocsEntry[];
199
+
200
+ const baseCategoryMeta = loadCategoryMeta(version.docsDir);
201
+ const localeCategoryMeta = localeDir
202
+ ? loadCategoryMeta(localeDir)
203
+ : new Map();
204
+ const categoryMeta = new Map([...baseCategoryMeta, ...localeCategoryMeta]);
205
+
206
+ const navDocs = allDocs.filter(isNavVisible);
207
+ const tree = buildNavTree(navDocs, locale, categoryMeta);
208
+
209
+ for (const doc of allDocs) {
210
+ const slug = doc.data.slug ?? toRouteSlug(doc.id);
211
+ urls.push(versionedDocsUrl(slug, version.slug, locale as string));
212
+ }
213
+ for (const node of collectAutoIndexNodes(tree)) {
214
+ urls.push(versionedDocsUrl(node.slug, version.slug, locale as string));
215
+ }
216
+ }
217
+
218
+ return [...new Set(urls)];
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // enumerateAllRoutes
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Compose all route enumerators into a deduped Map<url, lastmod>.
227
+ *
228
+ * Covers:
229
+ * - Site root
230
+ * - Default-locale docs + tags
231
+ * - Per-locale homepages, docs, and tags
232
+ * - Versioned EN docs (for each version in settings.versions)
233
+ * - Versioned locale docs (for each locale in settings.locales)
234
+ *
235
+ * The map keys are absolute paths (with settings.base prefix + trailing
236
+ * slash). The sitemap renderer prefixes each with settings.siteUrl.
237
+ */
238
+ export function enumerateAllRoutes(): Map<string, string> {
239
+ const today = new Date().toISOString().split("T")[0];
240
+ const routes = new Map<string, string>();
241
+
242
+ function add(url: string): void {
243
+ if (!routes.has(url)) {
244
+ routes.set(url, today);
245
+ }
246
+ }
247
+
248
+ // Site root
249
+ add(withBase("/"));
250
+
251
+ // Default locale docs
252
+ for (const url of enumerateDocsRoutes(defaultLocale)) {
253
+ add(url);
254
+ }
255
+
256
+ // Default locale tags
257
+ for (const url of enumerateTagsRoutes(defaultLocale)) {
258
+ add(url);
259
+ }
260
+
261
+ // Non-default locales
262
+ for (const locale of Object.keys(settings.locales)) {
263
+ add(withBase(`/${locale}`));
264
+
265
+ for (const url of enumerateDocsRoutes(locale)) {
266
+ add(url);
267
+ }
268
+
269
+ for (const url of enumerateTagsRoutes(locale)) {
270
+ add(url);
271
+ }
272
+ }
273
+
274
+ // Versions listing pages — /docs/versions/ and /{locale}/docs/versions/.
275
+ // These static utility pages are built by pages/docs/versions.tsx and
276
+ // pages/[locale]/docs/versions.tsx whenever versioning is configured.
277
+ // They are not part of any content collection so they are added explicitly.
278
+ if (settings.versions) {
279
+ add(withBase("/docs/versions"));
280
+ for (const locale of Object.keys(settings.locales)) {
281
+ add(withBase(`/${locale}/docs/versions`));
282
+ }
283
+ }
284
+
285
+ // Versioned docs
286
+ if (settings.versions) {
287
+ for (const version of settings.versions as VersionConfig[]) {
288
+ for (const url of enumerateVersionedRoutes(version, defaultLocale)) {
289
+ add(url);
290
+ }
291
+ // Non-default locales always have versioned pages (they fall back to EN
292
+ // when a locale-specific collection is not configured).
293
+ for (const locale of Object.keys(settings.locales)) {
294
+ for (const url of enumerateVersionedRoutes(version, locale)) {
295
+ add(url);
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ return routes;
302
+ }
@@ -0,0 +1,51 @@
1
+ // Port of `src/integrations/sitemap.ts` onto zfb's pages-style filename
2
+ // convention.
3
+ //
4
+ // Filename → output extension mapping (zfb convention): the
5
+ // second-to-last `.`-separated segment of the stem becomes the output
6
+ // extension, so `sitemap.xml.tsx` builds `dist/sitemap.xml`. The
7
+ // explicit `contentType` export pins the dev-server `Content-Type`
8
+ // header to `application/xml` regardless of the filename hint.
9
+ //
10
+ // URL enumeration is delegated to `pages/lib/route-enumerators.ts` so
11
+ // the sitemap cannot drift from the actual routes the page modules build.
12
+ // Previously the sitemap walked raw collection slugs directly and missed:
13
+ // (a) tag pages (b) JA fallback URLs
14
+ // (c) versioned JA routes (d) wrong URL pattern for versioned-locale pages
15
+ // (e) emitted /index/ suffix on category pages (closes #690)
16
+
17
+ import { settings } from "@/config/settings";
18
+ import { enumerateAllRoutes } from "./lib/route-enumerators";
19
+
20
+ export const frontmatter = { title: "Sitemap" };
21
+ export const contentType = "application/xml";
22
+
23
+ function escapeXml(str: string): string {
24
+ return str
25
+ .replace(/&/g, "&amp;")
26
+ .replace(/</g, "&lt;")
27
+ .replace(/>/g, "&gt;")
28
+ .replace(/"/g, "&quot;")
29
+ .replace(/'/g, "&apos;");
30
+ }
31
+
32
+ export default function Sitemap(): string {
33
+ const routeMap = enumerateAllRoutes();
34
+ const siteUrlBase = (settings.siteUrl ?? "").replace(/\/$/, "");
35
+
36
+ const urlEntries = [...routeMap.entries()]
37
+ .sort(([a], [b]) => a.localeCompare(b))
38
+ .map(
39
+ ([url, lastmod]) => ` <url>
40
+ <loc>${escapeXml(siteUrlBase + url)}</loc>
41
+ <lastmod>${lastmod}</lastmod>
42
+ </url>`,
43
+ )
44
+ .join("\n");
45
+
46
+ return `<?xml version="1.0" encoding="UTF-8"?>
47
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
48
+ ${urlEntries}
49
+ </urlset>
50
+ `;
51
+ }
@@ -0,0 +1,144 @@
1
+ // Adapter from Connect-style middleware (`(req, res, next) => void`) to
2
+ // the request-response shape zfb's `devMiddleware` lifecycle hook
3
+ // expects (`(req: ZfbDevMiddlewareRequest) => Promise<ZfbDevMiddlewareResponse | undefined>`).
4
+ //
5
+ // zfb's plugin host runs in a separate Node subprocess and only sees a
6
+ // JSON envelope of the request — `{ method, url, headers, body? }` —
7
+ // not real Node IPC. The adapter mocks just enough of `IncomingMessage`
8
+ // and `ServerResponse` for the v2 integration middlewares (which were
9
+ // written against Node's `http` types) to think they are talking to a
10
+ // regular HTTP server, then captures the response status / headers /
11
+ // body and returns them in the shape the host expects.
12
+ //
13
+ // The adapter is shared by every plugin module under this directory so
14
+ // any future Connect-style middleware can be wired into zfb's
15
+ // devMiddleware hook without rewriting it. Lives at the host repo —
16
+ // the v2 integration package keeps its Connect-style API surface so
17
+ // non-zfb embedders (Astro, plain Vite, a unit test) continue to work.
18
+
19
+ import { Buffer } from "node:buffer";
20
+
21
+ /**
22
+ * Convert a Connect-style middleware to a zfb devMiddleware handler.
23
+ * The returned async function takes a `ZfbDevMiddlewareRequest` and
24
+ * returns either `undefined` (passthrough — zfb falls through to its
25
+ * built-in routes) or a `ZfbDevMiddlewareResponse` envelope.
26
+ *
27
+ * Behaviour:
28
+ *
29
+ * - `next()` from the middleware → resolves with `undefined`
30
+ * (passthrough).
31
+ * - `res.end(body)` → resolves with `{ status, headers, body }`.
32
+ * `status` defaults to 200 if the middleware didn't set one,
33
+ * mirroring Node's `ServerResponse` default.
34
+ * - `next(err)` or a thrown error → rejects so the host surfaces a
35
+ * 500 with the error message the same way it does for any other
36
+ * plugin throw.
37
+ * - Binary bodies (Buffer / Uint8Array) → encoded as base64 and
38
+ * flagged `bodyEncoding: "base64"` so the JSON envelope round-trip
39
+ * stays loss-less.
40
+ */
41
+ export function connectToZfbHandler(middleware) {
42
+ return (zfbReq) => {
43
+ return new Promise((resolveResponse, rejectResponse) => {
44
+ // Build a minimal `IncomingMessage` shim. Only the fields the v2
45
+ // integration middlewares actually read are populated — `method`,
46
+ // `url`, and `headers`. Body parsing is not used by any of the
47
+ // three middlewares (they're all GET routes), so we leave the
48
+ // stream surface unimplemented.
49
+ const req = {
50
+ method: zfbReq.method,
51
+ url: zfbReq.url,
52
+ headers: zfbReq.headers ?? {},
53
+ };
54
+
55
+ // Build a `ServerResponse` shim that captures status, headers,
56
+ // and body. We expose the API surface the v2 middlewares touch
57
+ // today (`statusCode`, `setHeader`, `getHeader`, `end`) — extend
58
+ // here if a future middleware needs more.
59
+ let statusCode = 200;
60
+ const headers = {};
61
+ let settled = false;
62
+
63
+ const finish = (body) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ // Lower-case header names so the host's response shape matches
67
+ // axum's expectation (`Record<string, string>` of arbitrary
68
+ // case). Last-wins on collision; with `setHeader` callers this
69
+ // shouldn't happen.
70
+ const normalisedHeaders = {};
71
+ for (const [k, v] of Object.entries(headers)) {
72
+ normalisedHeaders[k.toLowerCase()] = String(v);
73
+ }
74
+
75
+ if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
76
+ resolveResponse({
77
+ status: statusCode,
78
+ headers: normalisedHeaders,
79
+ body: Buffer.from(body).toString("base64"),
80
+ bodyEncoding: "base64",
81
+ });
82
+ return;
83
+ }
84
+ resolveResponse({
85
+ status: statusCode,
86
+ headers: normalisedHeaders,
87
+ body: body == null ? "" : String(body),
88
+ bodyEncoding: "utf8",
89
+ });
90
+ };
91
+
92
+ const res = {
93
+ get statusCode() {
94
+ return statusCode;
95
+ },
96
+ set statusCode(v) {
97
+ statusCode = v;
98
+ },
99
+ setHeader(name, value) {
100
+ headers[name] = value;
101
+ },
102
+ getHeader(name) {
103
+ // Header lookup is case-insensitive in Node's real
104
+ // ServerResponse — mirror that so middlewares that probe an
105
+ // existing header before overwriting it (`if
106
+ // (!res.getHeader("Content-Type"))`) keep working.
107
+ const lower = name.toLowerCase();
108
+ for (const [k, v] of Object.entries(headers)) {
109
+ if (k.toLowerCase() === lower) return v;
110
+ }
111
+ return undefined;
112
+ },
113
+ get headersSent() {
114
+ return settled;
115
+ },
116
+ end(body) {
117
+ finish(body);
118
+ },
119
+ };
120
+
121
+ const next = (err) => {
122
+ if (settled) return;
123
+ if (err) {
124
+ settled = true;
125
+ rejectResponse(err instanceof Error ? err : new Error(String(err)));
126
+ return;
127
+ }
128
+ // Connect's `next()` with no error means "I did not handle
129
+ // this request". Resolve with `undefined` so zfb's host
130
+ // surfaces a passthrough.
131
+ settled = true;
132
+ resolveResponse(undefined);
133
+ };
134
+
135
+ try {
136
+ middleware(req, res, next);
137
+ } catch (err) {
138
+ if (settled) return;
139
+ settled = true;
140
+ rejectResponse(err instanceof Error ? err : new Error(String(err)));
141
+ }
142
+ });
143
+ };
144
+ }
@@ -0,0 +1,50 @@
1
+ // zfb plugin module: copy-public.
2
+ //
3
+ // Workaround for upstream zfb gap — `zfb build` does not copy `public/`
4
+ // contents to `outDir`. See: zudolab/zudo-doc#1394;
5
+ // upstream issue: https://github.com/Takazudo/zudo-front-builder/issues/158
6
+ //
7
+ // postBuild — recursively copies `<projectRoot>/public/` directly into
8
+ // `<outDir>/` (FLAT, matching zfb's own dist/ convention —
9
+ // zfb emits dist/index.html, dist/assets/..., NOT
10
+ // dist/<base>/index.html). Under the Workers static assets
11
+ // deploy (base="/"), `dist/` is served at root directly by
12
+ // `wrangler deploy` — no deploy-pipeline relocation step is
13
+ // needed. The `base` option is intentionally unused here.
14
+ //
15
+ // Example: `public/img/logo.svg` becomes `dist/img/logo.svg`,
16
+ // served at `/img/logo.svg` by the Workers static asset layer.
17
+ //
18
+ // Missing or empty `public/` is treated as a no-op (no error).
19
+ //
20
+ // `options` carries `{ publicDir }` from the matching entry in
21
+ // `zfb.config.ts`. The `base` option is intentionally unused — see
22
+ // rationale above.
23
+
24
+ import { cp } from "node:fs/promises";
25
+ import { resolve } from "node:path";
26
+
27
+ export default {
28
+ name: "copy-public",
29
+
30
+ async postBuild(ctx) {
31
+ const { publicDir: publicDirOption } = ctx.options;
32
+ const publicDir = resolve(ctx.projectRoot, publicDirOption ?? "public");
33
+ const dest = ctx.outDir;
34
+
35
+ ctx.logger.info(`copying ${publicDir} → ${dest}`);
36
+
37
+ await cp(publicDir, dest, {
38
+ recursive: true,
39
+ force: true,
40
+ errorOnExist: false,
41
+ }).catch((err) => {
42
+ if (err.code === "ENOENT") {
43
+ // publicDir does not exist or is empty — treat as no-op.
44
+ ctx.logger.info("public/ not found — skipping copy");
45
+ return;
46
+ }
47
+ throw err;
48
+ });
49
+ },
50
+ };
@@ -0,0 +1,54 @@
1
+ // zfb plugin module: search-index.
2
+ //
3
+ // Wires two lifecycle hooks for the search-index integration:
4
+ //
5
+ // postBuild — invokes `emitSearchIndex` to write `dist/search-index.json`.
6
+ // There is no settings gate — the index is always emitted,
7
+ // matching the legacy Astro behaviour.
8
+ //
9
+ // devMiddleware — rebuilds the in-memory search index from disk on every
10
+ // request so authoring edits surface without a dev-server
11
+ // restart. Registered at `/search-index.json`.
12
+ //
13
+ // `options` carries `{ docsDir, locales, base }` from the matching entry
14
+ // in `zfb.config.ts`.
15
+ //
16
+ // Inline functions are not supported by zfb's plugin runtime; see the
17
+ // sibling `doc-history-plugin.mjs` for the rationale.
18
+
19
+ import { emitSearchIndex, createSearchIndexDevMiddleware } from "@takazudo/zudo-doc/integrations/search-index";
20
+ import { connectToZfbHandler } from "./connect-adapter.mjs";
21
+
22
+ export default {
23
+ name: "search-index",
24
+
25
+ postBuild(ctx) {
26
+ const { docsDir, locales, base } = ctx.options;
27
+ emitSearchIndex({
28
+ outDir: ctx.outDir,
29
+ docsDir,
30
+ locales,
31
+ base,
32
+ logger: ctx.logger,
33
+ });
34
+ },
35
+
36
+ devMiddleware(ctx) {
37
+ const middleware = createSearchIndexDevMiddleware(ctx.options);
38
+ // zfb's `register(path, handler)` matches against the FULL request
39
+ // URL (no base-stripping). For a non-root base (e.g. "/my-docs/"),
40
+ // requests arrive as `/my-docs/search-index.json`, so we register
41
+ // the full base-prefixed route. For base="/", the prefix is empty
42
+ // and the route is `/search-index.json` as expected. The v2
43
+ // middleware itself is base-tolerant (matches via
44
+ // `endsWith("/search-index.json")`), so it does not need a
45
+ // separate base-stripping pass.
46
+ const basePrefix = stripTrailingSlash(ctx.options.base ?? "");
47
+ ctx.register(`${basePrefix}/search-index.json`, connectToZfbHandler(middleware));
48
+ },
49
+ };
50
+
51
+ function stripTrailingSlash(s) {
52
+ if (typeof s !== "string" || s.length === 0) return "";
53
+ return s.endsWith("/") ? s.slice(0, -1) : s;
54
+ }