@takazudo/zudo-doc 5.20.0 → 5.22.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 (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/compiled.css +72 -0
  3. package/dist/config.d.ts +12 -1
  4. package/dist/config.js +3 -1
  5. package/dist/content.css +55 -0
  6. package/dist/doc-history-area/index.d.ts +2 -0
  7. package/dist/doc-history-area/index.js +5 -4
  8. package/dist/doc-metainfo-area/index.d.ts +2 -0
  9. package/dist/doc-metainfo-area/index.js +8 -3
  10. package/dist/factory-context/index.d.ts +2 -0
  11. package/dist/home-intro/index.d.ts +10 -0
  12. package/dist/home-intro/index.js +20 -0
  13. package/dist/home-intro/prepare.d.ts +9 -0
  14. package/dist/home-intro/prepare.js +93 -0
  15. package/dist/home-intro/resolve.d.ts +5 -0
  16. package/dist/home-intro/resolve.js +10 -0
  17. package/dist/home-intro/types.d.ts +10 -0
  18. package/dist/home-intro/types.js +0 -0
  19. package/dist/home-page/index.js +42 -22
  20. package/dist/i18n-defaults/index.js +2 -0
  21. package/dist/plugins/doc-history.js +7 -3
  22. package/dist/plugins/img-src-check.d.ts +3 -0
  23. package/dist/plugins/img-src-check.js +21 -0
  24. package/dist/plugins/internal/claude-resources/generate.js +2 -2
  25. package/dist/plugins/internal/doc-history/index.d.ts +6 -1
  26. package/dist/plugins/internal/doc-history/index.js +1 -0
  27. package/dist/plugins/internal/img-src-check/index.d.ts +65 -0
  28. package/dist/plugins/internal/img-src-check/index.js +179 -0
  29. package/dist/plugins/internal/resource-docs-shared/index.d.ts +1 -1
  30. package/dist/plugins/internal/resource-docs-shared/index.js +6 -1
  31. package/dist/plugins/internal/resource-docs-shared/links.d.ts +6 -1
  32. package/dist/plugins/internal/resource-docs-shared/links.js +21 -3
  33. package/dist/plugins/internal/resource-docs-shared/skills.js +27 -5
  34. package/dist/plugins/routes.js +4 -1
  35. package/dist/preset.d.ts +9 -1
  36. package/dist/preset.js +16 -2
  37. package/dist/route-context/index.js +1 -0
  38. package/dist/route-context-payload/index.d.ts +2 -0
  39. package/dist/route-context-payload/index.js +2 -1
  40. package/dist/route-context-payload/types.d.ts +2 -0
  41. package/dist/safelist.css +1 -1
  42. package/dist/settings.d.ts +12 -0
  43. package/package.json +15 -2
  44. package/routes-src/_virtual.d.ts +3 -1
  45. package/virtual-modules.d.ts +3 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,29 @@ The format is based on Keep a Changelog, and release notes are generated from th
8
8
 
9
9
  No unreleased changes.
10
10
 
11
+ ## [5.22.0] - 2026-09-11
12
+
13
+ ### Features
14
+
15
+ - Added `docMetainfoFields` to select the metadata fields displayed on documentation pages. It defaults to all fields; an empty array hides them all, and `["updated"]` displays Updated even when Created has the same formatted date. (`06044fa30`, `3832e08af`)
16
+ - Added `docHistoryUi: false` to retain generated git metadata while disabling the history viewer, history JSON generation, and development proxy. Configured view-source links remain available. (`84cd63af5`)
17
+ - Added a post-build check for missing site-absolute `<img src>` files, using the existing `onBrokenMarkdownLinks` severity setting. (`9cc259d65`)
18
+
19
+ ### Bug Fixes
20
+
21
+ - Mirrored Claude commands and agents and shared Claude/Codex skills now render repository-relative links as labels when their targets are not emitted. Code examples and links to emitted skill-body sub-pages are preserved. (`bb6d0390c`)
22
+
23
+ ## [5.21.0] - 2026-09-09
24
+
25
+ ### Features
26
+
27
+ - Added compact Markdown introductions to the home pages. `home.introMarkdown` renders serializable Markdown below the home identity on `/`, and each non-default locale can override it with `LocaleConfig.introMarkdown`. Empty values keep the previous layout, so existing sites are unaffected. (`77ed96b3f`, `8e10ecbc1`)
28
+ - Added `home.sitemapHeading` and `LocaleConfig.sitemapHeading` to override the home sitemap section title. A blank value restores the translated package default. (`77ed96b3f`)
29
+
30
+ ### Bug Fixes
31
+
32
+ - Rendered the home sitemap and tag section headings through one shared component, so both align with the full-width content band instead of the sitemap heading sitting on the narrower inner column. (`849e04a57`)
33
+
11
34
  ## [5.20.0] - 2026-09-09
12
35
 
13
36
  ### Features
package/dist/compiled.css CHANGED
@@ -2155,6 +2155,11 @@
2155
2155
  max-width: 52.5rem;
2156
2156
  }
2157
2157
  }
2158
+ .lg\:flex-1 {
2159
+ @media (width >= 1024px) {
2160
+ flex: 1;
2161
+ }
2162
+ }
2158
2163
  .lg\:grid-cols-3 {
2159
2164
  @media (width >= 1024px) {
2160
2165
  grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -2727,6 +2732,73 @@ a.text-accent.underline {
2727
2732
  border-radius: var(--zdc-admonition-radius, 0 var(--radius-DEFAULT) var(--radius-DEFAULT) 0);
2728
2733
  border-left-width: var(--zdc-admonition-border-width, 4px);
2729
2734
  }
2735
+ .zd-home-inner {
2736
+ inline-size: 100%;
2737
+ max-inline-size: 60rem;
2738
+ min-inline-size: 0;
2739
+ margin-inline: auto;
2740
+ overflow-wrap: anywhere;
2741
+ }
2742
+ .zd-home-copy, .zd-home-links {
2743
+ min-inline-size: 0;
2744
+ }
2745
+ .zd-home-links {
2746
+ overflow-wrap: anywhere;
2747
+ }
2748
+ .zd-home-rule {
2749
+ inline-size: 100%;
2750
+ margin-block: var(--spacing-vsp-xl);
2751
+ margin-inline: 0;
2752
+ border: 0;
2753
+ border-block-start: 1px solid var(--color-muted);
2754
+ }
2755
+ .zd-compact-prose {
2756
+ min-inline-size: 0;
2757
+ text-align: left;
2758
+ font-size: 1rem;
2759
+ line-height: 1.75;
2760
+ overflow-wrap: anywhere;
2761
+ }
2762
+ .zd-compact-prose > :where(* + *) {
2763
+ margin-top: var(--flow-space, 1rem);
2764
+ }
2765
+ .zd-compact-prose :where(h2, h3, h4, h5, h6) {
2766
+ font-weight: var(--font-weight-bold, 700);
2767
+ line-height: 1.4;
2768
+ padding: 0;
2769
+ border: 0;
2770
+ background: none;
2771
+ }
2772
+ .zd-compact-prose :where(h2) {
2773
+ font-size: 1.3rem;
2774
+ --flow-space: 1.75rem;
2775
+ }
2776
+ .zd-compact-prose :where(h3) {
2777
+ font-size: 1.125rem;
2778
+ --flow-space: 1.5rem;
2779
+ }
2780
+ .zd-compact-prose :where(h4) {
2781
+ font-size: 1rem;
2782
+ --flow-space: 1.25rem;
2783
+ }
2784
+ .zd-compact-prose :where(h5, h6) {
2785
+ font-size: .9375rem;
2786
+ --flow-space: 1.25rem;
2787
+ }
2788
+ .zd-compact-prose > :where(h2, h3, h4, h5, h6) + * {
2789
+ margin-top: .5rem;
2790
+ }
2791
+ .zd-compact-prose > :first-child {
2792
+ margin-top: 0;
2793
+ }
2794
+ .zd-compact-prose :where(pre) {
2795
+ max-inline-size: 100%;
2796
+ overflow-x: auto;
2797
+ }
2798
+ .zd-compact-prose :where(img) {
2799
+ max-inline-size: 100%;
2800
+ block-size: auto;
2801
+ }
2730
2802
  .page-loading-overlay {
2731
2803
  position: fixed;
2732
2804
  inset: 0;
package/dist/config.d.ts CHANGED
@@ -143,7 +143,7 @@ export interface ZudoDocConfig {
143
143
  /**
144
144
  * Package-owned home-page layout. Set `wide: true` to let the category grid
145
145
  * fill most of the viewport on both `/` and locale home routes.
146
- * @default { wide: false }
146
+ * @default { wide: false, introMarkdown: "", sitemapHeading: "" }
147
147
  */
148
148
  home?: HomeConfig;
149
149
  /**
@@ -249,6 +249,11 @@ export interface ZudoDocConfig {
249
249
  * @default false
250
250
  */
251
251
  docMetainfo?: boolean;
252
+ /**
253
+ * Metadata fields to show in the doc metadata area.
254
+ * @default ["created", "updated", "author"]
255
+ */
256
+ docMetainfoFields?: Array<"created" | "updated" | "author">;
252
257
  /**
253
258
  * Enable the `/docs/tags` + `/docs/tags/[tag]` tag index routes.
254
259
  * @default false
@@ -378,6 +383,12 @@ export interface ZudoDocConfig {
378
383
  * @default false
379
384
  */
380
385
  docHistory?: boolean;
386
+ /**
387
+ * Enable the doc history dropdown UI, history JSON generation, and dev
388
+ * history proxy while retaining the history metadata manifest.
389
+ * @default true
390
+ */
391
+ docHistoryUi?: boolean;
381
392
  /**
382
393
  * Glob patterns matched against the doc slug (path minus extension, `/index`
383
394
  * stripped, root = `index`) that exclude matching pages from git-history
package/dist/config.js CHANGED
@@ -45,7 +45,7 @@ const DEFAULT_SETTINGS = {
45
45
  favicon: void 0,
46
46
  base: "/",
47
47
  trailingSlash: false,
48
- home: { wide: false },
48
+ home: { wide: false, introMarkdown: "", sitemapHeading: "" },
49
49
  siteTreeNavIgnore: [],
50
50
  minifyHtml: true,
51
51
  docsDir: "src/content/docs",
@@ -68,6 +68,7 @@ const DEFAULT_SETTINGS = {
68
68
  },
69
69
  sitemap: false,
70
70
  docMetainfo: false,
71
+ docMetainfoFields: ["created", "updated", "author"],
71
72
  docTags: false,
72
73
  tagPlacement: "after-title",
73
74
  tagGovernance: "off",
@@ -94,6 +95,7 @@ const DEFAULT_SETTINGS = {
94
95
  dynamicPageTransition: false,
95
96
  frontmatterPreview: false,
96
97
  docHistory: false,
98
+ docHistoryUi: true,
97
99
  docHistoryExclude: [],
98
100
  assetViewer: false,
99
101
  assetViewerDir: "assets",
package/dist/content.css CHANGED
@@ -533,3 +533,58 @@ a.text-accent.underline {
533
533
  }
534
534
 
535
535
  /* END --zdc-* component tokens */
536
+
537
+ /* Compact homepage Markdown uses the same non-heading components and tokens.
538
+ * Shared width primitive is also available to the home identity group. */
539
+ .zd-home-inner {
540
+ inline-size: 100%;
541
+ max-inline-size: 60rem;
542
+ min-inline-size: 0;
543
+ margin-inline: auto;
544
+ overflow-wrap: anywhere;
545
+ }
546
+
547
+ .zd-home-copy,
548
+ .zd-home-links {
549
+ min-inline-size: 0;
550
+ }
551
+
552
+ .zd-home-links {
553
+ overflow-wrap: anywhere;
554
+ }
555
+
556
+ .zd-home-rule {
557
+ inline-size: 100%;
558
+ margin-block: var(--spacing-vsp-xl);
559
+ margin-inline: 0;
560
+ border: 0;
561
+ border-block-start: 1px solid var(--color-muted);
562
+ }
563
+
564
+ .zd-compact-prose {
565
+ min-inline-size: 0;
566
+ text-align: left;
567
+ font-size: 1rem;
568
+ line-height: 1.75;
569
+ overflow-wrap: anywhere;
570
+ }
571
+
572
+ .zd-compact-prose > :where(* + *) {
573
+ margin-top: var(--flow-space, 1rem);
574
+ }
575
+
576
+ .zd-compact-prose :where(h2, h3, h4, h5, h6) {
577
+ font-weight: var(--font-weight-bold, 700);
578
+ line-height: 1.4;
579
+ padding: 0;
580
+ border: 0;
581
+ background: none;
582
+ }
583
+ .zd-compact-prose :where(h2) { font-size: 1.3rem; --flow-space: 1.75rem; }
584
+ .zd-compact-prose :where(h3) { font-size: 1.125rem; --flow-space: 1.5rem; }
585
+ .zd-compact-prose :where(h4) { font-size: 1rem; --flow-space: 1.25rem; }
586
+ .zd-compact-prose :where(h5, h6) { font-size: .9375rem; --flow-space: 1.25rem; }
587
+ .zd-compact-prose > :where(h2, h3, h4, h5, h6) + * { margin-top: .5rem; }
588
+ .zd-compact-prose > :first-child { margin-top: 0; }
589
+ .zd-compact-prose :where(pre) { max-inline-size: 100%; overflow-x: auto; }
590
+ .zd-compact-prose :where(img) { max-inline-size: 100%; block-size: auto; }
@@ -14,6 +14,8 @@ export interface DocHistoryMetaEntry {
14
14
  /** Settings subset read by the DocHistoryArea factory. */
15
15
  export interface DocHistoryAreaSettings {
16
16
  docHistory: boolean;
17
+ /** Keep the metadata manifest while suppressing the history island. */
18
+ docHistoryUi?: boolean;
17
19
  docHistoryExclude?: string[];
18
20
  bodyFootUtilArea: {
19
21
  viewSourceLink?: boolean;
@@ -31,7 +31,8 @@ function createDocHistoryArea(ctx) {
31
31
  }) {
32
32
  if (!settings.docHistory) return null;
33
33
  const historySlug = toHistorySlug(slug);
34
- if (isHistoryExcluded(historySlug)) return null;
34
+ const showDocHistoryUi = settings.docHistoryUi !== false;
35
+ if (isHistoryExcluded(historySlug) && showDocHistoryUi) return null;
35
36
  const effectiveHistoryLocale = isFallback ? defaultLocale : locale;
36
37
  const composedSlug = effectiveHistoryLocale === defaultLocale ? historySlug : `${effectiveHistoryLocale}/${historySlug}`;
37
38
  const meta = docHistoryMeta[composedSlug];
@@ -56,7 +57,7 @@ function createDocHistoryArea(ctx) {
56
57
  updatedDate ? `: ${updatedDate}` : ""
57
58
  ] })
58
59
  ] });
59
- const docHistoryIsland = Island({
60
+ const docHistoryIsland = showDocHistoryUi ? Island({
60
61
  when: "idle",
61
62
  ssrFallback: fallback,
62
63
  children: /* @__PURE__ */ jsx(
@@ -69,12 +70,12 @@ function createDocHistoryArea(ctx) {
69
70
  dateFormats: docHistoryDateFormats
70
71
  }
71
72
  )
72
- });
73
+ }) : null;
74
+ void historyLabel;
73
75
  const utilSettings = settings.bodyFootUtilArea;
74
76
  const sourceExt = meta ? meta.ext : sourceFileExt;
75
77
  const sourceUrl = utilSettings && utilSettings.viewSourceLink && entrySlug && sourceExt && contentDir ? buildGitHubSourceUrl(contentDir, entrySlug + sourceExt) : null;
76
78
  const viewSourceLabel = t("doc.viewSource", locale);
77
- void historyLabel;
78
79
  return /* @__PURE__ */ jsx(
79
80
  BodyFootUtilArea,
80
81
  {
@@ -13,6 +13,8 @@ export interface DocHistoryMetaEntry {
13
13
  /** Settings subset read by the DocMetainfoArea factory. */
14
14
  export interface DocMetainfoAreaSettings {
15
15
  docMetainfo: boolean;
16
+ /** Metadata fields shown in the doc metadata area; omitted means all fields. */
17
+ docMetainfoFields?: Array<"created" | "updated" | "author">;
16
18
  }
17
19
  export interface DocMetainfoAreaProps {
18
20
  /** Page slug, e.g. "getting-started/intro". */
@@ -13,6 +13,11 @@ function createDocMetainfoArea(ctx) {
13
13
  const dateFormatsFor = deriveDateFormats(ctx);
14
14
  function DocMetainfoArea({ slug, locale, isFallback }) {
15
15
  if (!settings.docMetainfo) return null;
16
+ const fields = settings.docMetainfoFields;
17
+ const showCreated = fields === void 0 || fields.includes("created");
18
+ const showUpdated = fields === void 0 || fields.includes("updated");
19
+ const showAuthor = fields === void 0 || fields.includes("author");
20
+ if (!showCreated && !showUpdated && !showAuthor) return null;
16
21
  const historySlug = toHistorySlug(slug);
17
22
  const effectiveHistoryLocale = isFallback ? defaultLocale : locale;
18
23
  const composedSlug = effectiveHistoryLocale === defaultLocale ? historySlug : `${effectiveHistoryLocale}/${historySlug}`;
@@ -21,9 +26,9 @@ function createDocMetainfoArea(ctx) {
21
26
  return /* @__PURE__ */ jsx(
22
27
  DocMetainfo,
23
28
  {
24
- createdAt: meta.createdDate ? formatDate(meta.createdDate, locale, dateFormatsFor(locale).full) : null,
25
- updatedAt: meta.updatedDate ? formatDate(meta.updatedDate, locale, dateFormatsFor(locale).full) : null,
26
- author: meta.author || null,
29
+ createdAt: showCreated && meta.createdDate ? formatDate(meta.createdDate, locale, dateFormatsFor(locale).full) : null,
30
+ updatedAt: showUpdated && meta.updatedDate ? formatDate(meta.updatedDate, locale, dateFormatsFor(locale).full) : null,
31
+ author: showAuthor ? meta.author || null : null,
27
32
  createdLabel: t("doc.created", locale),
28
33
  updatedLabel: t("doc.updated", locale)
29
34
  }
@@ -124,6 +124,8 @@ export interface RouteContext<S = Settings> extends UrlHelpers, NavSourceDocsAPI
124
124
  themePackRegistry: ThemePackRegistry | null;
125
125
  /** Author-facing asset index, or `null` when the viewer is disabled. */
126
126
  assetManifest: AssetManifest | null;
127
+ /** Prepared, serializable homepage introductions. */
128
+ homeIntros: import("../home-intro/types.js").PreparedHomeIntros;
127
129
  /** The reconstructed i18n surface. */
128
130
  i18n: FactoryI18n;
129
131
  /** Default locale code (un-prefixed `/docs/...`). */
@@ -0,0 +1,10 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import { h } from "preact";
4
+ import type { PreparedHomeIntro } from "./types.js";
5
+ export type { IntroNode, PreparedHomeIntro, PreparedHomeIntros } from "./types.js";
6
+ export { resolveHomeIntro } from "./resolve.js";
7
+ /** Synchronous SSR view of output produced by home-intro/prepare. No raw HTML sink. */
8
+ export declare function CompactProse({ intro }: {
9
+ intro: PreparedHomeIntro | null | undefined;
10
+ }): h.JSX.Element | null;
@@ -0,0 +1,20 @@
1
+ import { jsx } from "preact/jsx-runtime";
2
+ import { h } from "preact";
3
+ import { defaultComponents } from "../content/index.js";
4
+ import { makeAdmonition } from "../content-admonition/index.js";
5
+ import { resolveHomeIntro } from "./resolve.js";
6
+ const { h2: _h2, h3: _h3, h4: _h4, ...typography } = defaultComponents;
7
+ const components = { ...typography };
8
+ for (const variant of ["note", "tip", "info", "warning", "danger", "caution", "important"]) components[variant] = makeAdmonition(variant);
9
+ function renderNode(node) {
10
+ if (typeof node === "string") return node;
11
+ return h(components[node.tag] ?? node.tag, node.attrs, node.children.map(renderNode));
12
+ }
13
+ function CompactProse({ intro }) {
14
+ if (!intro?.nodes.length) return null;
15
+ return /* @__PURE__ */ jsx("div", { class: "zd-content zd-compact-prose", children: intro.nodes.map(renderNode) });
16
+ }
17
+ export {
18
+ CompactProse,
19
+ resolveHomeIntro
20
+ };
@@ -0,0 +1,9 @@
1
+ import type { Settings } from "../settings.js";
2
+ import type { PreparedHomeIntro, PreparedHomeIntros } from "./types.js";
3
+ export type HomeIntroSettings = Pick<Settings, "home" | "locales" | "defaultLocale" | "base" | "mermaid" | "transclude">;
4
+ /** URL context is the localized homepage, never an invented Markdown file. */
5
+ export declare function resolveIntroUrl(value: string, settings: Pick<HomeIntroSettings, "base" | "defaultLocale">, locale: string, image?: boolean): string;
6
+ /** Async preparation seam. No author JavaScript is ever compiled or evaluated. */
7
+ export declare function prepareHomeIntro(source: string, settings: HomeIntroSettings, locale: string): Promise<PreparedHomeIntro | null>;
8
+ /** Prepare every configured locale once in the async virtual-context loader. */
9
+ export declare function prepareHomeIntros(settings: HomeIntroSettings): Promise<PreparedHomeIntros>;
@@ -0,0 +1,93 @@
1
+ import { parseFragment } from "parse5";
2
+ import { buildMarkdownFeatures } from "../preset.js";
3
+ import { resolveHomeIntro } from "./resolve.js";
4
+ const origin = "https://home-intro.invalid";
5
+ function resolveIntroUrl(value, settings, locale, image = false) {
6
+ const url = value.trim();
7
+ if (/[\u0000-\u001f\u007f\\]/u.test(url)) throw new Error("home.introMarkdown: URL contains controls or backslashes");
8
+ if (url.startsWith("#")) return url;
9
+ if (url.startsWith("//")) throw new Error("home.introMarkdown: protocol-relative URLs are unsupported");
10
+ const base = `/${settings.base.split("/").filter(Boolean).join("/")}`;
11
+ const prefix = base === "/" ? "" : base;
12
+ const home = `${prefix}/${locale === settings.defaultLocale ? "" : `${locale}/`}`;
13
+ if (url.startsWith("/")) {
14
+ const based = prefix && url !== prefix && !url.startsWith(`${prefix}/`) && !url.startsWith(`${prefix}?`) && !url.startsWith(`${prefix}#`) ? `${prefix}${url}` : url;
15
+ const parsed2 = new URL(based, origin);
16
+ return `${parsed2.pathname}${parsed2.search}${parsed2.hash}`;
17
+ }
18
+ const parsed = new URL(url, `${origin}${home}`);
19
+ const allowed = image ? ["https:", "http:"] : ["https:", "http:", "mailto:", "tel:"];
20
+ if (!allowed.includes(parsed.protocol)) throw new Error(`home.introMarkdown: unsafe URL scheme ${parsed.protocol}`);
21
+ if (!/^[a-z][a-z0-9+.-]*:/iu.test(url)) return `${parsed.pathname}${parsed.search}${parsed.hash}`;
22
+ return url;
23
+ }
24
+ function inspect(node, settings, locale, source = "") {
25
+ if (node.type === "mdxTextExpression" && (source.slice(node.position.end.offset, node.position.end.offset + 2) === "^{" || source[node.position.start.offset - 1] === "^")) return;
26
+ if (node.type === "html" || node.type.startsWith("mdx") || node.type === "yaml" || node.type.endsWith("Directive")) {
27
+ throw new Error(`home.introMarkdown: unsupported ${node.type} at line ${node.position.start.line}; use plain Markdown, without HTML, JSX, imports, frontmatter or directives`);
28
+ }
29
+ if (node.type === "paragraph" && Array.isArray(node.children) && node.children[0]?.type === "text") {
30
+ const first = node.children[0].value.trimStart();
31
+ if (["import", "export"].includes(first.split(/\s+/u)[0] ?? "")) {
32
+ throw new Error("home.introMarkdown: import/export statements are unsupported");
33
+ }
34
+ }
35
+ if ("url" in node && typeof node.url === "string") resolveIntroUrl(node.url, settings, locale, node.type === "image");
36
+ if ("children" in node && Array.isArray(node.children)) for (const child of node.children) inspect(child, settings, locale, source);
37
+ }
38
+ const allowedTags = new Set("p h1 h2 h3 h4 h5 h6 a img em strong del ul ol li blockquote pre code span br hr table thead tbody tr th td input div ruby rb rt rp note tip info warning danger caution important".split(" "));
39
+ const plainAttrs = /* @__PURE__ */ new Set(["title", "alt", "class", "id", "start", "align", "width", "height", "data-mermaid", "data-lang"]);
40
+ function prepareNode(node, settings, locale) {
41
+ if (node.nodeName === "#text") return node.value;
42
+ if (!("tagName" in node)) return null;
43
+ if (!allowedTags.has(node.tagName) || node.namespaceURI !== "http://www.w3.org/1999/xhtml") throw new Error(`home.introMarkdown: unsupported rendered element ${node.tagName}`);
44
+ if (node.tagName === "a" && node.attrs.some((attr) => attr.name === "class" && attr.value.split(" ").includes("hash-link"))) return null;
45
+ const attrs = {};
46
+ for (const { name, value } of node.attrs) {
47
+ if (name === "href" && node.tagName === "a") attrs.href = resolveIntroUrl(value, settings, locale);
48
+ else if (name === "src" && node.tagName === "img") attrs.src = resolveIntroUrl(value, settings, locale, true);
49
+ else if (name === "style" && ["th", "td"].includes(node.tagName) && ["text-align: left", "text-align: center", "text-align: right"].includes(value)) attrs.style = value;
50
+ else if (plainAttrs.has(name)) attrs[name === "class" ? "className" : name] = value;
51
+ else if (node.tagName === "input" && name === "type" && value === "checkbox") attrs.type = value;
52
+ else if (node.tagName === "input" && (name === "checked" || name === "disabled")) attrs[name] = true;
53
+ else throw new Error(`home.introMarkdown: unsupported rendered attribute ${name}`);
54
+ }
55
+ if (node.tagName === "input") {
56
+ attrs.type = "checkbox";
57
+ attrs.disabled = true;
58
+ }
59
+ return { tag: node.tagName === "h1" ? "h2" : node.tagName, attrs, children: node.childNodes.map((child) => prepareNode(child, settings, locale)).filter((child) => child !== null) };
60
+ }
61
+ async function prepareHomeIntro(source, settings, locale) {
62
+ if (!source.trim()) return null;
63
+ const [{ parseToAst }, { renderHtml }] = await Promise.all([
64
+ import("@takazudo/zfb-md-wasm/parse"),
65
+ import("@takazudo/zfb-md-wasm/render")
66
+ ]).catch((cause) => {
67
+ throw new Error("home.introMarkdown: the public @takazudo/zfb-md-wasm parse/render peer is required for nonblank introductions", { cause });
68
+ });
69
+ const parsed = await parseToAst(source, { dialect: "markdown", directives: true, frontmatter: "node" });
70
+ if (!parsed.ast) throw new Error(`home.introMarkdown: ${parsed.diagnostics.map((d) => d.message).join("; ")}`);
71
+ inspect(parsed.ast, settings, locale);
72
+ const mdx = await parseToAst(source, { dialect: "mdx", frontmatter: "none" });
73
+ if (mdx.ast) inspect(mdx.ast, settings, locale, source);
74
+ const features = buildMarkdownFeatures(settings, {});
75
+ for (const feature of ["transclude", "imageDimensions", "linkValidation", "codeTabs", "codeEnrichment", "tocExport", "readingTime", "headingMarkerToc"]) delete features[feature];
76
+ const rendered = await renderHtml(source, { dialect: "markdown", pipeline: { gfm: { taskListItem: true, strikethrough: true, table: true, autolinkLiteral: true }, codeHighlight: { mode: "class" }, features } });
77
+ if (rendered.html === null || rendered.diagnostics.length) throw new Error(`home.introMarkdown: ${rendered.diagnostics.map((d) => d.message).join("; ")}`);
78
+ const nodes = parseFragment(rendered.html).childNodes.map((node) => prepareNode(node, settings, locale)).filter((node) => node !== null);
79
+ return nodes.length ? { nodes } : null;
80
+ }
81
+ async function prepareHomeIntros(settings) {
82
+ const result = {};
83
+ if (!settings.home?.introMarkdown?.trim() && !Object.values(settings.locales ?? {}).some((locale) => locale.introMarkdown?.trim())) return result;
84
+ for (const locale of /* @__PURE__ */ new Set([settings.defaultLocale, ...Object.keys(settings.locales ?? {})])) {
85
+ result[locale] = await prepareHomeIntro(resolveHomeIntro(settings, locale, "").introMarkdown, settings, locale);
86
+ }
87
+ return result;
88
+ }
89
+ export {
90
+ prepareHomeIntro,
91
+ prepareHomeIntros,
92
+ resolveIntroUrl
93
+ };
@@ -0,0 +1,5 @@
1
+ import type { Settings } from "../settings.js";
2
+ export declare function resolveHomeIntro(settings: Pick<Settings, "home" | "locales">, locale: string, translatedSitemapHeading: string): {
3
+ introMarkdown: string;
4
+ sitemapHeading: string;
5
+ };
@@ -0,0 +1,10 @@
1
+ function resolveHomeIntro(settings, locale, translatedSitemapHeading) {
2
+ const localized = settings.locales[locale];
3
+ return {
4
+ introMarkdown: localized?.introMarkdown ?? settings.home?.introMarkdown ?? "",
5
+ sitemapHeading: (localized?.sitemapHeading ?? settings.home?.sitemapHeading)?.trim() || translatedSitemapHeading
6
+ };
7
+ }
8
+ export {
9
+ resolveHomeIntro
10
+ };
@@ -0,0 +1,10 @@
1
+ /** Safe, serializable production-renderer output. Only the preparation helper creates it. */
2
+ export type IntroNode = string | {
3
+ tag: string;
4
+ attrs: Record<string, string | boolean>;
5
+ children: IntroNode[];
6
+ };
7
+ export interface PreparedHomeIntro {
8
+ nodes: IntroNode[];
9
+ }
10
+ export type PreparedHomeIntros = Record<string, PreparedHomeIntro | null>;
File without changes
@@ -3,6 +3,7 @@ import { Fragment as Fragment2 } from "preact";
3
3
  import { Island } from "@takazudo/zfb";
4
4
  import { AutoLogo } from "../auto-logo/index.js";
5
5
  import { DocLayoutWithDefaults } from "../doclayout/index.js";
6
+ import { CompactProse, resolveHomeIntro } from "../home-intro/index.js";
6
7
  import { SiteTreeNav } from "../site-tree-nav-island/index.js";
7
8
  import { TagNav } from "../nav-indexing/tag-nav.js";
8
9
  import { CategoryLinkIcon } from "../tree-nav-shared/index.js";
@@ -12,6 +13,9 @@ import { deriveComposeMetaTitle, deriveBodyEndIslands, deriveDateFormats } from
12
13
  import { derivePrimaryChromeSlots } from "../chrome/primary-slots.js";
13
14
  import { assertChromeContext } from "../chrome/assert-chrome-context.js";
14
15
  import { prepareHomeData } from "./prepare-home-data.js";
16
+ function HomeSectionHeading({ children }) {
17
+ return /* @__PURE__ */ jsx("h2", { class: "text-title font-bold mb-vsp-md", children });
18
+ }
15
19
  function cssUrl(url) {
16
20
  return `url("${url.replace(/[\\"]/g, "\\$&")}")`;
17
21
  }
@@ -28,6 +32,7 @@ function createHomePageView(ctx) {
28
32
  const { Header: HeaderWithDefaults, Footer: FooterWithDefaults } = derivePrimaryChromeSlots(ctx);
29
33
  const BodyEndIslands = deriveBodyEndIslands(ctx);
30
34
  const homeExtras = ctx.hostBindings.homeExtras;
35
+ const homeIntros = ctx.homeIntros;
31
36
  const dataThemePack = resolveThemePackSsrSlug(
32
37
  ctx.themePackRegistry,
33
38
  ctx.settings
@@ -49,11 +54,18 @@ function createHomePageView(ctx) {
49
54
  const primary = heroLink ? { href: withBase(`${prefix}${heroLink.path}`), label: t(heroLink.labelKey, locale) } : ctaNav ? { href: withBase(`${prefix}${ctaNav.path}`), label: t("nav.overview", locale) } : null;
50
55
  const logoSetting = settings.logo ?? "auto";
51
56
  const resolvedExtras = extras ?? homeExtras?.({ locale });
57
+ const intro = homeIntros?.[locale] ?? null;
58
+ const hasIntro = Boolean(intro?.nodes.length);
59
+ const sitemapHeading = resolveHomeIntro(
60
+ settings,
61
+ locale,
62
+ t("home.sitemapHeading", locale)
63
+ ).sitemapHeading;
52
64
  const hasExtras = resolvedExtras != null && typeof resolvedExtras !== "boolean";
53
65
  const rowItems = [];
54
66
  if (primary) {
55
67
  rowItems.push(
56
- /* @__PURE__ */ jsx("a", { href: primary.href, class: "whitespace-nowrap text-fg underline hover:text-accent", children: primary.label })
68
+ /* @__PURE__ */ jsx("a", { href: primary.href, class: "text-fg underline hover:text-accent", children: primary.label })
57
69
  );
58
70
  }
59
71
  if (settings.githubUrl) {
@@ -62,7 +74,7 @@ function createHomePageView(ctx) {
62
74
  "a",
63
75
  {
64
76
  href: settings.githubUrl,
65
- class: "inline-flex items-center gap-[0.3em] whitespace-nowrap text-fg underline hover:text-accent",
77
+ class: "inline-flex items-center gap-[0.3em] text-fg underline hover:text-accent",
66
78
  target: "_blank",
67
79
  rel: "noopener noreferrer",
68
80
  children: [
@@ -99,7 +111,7 @@ function createHomePageView(ctx) {
99
111
  bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
100
112
  enableClientRouter: settings.dynamicPageTransition,
101
113
  children: [
102
- /* @__PURE__ */ jsx("div", { class: "flex justify-center mb-vsp-xl", children: /* @__PURE__ */ jsxs("div", { class: "flex flex-col items-center text-center gap-hsp-md lg:flex-row lg:text-left lg:gap-hsp-xl", children: [
114
+ /* @__PURE__ */ jsx("div", { class: "zd-home-hero mb-vsp-xl", children: /* @__PURE__ */ jsxs("div", { class: "zd-home-inner flex flex-col items-center justify-center text-center gap-hsp-md lg:flex-row lg:text-left lg:gap-hsp-xl", children: [
103
115
  logoSetting === "auto" ? /* @__PURE__ */ jsx(
104
116
  AutoLogo,
105
117
  {
@@ -117,32 +129,40 @@ function createHomePageView(ctx) {
117
129
  "aria-hidden": "true"
118
130
  }
119
131
  ) : null,
120
- /* @__PURE__ */ jsxs("div", { class: "min-w-0", children: [
132
+ /* @__PURE__ */ jsxs("div", { class: "zd-home-copy min-w-0 lg:flex-1", children: [
121
133
  /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs break-words", children: settings.siteName }),
122
134
  /* @__PURE__ */ jsx("p", { class: "text-muted text-small mb-vsp-sm", children: settings.locales[locale]?.description ?? settings.siteDescription }),
123
- /* @__PURE__ */ jsx("div", { class: "flex flex-wrap items-center justify-center lg:justify-start gap-hsp-md text-small", children: rowItems.map((item, index) => /* @__PURE__ */ jsxs(Fragment2, { children: [
135
+ /* @__PURE__ */ jsx("div", { class: "zd-home-links flex flex-wrap items-center justify-center lg:justify-start gap-hsp-md text-small", children: rowItems.map((item, index) => /* @__PURE__ */ jsxs(Fragment2, { children: [
124
136
  index > 0 && /* @__PURE__ */ jsx("span", { class: "text-muted", children: "/" }),
125
137
  item
126
138
  ] }, index)) })
127
139
  ] })
128
140
  ] }) }),
129
- Island({
130
- when: "idle",
131
- children: /* @__PURE__ */ jsx(
132
- SiteTreeNav,
133
- {
134
- tree,
135
- categoryOrder,
136
- categoryIgnore,
137
- initiallyCollapsedCategorySlugs,
138
- locale,
139
- updatedLabel: t("doc.updated", locale),
140
- dateFormats: dateFormatsFor(locale)
141
- }
142
- )
143
- }),
141
+ hasIntro && /* @__PURE__ */ jsxs(Fragment, { children: [
142
+ /* @__PURE__ */ jsx("hr", { class: "zd-home-rule", "data-home-rule": "upper" }),
143
+ /* @__PURE__ */ jsx("div", { class: "zd-home-intro", children: /* @__PURE__ */ jsx("div", { class: "zd-home-inner", children: /* @__PURE__ */ jsx(CompactProse, { intro }) }) })
144
+ ] }),
145
+ /* @__PURE__ */ jsx("hr", { class: "zd-home-rule", "data-home-rule": "lower" }),
146
+ /* @__PURE__ */ jsxs("section", { class: "zd-home-sitemap", children: [
147
+ /* @__PURE__ */ jsx(HomeSectionHeading, { children: sitemapHeading }),
148
+ Island({
149
+ when: "idle",
150
+ children: /* @__PURE__ */ jsx(
151
+ SiteTreeNav,
152
+ {
153
+ tree,
154
+ categoryOrder,
155
+ categoryIgnore,
156
+ initiallyCollapsedCategorySlugs,
157
+ locale,
158
+ updatedLabel: t("doc.updated", locale),
159
+ dateFormats: dateFormatsFor(locale)
160
+ }
161
+ )
162
+ })
163
+ ] }),
144
164
  settings.docTags && tagCount > 0 && /* @__PURE__ */ jsx("section", { class: "mt-vsp-xl", children: tags && tags.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
145
- /* @__PURE__ */ jsx("h2", { class: "text-title font-bold mb-vsp-md", children: t("doc.tags", locale) }),
165
+ /* @__PURE__ */ jsx(HomeSectionHeading, { children: t("doc.tags", locale) }),
146
166
  /* @__PURE__ */ jsx(
147
167
  TagNav,
148
168
  {
@@ -171,7 +191,7 @@ function createHomePageView(ctx) {
171
191
  // reproduce the pre-#3027 single "All Tags" link so existing
172
192
  // `@takazudo/zudo-doc/home-page` consumers are unaffected.
173
193
  /* @__PURE__ */ jsxs(Fragment, { children: [
174
- /* @__PURE__ */ jsx("h2", { class: "text-title font-bold mb-vsp-md", children: t("doc.allTags", locale) }),
194
+ /* @__PURE__ */ jsx(HomeSectionHeading, { children: t("doc.allTags", locale) }),
175
195
  /* @__PURE__ */ jsx(
176
196
  "a",
177
197
  {
@@ -1,5 +1,6 @@
1
1
  const defaultTranslations = {
2
2
  en: {
3
+ "home.sitemapHeading": "Explore the documentation",
3
4
  "nav.gettingStarted": "Getting Started",
4
5
  "nav.learn": "Learn",
5
6
  "nav.guides": "Guides",
@@ -128,6 +129,7 @@ const defaultTranslations = {
128
129
  "version.page.docs": "Docs"
129
130
  },
130
131
  ja: {
132
+ "home.sitemapHeading": "\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u3092\u63A2\u3059",
131
133
  "nav.gettingStarted": "\u306F\u3058\u3081\u306B",
132
134
  "nav.learn": "\u5B66\u3076",
133
135
  "nav.guides": "\u30AC\u30A4\u30C9",