create-zudo-doc 5.9.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ All notable changes to `create-zudo-doc` are documented in this file.
4
+
5
+ The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
6
+
7
+ ## [5.11.0] - 2026-08-23
8
+
9
+ ### Other Changes
10
+
11
+ - Updated newly generated projects to use the zfb 2.10.0 package family and the 5.11.0 zudo-doc package family. (`acaa01cf`)
package/README.md CHANGED
@@ -20,6 +20,10 @@ bunx create-zudo-doc
20
20
 
21
21
  Running without arguments starts the **interactive mode**: the CLI prompts for project name, language, color scheme, and which optional features to enable, then scaffolds the project and optionally installs dependencies.
22
22
 
23
+ Release history is shipped as `CHANGELOG.md` in the npm package. The file is
24
+ generated from the repository's changelog MDX pages; edit those source pages
25
+ instead of editing the generated markdown directly.
26
+
23
27
  ## Non-Interactive Usage
24
28
 
25
29
  Pass `--yes` to accept all defaults and skip prompts, or provide flags to pre-answer specific questions:
@@ -91,6 +95,7 @@ Each feature has a `--[no-]<flag>` form. Passing `--feature` enables it; `--no-f
91
95
  | `--[no-]footer-copyright` | Copyright notice in the footer | on |
92
96
  | `--[no-]footer-taglist` | Grouped tag index in the footer (requires tag-governance) | off |
93
97
  | `--[no-]changelog` | Changelog page | off |
98
+ | `--changelog-packages <a,b>` | Per-package changelog pages and a Changelog dropdown (implies `--changelog`) | none |
94
99
 
95
100
  ### Advanced
96
101
 
@@ -121,6 +126,9 @@ pnpm create zudo-doc my-docs \
121
126
 
122
127
  # Fully featured site from a preset file
123
128
  pnpm create zudo-doc my-docs --preset ./my-preset.json --install
129
+
130
+ # Per-package changelog pages
131
+ pnpm create zudo-doc my-docs --changelog-packages core,cli --yes
124
132
  ```
125
133
 
126
134
  ## Programmatic API
@@ -134,6 +142,7 @@ await createZudoDoc({
134
142
  colorSchemeMode: "single",
135
143
  singleScheme: "Default Dark",
136
144
  features: ["search", "sidebarFilter", "tagGovernance"],
145
+ changelogPackages: ["core", "cli"],
137
146
  packageManager: "pnpm",
138
147
  install: true,
139
148
  });
package/dist/api.d.ts CHANGED
@@ -13,6 +13,8 @@ export interface CreateOptions {
13
13
  /** Theme pack slug (ADR #2818 Decision 7), validated against THEME_PACKS. Default: "default". */
14
14
  themePack?: string;
15
15
  features: string[];
16
+ /** Package slugs for the nested changelog layout; implies `changelog`. */
17
+ changelogPackages?: string[];
16
18
  /** GitHub repository URL — drives the header GitHub link and body-foot
17
19
  * "View source on GitHub" link. Empty = disabled. */
18
20
  githubUrl?: string;
package/dist/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import { SINGLE_SCHEMES, THEME_PACKS } from "./constants.js";
3
- import { validateHeaderRightItems, validateMetaTags } from "./preset.js";
3
+ import { parseChangelogPackages, validateChangelogPackages, validateHeaderRightItems, validateMetaTags, } from "./preset.js";
4
4
  import { scaffold } from "./scaffold.js";
5
5
  import { initGitRepo, installDependencies, validateProjectName } from "./utils.js";
6
6
  export async function createZudoDoc(options) {
@@ -45,7 +45,18 @@ export async function createZudoDoc(options) {
45
45
  if (err)
46
46
  throw new Error(err);
47
47
  }
48
- const choices = { ...rest, defaultLang: rest.defaultLang ?? "en" };
48
+ let changelogPackages = rest.changelogPackages;
49
+ if (changelogPackages !== undefined) {
50
+ const err = validateChangelogPackages(changelogPackages);
51
+ if (err)
52
+ throw new Error(err);
53
+ changelogPackages = parseChangelogPackages(changelogPackages);
54
+ }
55
+ const choices = {
56
+ ...rest,
57
+ defaultLang: rest.defaultLang ?? "en",
58
+ changelogPackages,
59
+ };
49
60
  await scaffold(choices);
50
61
  const targetDir = path.resolve(process.cwd(), choices.projectName);
51
62
  if (install) {
@@ -128,7 +128,9 @@ export function generateCLAUDEFile(choices) {
128
128
  llmsTxt: "Generates llms.txt for LLM consumption",
129
129
  claudeResources: "Auto-generated docs for Claude Code resources",
130
130
  codexResources: "Auto-generated docs for Codex resources (.codex/, AGENTS.md)",
131
- changelog: "Changelog page at `/docs/changelog`",
131
+ changelog: choices.changelogPackages && choices.changelogPackages.length > 0
132
+ ? `Changelog pages at \`/docs/changelog/<slug>\` — one per package: ${choices.changelogPackages.join(", ")}`
133
+ : "Changelog page at `/docs/changelog`",
132
134
  tauri: "Desktop app wrapper (`cargo tauri dev` / `cargo tauri build`) — Cmd/Ctrl+F find bar via the package-owned `FindInPageInit` island (`findInPage: true` in `zfb.config.ts`)",
133
135
  tagGovernance: "Vocabulary-aware tag audit (`tags:audit`) / suggest (`tags:suggest`) scripts",
134
136
  };
package/dist/cli.d.ts CHANGED
@@ -29,6 +29,7 @@ export interface CliArgs {
29
29
  dynamicPageTransition?: boolean;
30
30
  footerCopyright?: boolean;
31
31
  changelog?: boolean;
32
+ changelogPackages?: string[];
32
33
  tagGovernance?: boolean;
33
34
  footerTaglist?: boolean;
34
35
  bodyFootUtil?: boolean;
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import minimist from "minimist";
2
2
  import pc from "picocolors";
3
3
  import { FEATURES, SINGLE_SCHEMES, SUPPORTED_LANGS, THEME_PACKS } from "./constants.js";
4
+ import { parseChangelogPackages, validateChangelogPackages, } from "./preset.js";
4
5
  import { validateProjectName } from "./utils.js";
5
6
  export function parseArgs(argv = process.argv.slice(2)) {
6
7
  const raw = minimist(argv, {
@@ -13,6 +14,7 @@ export function parseArgs(argv = process.argv.slice(2)) {
13
14
  "dark-scheme",
14
15
  "default-mode",
15
16
  "theme-pack",
17
+ "changelog-packages",
16
18
  "github-url",
17
19
  "preset",
18
20
  "pm",
@@ -52,6 +54,11 @@ export function parseArgs(argv = process.argv.slice(2)) {
52
54
  args.defaultMode = raw["default-mode"];
53
55
  if (raw["theme-pack"])
54
56
  args.themePack = raw["theme-pack"];
57
+ if (raw["changelog-packages"] !== undefined) {
58
+ args.changelogPackages = parseChangelogPackages(typeof raw["changelog-packages"] === "string"
59
+ ? raw["changelog-packages"]
60
+ : String(raw["changelog-packages"] ?? ""));
61
+ }
55
62
  if (raw.preset)
56
63
  args.preset = raw.preset;
57
64
  if (raw.pm)
@@ -99,6 +106,7 @@ ${pc.bold("Options:")}
99
106
  --theme-pack <slug> Theme pack (${themePackList})
100
107
  Default: default
101
108
  ${featureHelp}
109
+ --changelog-packages <a,b> Per-package changelog pages (implies changelog)
102
110
  --github-url <url> GitHub repository URL (drives header link + source link)
103
111
  --preset <path> Load settings from a JSON preset file (use "-" for stdin)
104
112
  --pm <manager> pnpm | npm | yarn | bun
@@ -117,6 +125,9 @@ ${pc.bold("Examples:")}
117
125
 
118
126
  ${pc.dim("# Fully specified")}
119
127
  create-zudo-doc my-docs --lang ja --scheme "Default Dark" --no-i18n --pm pnpm --install
128
+
129
+ ${pc.dim("# Per-package changelog pages")}
130
+ create-zudo-doc my-docs --changelog-packages core,cli --yes
120
131
  `);
121
132
  }
122
133
  export function validateArgs(args) {
@@ -148,6 +159,11 @@ export function validateArgs(args) {
148
159
  if (args.pm && !["pnpm", "npm", "yarn", "bun"].includes(args.pm)) {
149
160
  return `Invalid package manager "${args.pm}". Must be pnpm, npm, yarn, or bun`;
150
161
  }
162
+ if (args.changelogPackages !== undefined) {
163
+ const changelogError = validateChangelogPackages(args.changelogPackages);
164
+ if (changelogError)
165
+ return changelogError;
166
+ }
151
167
  // Validate scheme combinations
152
168
  if (args.colorSchemeMode === "single" && (args.lightScheme || args.darkScheme)) {
153
169
  return `--light-scheme and --dark-scheme are only valid with --color-scheme-mode light-dark`;
package/dist/index.js CHANGED
@@ -61,6 +61,9 @@ async function main() {
61
61
  prefilled.packageManager = args.pm;
62
62
  if (args.githubUrl !== undefined)
63
63
  prefilled.githubUrl = args.githubUrl;
64
+ if (args.changelogPackages !== undefined) {
65
+ prefilled.changelogPackages = args.changelogPackages;
66
+ }
64
67
  // Build feature overrides from explicit flags — driven by FEATURES constant
65
68
  const featureFlags = {};
66
69
  for (const f of FEATURES) {
@@ -96,6 +99,9 @@ async function main() {
96
99
  prefilled.themePack ??= "default";
97
100
  prefilled.packageManager ??= "pnpm";
98
101
  prefilled.githubUrl ??= "";
102
+ // A blank package list is the explicit non-interactive choice for the
103
+ // existing single-page changelog. A non-empty CLI/preset list above wins.
104
+ prefilled.changelogPackages ??= [];
99
105
  // For features: set defaults for any not explicitly specified
100
106
  const featureDefaults = {};
101
107
  for (const f of FEATURES) {
package/dist/preset.d.ts CHANGED
@@ -26,6 +26,18 @@ export interface PresetMetaTagsConfig {
26
26
  twitterSite?: string;
27
27
  twitterCreator?: string;
28
28
  }
29
+ /**
30
+ * Parse the comma-separated CLI form (or normalize the array form used by
31
+ * presets and the programmatic API). Empty entries are ignored so a trailing
32
+ * comma is harmless; validation below still rejects an entirely empty list.
33
+ */
34
+ export declare function parseChangelogPackages(value: string | readonly string[]): string[];
35
+ /**
36
+ * Shared changelog-package validation for CLI, JSON presets, and the
37
+ * programmatic API. Keeping this next to the parser prevents the three entry
38
+ * points from accepting different slug grammars or duplicate lists.
39
+ */
40
+ export declare function validateChangelogPackages(list: unknown): string | null;
29
41
  /**
30
42
  * Validates a `headerRightItems` value against the v1 preset allowlist.
31
43
  * Shared by `validatePreset()` (JSON preset path) and `createZudoDoc()`
@@ -50,6 +62,8 @@ export interface PresetJson {
50
62
  /** Theme pack slug (ADR #2818 Decision 7), validated against THEME_PACKS. */
51
63
  themePack?: string;
52
64
  features?: string[];
65
+ /** Package slugs for the nested changelog layout. */
66
+ changelogPackages?: string[];
53
67
  githubUrl?: string;
54
68
  cjkFriendly?: boolean;
55
69
  minifyHtml?: boolean;
package/dist/preset.js CHANGED
@@ -12,6 +12,46 @@ const VALID_HEADER_RIGHT_TRIGGERS = new Set([
12
12
  "design-token-panel",
13
13
  "ai-chat",
14
14
  ]);
15
+ const CHANGELOG_PACKAGE_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
16
+ /**
17
+ * Parse the comma-separated CLI form (or normalize the array form used by
18
+ * presets and the programmatic API). Empty entries are ignored so a trailing
19
+ * comma is harmless; validation below still rejects an entirely empty list.
20
+ */
21
+ export function parseChangelogPackages(value) {
22
+ const values = typeof value === "string" ? value.split(",") : value;
23
+ return values.map((slug) => slug.trim()).filter(Boolean);
24
+ }
25
+ /**
26
+ * Shared changelog-package validation for CLI, JSON presets, and the
27
+ * programmatic API. Keeping this next to the parser prevents the three entry
28
+ * points from accepting different slug grammars or duplicate lists.
29
+ */
30
+ export function validateChangelogPackages(list) {
31
+ if (!Array.isArray(list)) {
32
+ return `"changelogPackages" must be an array`;
33
+ }
34
+ for (let i = 0; i < list.length; i++) {
35
+ if (typeof list[i] !== "string") {
36
+ return `changelogPackages[${i}] must be a string`;
37
+ }
38
+ }
39
+ const packages = parseChangelogPackages(list);
40
+ if (packages.length === 0) {
41
+ return `"changelogPackages" must contain at least one package`;
42
+ }
43
+ const seen = new Set();
44
+ for (const slug of packages) {
45
+ if (!CHANGELOG_PACKAGE_SLUG_RE.test(slug)) {
46
+ return `Invalid changelog package slug "${slug}". Slugs must match /^[a-z0-9][a-z0-9-]*$/`;
47
+ }
48
+ if (seen.has(slug)) {
49
+ return `Duplicate changelog package "${slug}"`;
50
+ }
51
+ seen.add(slug);
52
+ }
53
+ return null;
54
+ }
15
55
  /**
16
56
  * Validates a `headerRightItems` value against the v1 preset allowlist.
17
57
  * Shared by `validatePreset()` (JSON preset path) and `createZudoDoc()`
@@ -131,6 +171,17 @@ export function validatePreset(json) {
131
171
  if (p.features !== undefined && !Array.isArray(p.features)) {
132
172
  return `"features" must be an array in preset`;
133
173
  }
174
+ if (p.changelogPackages !== undefined) {
175
+ if (!Array.isArray(p.changelogPackages)) {
176
+ return `"changelogPackages" must be an array in preset`;
177
+ }
178
+ const changelogPackages = p.changelogPackages.every((slug) => typeof slug === "string")
179
+ ? parseChangelogPackages(p.changelogPackages)
180
+ : p.changelogPackages;
181
+ const err = validateChangelogPackages(changelogPackages);
182
+ if (err)
183
+ return err;
184
+ }
134
185
  if (p.defaultLang && !VALID_LANGS.has(p.defaultLang)) {
135
186
  return `Invalid language "${p.defaultLang}" in preset`;
136
187
  }
@@ -215,6 +266,9 @@ export function presetToChoices(json) {
215
266
  choices.cjkFriendly = json.cjkFriendly;
216
267
  if (json.minifyHtml !== undefined)
217
268
  choices.minifyHtml = json.minifyHtml;
269
+ if (json.changelogPackages !== undefined) {
270
+ choices.changelogPackages = parseChangelogPackages(json.changelogPackages);
271
+ }
218
272
  if (json.headerRightItems !== undefined) {
219
273
  choices.headerRightItems = json.headerRightItems;
220
274
  }
package/dist/prompts.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PresetHeaderRightItem, PresetMetaTagsConfig } from "./preset.js";
1
+ import { type PresetHeaderRightItem, type PresetMetaTagsConfig } from "./preset.js";
2
2
  export interface UserChoices {
3
3
  projectName: string;
4
4
  defaultLang: string;
@@ -10,6 +10,7 @@ export interface UserChoices {
10
10
  defaultMode?: "light" | "dark";
11
11
  themePack?: string;
12
12
  features: string[];
13
+ changelogPackages?: string[];
13
14
  explicitlyDisabledFeatures?: string[];
14
15
  githubUrl?: string;
15
16
  cjkFriendly?: boolean;
@@ -29,6 +30,7 @@ export interface PartialChoices {
29
30
  defaultMode?: "light" | "dark";
30
31
  themePack?: string;
31
32
  features?: Partial<Record<string, boolean>>;
33
+ changelogPackages?: string[];
32
34
  explicitlyDisabledFeatures?: string[];
33
35
  githubUrl?: string;
34
36
  cjkFriendly?: boolean;
package/dist/prompts.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { SINGLE_SCHEMES, FEATURES, SUPPORTED_LANGS, THEME_PACKS } from "./constants.js";
3
+ import { parseChangelogPackages, validateChangelogPackages, } from "./preset.js";
3
4
  import { validateProjectName } from "./utils.js";
4
5
  export async function runPrompts(prefilled = {}) {
5
6
  // 1. Project name
@@ -170,6 +171,26 @@ export async function runPrompts(prefilled = {}) {
170
171
  process.exit(0);
171
172
  features = result;
172
173
  }
174
+ // 4.5 Changelog package layout. A blank answer deliberately preserves the
175
+ // existing single-page starter. CLI/preset/--yes callers prefill this value
176
+ // so they remain non-interactive.
177
+ let changelogPackages = prefilled.changelogPackages;
178
+ if (features.includes("changelog") && changelogPackages === undefined) {
179
+ const result = await p.text({
180
+ message: "Package changelogs (comma-separated slugs; leave empty for a single changelog):",
181
+ placeholder: "core,cli",
182
+ defaultValue: "",
183
+ validate(value) {
184
+ if (!value.trim())
185
+ return;
186
+ const parsed = parseChangelogPackages(value);
187
+ return validateChangelogPackages(parsed) ?? undefined;
188
+ },
189
+ });
190
+ if (p.isCancel(result))
191
+ process.exit(0);
192
+ changelogPackages = parseChangelogPackages(result);
193
+ }
173
194
  // 5. GitHub URL (drives header GitHub icon + view-source link)
174
195
  let githubUrl = prefilled.githubUrl;
175
196
  if (githubUrl === undefined) {
@@ -218,6 +239,7 @@ export async function runPrompts(prefilled = {}) {
218
239
  defaultMode,
219
240
  themePack,
220
241
  features,
242
+ changelogPackages,
221
243
  explicitlyDisabledFeatures: prefilled.explicitlyDisabledFeatures,
222
244
  githubUrl,
223
245
  cjkFriendly: prefilled.cjkFriendly,
@@ -32,5 +32,5 @@ export declare function deriveDocSkillName(projectName: string): string;
32
32
  *
33
33
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
34
34
  */
35
- export declare const ZUDO_DOC_PIN = "^5.9.0";
35
+ export declare const ZUDO_DOC_PIN = "^5.11.0";
36
36
  export declare function scaffold(choices: UserChoices): Promise<void>;
package/dist/scaffold.js CHANGED
@@ -43,7 +43,7 @@ export function deriveDocSkillName(projectName) {
43
43
  *
44
44
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
45
45
  */
46
- export const ZUDO_DOC_PIN = "^5.9.0";
46
+ export const ZUDO_DOC_PIN = "^5.11.0";
47
47
  /**
48
48
  * Files in `templates/base/**` that must not be copied by the unconditional
49
49
  * base mirror. Each entry is matched against the path relative to
@@ -186,8 +186,6 @@ title: Changelog
186
186
  sidebar_position: 99
187
187
  ---
188
188
 
189
- # Changelog
190
-
191
189
  ## Unreleased
192
190
 
193
191
  - Initial release
@@ -197,7 +195,45 @@ title: 変更履歴
197
195
  sidebar_position: 99
198
196
  ---
199
197
 
200
- # 変更履歴
198
+ ## 未リリース
199
+
200
+ - 初回リリース
201
+ `;
202
+ const CHANGELOG_LANDING_CONTENT_EN = () => `---
203
+ title: Changelog
204
+ description: Release notes for each package.
205
+ sidebar_position: 99
206
+ ---
207
+
208
+ Release notes for each package.
209
+
210
+ <CategoryNav category="changelog" />
211
+ `;
212
+ const CHANGELOG_LANDING_CONTENT_JA = () => `---
213
+ title: 変更履歴
214
+ description: パッケージごとのリリースノート。
215
+ sidebar_position: 99
216
+ ---
217
+
218
+ パッケージごとのリリースノート。
219
+
220
+ <CategoryNav category="changelog" />
221
+ `;
222
+ const CHANGELOG_PACKAGE_CONTENT_EN = (slug, position) => `---
223
+ title: "${slug}"
224
+ description: Release notes for ${slug}.
225
+ sidebar_position: ${position}
226
+ ---
227
+
228
+ ## Unreleased
229
+
230
+ - Initial release
231
+ `;
232
+ const CHANGELOG_PACKAGE_CONTENT_JA = (slug, position) => `---
233
+ title: "${slug}"
234
+ description: ${slug} のリリースノート。
235
+ sidebar_position: ${position}
236
+ ---
201
237
 
202
238
  ## 未リリース
203
239
 
@@ -222,6 +258,16 @@ export async function scaffold(choices) {
222
258
  }
223
259
  choices.features = [...choices.features, "docHistory"];
224
260
  }
261
+ // A non-empty package list implies the changelog feature. Warn when the
262
+ // CLI explicitly disabled it, then honor the package-layout request.
263
+ if (choices.changelogPackages !== undefined &&
264
+ choices.changelogPackages.length > 0 &&
265
+ !choices.features.includes("changelog")) {
266
+ if (choices.explicitlyDisabledFeatures?.includes("changelog")) {
267
+ console.warn("changelog-packages requires changelog; enabling it despite --no-changelog");
268
+ }
269
+ choices.features = [...choices.features, "changelog"];
270
+ }
225
271
  // Resolve template directories
226
272
  const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
227
273
  const templatesDir = path.join(pkgRoot, "templates");
@@ -324,14 +370,44 @@ export async function scaffold(choices) {
324
370
  }
325
371
  // When changelog is ON, create a starter changelog page
326
372
  if (choices.features.includes("changelog")) {
327
- const changelogContent = defaultLang === "ja" ? CHANGELOG_CONTENT_JA() : CHANGELOG_CONTENT_EN();
328
- await fs.outputFile(path.join(targetDir, "src/content/docs/changelog/index.mdx"), changelogContent);
373
+ const packageSlugs = choices.changelogPackages ?? [];
374
+ if (packageSlugs.length === 0) {
375
+ const changelogContent = defaultLang === "ja" ? CHANGELOG_CONTENT_JA() : CHANGELOG_CONTENT_EN();
376
+ await fs.outputFile(path.join(targetDir, "src/content/docs/changelog/index.mdx"), changelogContent);
377
+ }
378
+ else {
379
+ const landingContent = defaultLang === "ja"
380
+ ? CHANGELOG_LANDING_CONTENT_JA()
381
+ : CHANGELOG_LANDING_CONTENT_EN();
382
+ await fs.outputFile(path.join(targetDir, "src/content/docs/changelog/index.mdx"), landingContent);
383
+ for (const [index, slug] of packageSlugs.entries()) {
384
+ const packageContent = defaultLang === "ja"
385
+ ? CHANGELOG_PACKAGE_CONTENT_JA(slug, index + 1)
386
+ : CHANGELOG_PACKAGE_CONTENT_EN(slug, index + 1);
387
+ await fs.outputFile(path.join(targetDir, `src/content/docs/changelog/${slug}/index.mdx`), packageContent);
388
+ }
389
+ }
329
390
  if (choices.features.includes("i18n")) {
330
391
  const secondaryLang = getSecondaryLang(defaultLang);
331
- const secondaryChangelogContent = secondaryLang === "ja"
332
- ? CHANGELOG_CONTENT_JA()
333
- : CHANGELOG_CONTENT_EN();
334
- await fs.outputFile(path.join(targetDir, `src/content/docs-${secondaryLang}/changelog/index.mdx`), secondaryChangelogContent);
392
+ if (packageSlugs.length === 0) {
393
+ const secondaryChangelogContent = secondaryLang === "ja"
394
+ ? CHANGELOG_CONTENT_JA()
395
+ : CHANGELOG_CONTENT_EN();
396
+ await fs.outputFile(path.join(targetDir, `src/content/docs-${secondaryLang}/changelog/index.mdx`), secondaryChangelogContent);
397
+ }
398
+ else {
399
+ const secondaryLandingContent = secondaryLang === "ja"
400
+ ? CHANGELOG_LANDING_CONTENT_JA()
401
+ : CHANGELOG_LANDING_CONTENT_EN();
402
+ const secondaryDir = `src/content/docs-${secondaryLang}/changelog`;
403
+ await fs.outputFile(path.join(targetDir, `${secondaryDir}/index.mdx`), secondaryLandingContent);
404
+ for (const [index, slug] of packageSlugs.entries()) {
405
+ const secondaryPackageContent = secondaryLang === "ja"
406
+ ? CHANGELOG_PACKAGE_CONTENT_JA(slug, index + 1)
407
+ : CHANGELOG_PACKAGE_CONTENT_EN(slug, index + 1);
408
+ await fs.outputFile(path.join(targetDir, `${secondaryDir}/${slug}/index.mdx`), secondaryPackageContent);
409
+ }
410
+ }
335
411
  }
336
412
  }
337
413
  // 3. Generate the one config file. There is no more src/config/settings.ts —
@@ -624,9 +700,11 @@ function generatePackageJson(choices) {
624
700
  // default, so a fresh scaffold needs no config change to pick it up.
625
701
  // Also fixes the client router to tolerate about:srcdoc history
626
702
  // restrictions. No generator-side migration.
627
- "@takazudo/zfb": "2.7.1",
628
- "@takazudo/zfb-runtime": "2.7.1",
629
- "@takazudo/zfb-md-wasm": "2.7.1",
703
+ // 2.10.0: routine toolchain bump from 2.9.0, adopted in lockstep with the
704
+ // root package.json pins. No generator-side migration.
705
+ "@takazudo/zfb": "2.10.0",
706
+ "@takazudo/zfb-runtime": "2.10.0",
707
+ "@takazudo/zfb-md-wasm": "2.10.0",
630
708
  // @takazudo/zudo-doc — published from this monorepo via
631
709
  // .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
632
710
  // lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
@@ -748,7 +826,7 @@ function generatePackageJson(choices) {
748
826
  // `/exclude` at module scope from the always-bundled chrome graph; #3110
749
827
  // moved compileExclude into @takazudo/zudo-doc, so docHistory-OFF projects
750
828
  // no longer need the package at all.
751
- deps["@takazudo/zudo-doc-history-server"] = "^5.9.0";
829
+ deps["@takazudo/zudo-doc-history-server"] = "^5.11.0";
752
830
  // tsx is no longer needed here: the relocated package plugin imports the
753
831
  // runner directly (no `tsx -e` spawn) since the package ships compiled
754
832
  // dist/ — package-first migration #2321 (#2337).
@@ -309,11 +309,26 @@ function buildDesiredConfig(choices) {
309
309
  });
310
310
  }
311
311
  if (choices.features.includes("changelog")) {
312
- headerNav.push({
313
- label: "Changelog",
314
- path: "/docs/changelog",
315
- categoryMatch: "changelog",
316
- });
312
+ if (choices.changelogPackages && choices.changelogPackages.length > 0) {
313
+ headerNav.push({
314
+ label: "Changelog",
315
+ path: "/docs/changelog",
316
+ categoryMatch: "changelog",
317
+ // nav-scope matches the first slug segment only; nested children
318
+ // highlight by path and must not carry a multi-segment categoryMatch.
319
+ children: choices.changelogPackages.map((slug) => ({
320
+ label: slug,
321
+ path: `/docs/changelog/${slug}`,
322
+ })),
323
+ });
324
+ }
325
+ else {
326
+ headerNav.push({
327
+ label: "Changelog",
328
+ path: "/docs/changelog",
329
+ categoryMatch: "changelog",
330
+ });
331
+ }
317
332
  }
318
333
  desired.headerNav = headerNav;
319
334
  if (choices.headerRightItems !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "5.9.0",
3
+ "version": "5.11.0",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -40,7 +40,8 @@
40
40
  "bin",
41
41
  "dist",
42
42
  "templates",
43
- "!dist/__tests__"
43
+ "!dist/__tests__",
44
+ "CHANGELOG.md"
44
45
  ],
45
46
  "scripts": {
46
47
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
@@ -103,32 +103,61 @@ which package manager (`<pm>`) the project otherwise uses. `--no-git-tag-version
103
103
  also creating a commit/tag — this skill handles that later, once everything else is ready. A
104
104
  direct edit of the `"version"` field in `package.json` works identically if preferred.
105
105
 
106
+ The project has **one root version**. All packages represented in a multi-package changelog move
107
+ to `{NEW_VERSION}` in lockstep; independent per-package versions are out of scope for this skill.
108
+
106
109
  ## Update the changelog (if enabled)
107
110
 
108
- Check whether the project has a changelog page:
111
+ First check whether the default-language changelog exists:
109
112
 
110
113
  ```bash
111
114
  test -f src/content/docs/changelog/index.mdx && echo "changelog present"
112
115
  ```
113
116
 
114
- If that file does not exist, the changelog feature is not in use — skip this whole section and
115
- go straight to "Archive docs as a versioned snapshot".
117
+ If `src/content/docs/changelog/index.mdx` does not exist, the changelog feature is not in use —
118
+ skip this whole section and go straight to "Archive docs as a versioned snapshot".
116
119
 
117
- Unlike a per-version file, a scaffolded changelog is a single `index.mdx` page. Add the new
118
- release as a section **above** the existing ones (newest first), using only the categories that
119
- have entries.
120
+ ### Discover package and page layouts
120
121
 
121
- ### Default-language changelog (`src/content/docs/changelog/index.mdx`)
122
+ Treat `src/content/docs/changelog/` as the primary changelog root. List every immediate child
123
+ directory that contains an `index.mdx`; each directory name is a package slug:
122
124
 
123
- This is the project's PRIMARY changelog page. It was seeded in whichever language the project's
124
- `defaultLang` setting is, so use whichever heading set below already matches the page — don't
125
- assume English.
125
+ ```bash
126
+ find src/content/docs/changelog -mindepth 2 -maxdepth 2 -type f -name index.mdx \
127
+ -print | sed 's#/index\.mdx$##; s#.*/##' | sort
128
+ ```
126
129
 
127
- **If the page uses English headings:**
130
+ Then follow exactly one branch:
128
131
 
129
- ```mdx
130
- ## {NEW_VERSION}
132
+ 1. **One or more package directories:** the root `index.mdx` is a landing page and must never be
133
+ edited. Show the package slugs to the user and ask which packages this release touches. Default
134
+ to **all packages**; accept a comma-separated list. Reject names that are not in the discovered
135
+ list. Apply the per-directory procedure below to each selected package directory.
136
+ 2. **No package directories:** apply the per-directory procedure to the changelog root itself.
137
+
138
+ For every selected directory, detect its layout independently, before making changes:
139
+
140
+ ```bash
141
+ find "<changelog-directory>" -maxdepth 1 -type f -name '*.mdx' ! -name index.mdx \
142
+ -print -quit
143
+ ```
144
+
145
+ - Any output means **per-version-file layout**. Create a new version file; never insert a version
146
+ section into that directory's `index.mdx`.
147
+ - No output means **single-page layout**. Edit that directory's `index.mdx` using the existing
148
+ section procedure.
149
+
150
+ This per-directory check is mandatory: packages in one project may use different layouts.
151
+
152
+ ### Choose the language-specific headings
153
+
154
+ The primary content directory was seeded in the project's `defaultLang`, so do not assume it is
155
+ English. For a single-page directory, inspect its `index.mdx`. For a per-version-file directory,
156
+ inspect its existing sibling version entries. Use the heading set already used by those files.
131
157
 
158
+ **English heading set:**
159
+
160
+ ```mdx
132
161
  ### Breaking Changes
133
162
 
134
163
  - Description (commit-hash)
@@ -146,14 +175,9 @@ assume English.
146
175
  - Description (commit-hash)
147
176
  ```
148
177
 
149
- On the very first bump, the page still has the scaffold's starter `## Unreleased` section —
150
- replace that heading with `## {NEW_VERSION}` rather than adding a second heading.
151
-
152
- **If the page uses Japanese headings:**
178
+ **Japanese heading set:**
153
179
 
154
180
  ```mdx
155
- ## {NEW_VERSION}
156
-
157
181
  ### 破壊的変更
158
182
 
159
183
  - Description (commit-hash)
@@ -171,8 +195,44 @@ replace that heading with `## {NEW_VERSION}` rather than adding a second heading
171
195
  - Description (commit-hash)
172
196
  ```
173
197
 
174
- On the very first bump, the page still has the scaffold's starter `## 未リリース` section —
175
- replace that heading with `## {NEW_VERSION}` the same way.
198
+ In either language, include only categories that have entries. Each entry is the commit subject
199
+ with its short hash in parentheses.
200
+
201
+ ### Update a single-page directory
202
+
203
+ Add this release above all existing release sections in the directory's `index.mdx` (newest
204
+ first):
205
+
206
+ ```mdx
207
+ ## {NEW_VERSION}
208
+
209
+ <!-- categories and entries from the matching heading set above -->
210
+ ```
211
+
212
+ On the first bump, replace the starter `## Unreleased` or `## 未リリース` heading with
213
+ `## {NEW_VERSION}` instead of adding a second release heading.
214
+
215
+ ### Update a per-version-file directory
216
+
217
+ Create `<changelog-directory>/<version>.mdx`, where `<version>` is `{NEW_VERSION}` without a
218
+ leading `v`. Never section-edit this directory's `index.mdx`. Determine the greatest numeric
219
+ `sidebar_position` in the existing sibling version files and use that value plus one. The new file
220
+ must have localized frontmatter and body text matching its siblings, in this shape:
221
+
222
+ ```mdx
223
+ ---
224
+ title: "{NEW_VERSION}"
225
+ description: Release notes for {NEW_VERSION}.
226
+ sidebar_position: {MAX_EXISTING_PLUS_ONE}
227
+ ---
228
+
229
+ Released: {YYYY-MM-DD}
230
+
231
+ <!-- categories and entries from the matching heading set above -->
232
+ ```
233
+
234
+ Use a concise Japanese `description` when the sibling entries are Japanese, but keep the required
235
+ `Released: {YYYY-MM-DD}` line in either language. Use today's date for `{YYYY-MM-DD}`.
176
236
 
177
237
  ### Other-locale changelog
178
238
 
@@ -180,17 +240,13 @@ Only applies when i18n is enabled — i.e. a second content directory exists alo
180
240
  one. Which locale that is depends on the project's `defaultLang`: for an English-default project
181
241
  this is the Japanese changelog at `src/content/docs-ja/changelog/index.mdx`; for a
182
242
  Japanese-default project this is the English changelog under the `docs-en` directory instead. If
183
- the other-locale changelog page doesn't exist, skip this file.
184
-
185
- Use the OTHER heading set from above (the one you didn't use for the default-language page —
186
- English primary means Japanese secondary, and vice versa), following the same "add a new
187
- `## {NEW_VERSION}` section above the existing ones" rule, and replace that page's own starter
188
- heading on the very first bump.
243
+ the other-locale changelog root doesn't exist, skip mirroring.
189
244
 
190
- Rules:
191
-
192
- - Only include sections that have entries
193
- - Each entry should be the commit subject with the short hash in parentheses
245
+ Mirror exactly the selected primary targets by relative path under the other-locale root, detect
246
+ each mirror directory's layout independently, and apply the matching single-page or
247
+ per-version-file procedure. Use the other language's heading set. In a multi-package layout,
248
+ never edit either locale's landing `changelog/index.mdx`. If a selected package has no matching
249
+ other-locale directory, report it and stop instead of silently creating a divergent layout.
194
250
 
195
251
  ## Archive docs as a versioned snapshot (if enabled, major bumps only)
196
252
 
@@ -253,7 +309,7 @@ Stage and commit **all** version bump changes:
253
309
 
254
310
  ```bash
255
311
  git add package.json
256
- git add src/content/docs/changelog/index.mdx src/content/docs-ja/changelog/index.mdx 2>/dev/null
312
+ git add src/content/docs*/changelog/ 2>/dev/null
257
313
  git add src/content/docs-v* 2>/dev/null
258
314
  # Also stage any other modified files (e.g. formatting fixes from the build/test step)
259
315
  git diff --name-only | xargs -r git add
@@ -288,14 +344,44 @@ git tag v{NEW_VERSION}
288
344
  git push --tags
289
345
  ```
290
346
 
291
- After pushing the tag, create a GitHub release. If the changelog feature is enabled, pull the
292
- section you just wrote out of the single changelog page as the release notes:
347
+ After pushing the tag, create a GitHub release. If a root single-page changelog was updated, pull
348
+ the section you just wrote out as before:
293
349
 
294
350
  ```bash
295
351
  NOTES=$(awk -v ver="## {NEW_VERSION}" '$0==ver{f=1;next} f&&/^## /{f=0} f' src/content/docs/changelog/index.mdx)
296
352
  gh release create v{NEW_VERSION} --title "v{NEW_VERSION}" --notes "$NOTES"
297
353
  ```
298
354
 
355
+ For a root per-version-file layout, remove the frontmatter from the new entry and use the rest of
356
+ its body as `NOTES`:
357
+
358
+ ```bash
359
+ NOTES=$(awk 'NR==1&&$0=="---"{fm=1;next} fm&&$0=="---"{fm=0;next} !fm{print}' "src/content/docs/changelog/{NEW_VERSION}.mdx")
360
+ gh release create v{NEW_VERSION} --title "v{NEW_VERSION}" --notes "$NOTES"
361
+ ```
362
+
363
+ For a multi-package changelog, concatenate the primary-language notes for every selected package,
364
+ in the same order shown to the user, under a `## <slug>` heading. This concrete loop handles a mix
365
+ of single-page and per-version-file package directories:
366
+
367
+ ```bash
368
+ NOTES=""
369
+ for slug in $SELECTED_PACKAGES; do
370
+ dir="src/content/docs/changelog/$slug"
371
+ if test -f "$dir/{NEW_VERSION}.mdx"; then
372
+ body=$(awk 'NR==1&&$0=="---"{fm=1;next} fm&&$0=="---"{fm=0;next} !fm{print}' "$dir/{NEW_VERSION}.mdx")
373
+ else
374
+ body=$(awk -v ver="## {NEW_VERSION}" '$0==ver{f=1;next} f&&/^## /{f=0} f' "$dir/index.mdx")
375
+ fi
376
+ printf -v NOTES '%s## %s\n\n%s\n\n' "$NOTES" "$slug" "$body"
377
+ done
378
+ gh release create v{NEW_VERSION} --title "v{NEW_VERSION}" --notes "$NOTES"
379
+ ```
380
+
381
+ Set `SELECTED_PACKAGES` to the validated, space-separated package slugs chosen earlier. Build
382
+ release notes from the primary-language files only; the other-locale mirror is not duplicated in
383
+ the GitHub release body.
384
+
299
385
  If the changelog feature is off, write the release notes directly from the categorized commit
300
386
  analysis instead:
301
387
 
@@ -326,7 +412,7 @@ Package is marked as private — skipping npm publish.
326
412
  Report the summary:
327
413
 
328
414
  - Version bumped: `{OLD_VERSION}` → `{NEW_VERSION}`
329
- - Changelog updated (EN + JA, if the changelog feature is enabled)
415
+ - Changelog layout(s) and selected package(s) updated (EN + JA, if enabled)
330
416
  - Docs snapshot created (if the versioning feature is enabled and a snapshot was taken)
331
417
  - Git tag: `v{NEW_VERSION}`
332
418
  - GitHub release: link to the release