create-zudo-doc 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +4 -1
- package/dist/cli.js +4 -6
- package/dist/compose.d.ts +2 -3
- package/dist/compose.js +7 -4
- package/dist/features/tauri.d.ts +10 -5
- package/dist/features/tauri.js +49 -6
- package/dist/preset.js +11 -0
- package/dist/prompts.js +2 -6
- package/dist/scaffold.js +15 -9
- package/dist/settings-gen.js +9 -6
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +25 -0
- package/dist/zfb-config-gen.js +11 -50
- package/package.json +1 -1
- package/templates/base/pages/_data.ts +10 -23
- package/templates/base/pages/docs/[[...slug]].tsx +27 -168
- package/templates/base/pages/lib/_body-end-islands.tsx +3 -0
- package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
- package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
- package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
- package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
- package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
- package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
- package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
- package/templates/base/pages/lib/_header-with-defaults.tsx +54 -89
- package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
- package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
- package/templates/base/pages/lib/_search-widget-script.ts +32 -9
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
- package/templates/base/pages/lib/locale-merge.ts +1 -1
- package/templates/base/pages/lib/route-enumerators.ts +11 -7
- package/templates/base/plugins/connect-adapter.mjs +30 -1
- package/templates/base/plugins/copy-public-plugin.mjs +10 -2
- package/templates/base/plugins/search-index-plugin.mjs +20 -8
- package/templates/base/src/components/ai-chat-modal.tsx +2 -0
- package/templates/base/src/components/doc-history.tsx +2 -0
- package/templates/base/src/components/image-enlarge.tsx +2 -0
- package/templates/base/src/components/sidebar-toggle.tsx +1 -1
- package/templates/base/src/components/sidebar-tree.tsx +11 -5
- package/templates/base/src/components/theme-toggle.tsx +18 -102
- package/templates/base/src/config/color-schemes.ts +4 -0
- package/templates/base/src/config/docs-schema.ts +94 -0
- package/templates/base/src/config/i18n.ts +10 -3
- package/templates/base/src/styles/global.css +14 -0
- package/templates/base/src/types/docs-entry.ts +8 -26
- package/templates/base/src/utils/base.ts +5 -3
- package/templates/base/src/utils/docs.ts +144 -169
- package/templates/base/zfb-shim.d.ts +167 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
- package/templates/features/docHistory/files/src/components/doc-history.tsx +30 -8
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
- package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +2 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +9 -3
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
- package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
- package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
- package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
- package/templates/base/src/components/content/heading-h3.tsx +0 -20
- package/templates/base/src/hooks/use-active-heading.ts +0 -133
- package/templates/base/src/plugins/docs-source-map.ts +0 -103
- package/templates/base/src/plugins/hast-utils.ts +0 -10
- package/templates/base/src/plugins/rehype-code-title.ts +0 -50
- package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
- package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
- package/templates/base/src/plugins/url-utils.ts +0 -4
- package/templates/base/src/utils/dedent.ts +0 -24
- package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Shared, memoized route-entry builder for the 4 doc catch-all routes.
|
|
2
|
+
//
|
|
3
|
+
// Extracted (#2010) from the ~85%-duplicated paths() bodies of:
|
|
4
|
+
// pages/docs/[[...slug]].tsx
|
|
5
|
+
// pages/[locale]/docs/[[...slug]].tsx
|
|
6
|
+
// pages/v/[version]/docs/[[...slug]].tsx
|
|
7
|
+
// pages/v/[version]/[locale]/docs/[[...slug]].tsx
|
|
8
|
+
//
|
|
9
|
+
// Each route resolves its own identity-stable nav source (resolveNavSource /
|
|
10
|
+
// resolveVersionedLocaleSource) and URL closure, then delegates the per-entry
|
|
11
|
+
// derived-data work here. The result is memoized with the #1902 WeakMap
|
|
12
|
+
// pattern (memoizeDerived keyed on the identity-stable `source.docs` array +
|
|
13
|
+
// a per-route signature), so the expensive per-entry work — extractHeadings,
|
|
14
|
+
// buildBreadcrumbs, prev/next resolution — runs ONCE per entry per build,
|
|
15
|
+
// not once per entry per page (zfb re-invokes paths() once per built page).
|
|
16
|
+
//
|
|
17
|
+
// Versioned-vs-latest behavior is keyed on the presence of `urlFor` (#1916):
|
|
18
|
+
// - `urlFor` set (versioned routes): breadcrumbs resolve against the NAV
|
|
19
|
+
// tree (unlisted excluded) with crumbs remapped to the versioned URL
|
|
20
|
+
// space; prev/next hrefs are rewritten through the closure; auto-index
|
|
21
|
+
// child-card hrefs are ALWAYS remapped to the versioned URL.
|
|
22
|
+
// - `urlFor` unset (latest routes): breadcrumbs resolve against the FULL
|
|
23
|
+
// tree (unlisted included, for accurate crumbs); prev/next and child
|
|
24
|
+
// hrefs keep the latest `docsUrl` already baked into the nav nodes.
|
|
25
|
+
// These two behaviors travel together by construction: only versioned routes
|
|
26
|
+
// own a versioned URL closure (see _doc-route-paths.ts for the #1916
|
|
27
|
+
// rationale on why latest routes must never receive one).
|
|
28
|
+
|
|
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
|
+
import type { Locale } from "@/config/i18n";
|
|
38
|
+
import { extractHeadings } from "./_extract-headings";
|
|
39
|
+
import type { AutoIndexNode, DocPageBaseProps } from "./doc-page-props";
|
|
40
|
+
import { memoizeDerived } from "./_nav-source-cache";
|
|
41
|
+
import type { NavSourceDocs } from "./_nav-source-docs";
|
|
42
|
+
import {
|
|
43
|
+
resolveDocPrevNext,
|
|
44
|
+
flattenSubtree,
|
|
45
|
+
rewriteNavHref,
|
|
46
|
+
remapNavChildHrefs,
|
|
47
|
+
} from "./_doc-route-paths";
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Types
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** One enumerated doc route: a content entry or an auto-generated category
|
|
54
|
+
* index, with all per-page derived data pre-computed. */
|
|
55
|
+
export interface DocRouteEntry {
|
|
56
|
+
/** Canonical route slug ("" for the docs root index — #1891). */
|
|
57
|
+
slug: string;
|
|
58
|
+
/** Optional-catchall params array — `toSlugParams(slug)` ([] for the root). */
|
|
59
|
+
slugParams: string[];
|
|
60
|
+
/**
|
|
61
|
+
* True when the entry came from the base collection rather than the locale
|
|
62
|
+
* collection (`!localeSlugSet.has(slug)`). Only meaningful on routes whose
|
|
63
|
+
* nav source performs a locale merge — routes without one (default-locale /
|
|
64
|
+
* versioned-EN, where `localeSlugSet` is empty) must ignore this field.
|
|
65
|
+
* Always false for autoIndex items.
|
|
66
|
+
*/
|
|
67
|
+
isFallback: boolean;
|
|
68
|
+
/** Shared page props (kind/entry/autoIndex/breadcrumbs/prev/next/headings). */
|
|
69
|
+
props: DocPageBaseProps;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface BuildDocRouteEntriesArgs {
|
|
73
|
+
/** Identity-stable nav source for this route's (locale, version) context —
|
|
74
|
+
* from resolveNavSource / resolveVersionedLocaleSource. The memo is keyed
|
|
75
|
+
* on `source.docs` identity, so the source MUST come from those resolvers
|
|
76
|
+
* (a fresh array defeats the memo — harmless, but recomputes per call). */
|
|
77
|
+
source: NavSourceDocs;
|
|
78
|
+
/** Active locale for nav-tree labels and breadcrumbs. */
|
|
79
|
+
locale: Locale;
|
|
80
|
+
/**
|
|
81
|
+
* Unique memo signature for this route context. Each route file passes its
|
|
82
|
+
* own prefix plus the loop variables (version slug / locale), e.g.
|
|
83
|
+
* "docs;en", "locale-docs;ja", "v-docs;1.0", "v-locale-docs;1.0;ja" —
|
|
84
|
+
* call sites that share a docs array identity must never collide on a key.
|
|
85
|
+
*/
|
|
86
|
+
routeSig: string;
|
|
87
|
+
/** Versioned URL closure bound to the route's version (+ locale). Presence
|
|
88
|
+
* switches the versioned behaviors documented in the module header. */
|
|
89
|
+
urlFor?: (slug: string) => string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// buildDocRouteEntries
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Enumerate all doc routes (content entries + auto-index pages) for one
|
|
98
|
+
* (locale, version) context, with per-entry derived data pre-computed.
|
|
99
|
+
*
|
|
100
|
+
* Memoized per build on the identity-stable `source.docs` array (#1902), so
|
|
101
|
+
* repeated paths() invocations across the route's many pages return the SAME
|
|
102
|
+
* array instance without redoing the per-entry work. In the no-snapshot
|
|
103
|
+
* fallback path (unit tests / direct Node runs) `source.docs` is a fresh
|
|
104
|
+
* array per call, so the memo misses and this computes fresh — matching the
|
|
105
|
+
* deliberate no-memo policy in _nav-source-cache.ts.
|
|
106
|
+
*/
|
|
107
|
+
export function buildDocRouteEntries(
|
|
108
|
+
args: BuildDocRouteEntriesArgs,
|
|
109
|
+
): DocRouteEntry[] {
|
|
110
|
+
const { source, locale, routeSig, urlFor } = args;
|
|
111
|
+
|
|
112
|
+
return memoizeDerived([source.docs], `docRouteEntries;${routeSig}`, () => {
|
|
113
|
+
const { docs, navDocs, categoryMeta, localeSlugSet } = source;
|
|
114
|
+
|
|
115
|
+
// Nav docs: exclude unlisted (for sidebar/prev-next) but keep for breadcrumbs
|
|
116
|
+
const tree = buildNavTree(navDocs, locale, categoryMeta);
|
|
117
|
+
// Breadcrumb tree: latest routes use the full tree (including unlisted)
|
|
118
|
+
// for accurate crumbs; versioned routes resolve crumbs against the nav
|
|
119
|
+
// tree itself (#1916 #1).
|
|
120
|
+
const breadcrumbTree = urlFor ? tree : buildNavTree(docs, locale, categoryMeta);
|
|
121
|
+
|
|
122
|
+
const result: DocRouteEntry[] = [];
|
|
123
|
+
|
|
124
|
+
// Regular doc pages
|
|
125
|
+
for (const entry of docs) {
|
|
126
|
+
// A `category_no_page` index.mdx carries category metadata only — keep
|
|
127
|
+
// it in the nav tree (built above, used for breadcrumbs) but emit NO
|
|
128
|
+
// route for it. zfb's walker retains every .mdx as a collection entry,
|
|
129
|
+
// so without this explicit skip the metadata file would silently add a
|
|
130
|
+
// route.
|
|
131
|
+
if (entry.data.category_no_page === true) continue;
|
|
132
|
+
// Canonical route slug via the one shared rule (@/utils/slug) — yields
|
|
133
|
+
// "" for a root index (URL /docs/ — #1891).
|
|
134
|
+
const slug = entry.data.slug ?? toRouteSlug(entry.slug);
|
|
135
|
+
const navSection = getNavSectionForSlug(slug);
|
|
136
|
+
const subtree = getNavSubtree(tree, navSection);
|
|
137
|
+
|
|
138
|
+
// Prev/next + frontmatter pagination overrides resolved against THIS
|
|
139
|
+
// route's own `tree`; versioned routes then rewrite the hrefs through
|
|
140
|
+
// their urlFor closure (latest routes pass it through unchanged).
|
|
141
|
+
const { prev: prevNode, next: nextNode } = resolveDocPrevNext(
|
|
142
|
+
tree,
|
|
143
|
+
flattenSubtree(subtree),
|
|
144
|
+
slug,
|
|
145
|
+
entry.data,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
result.push({
|
|
149
|
+
slug,
|
|
150
|
+
slugParams: toSlugParams(slug),
|
|
151
|
+
isFallback: !localeSlugSet.has(slug),
|
|
152
|
+
props: {
|
|
153
|
+
kind: "entry",
|
|
154
|
+
entry,
|
|
155
|
+
breadcrumbs: buildBreadcrumbs(breadcrumbTree, slug, locale, urlFor),
|
|
156
|
+
prev: rewriteNavHref(prevNode, urlFor),
|
|
157
|
+
next: rewriteNavHref(nextNode, urlFor),
|
|
158
|
+
headings: extractHeadings(entry.body ?? ""),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Auto-generated index pages for categories without index.mdx
|
|
164
|
+
for (const node of collectAutoIndexNodes(tree)) {
|
|
165
|
+
result.push({
|
|
166
|
+
slug: node.slug,
|
|
167
|
+
slugParams: toSlugParams(node.slug),
|
|
168
|
+
isFallback: false,
|
|
169
|
+
props: {
|
|
170
|
+
kind: "autoIndex",
|
|
171
|
+
autoIndex: urlFor
|
|
172
|
+
? // #1916 #2: child-card hrefs ALWAYS resolve to the versioned URL.
|
|
173
|
+
({
|
|
174
|
+
...node,
|
|
175
|
+
children: remapNavChildHrefs(node.children, urlFor) as NavNode[],
|
|
176
|
+
} as AutoIndexNode)
|
|
177
|
+
: (node as AutoIndexNode),
|
|
178
|
+
breadcrumbs: buildBreadcrumbs(breadcrumbTree, node.slug, locale, urlFor),
|
|
179
|
+
prev: null,
|
|
180
|
+
next: null,
|
|
181
|
+
headings: [],
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -34,12 +34,17 @@ import { DocTags } from "@takazudo/zudo-doc/metainfo";
|
|
|
34
34
|
*
|
|
35
35
|
* Inlined from _footer-with-defaults.tsx `tagHref` — not extracted to avoid
|
|
36
36
|
* ripple (spec rule: no opportunistic refactor on tagHref extraction).
|
|
37
|
+
*
|
|
38
|
+
* The tag segment is URL-encoded at the href site only — route params stay
|
|
39
|
+
* raw, so the built output dir keeps the raw tag name and the server decodes
|
|
40
|
+
* the percent-encoded href back to it (e.g. "type:guide" → "type%3Aguide").
|
|
37
41
|
*/
|
|
38
42
|
function tagHref(tag: string, locale: string): string {
|
|
43
|
+
const encoded = encodeURIComponent(tag);
|
|
39
44
|
const path =
|
|
40
45
|
locale === defaultLocale
|
|
41
|
-
? `/docs/tags/${
|
|
42
|
-
: `/${locale}/docs/tags/${
|
|
46
|
+
? `/docs/tags/${encoded}`
|
|
47
|
+
: `/${locale}/docs/tags/${encoded}`;
|
|
43
48
|
return withBase(path);
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -30,8 +30,8 @@ import { defaultLocale } from "@/config/i18n";
|
|
|
30
30
|
import { tagVocabulary } from "@/config/tag-vocabulary";
|
|
31
31
|
import { collectTags } from "@/utils/tags";
|
|
32
32
|
import { toRouteSlug } from "@/utils/slug";
|
|
33
|
-
import { loadDocs } from "../_data";
|
|
34
33
|
import { mergeLocaleDocs } from "./locale-merge";
|
|
34
|
+
import { stableDocs, memoizeDerived } from "./_nav-source-cache";
|
|
35
35
|
import type { DocsEntry } from "@/types/docs-entry";
|
|
36
36
|
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
@@ -51,12 +51,18 @@ function localizeHref(href: string, lang: string): string {
|
|
|
51
51
|
return resolveHref(href);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Build the base-prefixed tag detail page href for the given locale.
|
|
56
|
+
* The tag segment is URL-encoded at the href site only — route params stay
|
|
57
|
+
* raw, so the built output dir keeps the raw tag name and the server decodes
|
|
58
|
+
* the percent-encoded href back to it (e.g. "type:guide" → "type%3Aguide").
|
|
59
|
+
*/
|
|
55
60
|
function tagHref(tag: string, lang: string): string {
|
|
61
|
+
const encoded = encodeURIComponent(tag);
|
|
56
62
|
const path =
|
|
57
63
|
lang === defaultLocale
|
|
58
|
-
? `/docs/tags/${
|
|
59
|
-
: `/${lang}/docs/tags/${
|
|
64
|
+
? `/docs/tags/${encoded}`
|
|
65
|
+
: `/${lang}/docs/tags/${encoded}`;
|
|
60
66
|
return withBase(path);
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -110,15 +116,22 @@ export function FooterWithDefaults({
|
|
|
110
116
|
let tagColumns: FooterTagColumn[] = [];
|
|
111
117
|
|
|
112
118
|
if (taglist?.enabled) {
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
(
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// Memoize docs loading and tag aggregation on the snapshot-anchored stable
|
|
120
|
+
// docs identity — same pattern as _nav-source-cache — so this block runs
|
|
121
|
+
// once per locale per build, not once per page render.
|
|
122
|
+
const allTags = (() => {
|
|
123
|
+
if (lang === defaultLocale) {
|
|
124
|
+
const baseDocs = stableDocs("docs");
|
|
125
|
+
return memoizeDerived([baseDocs], "footerTaglist;default", () => {
|
|
126
|
+
// category_no_page index files build no route — drop them so the
|
|
127
|
+
// footer taglist matches the tag-route pages (which all now filter too).
|
|
128
|
+
const docs: DocsEntry[] = baseDocs.filter(
|
|
129
|
+
(d) => !d.data.draft && !d.data.unlisted && !d.data.category_no_page,
|
|
130
|
+
);
|
|
131
|
+
const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
|
|
132
|
+
return [...tagMap.values()].sort((a, b) => a.tag.localeCompare(b.tag, lang));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
122
135
|
// Apply the default-locale-only filter so the footer taglist only counts
|
|
123
136
|
// tags that have a locale-routable tag page — matching the tag-route
|
|
124
137
|
// pages ([tag].tsx / tags/index.tsx) and enumerateTagsRoutes, which all
|
|
@@ -127,21 +140,19 @@ export function FooterWithDefaults({
|
|
|
127
140
|
// prefix pages. category_no_page is dropped for the same reason — AFTER
|
|
128
141
|
// the merge, so a locale override carrying the flag first wins the merge
|
|
129
142
|
// (pre-merge filtering would let the unflagged base doc resurface).
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
const baseDocs = stableDocs("docs");
|
|
144
|
+
const localeDocs = stableDocs(`docs-${lang}`);
|
|
145
|
+
return memoizeDerived([baseDocs, localeDocs], `footerTaglist;${lang}`, () => {
|
|
146
|
+
const result = mergeLocaleDocs({
|
|
147
|
+
baseDocs: baseDocs.filter((d) => !d.data.draft),
|
|
148
|
+
localeDocs: localeDocs.filter((d) => !d.data.draft),
|
|
149
|
+
applyDefaultLocaleOnlyFilter: true,
|
|
150
|
+
});
|
|
151
|
+
const docs: DocsEntry[] = result.docs.filter((d) => !d.data.category_no_page);
|
|
152
|
+
const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
|
|
153
|
+
return [...tagMap.values()].sort((a, b) => a.tag.localeCompare(b.tag, lang));
|
|
134
154
|
});
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const tagMap = collectTags(
|
|
139
|
-
docs,
|
|
140
|
-
(id, data) => data.slug ?? toRouteSlug(id),
|
|
141
|
-
);
|
|
142
|
-
const allTags = [...tagMap.values()].sort((a, b) =>
|
|
143
|
-
a.tag.localeCompare(b.tag, lang),
|
|
144
|
-
);
|
|
155
|
+
})();
|
|
145
156
|
|
|
146
157
|
const vocabularyActive =
|
|
147
158
|
Boolean(settings.tagVocabulary) && settings.tagGovernance !== "off";
|
|
@@ -20,16 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
import type { JSX } from "preact";
|
|
22
22
|
import { OgTags, TwitterCard } from "@takazudo/zudo-doc/head";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
// barrel also re-exports
|
|
27
|
-
//
|
|
28
|
-
// modules
|
|
29
|
-
//
|
|
30
|
-
// package exposes a dedicated `./theme/color-scheme-provider` subpath whose
|
|
31
|
-
// only output is the SSR-only ColorSchemeProvider component, keeping this
|
|
32
|
-
// head emission free of the panel-module dependency chain.
|
|
23
|
+
import { SIDEBAR_RESIZER_RESTORE_SCRIPT } from "@takazudo/zudo-doc/sidebar-resizer";
|
|
24
|
+
// Import ColorSchemeProvider from the dedicated
|
|
25
|
+
// `./theme/color-scheme-provider` subpath rather than the
|
|
26
|
+
// "@takazudo/zudo-doc/theme" barrel — the barrel also re-exports the
|
|
27
|
+
// ColorTweakExportModal island and the design-token SerDe/iframe-bridge
|
|
28
|
+
// modules, which this SSR-only head emission does not need in its zfb
|
|
29
|
+
// esbuild graph.
|
|
33
30
|
import ColorSchemeProvider from "@takazudo/zudo-doc/theme/color-scheme-provider";
|
|
34
31
|
import { composeMetaTitle } from "./_compose-meta-title";
|
|
35
32
|
import { withBase, absoluteUrl } from "@/utils/base";
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
// hides it on pages with hide_sidebar). When `navSection` is defined the
|
|
26
26
|
// panel gets the full section tree; when undefined (home, 404, tags,
|
|
27
27
|
// versions) nodes=[] so the panel shows only rootMenuItems.
|
|
28
|
-
// -
|
|
29
|
-
// Header.themeToggle so the ThemeToggle island marker appears in the
|
|
28
|
+
// - The bare package ThemeToggle (wrapped in Island below) is always passed
|
|
29
|
+
// to Header.themeToggle so the ThemeToggle island marker appears in the
|
|
30
30
|
// header on every page — matching the documented header contract.
|
|
31
31
|
//
|
|
32
32
|
// Locale switcher strategy (refs #1453):
|
|
@@ -45,68 +45,33 @@ import {
|
|
|
45
45
|
VersionSwitcher,
|
|
46
46
|
type VersionSwitcherLabels,
|
|
47
47
|
} from "@takazudo/zudo-doc/i18n-version";
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
import ThemeToggle from "@/components/theme-toggle";
|
|
48
|
+
// ThemeToggle via the project-source shim (`@/components/theme-toggle`)
|
|
49
|
+
// rather than the package subpath directly. zfb's island scanner does not
|
|
50
|
+
// register "use client" modules under node_modules (zfb#999), so importing
|
|
51
|
+
// the package component here would emit an island marker with no registry
|
|
52
|
+
// entry and the toggle would never hydrate (zudolab/zudo-doc#2048). The shim
|
|
53
|
+
// re-wraps the bare (non-island-wrapped) package ThemeToggle; this wrapper
|
|
54
|
+
// composes its own Island below, avoiding nesting an island inside an island.
|
|
55
|
+
import { ThemeToggle } from "@/components/theme-toggle";
|
|
56
56
|
import SidebarToggle from "@/components/sidebar-toggle";
|
|
57
57
|
import { settings } from "@/config/settings";
|
|
58
58
|
import { defaultLocale, locales, t, type Locale } from "@/config/i18n";
|
|
59
59
|
import { buildGitHubRepoUrl } from "@/utils/github";
|
|
60
60
|
import {
|
|
61
|
-
buildLocaleLinks,
|
|
62
61
|
docsUrl,
|
|
63
62
|
navHref,
|
|
64
63
|
stripBase,
|
|
65
64
|
versionedDocsUrl,
|
|
66
65
|
withBase,
|
|
67
66
|
} from "@/utils/base";
|
|
68
|
-
import {
|
|
69
|
-
type NavNode,
|
|
70
|
-
} from "@/utils/docs";
|
|
71
|
-
import { buildSidebarForSection } from "@/utils/sidebar";
|
|
72
67
|
import { filterHeaderRightItems } from "@takazudo/zudo-doc/header";
|
|
73
68
|
import { SearchWidget } from "./_search-widget";
|
|
74
|
-
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Walk the nav tree and rewrite each node's `href` to its versioned form.
|
|
82
|
-
*
|
|
83
|
-
* `buildNavTree` always emits hrefs via `docsUrl()`; when the active route
|
|
84
|
-
* lives under `/v/{version}/...` we need the same nodes pointing at the
|
|
85
|
-
* versioned URL so internal nav clicks stay inside the version. Skips
|
|
86
|
-
* nodes without an href (link-only or category placeholders).
|
|
87
|
-
*
|
|
88
|
-
* Intentionally kept as a local copy in this module (not extracted) —
|
|
89
|
-
* T2 only dedupes loadNavSourceDocs; remapVersionedHrefs is out of scope.
|
|
90
|
-
*/
|
|
91
|
-
function remapVersionedHrefs(
|
|
92
|
-
nodes: NavNode[],
|
|
93
|
-
version: string,
|
|
94
|
-
nodeLang: Locale,
|
|
95
|
-
): NavNode[] {
|
|
96
|
-
return nodes.map((node) => {
|
|
97
|
-
const children =
|
|
98
|
-
node.children.length > 0
|
|
99
|
-
? remapVersionedHrefs(node.children, version, nodeLang)
|
|
100
|
-
: node.children;
|
|
101
|
-
|
|
102
|
-
if (!node.href || node.slug.startsWith("__link__")) {
|
|
103
|
-
return children !== node.children ? { ...node, children } : node;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const newHref = versionedDocsUrl(node.slug, version, nodeLang);
|
|
107
|
-
return { ...node, href: newHref, children };
|
|
108
|
-
});
|
|
109
|
-
}
|
|
69
|
+
import {
|
|
70
|
+
buildRootMenuItems,
|
|
71
|
+
buildLocaleLinksForNav,
|
|
72
|
+
buildSidebarNodes,
|
|
73
|
+
getThemeDefaultMode,
|
|
74
|
+
} from "./_nav-data-prep";
|
|
110
75
|
|
|
111
76
|
// ---------------------------------------------------------------------------
|
|
112
77
|
// Component
|
|
@@ -114,7 +79,7 @@ function remapVersionedHrefs(
|
|
|
114
79
|
|
|
115
80
|
export interface HeaderWithDefaultsProps {
|
|
116
81
|
/** Active locale; defaults to the configured defaultLocale. */
|
|
117
|
-
lang?: Locale;
|
|
82
|
+
lang?: Locale | string;
|
|
118
83
|
/**
|
|
119
84
|
* Current page URL path (as the layout passes from Astro.url.pathname or
|
|
120
85
|
* the zfb equivalent). Used by the Header to compute the active nav item
|
|
@@ -151,27 +116,21 @@ export function HeaderWithDefaults(
|
|
|
151
116
|
props: HeaderWithDefaultsProps,
|
|
152
117
|
): JSX.Element {
|
|
153
118
|
const {
|
|
154
|
-
lang = defaultLocale,
|
|
119
|
+
lang: langProp = defaultLocale,
|
|
155
120
|
currentPath = "",
|
|
156
121
|
currentVersion,
|
|
157
122
|
currentSlug,
|
|
158
123
|
navSection,
|
|
159
124
|
} = props;
|
|
125
|
+
// Route params arrive as `string`; cast to Locale since keys of settings.locales
|
|
126
|
+
// are always valid locale codes. The prop accepts `Locale | string` so callers
|
|
127
|
+
// without a Locale variable don't need to cast (e.g. _tag-pages.tsx).
|
|
128
|
+
const lang = langProp as Locale;
|
|
160
129
|
|
|
161
|
-
// Root-menu items
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
? t(item.labelKey as Parameters<typeof t>[0], lang)
|
|
166
|
-
: item.label,
|
|
167
|
-
href: navHref(item.path, lang, currentVersion),
|
|
168
|
-
children: item.children?.map((child) => ({
|
|
169
|
-
label: child.labelKey
|
|
170
|
-
? t(child.labelKey as Parameters<typeof t>[0], lang)
|
|
171
|
-
: child.label,
|
|
172
|
-
href: navHref(child.path, lang, currentVersion),
|
|
173
|
-
})),
|
|
174
|
-
}));
|
|
130
|
+
// Root-menu items, locale links, sidebar nodes, and theme mode — all
|
|
131
|
+
// delegated to the shared _nav-data-prep helpers so header and sidebar
|
|
132
|
+
// wrappers stay in sync without duplicating the logic.
|
|
133
|
+
const rootMenuItems = buildRootMenuItems(lang, currentVersion);
|
|
175
134
|
|
|
176
135
|
// Build the mobile sidebar toggle unconditionally — SidebarToggle is rendered
|
|
177
136
|
// on every page (refs #1453); the host CSS hides it where unneeded. When navSection is
|
|
@@ -182,21 +141,11 @@ export function HeaderWithDefaults(
|
|
|
182
141
|
|
|
183
142
|
// Locale-switcher links in the mobile sidebar footer — only when
|
|
184
143
|
// multiple locales are configured (mirrors _sidebar-with-defaults.tsx).
|
|
185
|
-
const localeLinks =
|
|
186
|
-
locales.length > 1 ? buildLocaleLinks(currentPath, lang) : undefined;
|
|
144
|
+
const localeLinks = buildLocaleLinksForNav(currentPath, lang, locales.length);
|
|
187
145
|
|
|
188
|
-
const themeDefaultMode =
|
|
189
|
-
? settings.colorMode.defaultMode
|
|
190
|
-
: undefined;
|
|
146
|
+
const themeDefaultMode = getThemeDefaultMode();
|
|
191
147
|
|
|
192
|
-
|
|
193
|
-
if (navSection !== undefined) {
|
|
194
|
-
const { navDocs, categoryMeta } = loadNavSourceDocs(lang, currentVersion);
|
|
195
|
-
const rawNodes = buildSidebarForSection(navDocs, lang, navSection, categoryMeta);
|
|
196
|
-
sidebarNodes = currentVersion
|
|
197
|
-
? remapVersionedHrefs(rawNodes, currentVersion, lang)
|
|
198
|
-
: rawNodes;
|
|
199
|
-
}
|
|
148
|
+
const sidebarNodes = buildSidebarNodes(lang, navSection, currentVersion);
|
|
200
149
|
|
|
201
150
|
// Wrap SidebarToggle (hamburger button + slide-in aside + SidebarTree) in
|
|
202
151
|
// Island so the SSG output carries the full tree HTML AND the
|
|
@@ -211,8 +160,25 @@ export function HeaderWithDefaults(
|
|
|
211
160
|
// nested as a JSX child its data was dropped during hydration and
|
|
212
161
|
// SidebarToggle re-rendered with `children=undefined`, wiping the SSR
|
|
213
162
|
// tree DOM. zudolab/zudo-doc#1355 wave 13.5.
|
|
163
|
+
//
|
|
164
|
+
// C4 — media-gated hydration. zfb only supports load|idle|visible
|
|
165
|
+
// strategies (no "media" strategy; matchMedia inside the component is too
|
|
166
|
+
// late — props are already emitted, bundle already downloaded).
|
|
167
|
+
// Upstream feature request: Takazudo/zudo-front-builder#969.
|
|
168
|
+
//
|
|
169
|
+
// Best achievable downstream: when="visible" + all SidebarToggle children
|
|
170
|
+
// are lg:hidden, so on desktop the Island wrapper div has zero rendered
|
|
171
|
+
// dimensions. IntersectionObserver fires isIntersecting=false on desktop
|
|
172
|
+
// (zero-size element) → Preact hydrate() is never called. On mobile (and
|
|
173
|
+
// on desktop→mobile resize) the children become visible, the element gains
|
|
174
|
+
// size, IO fires isIntersecting=true, and hydration completes normally.
|
|
175
|
+
//
|
|
176
|
+
// Residual: data-props JSON (~2.4 KB) is still emitted in the SSR HTML on
|
|
177
|
+
// every page regardless of viewport, because it is serialised at build time
|
|
178
|
+
// and not gated by media. Eliminating it requires a zfb "media" hydration
|
|
179
|
+
// strategy — tracked in Takazudo/zudo-front-builder#969.
|
|
214
180
|
const sidebarToggle = Island({
|
|
215
|
-
when: "
|
|
181
|
+
when: "visible",
|
|
216
182
|
children: (
|
|
217
183
|
<SidebarToggle
|
|
218
184
|
nodes={sidebarNodes}
|
|
@@ -225,15 +191,12 @@ export function HeaderWithDefaults(
|
|
|
225
191
|
),
|
|
226
192
|
}) as unknown as VNode;
|
|
227
193
|
|
|
228
|
-
// Wrap the
|
|
194
|
+
// Wrap the bare ThemeToggle in Island({when:"load"}) so the SSG
|
|
229
195
|
// output emits a data-zfb-island="ThemeToggle" marker the hydration
|
|
230
|
-
// runtime can find — matching the documented header contract.
|
|
231
|
-
// package's <ThemeToggle> already does this internally, but importing it
|
|
232
|
-
// forces the v2 theme barrel into the bundle (see import note at the top
|
|
233
|
-
// of this file).
|
|
196
|
+
// runtime can find — matching the documented header contract.
|
|
234
197
|
const themeToggle = Island({
|
|
235
198
|
when: "load",
|
|
236
|
-
children: <ThemeToggle />,
|
|
199
|
+
children: <ThemeToggle defaultMode={themeDefaultMode} />,
|
|
237
200
|
}) as unknown as VNode;
|
|
238
201
|
|
|
239
202
|
// Locale-aware search widget. Renders the full dialog markup in SSR
|
|
@@ -269,7 +232,9 @@ export function HeaderWithDefaults(
|
|
|
269
232
|
);
|
|
270
233
|
// "Latest" entry links to the current page in the latest (unversioned)
|
|
271
234
|
// docs when a slug is available, or falls back to the versions index page.
|
|
272
|
-
|
|
235
|
+
// Null check, not truthiness: "" is the canonical root-index slug (#1891)
|
|
236
|
+
// and must produce real per-version root URLs.
|
|
237
|
+
const latestUrl = currentSlug != null
|
|
273
238
|
? docsUrl(currentSlug, lang)
|
|
274
239
|
: versionsPageUrl;
|
|
275
240
|
|
|
@@ -278,7 +243,7 @@ export function HeaderWithDefaults(
|
|
|
278
243
|
// index — matching the documented version-switcher contract.
|
|
279
244
|
const versionUrls: Record<string, string> = {};
|
|
280
245
|
for (const v of settings.versions) {
|
|
281
|
-
versionUrls[v.slug] = currentSlug
|
|
246
|
+
versionUrls[v.slug] = currentSlug != null
|
|
282
247
|
? versionedDocsUrl(currentSlug, v.slug, lang)
|
|
283
248
|
: versionsPageUrl;
|
|
284
249
|
}
|
|
@@ -51,13 +51,14 @@ export function buildInlineVersionSwitcher(
|
|
|
51
51
|
const versionsPageUrl = withBase(
|
|
52
52
|
isNonDefaultLocale ? `/${locale}/docs/versions` : "/docs/versions",
|
|
53
53
|
);
|
|
54
|
-
|
|
54
|
+
// The docs root index has the canonical empty slug "" (#1891), and
|
|
55
|
+
// docsUrl("")/versionedDocsUrl("") resolve to the per-version docs roots —
|
|
56
|
+
// so an empty slug must NOT fall back to the versions page.
|
|
57
|
+
const latestUrl = docsUrl(slug, locale);
|
|
55
58
|
|
|
56
59
|
const versionUrls: Record<string, string> = {};
|
|
57
60
|
for (const v of settings.versions) {
|
|
58
|
-
versionUrls[v.slug] = slug
|
|
59
|
-
? versionedDocsUrl(slug, v.slug, locale)
|
|
60
|
-
: versionsPageUrl;
|
|
61
|
+
versionUrls[v.slug] = versionedDocsUrl(slug, v.slug, locale);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const labels: VersionSwitcherLabels = {
|