create-zudo-doc 0.2.21 → 0.2.22

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 (71) hide show
  1. package/dist/features/image-enlarge.d.ts +10 -2
  2. package/dist/features/image-enlarge.js +10 -2
  3. package/dist/scaffold.js +20 -22
  4. package/dist/zfb-config-gen.d.ts +16 -10
  5. package/dist/zfb-config-gen.js +34 -239
  6. package/package.json +1 -1
  7. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  8. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  9. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  10. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  11. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  12. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  13. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  15. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  16. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  17. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  18. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  19. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  20. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  21. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  22. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  23. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  24. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  25. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  26. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  27. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  28. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  29. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  30. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  31. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  33. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  34. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  35. package/templates/base/pages/lib/locale-merge.ts +32 -145
  36. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  37. package/templates/base/pages/robots.txt.tsx +2 -26
  38. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  39. package/templates/base/src/components/doc-history.tsx +9 -8
  40. package/templates/base/src/components/image-enlarge.tsx +11 -8
  41. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  42. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  43. package/templates/base/src/components/site-tree-nav.tsx +6 -220
  44. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  45. package/templates/base/src/config/i18n.ts +9 -0
  46. package/templates/base/src/config/z-index-tokens.ts +5 -4
  47. package/templates/base/src/styles/global.css +5 -579
  48. package/templates/base/src/utils/base.ts +1 -1
  49. package/templates/base/src/utils/docs.ts +47 -16
  50. package/templates/base/src/utils/github.ts +12 -9
  51. package/templates/base/src/utils/nav-scope.ts +13 -42
  52. package/templates/base/src/utils/sidebar.ts +18 -86
  53. package/templates/base/src/utils/slug.ts +10 -53
  54. package/templates/base/src/utils/smart-break.tsx +12 -120
  55. package/templates/base/src/utils/tags.ts +25 -68
  56. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  57. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  58. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  59. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  60. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  61. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  62. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  63. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  64. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  65. package/templates/base/plugins/connect-adapter.mjs +0 -169
  66. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  67. package/templates/base/scripts/gen-z-index.mjs +0 -157
  68. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  69. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  70. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  71. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,101 +1,16 @@
1
- // Shared, pure prop-builder helpers for the 4 doc-route paths() functions.
2
- //
3
- // Extracted (#1917) from the near-verbatim paths() bodies of the 4 doc routes.
4
- // These are version- and i18n-AGNOSTIC: every URL is produced by an injected
5
- // `urlFor(slug) => string` closure, so the same code serves latest, locale,
6
- // versioned, and versioned-locale routes without branching on context. The
7
- // route file owns the closure (docsUrl vs versionedDocsUrl bound to its
8
- // version/locale), which is what guarantees a latest-page pagination override
9
- // is resolved + rewritten against the LATEST tree — it can never be rebound to
10
- // a /v/ URL because a latest route never receives a versioned closure
11
- // (#1916 correctness; see the behavioral tests).
12
-
13
- import { flattenTree, findNode, type NavNode } from "@/utils/docs";
14
-
15
- /** The two pagination-override fields read off entry frontmatter. */
16
- export interface PaginationOverrides {
17
- /** `undefined` = no override; `null` = suppress; string = target slug. */
18
- pagination_prev?: string | null;
19
- pagination_next?: string | null;
20
- }
21
-
22
- /**
23
- * Resolve prev/next nav nodes for an entry against the route's OWN nav tree.
24
- *
25
- * - Sequential prev/next come from the flattened sub-tree (`subtreeFlat`).
26
- * - Frontmatter `pagination_prev` / `pagination_next` overrides resolve via
27
- * `findNode(tree, …)` against the SAME `tree` the route built — never a
28
- * foreign tree. The caller passes its own version/locale-scoped tree, so a
29
- * `/v/` override resolves to a `/v/` node and a latest override to a latest
30
- * node. (#1916 — pagination-override must bind to the correct tree.)
31
- *
32
- * Returns the raw NavNodes (hrefs untouched). Callers that need versioned
33
- * hrefs run the result through `rewriteNavHref` with their `urlFor` closure.
34
- */
35
- export function resolveDocPrevNext(
36
- tree: NavNode[],
37
- subtreeFlat: NavNode[],
38
- slug: string,
39
- overrides: PaginationOverrides,
40
- ): { prev: NavNode | null; next: NavNode | null } {
41
- const idx = subtreeFlat.findIndex((n) => n.slug === slug);
42
-
43
- let prev = idx > 0 ? subtreeFlat[idx - 1] ?? null : null;
44
- let next = idx >= 0 && idx < subtreeFlat.length - 1 ? subtreeFlat[idx + 1] ?? null : null;
45
-
46
- if (overrides.pagination_prev !== undefined) {
47
- if (overrides.pagination_prev === null) {
48
- prev = null;
49
- } else {
50
- const found = findNode(tree, overrides.pagination_prev);
51
- prev = found ?? prev;
52
- }
53
- }
54
- if (overrides.pagination_next !== undefined) {
55
- if (overrides.pagination_next === null) {
56
- next = null;
57
- } else {
58
- const found = findNode(tree, overrides.pagination_next);
59
- next = found ?? next;
60
- }
61
- }
62
-
63
- return { prev, next };
64
- }
65
-
66
- /** Flatten the relevant sub-tree for an entry — convenience over flattenTree. */
67
- export function flattenSubtree(subtree: NavNode[]): NavNode[] {
68
- return flattenTree(subtree);
69
- }
70
-
71
- /**
72
- * Rewrite a single nav node's href via the route's `urlFor` closure.
73
- *
74
- * Returns `null` for a `null` node. Latest routes pass `undefined` (leave the
75
- * href as the latest `docsUrl` already baked into the node); versioned routes
76
- * pass `urlFor` so prev/next links point at the versioned URL.
77
- */
78
- export function rewriteNavHref(
79
- node: NavNode | null,
80
- urlFor: ((slug: string) => string) | undefined,
81
- ): NavNode | null {
82
- if (!node) return null;
83
- if (!urlFor) return node;
84
- return { ...node, href: urlFor(node.slug) };
85
- }
86
-
87
- /**
88
- * Remap an auto-index node's child-card hrefs via `urlFor`.
89
- *
90
- * #1916 #2: on versioned routes the children carry latest `docsUrl` hrefs
91
- * (every nav node does — see toNavNodes). They MUST be overridden to the
92
- * versioned URL ALWAYS, not only when `c.href` is missing. Passing `urlFor`
93
- * here does exactly that. Latest routes pass `undefined` to keep the original.
94
- */
95
- export function remapNavChildHrefs(
96
- children: NavNode[],
97
- urlFor: ((slug: string) => string) | undefined,
98
- ): NavNode[] {
99
- if (!urlFor) return children;
100
- return children.map((c) => ({ ...c, href: urlFor(c.slug) }));
101
- }
1
+ // Thin stub — doc-route-paths moved to the package (epic #2344, S6).
2
+ // Re-exports the pure prop-builder helpers from @takazudo/zudo-doc/doc-route-paths.
3
+
4
+ export type {
5
+ DocNavNode,
6
+ PaginationOverrides,
7
+ } from "@takazudo/zudo-doc/doc-route-paths";
8
+
9
+ export {
10
+ flattenTree,
11
+ findNode,
12
+ flattenSubtree,
13
+ resolveDocPrevNext,
14
+ rewriteNavHref,
15
+ remapNavChildHrefs,
16
+ } from "@takazudo/zudo-doc/doc-route-paths";
@@ -1,44 +1,15 @@
1
1
  /** @jsxRuntime automatic */
2
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 (doc-tags placement
7
- // — after-title, between the date block and description paragraph).
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";
3
+ // Host thin-stub see @takazudo/zudo-doc/doc-tags-area (epic #2344, S7).
22
4
  import { settings } from "@/config/settings";
23
5
  import { defaultLocale, t } from "@/config/i18n";
24
6
  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
- // ---------------------------------------------------------------------------
7
+ import { tagVocabulary } from "@/config/tag-vocabulary";
8
+ import { createDocTagsArea } from "@takazudo/zudo-doc/doc-tags-area";
31
9
 
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
- * 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").
41
- */
10
+ // Inlined from the original _doc-tags-area.tsx `tagHref` helper.
11
+ // Builds the base-prefixed tag detail page href for the given locale.
12
+ // The tag segment is URL-encoded at the href site only.
42
13
  function tagHref(tag: string, locale: string): string {
43
14
  const encoded = encodeURIComponent(tag);
44
15
  const path =
@@ -48,46 +19,12 @@ function tagHref(tag: string, locale: string): string {
48
19
  return withBase(path);
49
20
  }
50
21
 
51
- // ---------------------------------------------------------------------------
52
- // Component
53
- // ---------------------------------------------------------------------------
54
-
55
- interface DocTagsAreaProps {
56
- /** Page slug, e.g. "guides/sidebar". */
57
- slug: string;
58
- /** Active locale string, e.g. "en", "ja". */
59
- locale: string;
60
- /** Raw tag strings from the page frontmatter (entry.data.tags). */
61
- tags: readonly string[] | undefined;
62
- }
63
-
64
- /**
65
- * Renders the page-level tag chip block when `settings.docTags` is enabled
66
- * and the page has at least one resolved (non-deprecated) tag.
67
- *
68
- * Returns null when `docTags` is disabled, the page has no tags, or all
69
- * raw tags resolve to deprecated entries.
70
- *
71
- * Placement is "after-title" (between the date block and description paragraph).
72
- */
73
- export function DocTagsArea({ locale, tags }: DocTagsAreaProps): VNode | null {
74
- if (!settings.docTags) return null;
75
-
76
- const rawTags = tags ?? [];
77
- const canonicalTags = resolvePageTags(rawTags);
78
- if (canonicalTags.length === 0) return null;
22
+ export type { DocTagsAreaProps } from "@takazudo/zudo-doc/doc-tags-area";
79
23
 
80
- const resolvedTags = canonicalTags.map((tag) => ({
81
- tag,
82
- href: tagHref(tag, locale),
83
- }));
84
-
85
- return (
86
- <DocTags
87
- placement="after-title"
88
- tags={resolvedTags}
89
- tagsLabel={t("doc.tags", locale)}
90
- taggedWithLabel={t("doc.taggedWith", locale)}
91
- />
92
- );
93
- }
24
+ export const DocTagsArea = createDocTagsArea({
25
+ settings,
26
+ defaultLocale,
27
+ tagVocabularyEntries: tagVocabulary,
28
+ tagHref,
29
+ t,
30
+ });
@@ -1,30 +1,9 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // Locale-aware Footer wrapper for the zfb doc pages.
4
- //
5
- // Footer data-prep utilities reads settings.footer, localizes link
6
- // hrefs and titles, and optionally collects tag columns when taglist is
7
- // enabled — then feeds the result into the presentational <Footer> shell
8
- // from @takazudo/zudo-doc/footer.
9
- //
10
- // Callers pass a `lang` prop (the active locale string, e.g. "en", "ja").
11
- // The component returns a fully populated <Footer> when settings.footer is
12
- // configured, or a bare <Footer /> shell when it is not (the shell still
13
- // emits the contentinfo ARIA landmark).
14
- //
15
- // Data-prep helpers used:
16
- // settings.footer — link columns, copyright, taglist config
17
- // isExternal / resolveHref / withBase — href normalization
18
- // defaultLocale — determines when locale prefix is needed
19
- // tagVocabulary — group-by ordering for grouped taglist mode
20
- // collectTags — builds tag → { count, docs } map
21
- // toRouteSlug — derives route slug from collection id
22
- // loadDocs / filterDrafts — synchronous zfb collection helpers (ADR-004)
23
-
24
- import type { VNode } from "preact";
3
+ // Host thin-stub see @takazudo/zudo-doc/footer-with-defaults (epic #2344, S5).
4
+ // Wires host singletons into the package factory and passes the tag loading
5
+ // function as a dependency so the factory never imports @/ aliases.
25
6
  import { settings } from "@/config/settings";
26
- import { Footer } from "@takazudo/zudo-doc/footer";
27
- import type { FooterLinkColumn, FooterTagColumn } from "@takazudo/zudo-doc/footer";
28
7
  import { isExternal, resolveHref, withBase } from "@/utils/base";
29
8
  import { defaultLocale } from "@/config/i18n";
30
9
  import { tagVocabulary } from "@/config/tag-vocabulary";
@@ -33,214 +12,47 @@ import { toRouteSlug } from "@/utils/slug";
33
12
  import { mergeLocaleDocs } from "./locale-merge";
34
13
  import { stableDocs, memoizeDerived } from "./_nav-source-cache";
35
14
  import type { DocsEntry } from "@/types/docs-entry";
15
+ import { createFooterWithDefaults } from "@takazudo/zudo-doc/footer-with-defaults";
36
16
 
37
17
  // ---------------------------------------------------------------------------
38
- // Internal helpers
18
+ // Tag loading (host-side, reads collections via stableDocs / memoizeDerived)
39
19
  // ---------------------------------------------------------------------------
40
20
 
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
- function localizeHref(href: string, lang: string): string {
46
- if (isExternal(href)) return href;
47
- if (lang !== defaultLocale) {
48
- const path = href.startsWith("/") ? href : `/${href}`;
49
- return resolveHref(`/${lang}${path}`);
21
+ function loadTagsForLocale(lang: string) {
22
+ if (lang === defaultLocale) {
23
+ const baseDocs = stableDocs("docs");
24
+ return memoizeDerived([baseDocs], "footerTaglist;default", () => {
25
+ const docs: DocsEntry[] = baseDocs.filter(
26
+ (d) => !d.data.draft && !d.data.unlisted && !d.data.category_no_page,
27
+ );
28
+ const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
29
+ return [...tagMap.values()].sort((a, b) => a.tag.localeCompare(b.tag, lang));
30
+ });
50
31
  }
51
- return resolveHref(href);
52
- }
53
-
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
- */
60
- function tagHref(tag: string, lang: string): string {
61
- const encoded = encodeURIComponent(tag);
62
- const path =
63
- lang === defaultLocale
64
- ? `/docs/tags/${encoded}`
65
- : `/${lang}/docs/tags/${encoded}`;
66
- return withBase(path);
32
+ const baseDocs = stableDocs("docs");
33
+ const localeDocs = stableDocs(`docs-${lang}`);
34
+ return memoizeDerived([baseDocs, localeDocs], `footerTaglist;${lang}`, () => {
35
+ const result = mergeLocaleDocs({
36
+ baseDocs: baseDocs.filter((d) => !d.data.draft),
37
+ localeDocs: localeDocs.filter((d) => !d.data.draft),
38
+ applyDefaultLocaleOnlyFilter: true,
39
+ });
40
+ const docs: DocsEntry[] = result.docs.filter((d) => !d.data.category_no_page);
41
+ const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
42
+ return [...tagMap.values()].sort((a, b) => a.tag.localeCompare(b.tag, lang));
43
+ });
67
44
  }
68
45
 
69
46
  // ---------------------------------------------------------------------------
70
- // Component
47
+ // Thin-stub export
71
48
  // ---------------------------------------------------------------------------
72
49
 
73
- interface FooterWithDefaultsProps {
74
- /** Active locale string, e.g. "en", "ja". Defaults to defaultLocale. */
75
- lang?: string;
76
- }
77
-
78
- /**
79
- * Locale-aware Footer wrapper.
80
- *
81
- * Reads settings.footer and assembles the linkColumns / tagColumns /
82
- * copyright props expected by the presentational <Footer> shell. When
83
- * settings.footer is false, a bare <Footer /> shell is returned (the
84
- * contentinfo ARIA landmark is still present).
85
- */
86
- export function FooterWithDefaults({
87
- lang = defaultLocale,
88
- }: FooterWithDefaultsProps): VNode {
89
- const footer = settings.footer;
90
-
91
- // Locale-keyed persist key: same-locale swaps preserve DOM identity;
92
- // cross-locale swaps discard the stale footer and re-render. (#1546)
93
- const persistKey = `footer-${lang}`;
94
-
95
- // When footer is not configured, return the bare shell so the
96
- // contentinfo ARIA landmark is present.
97
- if (!footer) {
98
- return <Footer persistKey={persistKey} />;
99
- }
100
-
101
- const { links, copyright, taglist } = footer;
102
-
103
- // ── Link columns ────────────────────────────────────────────────────────
104
-
105
- const linkColumns: FooterLinkColumn[] = links.map((column) => ({
106
- title: (column.locales as Record<string, { title: string }> | undefined)?.[lang]?.title ?? column.title,
107
- items: column.items.map((item) => ({
108
- label: (item.locales as Record<string, { label: string }> | undefined)?.[lang]?.label ?? item.label,
109
- href: localizeHref(item.href, lang),
110
- isExternal: isExternal(item.href),
111
- })),
112
- }));
113
-
114
- // ── Tag columns (optional) ───────────────────────────────────────────────
115
-
116
- let tagColumns: FooterTagColumn[] = [];
117
-
118
- if (taglist?.enabled) {
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
- }
135
- // Apply the default-locale-only filter so the footer taglist only counts
136
- // tags that have a locale-routable tag page — matching the tag-route
137
- // pages ([tag].tsx / tags/index.tsx) and enumerateTagsRoutes, which all
138
- // now filter. Without this, the footer would link to /{locale}/docs/tags/
139
- // pages that are never built for tags living only on default-locale-only
140
- // prefix pages. category_no_page is dropped for the same reason — AFTER
141
- // the merge, so a locale override carrying the flag first wins the merge
142
- // (pre-merge filtering would let the unflagged base doc resurface).
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));
154
- });
155
- })();
156
-
157
- const vocabularyActive =
158
- Boolean(settings.tagVocabulary) && settings.tagGovernance !== "off";
159
- const requestedGroupBy = taglist.groupBy ?? "group";
160
- const effectiveGroupBy = vocabularyActive ? requestedGroupBy : "flat";
161
-
162
- const localeOverrides = (taglist.locales as Record<string, { title?: string; groupTitles?: Record<string, string> }> | undefined)?.[lang];
163
- const groupTitles: Record<string, string> = {
164
- ...taglist.groupTitles,
165
- ...localeOverrides?.groupTitles,
166
- };
167
- const flatTitle = localeOverrides?.title ?? taglist.title ?? "Tags";
168
-
169
- if (effectiveGroupBy === "flat" || !vocabularyActive) {
170
- if (allTags.length > 0) {
171
- tagColumns = [
172
- {
173
- group: "__flat__",
174
- title: flatTitle,
175
- tags: allTags.map(({ tag, count }) => ({
176
- tag,
177
- count,
178
- href: tagHref(tag, lang),
179
- })),
180
- },
181
- ];
182
- }
183
- } else {
184
- // Grouped mode: one column per vocabulary group, in declaration order.
185
- const groupByCanonical = new Map<string, string>();
186
- const groupOrder: string[] = [];
187
- const seenGroups = new Set<string>();
188
- for (const entry of tagVocabulary) {
189
- if (!entry.group) continue;
190
- groupByCanonical.set(entry.id, entry.group);
191
- if (!seenGroups.has(entry.group)) {
192
- seenGroups.add(entry.group);
193
- groupOrder.push(entry.group);
194
- }
195
- }
196
-
197
- const buckets = new Map<string, typeof allTags>();
198
- for (const group of groupOrder) buckets.set(group, []);
199
- const ungrouped: typeof allTags = [];
200
-
201
- for (const info of allTags) {
202
- const group = groupByCanonical.get(info.tag);
203
- if (group && buckets.has(group)) {
204
- buckets.get(group)!.push(info);
205
- } else {
206
- ungrouped.push(info);
207
- }
208
- }
209
-
210
- tagColumns = groupOrder
211
- .filter((g) => (buckets.get(g)?.length ?? 0) > 0)
212
- .map((g) => ({
213
- group: g,
214
- title:
215
- groupTitles[g] ??
216
- g.charAt(0).toUpperCase() + g.slice(1),
217
- tags: buckets.get(g)!.map(({ tag, count }) => ({
218
- tag,
219
- count,
220
- href: tagHref(tag, lang),
221
- })),
222
- }));
223
-
224
- if (ungrouped.length > 0) {
225
- tagColumns.push({
226
- group: "__flat__",
227
- title: flatTitle,
228
- tags: ungrouped.map(({ tag, count }) => ({
229
- tag,
230
- count,
231
- href: tagHref(tag, lang),
232
- })),
233
- });
234
- }
235
- }
236
- }
237
-
238
- return (
239
- <Footer
240
- linkColumns={linkColumns}
241
- tagColumns={tagColumns}
242
- copyright={copyright}
243
- persistKey={persistKey}
244
- />
245
- );
246
- }
50
+ export const FooterWithDefaults = createFooterWithDefaults({
51
+ settings,
52
+ defaultLocale,
53
+ tagVocabulary,
54
+ isExternal,
55
+ resolveHref,
56
+ withBase,
57
+ loadTagsForLocale,
58
+ });
@@ -17,37 +17,11 @@
17
17
  // are wired by the page template directly on the `<FrontmatterPreview>` call site
18
18
  // via the `renderers` prop. This helper only produces the filtered entries array.
19
19
 
20
+ import { createBuildFrontmatterPreviewEntries } from "@takazudo/zudo-doc/frontmatter-preview-data";
20
21
  import { settings } from "@/config/settings";
21
22
  import { DEFAULT_FRONTMATTER_IGNORE_KEYS } from "@/config/frontmatter-preview-defaults";
22
23
 
23
- export function buildFrontmatterPreviewEntries(
24
- data: Record<string, unknown> | null | undefined,
25
- ): Array<[string, unknown]> {
26
- if (!data) return [];
27
-
28
- // `frontmatterPreview` may be absent (undefined) in fixtures that
29
- // don't opt into the feature. `false` means the host explicitly
30
- // disabled the block. Both should produce an empty entries array.
31
- const config = (settings as { frontmatterPreview?: unknown })
32
- .frontmatterPreview;
33
- if (config === false || config === undefined) return [];
34
-
35
- const cfg = config as {
36
- ignoreKeys?: string[];
37
- extraIgnoreKeys?: string[];
38
- };
39
- const ignoreSet = new Set<string>(
40
- cfg.ignoreKeys ?? [
41
- ...DEFAULT_FRONTMATTER_IGNORE_KEYS,
42
- ...(cfg.extraIgnoreKeys ?? []),
43
- ],
44
- );
45
-
46
- const entries: Array<[string, unknown]> = [];
47
- for (const [key, value] of Object.entries(data)) {
48
- if (ignoreSet.has(key)) continue;
49
- if (value === null || value === undefined) continue;
50
- entries.push([key, value]);
51
- }
52
- return entries;
53
- }
24
+ export const buildFrontmatterPreviewEntries = createBuildFrontmatterPreviewEntries({
25
+ frontmatterPreview: settings.frontmatterPreview,
26
+ defaultIgnoreKeys: DEFAULT_FRONTMATTER_IGNORE_KEYS,
27
+ });