create-zudo-doc 0.2.0 → 0.2.1

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 (72) hide show
  1. package/dist/api.js +4 -1
  2. package/dist/cli.js +4 -6
  3. package/dist/preset.js +11 -0
  4. package/dist/prompts.js +2 -6
  5. package/dist/scaffold.js +15 -9
  6. package/dist/settings-gen.js +7 -7
  7. package/dist/utils.d.ts +8 -0
  8. package/dist/utils.js +25 -0
  9. package/dist/zfb-config-gen.js +11 -50
  10. package/package.json +1 -1
  11. package/templates/base/pages/_data.ts +10 -23
  12. package/templates/base/pages/docs/[[...slug]].tsx +27 -168
  13. package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
  14. package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
  15. package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
  16. package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
  17. package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
  18. package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
  19. package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
  20. package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
  21. package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
  22. package/templates/base/pages/lib/_header-with-defaults.tsx +51 -89
  23. package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
  24. package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
  25. package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
  26. package/templates/base/pages/lib/_search-widget-script.ts +32 -9
  27. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
  28. package/templates/base/pages/lib/locale-merge.ts +1 -1
  29. package/templates/base/pages/lib/route-enumerators.ts +11 -7
  30. package/templates/base/plugins/connect-adapter.mjs +30 -1
  31. package/templates/base/plugins/copy-public-plugin.mjs +10 -2
  32. package/templates/base/plugins/search-index-plugin.mjs +20 -8
  33. package/templates/base/src/components/sidebar-toggle.tsx +1 -1
  34. package/templates/base/src/components/sidebar-tree.tsx +10 -4
  35. package/templates/base/src/config/color-schemes.ts +4 -0
  36. package/templates/base/src/config/docs-schema.ts +94 -0
  37. package/templates/base/src/config/i18n.ts +10 -3
  38. package/templates/base/src/styles/global.css +14 -0
  39. package/templates/base/src/types/docs-entry.ts +8 -26
  40. package/templates/base/src/utils/base.ts +5 -3
  41. package/templates/base/src/utils/docs.ts +144 -169
  42. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
  43. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
  44. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
  45. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
  46. package/templates/features/docHistory/files/src/components/doc-history.tsx +28 -8
  47. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
  48. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
  49. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
  50. package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
  51. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
  52. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
  53. package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
  54. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
  55. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
  56. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
  57. package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
  58. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
  59. package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
  60. package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
  61. package/templates/base/src/components/content/heading-h3.tsx +0 -20
  62. package/templates/base/src/components/theme-toggle.tsx +0 -107
  63. package/templates/base/src/hooks/use-active-heading.ts +0 -133
  64. package/templates/base/src/plugins/docs-source-map.ts +0 -103
  65. package/templates/base/src/plugins/hast-utils.ts +0 -10
  66. package/templates/base/src/plugins/rehype-code-title.ts +0 -50
  67. package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
  68. package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
  69. package/templates/base/src/plugins/url-utils.ts +0 -4
  70. package/templates/base/src/utils/dedent.ts +0 -24
  71. package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
  72. package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
@@ -2,7 +2,7 @@
2
2
  /** @jsxImportSource preact */
3
3
  // Page module for the default-locale per-tag detail route.
4
4
  //
5
- // Default-locale (en) per-tag detail page. paths() enumerates one route per
5
+ // Default-locale per-tag detail page. paths() enumerates one route per
6
6
  // unique tag in the "docs" collection and passes the resolved TagInfo as a
7
7
  // prop so the component has zero extra collection reads at render time.
8
8
  //
@@ -10,29 +10,13 @@
10
10
  // params: { tag: string }
11
11
  // props: { tagInfo: TagInfo }
12
12
  //
13
- // Data flow:
14
- // getCollection("docs") [sync] collectTags() one route per tag
15
- // render: DocCardGrid with pre-resolved TagInfo.docs items
13
+ // Tag collection + rendering are shared with the locale-prefixed route via
14
+ // pages/lib/_tag-pages.tsx (#2010) this file owns only the param shape.
16
15
 
17
- import { getCollection } from "zfb/content";
18
- import { collectTags } from "@/utils/tags";
16
+ import { defaultLocale } from "@/config/i18n";
19
17
  import type { TagInfo } from "@/utils/tags";
20
- import { toRouteSlug } from "@/utils/slug";
21
- import { t, defaultLocale } from "@/config/i18n";
22
- import { settings } from "@/config/settings";
23
- import { withBase, docsUrl } from "@/utils/base";
24
- import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
25
- import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
26
- import type { BreadcrumbItem } from "@takazudo/zudo-doc/breadcrumb";
27
- import { DocCardGrid } from "@takazudo/zudo-doc/nav-indexing";
28
18
  import type { JSX } from "preact";
29
- import { bridgeDocsEntries, type ZfbDocsData } from "../../_data";
30
- import { FooterWithDefaults } from "../../lib/_footer-with-defaults";
31
- import { HeaderWithDefaults } from "../../lib/_header-with-defaults";
32
- import { HeadWithDefaults } from "../../lib/_head-with-defaults";
33
- import { composeMetaTitle } from "../../lib/_compose-meta-title";
34
- import { DocHistoryArea } from "../../lib/_doc-history-area";
35
- import { BodyEndIslands } from "../../lib/_body-end-islands";
19
+ import { collectTagMapForLocale, TagDetailPageView } from "../../lib/_tag-pages";
36
20
 
37
21
  export const frontmatter = { title: "Tag" };
38
22
 
@@ -41,14 +25,7 @@ export function paths(): Array<{
41
25
  params: { tag: string };
42
26
  props: { tagInfo: TagInfo };
43
27
  }> {
44
- const allDocs = bridgeDocsEntries(getCollection<ZfbDocsData>("docs"), "docs");
45
- // category_no_page index.mdx builds no route — drop it so a tag it carries
46
- // doesn't render a DocCard linking to a non-existent /docs/<cat>/ page.
47
- const docs = allDocs.filter(
48
- (doc) =>
49
- !doc.data.unlisted && !doc.data.draft && !doc.data.category_no_page,
50
- );
51
- const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
28
+ const tagMap = collectTagMapForLocale(defaultLocale);
52
29
 
53
30
  return [...tagMap.entries()].map(([tag, tagInfo]) => ({
54
31
  params: { tag },
@@ -62,44 +39,5 @@ interface PageProps {
62
39
  }
63
40
 
64
41
  export default function DocTagPage({ params, tagInfo }: PageProps): JSX.Element {
65
- const { tag } = params;
66
- const locale = defaultLocale;
67
-
68
- const countText =
69
- tagInfo.count === 1
70
- ? t("doc.pageCountSingle", locale).replace("{count}", String(tagInfo.count))
71
- : t("doc.pageCount", locale).replace("{count}", String(tagInfo.count));
72
-
73
- const pageTitle = `${t("doc.taggedWith", locale)}: ${tag}`;
74
-
75
- const breadcrumbItems: BreadcrumbItem[] = [
76
- { label: "Docs" },
77
- { label: t("doc.allTags", locale), href: withBase("/docs/tags") },
78
- { label: tag },
79
- ];
80
-
81
- const cardItems = tagInfo.docs.map((doc) => ({
82
- href: docsUrl(doc.slug, locale),
83
- title: doc.title,
84
- description: doc.description,
85
- }));
86
-
87
- return (
88
- <DocLayoutWithDefaults
89
- title={composeMetaTitle(pageTitle)}
90
- head={<HeadWithDefaults title={pageTitle} />}
91
- noindex={settings.noindex}
92
- hideSidebar={true}
93
- hideToc={true}
94
- headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`/docs/tags/${tag}`)} />}
95
- breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
96
- footerOverride={<FooterWithDefaults lang={locale} />}
97
- bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
98
- >
99
- <h1 class="text-heading font-bold mb-vsp-xs">{pageTitle}</h1>
100
- <p class="text-muted mb-vsp-lg">{countText}</p>
101
- <DocCardGrid ariaLabel={pageTitle} items={cardItems} />
102
- <DocHistoryArea slug={`tags/${tag}`} locale={locale} />
103
- </DocLayoutWithDefaults>
104
- );
42
+ return <TagDetailPageView locale={defaultLocale} tag={params.tag} tagInfo={tagInfo} />;
105
43
  }
@@ -2,89 +2,19 @@
2
2
  /** @jsxImportSource preact */
3
3
  // Page module for the default-locale "All Tags" index route.
4
4
  //
5
- // Default-locale (en) "All Tags" index page. Collects every tag across the
5
+ // Default-locale "All Tags" index page. Collects every tag across the
6
6
  // "docs" collection, sorts them alphabetically, and renders a full tag cloud
7
7
  // via the v2 TagNav component. No dynamic params — single static route.
8
8
  //
9
- // Data flow:
10
- // getCollection("docs") [sync, zfb/content]
11
- // → collectTags() builds { tag → { count, docs[] } }
12
- // → sort by tag preserves sort parity with Astro original
13
- // → TagNav variant="all" renders the chip cloud
9
+ // Tag collection + rendering are shared with the locale-prefixed route via
10
+ // pages/lib/_tag-pages.tsx (#2010).
14
11
 
15
- import { getCollection } from "zfb/content";
16
- import { collectTags } from "@/utils/tags";
17
- import { toRouteSlug } from "@/utils/slug";
18
- import { t, defaultLocale } from "@/config/i18n";
19
- import { withBase } from "@/utils/base";
20
- import { settings } from "@/config/settings";
21
- import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
22
- import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
23
- import type { BreadcrumbItem } from "@takazudo/zudo-doc/breadcrumb";
24
- import { TagNav } from "@takazudo/zudo-doc/nav-indexing";
25
- import type { TagItem, TagNavLabels } from "@takazudo/zudo-doc/nav-indexing";
12
+ import { defaultLocale } from "@/config/i18n";
26
13
  import type { JSX } from "preact";
27
- import { bridgeDocsEntries, type ZfbDocsData } from "../../_data";
28
- import { FooterWithDefaults } from "../../lib/_footer-with-defaults";
29
- import { HeaderWithDefaults } from "../../lib/_header-with-defaults";
30
- import { HeadWithDefaults } from "../../lib/_head-with-defaults";
31
- import { composeMetaTitle } from "../../lib/_compose-meta-title";
32
- import { DocHistoryArea } from "../../lib/_doc-history-area";
33
- import { BodyEndIslands } from "../../lib/_body-end-islands";
14
+ import { TagsIndexPageView } from "../../lib/_tag-pages";
34
15
 
35
16
  export const frontmatter = { title: "All Tags" };
36
17
 
37
18
  export default function DocsTagsIndexPage(): JSX.Element {
38
- const locale = defaultLocale;
39
- const pageTitle = t("doc.allTags", locale);
40
-
41
- const allDocs = bridgeDocsEntries(getCollection<ZfbDocsData>("docs"), "docs");
42
- // category_no_page index.mdx builds no route — drop it so a tag it carries
43
- // doesn't inflate the tag list with a card linking to a non-existent page.
44
- const docs = allDocs.filter(
45
- (doc) =>
46
- !doc.data.unlisted && !doc.data.draft && !doc.data.category_no_page,
47
- );
48
- const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
49
-
50
- const labels: TagNavLabels = {
51
- tags: t("doc.tags", locale),
52
- taggedWith: t("doc.taggedWith", locale),
53
- };
54
-
55
- // Sort alphabetically — matches documented tag-nav sort order.
56
- const tags: TagItem[] = [...tagMap.values()]
57
- .sort((a, b) => a.tag.localeCompare(b.tag, locale))
58
- .map((info) => ({
59
- tag: info.tag,
60
- count: info.count,
61
- href: withBase(`/docs/tags/${info.tag}`),
62
- }));
63
-
64
- const breadcrumbItems: BreadcrumbItem[] = [
65
- { label: "Docs" },
66
- { label: pageTitle },
67
- ];
68
-
69
- return (
70
- <DocLayoutWithDefaults
71
- title={composeMetaTitle(pageTitle)}
72
- head={<HeadWithDefaults title={pageTitle} />}
73
- noindex={settings.noindex}
74
- hideSidebar={true}
75
- hideToc={true}
76
- headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase("/docs/tags")} />}
77
- breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
78
- footerOverride={<FooterWithDefaults lang={locale} />}
79
- bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
80
- >
81
- <h1 class="text-heading font-bold mb-vsp-lg">{pageTitle}</h1>
82
- {!settings.docTags || tags.length === 0 ? (
83
- <p class="text-muted">{t("doc.noTags", locale)}</p>
84
- ) : (
85
- <TagNav variant="all" tags={tags} labels={labels} />
86
- )}
87
- <DocHistoryArea slug="tags" locale={locale} />
88
- </DocLayoutWithDefaults>
89
- );
19
+ return <TagsIndexPageView locale={defaultLocale} />;
90
20
  }
@@ -0,0 +1,201 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Shared data + renderers for the doc-tags pages (#2010).
4
+ //
5
+ // Collapses the per-locale page pairs:
6
+ // pages/docs/tags/[tag].tsx + pages/[locale]/docs/tags/[tag].tsx
7
+ // pages/docs/tags/index.tsx + pages/[locale]/docs/tags/index.tsx
8
+ // into locale-parameterized helpers. The page files stay thin shells that own
9
+ // only their paths() param shapes; URL prefixes and the default-vs-locale
10
+ // data path branch live here.
11
+ //
12
+ // Data-path branch (kept exactly as the original pages had it):
13
+ // - Default locale: the "docs" collection directly, filtered
14
+ // unlisted/draft/category_no_page before tag collection.
15
+ // - Non-default locale: locale-first merge with base fallback
16
+ // (see locale-merge.ts) — draft-filtered inputs, unlisted dropped by the
17
+ // merge default, category_no_page filtered AFTER the merge so a locale
18
+ // override carrying the flag first wins the merge (suppressing the base
19
+ // doc); pre-merge filtering would drop it from localeSlugSet and the
20
+ // unflagged base doc would resurface as a card linking to a locale route
21
+ // the docs route never builds.
22
+
23
+ import { collectTags } from "@/utils/tags";
24
+ import type { TagInfo } from "@/utils/tags";
25
+ import { toRouteSlug } from "@/utils/slug";
26
+ import { t, defaultLocale } from "@/config/i18n";
27
+ import { settings } from "@/config/settings";
28
+ import { withBase, docsUrl } from "@/utils/base";
29
+ import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
30
+ import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
31
+ import type { BreadcrumbItem } from "@takazudo/zudo-doc/breadcrumb";
32
+ import { DocCardGrid, TagNav } from "@takazudo/zudo-doc/nav-indexing";
33
+ import type { TagItem, TagNavLabels } from "@takazudo/zudo-doc/nav-indexing";
34
+ import type { JSX } from "preact";
35
+ import { FooterWithDefaults } from "./_footer-with-defaults";
36
+ import { HeaderWithDefaults } from "./_header-with-defaults";
37
+ import { HeadWithDefaults } from "./_head-with-defaults";
38
+ import { composeMetaTitle } from "./_compose-meta-title";
39
+ import { DocHistoryArea } from "./_doc-history-area";
40
+ import { BodyEndIslands } from "./_body-end-islands";
41
+ import { stableDocs, memoizeDerived } from "./_nav-source-cache";
42
+ import { mergeLocaleDocs } from "./locale-merge";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Tag collection
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Build the { tag → TagInfo } map for one locale, preserving each original
50
+ * page's exact data path (see the module-header branch notes).
51
+ *
52
+ * Memoized on the snapshot-anchored stableDocs identity (same pattern as
53
+ * _nav-source-cache) so the collection bridging and tag aggregation run once
54
+ * per locale per build rather than once per built tag page.
55
+ */
56
+ export function collectTagMapForLocale(locale: string): Map<string, TagInfo> {
57
+ if (locale === defaultLocale) {
58
+ const baseDocs = stableDocs("docs");
59
+ return memoizeDerived([baseDocs], "tagMap;default", () => {
60
+ // category_no_page index.mdx builds no route — drop it so a tag it carries
61
+ // doesn't render a DocCard linking to a non-existent /docs/<cat>/ page.
62
+ const docs = baseDocs.filter(
63
+ (doc) =>
64
+ !doc.data.unlisted && !doc.data.draft && !doc.data.category_no_page,
65
+ );
66
+ return collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
67
+ });
68
+ }
69
+
70
+ const baseDocs = stableDocs("docs");
71
+ const localeDocs = stableDocs(`docs-${locale}`);
72
+ return memoizeDerived([baseDocs, localeDocs], `tagMap;${locale}`, () => {
73
+ const { docs: mergedDocs } = mergeLocaleDocs({
74
+ baseDocs: baseDocs.filter((d) => !d.data.draft),
75
+ localeDocs: localeDocs.filter((d) => !d.data.draft),
76
+ applyDefaultLocaleOnlyFilter: true,
77
+ });
78
+ const docs = mergedDocs.filter((d) => !d.data.category_no_page);
79
+ return collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
80
+ });
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Shared renderers
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /** URL prefix for a locale: "" for the default locale, "/{locale}" otherwise. */
88
+ function localePrefix(locale: string): string {
89
+ return locale === defaultLocale ? "" : `/${locale}`;
90
+ }
91
+
92
+ /** Per-tag detail page (chip target). */
93
+ export function TagDetailPageView({
94
+ locale,
95
+ tag,
96
+ tagInfo,
97
+ }: {
98
+ locale: string;
99
+ tag: string;
100
+ tagInfo: TagInfo;
101
+ }): JSX.Element {
102
+ const isDefault = locale === defaultLocale;
103
+ const prefix = localePrefix(locale);
104
+
105
+ const countText =
106
+ tagInfo.count === 1
107
+ ? t("doc.pageCountSingle", locale).replace("{count}", String(tagInfo.count))
108
+ : t("doc.pageCount", locale).replace("{count}", String(tagInfo.count));
109
+
110
+ const pageTitle = `${t("doc.taggedWith", locale)}: ${tag}`;
111
+
112
+ const breadcrumbItems: BreadcrumbItem[] = [
113
+ { label: "Docs" },
114
+ { label: t("doc.allTags", locale), href: withBase(`${prefix}/docs/tags`) },
115
+ { label: tag },
116
+ ];
117
+
118
+ const cardItems = tagInfo.docs.map((doc) => ({
119
+ href: docsUrl(doc.slug, locale),
120
+ title: doc.title,
121
+ description: doc.description,
122
+ }));
123
+
124
+ return (
125
+ <DocLayoutWithDefaults
126
+ title={composeMetaTitle(pageTitle)}
127
+ head={<HeadWithDefaults title={pageTitle} />}
128
+ // The original default-locale page omitted `lang` entirely; passing
129
+ // undefined relies on Preact treating an undefined prop as absent.
130
+ lang={isDefault ? undefined : locale}
131
+ noindex={settings.noindex}
132
+ hideSidebar={true}
133
+ hideToc={true}
134
+ // Tag segment URL-encoded — emitted href/path sites only; route params
135
+ // stay raw (e.g. "type:guide" → "type%3Aguide").
136
+ headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`${prefix}/docs/tags/${encodeURIComponent(tag)}`)} />}
137
+ breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
138
+ footerOverride={<FooterWithDefaults lang={locale} />}
139
+ bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
140
+ >
141
+ <h1 class="text-heading font-bold mb-vsp-xs">{pageTitle}</h1>
142
+ <p class="text-muted mb-vsp-lg">{countText}</p>
143
+ <DocCardGrid ariaLabel={pageTitle} items={cardItems} />
144
+ <DocHistoryArea slug={`tags/${tag}`} locale={locale} />
145
+ </DocLayoutWithDefaults>
146
+ );
147
+ }
148
+
149
+ /** "All Tags" index page — computes the tag map at render time (matching the
150
+ * original pages, which had no props from paths()). */
151
+ export function TagsIndexPageView({ locale }: { locale: string }): JSX.Element {
152
+ const isDefault = locale === defaultLocale;
153
+ const prefix = localePrefix(locale);
154
+ const pageTitle = t("doc.allTags", locale);
155
+
156
+ const tagMap = collectTagMapForLocale(locale);
157
+
158
+ const labels: TagNavLabels = {
159
+ tags: t("doc.tags", locale),
160
+ taggedWith: t("doc.taggedWith", locale),
161
+ };
162
+
163
+ // Sort alphabetically using the page locale — matches documented tag-nav sort order.
164
+ const tags: TagItem[] = [...tagMap.values()]
165
+ .sort((a, b) => a.tag.localeCompare(b.tag, locale))
166
+ .map((info) => ({
167
+ tag: info.tag,
168
+ count: info.count,
169
+ // Tag segment URL-encoded — href sites only; route params stay raw.
170
+ href: withBase(`${prefix}/docs/tags/${encodeURIComponent(info.tag)}`),
171
+ }));
172
+
173
+ const breadcrumbItems: BreadcrumbItem[] = [
174
+ { label: "Docs" },
175
+ { label: pageTitle },
176
+ ];
177
+
178
+ return (
179
+ <DocLayoutWithDefaults
180
+ title={composeMetaTitle(pageTitle)}
181
+ head={<HeadWithDefaults title={pageTitle} />}
182
+ // Same undefined-≡-absent reliance as TagDetailPageView above.
183
+ lang={isDefault ? undefined : locale}
184
+ noindex={settings.noindex}
185
+ hideSidebar={true}
186
+ hideToc={true}
187
+ headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`${prefix}/docs/tags`)} />}
188
+ breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
189
+ footerOverride={<FooterWithDefaults lang={locale} />}
190
+ bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
191
+ >
192
+ <h1 class="text-heading font-bold mb-vsp-lg">{pageTitle}</h1>
193
+ {!settings.docTags || tags.length === 0 ? (
194
+ <p class="text-muted">{t("doc.noTags", locale)}</p>
195
+ ) : (
196
+ <TagNav variant="all" tags={tags} labels={labels} />
197
+ )}
198
+ <DocHistoryArea slug="tags" locale={locale} />
199
+ </DocLayoutWithDefaults>
200
+ );
201
+ }
@@ -23,29 +23,18 @@
23
23
  // - Non-default locales emit /{locale}/docs/{slug}.
24
24
  // - Locale-first merge: locale docs take priority; base EN docs fill in
25
25
  // pages not translated yet (shown with a fallback notice).
26
+ //
27
+ // Enumeration + per-entry derived data are built by the shared, memoized
28
+ // buildDocRouteEntries (#2010); rendering by the shared renderDocPage. This
29
+ // file owns only the route's nav source and the param/prop shapes.
26
30
 
27
31
  import { settings } from "@/config/settings";
28
- import { docsUrl, absoluteUrl } from "@/utils/base";
29
- import {
30
- buildNavTree,
31
- buildBreadcrumbs,
32
- collectAutoIndexNodes,
33
- type NavNode,
34
- } from "@/utils/docs";
35
- import { getNavSectionForSlug, getNavSubtree } from "@/utils/nav-scope";
36
- import { toRouteSlug, toSlugParams } from "@/utils/slug";
37
- // Shared MDX components bag — see `pages/_mdx-components.ts`.
38
- import { createMdxComponents } from "../../_mdx-components";
32
+ import { getLocaleConfig, type Locale } from "@/config/i18n";
39
33
  import type { JSX } from "preact";
40
34
  import { resolveNavSource } from "../../lib/_nav-source-docs";
41
- import { extractHeadings } from "../../lib/_extract-headings";
42
- import type { DocPageEntry, AutoIndexNode, DocPageEntryProps, DocPageAutoIndexProps } from "../../lib/doc-page-props";
43
- import { DocHistoryArea } from "../../lib/_doc-history-area";
44
- import { DocMetainfoArea } from "../../lib/_doc-metainfo-area";
45
- import { buildInlineVersionSwitcher } from "../../lib/_inline-version-switcher";
46
- import { DocContentHeader } from "../../lib/_doc-content-header";
47
- import { DocPageShell } from "../../lib/_doc-page-shell";
48
- import { resolveDocPrevNext, flattenSubtree } from "../../lib/_doc-route-paths";
35
+ import type { DocPageEntryProps, DocPageAutoIndexProps } from "../../lib/doc-page-props";
36
+ import { buildDocRouteEntries } from "../../lib/_doc-route-entries";
37
+ import { renderDocPage } from "../../lib/_doc-page-renderer";
49
38
 
50
39
  export const frontmatter = { title: "Docs" };
51
40
 
@@ -53,8 +42,6 @@ export const frontmatter = { title: "Docs" };
53
42
  // Types
54
43
  // ---------------------------------------------------------------------------
55
44
 
56
- // DocPageEntry, AutoIndexNode imported from pages/lib/doc-page-props.ts
57
-
58
45
  /** Route-specific extra fields — present on both branches of the union. */
59
46
  interface LocaleDocPageExtra {
60
47
  /** Content directory for the active locale (or base EN for fallbacks). */
@@ -81,8 +68,9 @@ type DocPageProps =
81
68
  * 4. Track fallback slugs for the fallback-notice banner.
82
69
  * 5. Build nav tree, compute breadcrumbs and prev/next for each entry.
83
70
  *
84
- * Fallback slug set drives `isFallback` which the component uses to show
85
- * the "not yet translated" notice (matching the Astro original).
71
+ * Fallback detection (`isFallback`) comes from the merge's localeSlugSet
72
+ * the component uses it to show the "not yet translated" notice (matching
73
+ * the Astro original).
86
74
  */
87
75
  export function paths(): Array<{
88
76
  params: { locale: string; slug: string[] };
@@ -94,85 +82,36 @@ export function paths(): Array<{
94
82
  }> = [];
95
83
 
96
84
  for (const locale of Object.keys(settings.locales) as string[]) {
97
- const localeConfig = settings.locales[locale];
85
+ const localeConfig = getLocaleConfig(locale);
98
86
  const contentDir = localeConfig?.dir ?? settings.docsDir;
99
87
 
100
88
  // Identity-stable, locale-first merge with EN fallback. The same `docs` /
101
89
  // `navDocs` / `categoryMeta` instances are reused across this route's many
102
- // per-page paths() invocations so buildNavTree's identity fast-path skips
103
- // the key recomputation — see pages/lib/_nav-source-docs.ts (#1902).
104
- const { docs: allDocs, navDocs, categoryMeta, localeSlugSet } = resolveNavSource(
105
- locale,
106
- undefined,
107
- { applyDefaultLocaleOnlyFilter: true, keepUnlisted: true },
108
- );
109
- // isFallback: page came from base docs, not the locale collection.
110
- const fallbackSlugs = new Set(
111
- allDocs
112
- .filter((d) => !localeSlugSet.has(d.data.slug ?? d.id))
113
- .map((d) => d.data.slug ?? d.id),
114
- );
115
-
116
- const tree = buildNavTree(navDocs, locale, categoryMeta);
117
- const fullTree = buildNavTree(allDocs, locale, categoryMeta);
118
-
119
- // Regular doc pages
120
- for (const entry of allDocs) {
121
- // A `category_no_page` index.mdx is metadata-only — kept in the nav tree
122
- // for breadcrumbs but emits no route (zfb retains every .mdx as a
123
- // collection entry, so the skip must be explicit).
124
- if (entry.data.category_no_page === true) continue;
125
- // Canonical route slug via the one shared rule (@/utils/slug). `entry.id`
126
- // is already `toRouteSlug(entry.slug)` (bridgeEntries → stripIndexSuffix →
127
- // toRouteSlug), so this is identical to the previous `entry.id` form for
128
- // every entry — but stating it explicitly removes the historical id-vs-
129
- // toRouteSlug asymmetry with the EN route and the component below, all of
130
- // which now yield "" for a root index (URL /{locale}/docs/ — #1891).
131
- const slug = entry.data.slug ?? toRouteSlug(entry.slug);
132
- const isFallback = fallbackSlugs.has(slug);
133
- const entryContentDir = isFallback ? settings.docsDir : contentDir;
134
-
135
- const navSection = getNavSectionForSlug(slug);
136
- const subtree = getNavSubtree(tree, navSection);
137
-
138
- // Prev/next + pagination overrides against THIS locale's own `tree`.
139
- // Latest content (no version) — hrefs stay unversioned (no rewrite).
140
- const { prev: prevNode, next: nextNode } = resolveDocPrevNext(
141
- tree,
142
- flattenSubtree(subtree),
143
- slug,
144
- entry.data,
145
- );
146
-
147
- result.push({
148
- params: { locale, slug: toSlugParams(slug) },
149
- props: {
150
- kind: "entry",
151
- entry,
152
- contentDir: entryContentDir,
153
- isFallback,
154
- breadcrumbs: buildBreadcrumbs(fullTree, slug, locale),
155
- prev: prevNode,
156
- next: nextNode,
157
- headings: extractHeadings(entry.body ?? ""),
158
- },
159
- });
160
- }
161
-
162
- // Auto-generated index pages for categories without index.mdx
163
- for (const node of collectAutoIndexNodes(tree)) {
90
+ // per-page paths() invocations so both buildNavTree's identity fast-path
91
+ // and the buildDocRouteEntries memo key on them — see
92
+ // pages/lib/_nav-source-docs.ts (#1902).
93
+ const source = resolveNavSource(locale as Locale, undefined, {
94
+ applyDefaultLocaleOnlyFilter: true,
95
+ keepUnlisted: true,
96
+ });
97
+
98
+ for (const item of buildDocRouteEntries({
99
+ source,
100
+ locale: locale as Locale,
101
+ routeSig: `locale-docs;${locale}`,
102
+ })) {
103
+ // isFallback: page came from base docs, not the locale collection.
104
+ // Always false for autoIndex items (item.isFallback already is).
105
+ const extra: LocaleDocPageExtra = {
106
+ contentDir: item.isFallback ? settings.docsDir : contentDir,
107
+ isFallback: item.isFallback,
108
+ };
164
109
  result.push({
165
- params: { locale, slug: toSlugParams(node.slug) },
166
- props: {
167
- kind: "autoIndex",
168
- autoIndex: node as AutoIndexNode,
169
- contentDir,
170
- isFallback: false,
171
- breadcrumbs: buildBreadcrumbs(fullTree, node.slug, locale),
172
- prev: null,
173
- next: null,
174
- headings: [],
175
- },
110
+ params: { locale, slug: item.slugParams },
111
+ props:
112
+ item.props.kind === "entry"
113
+ ? { ...item.props, ...extra }
114
+ : { ...item.props, ...extra },
176
115
  });
177
116
  }
178
117
  }
@@ -187,86 +126,9 @@ export function paths(): Array<{
187
126
  type PageArgs = DocPageProps & { params: { locale: string; slug: string[] } };
188
127
 
189
128
  export default function LocaleDocsPage(props: PageArgs): JSX.Element {
190
- const { breadcrumbs, prev, next, headings, contentDir, isFallback } = props;
191
- const locale = props.params.locale;
192
-
193
- const slug = props.kind === "autoIndex"
194
- ? props.autoIndex.slug
195
- : (props.entry.data.slug ?? toRouteSlug(props.entry.slug));
196
-
197
- const title = props.kind === "autoIndex" ? props.autoIndex.label : props.entry.data.title;
198
- const description = props.kind === "autoIndex" ? props.autoIndex.description : props.entry.data.description;
199
-
200
- // Locale-aware components bag — creates nav wrappers bound to the active
201
- // locale so CategoryNav/CategoryTreeNav/SiteTreeNav query the right collection.
202
- const components = createMdxComponents(locale);
203
-
204
- // Latest content (no version) — keep the nav node's own docsUrl href.
205
- const autoIndexChildren = props.kind === "autoIndex"
206
- ? props.autoIndex.children
207
- .filter((c: NavNode) => c.hasPage || c.children.length > 0)
208
- .map((c: NavNode) => ({
209
- ...c,
210
- href: c.href ?? docsUrl(c.slug, locale),
211
- }))
212
- : [];
213
-
214
- // Canonical URL — base-prefixed locale page path, absolutized against siteUrl.
215
- const currentPath = docsUrl(slug, locale);
216
- const canonical = absoluteUrl(currentPath);
217
-
218
- // Persist key: locale + nav-section so the sidebar DOM node is reused
219
- // across same-locale + same-section navigations only. No sanitizer needed —
220
- // both lang (BCP-47 locale string) and navSection (filesystem-derived
221
- // kebab-case slug) come from controlled, trusted sources.
222
- const navSection = getNavSectionForSlug(slug);
223
- const hideSidebar = props.kind === "entry" ? props.entry.data.hide_sidebar : undefined;
224
- const sidebarPersistKey = hideSidebar
225
- ? undefined
226
- : `sidebar-${locale}-${navSection ?? "default"}`;
227
-
228
- return (
229
- <DocPageShell
230
- kind={props.kind}
231
- locale={locale}
232
- slug={slug}
233
- title={title}
234
- description={description}
235
- canonical={canonical}
236
- breadcrumbs={breadcrumbs}
237
- prev={prev}
238
- next={next}
239
- headings={headings}
240
- navSection={navSection}
241
- sidebarPersistKey={sidebarPersistKey}
242
- hideSidebar={hideSidebar}
243
- hideToc={props.kind === "entry" ? props.entry.data.hide_toc : undefined}
244
- currentPath={currentPath}
245
- versionSwitcher={buildInlineVersionSwitcher(slug, locale)}
246
- autoIndexLabel={props.kind === "autoIndex" ? props.autoIndex.label : undefined}
247
- autoIndexChildren={autoIndexChildren}
248
- metainfoSlot={
249
- props.kind === "autoIndex" ? <DocMetainfoArea slug={slug} locale={locale} /> : null
250
- }
251
- contentHeaderSlot={
252
- props.kind === "entry" ? (
253
- <DocContentHeader entry={props.entry} slug={slug} locale={locale} isFallback={isFallback} />
254
- ) : undefined
255
- }
256
- contentSlot={
257
- props.kind === "entry" ? <props.entry.Content components={components} /> : undefined
258
- }
259
- docHistorySlot={
260
- props.kind === "entry" && !props.entry.data.unlisted ? (
261
- <DocHistoryArea
262
- slug={slug}
263
- locale={locale}
264
- entrySlug={props.entry.slug}
265
- contentDir={contentDir}
266
- isFallback={isFallback}
267
- />
268
- ) : null
269
- }
270
- />
271
- );
129
+ return renderDocPage(props, {
130
+ locale: props.params.locale as Locale,
131
+ isFallback: props.isFallback,
132
+ docHistoryContentDir: props.contentDir,
133
+ });
272
134
  }