create-zudo-doc 0.2.21 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/compose.d.ts +6 -3
  2. package/dist/compose.js +6 -4
  3. package/dist/features/image-enlarge.d.ts +22 -12
  4. package/dist/features/image-enlarge.js +23 -191
  5. package/dist/scaffold.d.ts +11 -0
  6. package/dist/scaffold.js +51 -24
  7. package/dist/settings-gen.js +4 -0
  8. package/dist/zfb-config-gen.d.ts +16 -10
  9. package/dist/zfb-config-gen.js +34 -239
  10. package/package.json +1 -1
  11. package/templates/base/pages/_mdx-components.ts +64 -185
  12. package/templates/base/pages/index.tsx +1 -1
  13. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  14. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  15. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  16. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  17. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  18. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  19. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  20. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  21. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  22. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  23. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  24. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  25. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  26. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  27. package/templates/base/pages/lib/_extract-headings.ts +21 -295
  28. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  29. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  30. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  31. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  32. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  33. package/templates/base/pages/lib/_math-block.tsx +4 -63
  34. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  35. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  36. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  37. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  38. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  39. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
  40. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  41. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  42. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  43. package/templates/base/pages/lib/locale-merge.ts +32 -145
  44. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  45. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  46. package/templates/base/src/components/content/code-group.tsx +3 -76
  47. package/templates/base/src/components/content/content-admonition.tsx +4 -56
  48. package/templates/base/src/components/doc-history.tsx +9 -8
  49. package/templates/base/src/components/image-enlarge.tsx +11 -8
  50. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  51. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  52. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  53. package/templates/base/src/config/i18n.ts +9 -0
  54. package/templates/base/src/config/settings-types.ts +34 -172
  55. package/templates/base/src/config/z-index-tokens.ts +5 -4
  56. package/templates/base/src/styles/global.css +40 -587
  57. package/templates/base/src/utils/base.ts +1 -1
  58. package/templates/base/src/utils/docs.ts +47 -16
  59. package/templates/base/src/utils/github.ts +12 -9
  60. package/templates/base/src/utils/nav-scope.ts +13 -42
  61. package/templates/base/src/utils/sidebar.ts +18 -86
  62. package/templates/base/src/utils/slug.ts +10 -53
  63. package/templates/base/src/utils/smart-break.tsx +12 -120
  64. package/templates/base/src/utils/tags.ts +25 -68
  65. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  66. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  67. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  68. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  69. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  70. package/templates/features/i18n/files/pages/[locale]/index.tsx +1 -1
  71. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  72. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  73. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  74. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  75. package/templates/base/pages/404.tsx +0 -61
  76. package/templates/base/pages/robots.txt.tsx +0 -29
  77. package/templates/base/pages/sitemap.xml.tsx +0 -59
  78. package/templates/base/plugins/connect-adapter.mjs +0 -169
  79. package/templates/base/plugins/copy-public-plugin.mjs +0 -58
  80. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  81. package/templates/base/scripts/gen-z-index.mjs +0 -157
  82. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  83. package/templates/base/src/components/site-tree-nav.tsx +0 -220
  84. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  85. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  86. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +0 -59
  87. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +0 -39
  88. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +0 -43
  89. package/templates/features/docTags/files/pages/docs/tags/index.tsx +0 -20
  90. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
  91. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +0 -48
  92. package/templates/features/versioning/files/pages/docs/versions.tsx +0 -20
@@ -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
+ });
@@ -1,147 +1,22 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // og:title / og:description / color-scheme head injection for the zfb doc pages.
4
- //
5
- // Why this wrapper exists: the v2 `<DocLayout>` shell exposes a `head` slot
6
- // but intentionally does NOT emit og:* meta or mount `<ColorSchemeProvider>` —
7
- // that is the host's responsibility.
8
- //
9
- // Without OgTags the SSG output is missing og:title / og:description,
10
- // which crawlers and link-preview tools rely on. Without ColorSchemeProvider
11
- // the runtime `:root { --zd-* }` palette is missing, so every component that
12
- // resolves a color via `--zd-*` (search match-keyword highlight, image-overlay,
13
- // etc.) falls back to UA defaults — and the smoke-search "matched
14
- // keywords" regression guard at e2e/smoke-search.spec.ts:167 fires
15
- // because `getComputedStyle(root).getPropertyValue("--zd-matched-keyword-bg")`
16
- // returns "" instead of the resolved palette token.
17
- //
18
- // (#1355 wave 13 — ColorSchemeProvider mount restored after the retirement of
19
- // the zfb cutover's initial setup.)
20
-
21
- import type { JSX } from "preact";
22
- import { OgTags, TwitterCard } from "@takazudo/zudo-doc/head";
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.
30
- import ColorSchemeProvider from "@takazudo/zudo-doc/theme/color-scheme-provider";
31
- import { composeMetaTitle } from "./_compose-meta-title";
32
- import { withBase, absoluteUrl } from "@/utils/base";
3
+ // Host thin-stub see @takazudo/zudo-doc/head-with-defaults (epic #2344, S5).
33
4
  import { settings } from "@/config/settings";
34
- // W3B (#1730): cssText + colorMode are precomputed here — the v2
35
- // ColorSchemeProvider no longer reaches into the host config tree.
5
+ import { withBase, absoluteUrl } from "@/utils/base";
36
6
  import {
37
7
  generateCssCustomProperties,
38
8
  generateLightDarkCssProperties,
39
9
  } from "@/config/color-scheme-utils";
10
+ import { composeMetaTitle } from "./_compose-meta-title";
11
+ import { createHeadWithDefaults } from "@takazudo/zudo-doc/head-with-defaults";
40
12
 
41
- export interface HeadWithDefaultsProps {
42
- /** Page title forwarded to og:title. Required. */
43
- title: string;
44
- /** Optional page description forwarded to og:description. */
45
- description?: string;
46
- /**
47
- * Absolute canonical URL for this page. When supplied, emits
48
- * <link rel="canonical" href="...">. Compute via `absoluteUrl(pageUrl)`
49
- * (@/utils/base) in each host page; it returns undefined when
50
- * settings.siteUrl is empty so the link is simply omitted.
51
- */
52
- canonical?: string;
53
- }
54
-
55
- /**
56
- * Default-bearing host wrapper that injects og:title / og:description,
57
- * the ColorSchemeProvider (`:root { --zd-* }` palette + theme bootstrap),
58
- * the favicon link, and an optional canonical link into the v2 layout's
59
- * `head` slot.
60
- *
61
- * og:title is run through composeMetaTitle so it matches the
62
- * "<title> | <siteName>" shape emitted by the host's <title> element
63
- * (the legacy Astro layout produced both shapes; the zfb host has to
64
- * compose them itself).
65
- *
66
- * og:title is always emitted — it is the unconditional DocHead contract
67
- * (OgTags always emits og:title regardless of settings). All other tags
68
- * are gated by settings.metaTags.
69
- *
70
- * Pure SSR — no state, no client-only imports. Intended for use as:
71
- * head={<HeadWithDefaults title={title} description={description} canonical={canonical} />}
72
- * on every DocLayoutWithDefaults call site in the host pages.
73
- */
74
- export function HeadWithDefaults({
75
- title,
76
- description,
77
- canonical,
78
- }: HeadWithDefaultsProps): JSX.Element {
79
- const { metaTags } = settings;
80
-
81
- // og:image / twitter:image must be absolute URLs — crawlers silently drop
82
- // relative og:image values. absoluteUrl joins siteUrl (no trailing slash) +
83
- // the base-prefixed asset path, and returns undefined when siteUrl is empty
84
- // (e.g. a freshly scaffolded create-zudo-doc project that hasn't configured
85
- // siteUrl yet) so we never ship a useless relative og:image. OgTags /
86
- // TwitterCard already gate their image emission on the prop being defined;
87
- // the og:image:* companion tags below are gated explicitly because they
88
- // would dangle without the parent og:image.
89
- const ogImageUrl =
90
- metaTags.ogImage !== false
91
- ? absoluteUrl(withBase(metaTags.ogImage))
92
- : undefined;
93
-
94
- // Resolve the palette CSS body once per page render (the v2 component
95
- // is pure SSR — no caching needed).
96
- const colorMode = settings.colorMode ? settings.colorMode : null;
97
- const cssText = colorMode
98
- ? generateLightDarkCssProperties()
99
- : generateCssCustomProperties();
13
+ export type { HeadWithDefaultsProps } from "@takazudo/zudo-doc/head-with-defaults";
100
14
 
101
- return (
102
- <>
103
- <OgTags
104
- title={composeMetaTitle(title)}
105
- description={metaTags.description ? description : undefined}
106
- ogType="website"
107
- ogUrl={canonical}
108
- ogImage={ogImageUrl}
109
- ogSiteName={metaTags.ogSiteName ? settings.siteName : undefined}
110
- />
111
- {metaTags.keywords !== false && metaTags.keywords.length > 0 && (
112
- <meta name="keywords" content={metaTags.keywords} />
113
- )}
114
- {/* og:image:width / og:image:height / og:image:alt — not in OgTags API;
115
- emitted here directly to avoid expanding the shared HeadProps surface.
116
- Standard 1200×630 social preview dimensions. Gated on ogImageUrl so
117
- the companion tags don't dangle when og:image itself was suppressed. */}
118
- {ogImageUrl !== undefined && (
119
- <>
120
- <meta property="og:image:width" content="1200" />
121
- <meta property="og:image:height" content="630" />
122
- <meta property="og:image:alt" content={composeMetaTitle(title)} />
123
- </>
124
- )}
125
- {metaTags.twitterCard !== false && (
126
- <TwitterCard
127
- card={metaTags.twitterCard}
128
- image={ogImageUrl}
129
- site={metaTags.twitterSite}
130
- creator={metaTags.twitterCreator}
131
- />
132
- )}
133
- <ColorSchemeProvider cssText={cssText} colorMode={colorMode} />
134
- {/* Pre-paint inline script: restore persisted sidebar width to
135
- --zd-sidebar-w on :root before first paint, so a reload after
136
- drag-resizing the sidebar doesn't snap back to the CSS default
137
- clamp() width. Mirrors the sibling sidebar-toggle restore
138
- script emitted from the page's afterSidebar slot. */}
139
- {settings.sidebarResizer && <script dangerouslySetInnerHTML={{ __html: SIDEBAR_RESIZER_RESTORE_SCRIPT }} />}
140
- {/* favicon set — withBase() handles the configured base path prefix */}
141
- <link rel="icon" href={withBase("/favicon.ico")} sizes="any" />
142
- <link rel="icon" type="image/png" sizes="32x32" href={withBase("/favicon-32x32.png")} />
143
- <link rel="icon" type="image/png" sizes="16x16" href={withBase("/favicon-16x16.png")} />
144
- {canonical !== undefined && <link rel="canonical" href={canonical} />}
145
- </>
146
- );
147
- }
15
+ export const HeadWithDefaults = createHeadWithDefaults({
16
+ settings,
17
+ composeMetaTitle,
18
+ withBase,
19
+ absoluteUrl,
20
+ generateCssCustomProperties,
21
+ generateLightDarkCssProperties,
22
+ });