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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/create-zudo-doc.js +2 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +13 -0
- package/dist/claude-md-gen.d.ts +2 -0
- package/dist/claude-md-gen.js +113 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +157 -0
- package/dist/compose.d.ts +95 -0
- package/dist/compose.js +206 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +224 -0
- package/dist/features/body-foot-util.d.ts +10 -0
- package/dist/features/body-foot-util.js +12 -0
- package/dist/features/claude-resources.d.ts +2 -0
- package/dist/features/claude-resources.js +6 -0
- package/dist/features/design-token-panel.d.ts +14 -0
- package/dist/features/design-token-panel.js +27 -0
- package/dist/features/doc-history.d.ts +9 -0
- package/dist/features/doc-history.js +11 -0
- package/dist/features/doc-tags.d.ts +19 -0
- package/dist/features/doc-tags.js +33 -0
- package/dist/features/footer-taglist.d.ts +14 -0
- package/dist/features/footer-taglist.js +17 -0
- package/dist/features/footer.d.ts +8 -0
- package/dist/features/footer.js +10 -0
- package/dist/features/i18n.d.ts +22 -0
- package/dist/features/i18n.js +41 -0
- package/dist/features/image-enlarge.d.ts +11 -0
- package/dist/features/image-enlarge.js +13 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.js +53 -0
- package/dist/features/llms-txt.d.ts +11 -0
- package/dist/features/llms-txt.js +13 -0
- package/dist/features/search.d.ts +9 -0
- package/dist/features/search.js +11 -0
- package/dist/features/sidebar-resizer.d.ts +14 -0
- package/dist/features/sidebar-resizer.js +16 -0
- package/dist/features/sidebar-toggle.d.ts +13 -0
- package/dist/features/sidebar-toggle.js +15 -0
- package/dist/features/tag-governance.d.ts +14 -0
- package/dist/features/tag-governance.js +16 -0
- package/dist/features/tauri-dev.d.ts +2 -0
- package/dist/features/tauri-dev.js +25 -0
- package/dist/features/tauri.d.ts +11 -0
- package/dist/features/tauri.js +52 -0
- package/dist/features/versioning.d.ts +27 -0
- package/dist/features/versioning.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +150 -0
- package/dist/preset.d.ts +37 -0
- package/dist/preset.js +156 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +248 -0
- package/dist/scaffold.d.ts +4 -0
- package/dist/scaffold.js +344 -0
- package/dist/settings-gen.d.ts +2 -0
- package/dist/settings-gen.js +237 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +34 -0
- package/dist/zfb-config-gen.d.ts +19 -0
- package/dist/zfb-config-gen.js +222 -0
- package/package.json +65 -0
- package/templates/base/.htmlvalidate.json +5 -0
- package/templates/base/.zfb/doc-history-meta.json +1 -0
- package/templates/base/pages/404.tsx +55 -0
- package/templates/base/pages/_data.ts +179 -0
- package/templates/base/pages/_mdx-components.ts +249 -0
- package/templates/base/pages/docs/[...slug].tsx +448 -0
- package/templates/base/pages/index.tsx +158 -0
- package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
- package/templates/base/pages/lib/_category-nav.tsx +148 -0
- package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
- package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
- package/templates/base/pages/lib/_details.tsx +30 -0
- package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
- package/templates/base/pages/lib/_extract-headings.ts +81 -0
- package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
- package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
- package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
- package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
- package/templates/base/pages/lib/_math-block.tsx +63 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
- package/templates/base/pages/lib/_preset-generator.tsx +81 -0
- package/templates/base/pages/lib/_search-widget-script.ts +388 -0
- package/templates/base/pages/lib/_search-widget.tsx +196 -0
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
- package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
- package/templates/base/pages/lib/locale-merge.ts +58 -0
- package/templates/base/pages/lib/route-enumerators.ts +302 -0
- package/templates/base/pages/sitemap.xml.tsx +51 -0
- package/templates/base/plugins/connect-adapter.mjs +144 -0
- package/templates/base/plugins/copy-public-plugin.mjs +50 -0
- package/templates/base/plugins/search-index-plugin.mjs +54 -0
- package/templates/base/scripts/run-b4push.sh +102 -0
- package/templates/base/src/components/ai-chat-modal.tsx +15 -0
- package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
- package/templates/base/src/components/content/component-map.ts +25 -0
- package/templates/base/src/components/content/content-blockquote.tsx +16 -0
- package/templates/base/src/components/content/content-code.tsx +117 -0
- package/templates/base/src/components/content/content-link.tsx +83 -0
- package/templates/base/src/components/content/content-ol.tsx +19 -0
- package/templates/base/src/components/content/content-paragraph.tsx +10 -0
- package/templates/base/src/components/content/content-strong.tsx +16 -0
- package/templates/base/src/components/content/content-table.tsx +18 -0
- package/templates/base/src/components/content/content-ul.tsx +18 -0
- package/templates/base/src/components/content/heading-h2.tsx +26 -0
- package/templates/base/src/components/content/heading-h3.tsx +26 -0
- package/templates/base/src/components/content/heading-h4.tsx +26 -0
- package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
- package/templates/base/src/components/doc-history.tsx +18 -0
- package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
- package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
- package/templates/base/src/components/html-preview/preflight.ts +112 -0
- package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
- package/templates/base/src/components/image-enlarge.tsx +19 -0
- package/templates/base/src/components/mobile-toc.tsx +94 -0
- package/templates/base/src/components/preset-generator.tsx +14 -0
- package/templates/base/src/components/sidebar-toggle.tsx +98 -0
- package/templates/base/src/components/sidebar-tree.tsx +543 -0
- package/templates/base/src/components/site-tree-nav.tsx +233 -0
- package/templates/base/src/components/theme-toggle.tsx +93 -0
- package/templates/base/src/components/toc.tsx +63 -0
- package/templates/base/src/components/tree-nav-shared.tsx +71 -0
- package/templates/base/src/config/color-scheme-utils.ts +182 -0
- package/templates/base/src/config/color-schemes.ts +128 -0
- package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
- package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
- package/templates/base/src/config/i18n.ts +225 -0
- package/templates/base/src/config/settings-types.ts +162 -0
- package/templates/base/src/config/sidebars.ts +66 -0
- package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
- package/templates/base/src/config/tag-vocabulary.ts +20 -0
- package/templates/base/src/hooks/use-active-heading.ts +133 -0
- package/templates/base/src/plugins/docs-source-map.ts +103 -0
- package/templates/base/src/plugins/hast-utils.ts +10 -0
- package/templates/base/src/plugins/rehype-code-title.ts +50 -0
- package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
- package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
- package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
- package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
- package/templates/base/src/plugins/remark-admonitions.ts +99 -0
- package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
- package/templates/base/src/plugins/url-utils.ts +4 -0
- package/templates/base/src/styles/global.css +1066 -0
- package/templates/base/src/types/docs-entry.ts +39 -0
- package/templates/base/src/types/heading.ts +5 -0
- package/templates/base/src/types/locale.ts +10 -0
- package/templates/base/src/utils/base.ts +139 -0
- package/templates/base/src/utils/content-files.ts +106 -0
- package/templates/base/src/utils/dedent.ts +24 -0
- package/templates/base/src/utils/docs.ts +335 -0
- package/templates/base/src/utils/git-info.ts +70 -0
- package/templates/base/src/utils/github.ts +19 -0
- package/templates/base/src/utils/header-right-items.ts +38 -0
- package/templates/base/src/utils/nav-scope.ts +63 -0
- package/templates/base/src/utils/sidebar.ts +104 -0
- package/templates/base/src/utils/slug.ts +10 -0
- package/templates/base/src/utils/smart-break.tsx +126 -0
- package/templates/base/src/utils/tags.ts +126 -0
- package/templates/base/tsconfig.json +36 -0
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
- package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
- package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
- package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
- package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
- package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
- package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
- package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
- package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
- package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
- package/templates/features/tauri/files/src-tauri/build.rs +3 -0
- package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
- package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
- package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
- package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
- package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
- package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
- package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
- package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
- package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
- package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
- package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
- package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
- package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
- 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
|
+
}
|