@tenphi/cookbook 0.17.0 → 0.18.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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolveDocsTheme } from "@tenphi/starlight";
3
- import { createDocsGraph, formatDiagnostics, materializePackage, normalizeDocsConfig, readDocsLock, reconcileDocsLock, resolveDocsProject, writeDocsLock } from "@tenphi/docs";
3
+ import { createDocsGraph, formatDiagnostics, materializePackage, normalizeDocsConfig, readDocsLock, reconcileDocsLock, resolveDocsProject, validateBuiltDocs, validateProjectAssets, writeDocsLock } from "@tenphi/docs";
4
4
  import { cp, mkdir } from "node:fs/promises";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { parseArgs } from "node:util";
@@ -16,6 +16,9 @@ try {
16
16
  },
17
17
  root: { type: "string" },
18
18
  config: { type: "string" },
19
+ out: { type: "string" },
20
+ url: { type: "string" },
21
+ "public-dir": { type: "string" },
19
22
  "dry-run": {
20
23
  type: "boolean",
21
24
  default: false
@@ -31,14 +34,35 @@ try {
31
34
  if (values.help || positionals.length === 0) printHelp();
32
35
  else {
33
36
  const command = positionals[0];
34
- if (command !== "doctor" && command !== "update") throw new Error(`Unknown command: ${command ?? ""}.`);
35
- if (command === "doctor" && (positionals.length > 1 || values["dry-run"])) throw new Error("doctor does not accept package selectors or --dry-run.");
37
+ if (![
38
+ "doctor",
39
+ "update",
40
+ "check-build"
41
+ ].includes(command)) throw new Error(`Unknown command: ${command ?? ""}.`);
42
+ if (command !== "update" && (positionals.length > 1 || values["dry-run"])) throw new Error(`${command} does not accept package selectors or --dry-run.`);
43
+ if (command !== "check-build" && (values.out || values.url)) throw new Error("--out and --url are only supported by check-build.");
44
+ if (values["public-dir"] && command !== "doctor") throw new Error("--public-dir is only supported by doctor.");
36
45
  const project = await resolveDocsProject({
37
46
  root: resolve(values.root ?? process.cwd()),
38
47
  ...values.config ? { configFile: values.config } : {}
39
48
  });
40
- if (command === "doctor") await doctor(project, values.json);
41
- else await update(project, positionals.slice(1), values.json, values["dry-run"]);
49
+ if (command === "doctor") await doctor(project, values.json, values["public-dir"] ? resolve(values.root ?? process.cwd(), values["public-dir"]) : resolve(values.root ?? process.cwd(), "public"));
50
+ else if (command === "check-build") {
51
+ const graph = await createDocsGraph(project);
52
+ const report = await validateBuiltDocs({
53
+ directory: resolve(values.root ?? process.cwd(), values.out ?? "dist"),
54
+ graph,
55
+ ...values.url ? { deployedUrl: values.url } : {}
56
+ });
57
+ report.diagnostics.unshift(...graph.diagnostics);
58
+ report.ok = !report.diagnostics.some((d) => d.severity === "error");
59
+ if (values.json) console.log(JSON.stringify(report, null, 2));
60
+ else {
61
+ if (report.diagnostics.length) console.log(formatDiagnostics(report.diagnostics));
62
+ console.log(`${report.ok ? "Passed" : "Failed"} ${report.scope} checks: ${report.pages} pages, ${report.assets} assets${report.checkedUrls ? `, ${report.checkedUrls} deployed URLs` : ""}.`);
63
+ }
64
+ if (!report.ok) process.exitCode = 1;
65
+ } else await update(project, positionals.slice(1), values.json, values["dry-run"]);
42
66
  }
43
67
  } catch (error) {
44
68
  const message = error instanceof Error ? error.message : String(error);
@@ -56,19 +80,25 @@ try {
56
80
  else console.error(formatDiagnostics(diagnostics));
57
81
  process.exitCode = 1;
58
82
  }
59
- async function doctor(project, json) {
83
+ async function doctor(project, json, publicDirectory) {
60
84
  const theme = resolveDocsTheme(project.config.theme);
61
85
  const graph = await createDocsGraph(project);
62
- const diagnostics = [...graph.diagnostics, ...theme.diagnostics];
86
+ const diagnostics = [
87
+ ...graph.diagnostics,
88
+ ...theme.diagnostics,
89
+ ...await validateProjectAssets(project, graph, publicDirectory)
90
+ ];
63
91
  const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
64
92
  if (json) console.log(JSON.stringify({
65
93
  ok: !hasErrors,
94
+ scope: "preflight",
95
+ buildVerified: false,
66
96
  pages: graph.entries.length,
67
97
  assets: graph.assets.length,
68
98
  diagnostics
69
99
  }, null, 2));
70
100
  else if (diagnostics.length > 0) console.log(formatDiagnostics(diagnostics));
71
- else console.log(`Cookbook is healthy: ${graph.entries.length} pages, ${graph.assets.length} assets.`);
101
+ else console.log(`Preflight passed: ${graph.entries.length} pages, ${graph.assets.length} content assets. MDX compilation, remote fonts, and deployment are not checked. Run astro build and cookbook check-build.`);
72
102
  if (hasErrors) process.exitCode = 1;
73
103
  }
74
104
  async function update(project, packages, json, dryRun) {
@@ -111,7 +141,7 @@ async function update(project, packages, json, dryRun) {
111
141
  }
112
142
  }
113
143
  function printHelp() {
114
- console.log(`Usage: cookbook <command> [options]\n\nCommands:\n doctor Validate content, configuration, and theme\n update [packages] Reconcile configured package sources and their lock\n\nOptions:\n --root <directory>\n --config <path>\n --dry-run Preview an update without writing files\n --json\n -h, --help`);
144
+ console.log(`Usage: cookbook <command> [options]\n\nCommands:\n doctor Check configuration, content graph, theme, and local files\n check-build Inspect production output and optionally deployed URLs\n update [packages] Reconcile configured package sources and their lock\n\nOptions:\n --root <directory>\n --config <path>\n --public-dir <directory> Local public assets for doctor (default: public)\n --out <directory> Built output (default: dist)\n --url <base-url> Also check a deployed site\n --dry-run Preview an update without writing files\n --json\n -h, --help`);
115
145
  process.exit(0);
116
146
  }
117
147
  //#endregion
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { cp, mkdir } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport {\n createDocsGraph,\n formatDiagnostics,\n materializePackage,\n normalizeDocsConfig,\n readDocsLock,\n reconcileDocsLock,\n resolveDocsProject,\n writeDocsLock,\n type DocsDiagnostic,\n type DocsProject,\n} from \"@tenphi/docs\";\nimport { resolveDocsTheme } from \"@tenphi/starlight\";\n\nlet jsonOutput = process.argv.slice(2).includes(\"--json\");\n\ntry {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n json: { type: \"boolean\", default: false },\n root: { type: \"string\" },\n config: { type: \"string\" },\n \"dry-run\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n jsonOutput = values.json;\n if (values.help || positionals.length === 0) {\n printHelp();\n } else {\n const command = positionals[0];\n if (command !== \"doctor\" && command !== \"update\") {\n throw new Error(`Unknown command: ${command ?? \"\"}.`);\n }\n if (command === \"doctor\" && (positionals.length > 1 || values[\"dry-run\"])) {\n throw new Error(\"doctor does not accept package selectors or --dry-run.\");\n }\n const project = await resolveDocsProject({\n root: resolve(values.root ?? process.cwd()),\n ...(values.config ? { configFile: values.config } : {}),\n });\n if (command === \"doctor\") await doctor(project, values.json);\n else\n await update(\n project,\n positionals.slice(1),\n values.json,\n values[\"dry-run\"],\n );\n }\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const diagnostics: DocsDiagnostic[] =\n error && typeof error === \"object\" && \"diagnostics\" in error\n ? (error.diagnostics as DocsDiagnostic[])\n : [{ code: \"COOKBOOK_CLI_ERROR\", severity: \"error\", message }];\n if (jsonOutput) {\n console.log(\n JSON.stringify({ ok: false, pages: 0, assets: 0, diagnostics }, null, 2),\n );\n } else {\n console.error(formatDiagnostics(diagnostics));\n }\n process.exitCode = 1;\n}\n\nasync function doctor(project: DocsProject, json: boolean): Promise<void> {\n const theme = resolveDocsTheme(project.config.theme);\n const graph = await createDocsGraph(project);\n const diagnostics = [...graph.diagnostics, ...theme.diagnostics];\n const hasErrors = diagnostics.some(\n (diagnostic) => diagnostic.severity === \"error\",\n );\n if (json) {\n console.log(\n JSON.stringify(\n {\n ok: !hasErrors,\n pages: graph.entries.length,\n assets: graph.assets.length,\n diagnostics,\n },\n null,\n 2,\n ),\n );\n } else if (diagnostics.length > 0) {\n console.log(formatDiagnostics(diagnostics));\n } else {\n console.log(\n `Cookbook is healthy: ${graph.entries.length} pages, ${graph.assets.length} assets.`,\n );\n }\n if (hasErrors) process.exitCode = 1;\n}\n\nasync function update(\n project: DocsProject,\n packages: string[],\n json: boolean,\n dryRun: boolean,\n): Promise<void> {\n const existing = await readDocsLock(project.root);\n const next = await reconcileDocsLock(project.config, existing, { packages });\n const changes = next.sources.map((source) => ({\n requested: source.requested,\n from:\n existing?.sources.find(\n (current) => current.requested === source.requested,\n )?.resolved ?? null,\n to: source.resolved,\n }));\n const removed =\n existing?.sources\n .filter(\n (source) =>\n !next.sources.some(\n (current) => current.requested === source.requested,\n ),\n )\n .map((source) => source.requested) ?? [];\n if (!dryRun) {\n const build = normalizeDocsConfig(project.config).build;\n for (const source of next.sources) {\n if (!source.vendored) continue;\n const current = existing?.sources.find(\n (entry) => entry.requested === source.requested,\n );\n if (\n current?.vendored === source.vendored &&\n current.integrity === source.integrity\n )\n continue;\n const { vendored, ...remote } = source;\n const artifact = await materializePackage(remote, build, project.root);\n const destination = resolve(project.root, vendored);\n await mkdir(dirname(destination), { recursive: true });\n await cp(artifact, destination, { recursive: true });\n }\n await writeDocsLock(project.root, next);\n }\n if (json) {\n console.log(\n JSON.stringify(\n {\n ok: true,\n pages: 0,\n assets: 0,\n diagnostics: [],\n dryRun,\n changes,\n removed,\n lock: next,\n },\n null,\n 2,\n ),\n );\n } else {\n for (const change of changes)\n console.log(\n `${change.requested}: ${change.from ?? \"(new)\"} -> ${change.to}`,\n );\n for (const source of removed) console.log(`Removed ${source}`);\n console.log(\n dryRun\n ? \"Dry run: cookbook.lock.json is unchanged.\"\n : \"Updated cookbook.lock.json.\",\n );\n }\n}\n\nfunction printHelp(): never {\n console.log(\n `Usage: cookbook <command> [options]\\n\\nCommands:\\n doctor Validate content, configuration, and theme\\n update [packages] Reconcile configured package sources and their lock\\n\\nOptions:\\n --root <directory>\\n --config <path>\\n --dry-run Preview an update without writing files\\n --json\\n -h, --help`,\n );\n process.exit(0);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { cp, mkdir } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport {\n createDocsGraph,\n formatDiagnostics,\n materializePackage,\n normalizeDocsConfig,\n readDocsLock,\n reconcileDocsLock,\n resolveDocsProject,\n writeDocsLock,\n validateProjectAssets,\n validateBuiltDocs,\n type DocsDiagnostic,\n type DocsProject,\n} from \"@tenphi/docs\";\nimport { resolveDocsTheme } from \"@tenphi/starlight\";\n\nlet jsonOutput = process.argv.slice(2).includes(\"--json\");\n\ntry {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n json: { type: \"boolean\", default: false },\n root: { type: \"string\" },\n config: { type: \"string\" },\n out: { type: \"string\" },\n url: { type: \"string\" },\n \"public-dir\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n jsonOutput = values.json;\n if (values.help || positionals.length === 0) {\n printHelp();\n } else {\n const command = positionals[0];\n if (![\"doctor\", \"update\", \"check-build\"].includes(command!)) {\n throw new Error(`Unknown command: ${command ?? \"\"}.`);\n }\n if (command !== \"update\" && (positionals.length > 1 || values[\"dry-run\"])) {\n throw new Error(\n `${command} does not accept package selectors or --dry-run.`,\n );\n }\n if (command !== \"check-build\" && (values.out || values.url))\n throw new Error(\"--out and --url are only supported by check-build.\");\n if (values[\"public-dir\"] && command !== \"doctor\")\n throw new Error(\"--public-dir is only supported by doctor.\");\n const project = await resolveDocsProject({\n root: resolve(values.root ?? process.cwd()),\n ...(values.config ? { configFile: values.config } : {}),\n });\n if (command === \"doctor\")\n await doctor(\n project,\n values.json,\n values[\"public-dir\"]\n ? resolve(values.root ?? process.cwd(), values[\"public-dir\"])\n : resolve(values.root ?? process.cwd(), \"public\"),\n );\n else if (command === \"check-build\") {\n const graph = await createDocsGraph(project);\n const report = await validateBuiltDocs({\n directory: resolve(values.root ?? process.cwd(), values.out ?? \"dist\"),\n graph,\n ...(values.url ? { deployedUrl: values.url } : {}),\n });\n report.diagnostics.unshift(...graph.diagnostics);\n report.ok = !report.diagnostics.some((d) => d.severity === \"error\");\n if (values.json) console.log(JSON.stringify(report, null, 2));\n else {\n if (report.diagnostics.length)\n console.log(formatDiagnostics(report.diagnostics));\n console.log(\n `${report.ok ? \"Passed\" : \"Failed\"} ${report.scope} checks: ${report.pages} pages, ${report.assets} assets${report.checkedUrls ? `, ${report.checkedUrls} deployed URLs` : \"\"}.`,\n );\n }\n if (!report.ok) process.exitCode = 1;\n } else\n await update(\n project,\n positionals.slice(1),\n values.json,\n values[\"dry-run\"],\n );\n }\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const diagnostics: DocsDiagnostic[] =\n error && typeof error === \"object\" && \"diagnostics\" in error\n ? (error.diagnostics as DocsDiagnostic[])\n : [{ code: \"COOKBOOK_CLI_ERROR\", severity: \"error\", message }];\n if (jsonOutput) {\n console.log(\n JSON.stringify({ ok: false, pages: 0, assets: 0, diagnostics }, null, 2),\n );\n } else {\n console.error(formatDiagnostics(diagnostics));\n }\n process.exitCode = 1;\n}\n\nasync function doctor(\n project: DocsProject,\n json: boolean,\n publicDirectory?: string,\n): Promise<void> {\n const theme = resolveDocsTheme(project.config.theme);\n const graph = await createDocsGraph(project);\n const diagnostics = [\n ...graph.diagnostics,\n ...theme.diagnostics,\n ...(await validateProjectAssets(project, graph, publicDirectory)),\n ];\n const hasErrors = diagnostics.some(\n (diagnostic) => diagnostic.severity === \"error\",\n );\n if (json) {\n console.log(\n JSON.stringify(\n {\n ok: !hasErrors,\n scope: \"preflight\",\n buildVerified: false,\n pages: graph.entries.length,\n assets: graph.assets.length,\n diagnostics,\n },\n null,\n 2,\n ),\n );\n } else if (diagnostics.length > 0) {\n console.log(formatDiagnostics(diagnostics));\n } else {\n console.log(\n `Preflight passed: ${graph.entries.length} pages, ${graph.assets.length} content assets. MDX compilation, remote fonts, and deployment are not checked. Run astro build and cookbook check-build.`,\n );\n }\n if (hasErrors) process.exitCode = 1;\n}\n\nasync function update(\n project: DocsProject,\n packages: string[],\n json: boolean,\n dryRun: boolean,\n): Promise<void> {\n const existing = await readDocsLock(project.root);\n const next = await reconcileDocsLock(project.config, existing, { packages });\n const changes = next.sources.map((source) => ({\n requested: source.requested,\n from:\n existing?.sources.find(\n (current) => current.requested === source.requested,\n )?.resolved ?? null,\n to: source.resolved,\n }));\n const removed =\n existing?.sources\n .filter(\n (source) =>\n !next.sources.some(\n (current) => current.requested === source.requested,\n ),\n )\n .map((source) => source.requested) ?? [];\n if (!dryRun) {\n const build = normalizeDocsConfig(project.config).build;\n for (const source of next.sources) {\n if (!source.vendored) continue;\n const current = existing?.sources.find(\n (entry) => entry.requested === source.requested,\n );\n if (\n current?.vendored === source.vendored &&\n current.integrity === source.integrity\n )\n continue;\n const { vendored, ...remote } = source;\n const artifact = await materializePackage(remote, build, project.root);\n const destination = resolve(project.root, vendored);\n await mkdir(dirname(destination), { recursive: true });\n await cp(artifact, destination, { recursive: true });\n }\n await writeDocsLock(project.root, next);\n }\n if (json) {\n console.log(\n JSON.stringify(\n {\n ok: true,\n pages: 0,\n assets: 0,\n diagnostics: [],\n dryRun,\n changes,\n removed,\n lock: next,\n },\n null,\n 2,\n ),\n );\n } else {\n for (const change of changes)\n console.log(\n `${change.requested}: ${change.from ?? \"(new)\"} -> ${change.to}`,\n );\n for (const source of removed) console.log(`Removed ${source}`);\n console.log(\n dryRun\n ? \"Dry run: cookbook.lock.json is unchanged.\"\n : \"Updated cookbook.lock.json.\",\n );\n }\n}\n\nfunction printHelp(): never {\n console.log(\n `Usage: cookbook <command> [options]\\n\\nCommands:\\n doctor Check configuration, content graph, theme, and local files\\n check-build Inspect production output and optionally deployed URLs\\n update [packages] Reconcile configured package sources and their lock\\n\\nOptions:\\n --root <directory>\\n --config <path>\\n --public-dir <directory> Local public assets for doctor (default: public)\\n --out <directory> Built output (default: dist)\\n --url <base-url> Also check a deployed site\\n --dry-run Preview an update without writing files\\n --json\\n -h, --help`,\n );\n process.exit(0);\n}\n"],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- import { COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, ComponentStylesConfig, DocsConfig, DocsSource, ThemeConfig, ThemeFont, ThemeFontFile, ThemeFonts, defineDocsConfig, mergeDocsConfig } from "@tenphi/docs/config";
3
- export { COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, type ComponentStylesConfig, type DocsConfig, type DocsSource, type ThemeConfig, type ThemeFont, type ThemeFontFile, type ThemeFonts, defineDocsConfig, mergeDocsConfig };
2
+ import { BreadcrumbItem, COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, ComponentStylesConfig, DocsConfig, DocsSource, FontLoadingConfig, PageSeoConfig, SiteLogoConfig, SiteSeoConfig, SocialImage, TableOfContentsConfig, ThemeConfig, ThemeFont, ThemeFontFile, ThemeFonts, defineDocsConfig, mergeDocsConfig } from "@tenphi/docs/config";
3
+ export { type BreadcrumbItem, COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, type ComponentStylesConfig, type DocsConfig, type DocsSource, type FontLoadingConfig, type PageSeoConfig, type SiteLogoConfig, type SiteSeoConfig, type SocialImage, type TableOfContentsConfig, type ThemeConfig, type ThemeFont, type ThemeFontFile, type ThemeFonts, defineDocsConfig, mergeDocsConfig };
@@ -0,0 +1,3 @@
1
+
2
+ import { getCookbookCollection, getCookbookEntry } from "@tenphi/starlight/content";
3
+ export { getCookbookCollection, getCookbookEntry };
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { getCookbookCollection, getCookbookEntry } from "@tenphi/starlight/content";
3
+ export { getCookbookCollection, getCookbookEntry };
@@ -1,3 +1,3 @@
1
1
 
2
- import eslint_plugin_default, { ResolvedConfig, StyleFunctionConfig, TastyValidationConfig, recommended, strict, validationConfig } from "@tenphi/starlight/eslint-plugin";
3
- export { type ResolvedConfig, type StyleFunctionConfig, type TastyValidationConfig, eslint_plugin_default as default, recommended, strict, validationConfig };
2
+ import eslint_plugin_default, { ResolvedConfig, StyleFunctionConfig, TastyValidationConfig, createValidationConfig, recommended, strict, validationConfig } from "@tenphi/starlight/eslint-plugin";
3
+ export { type ResolvedConfig, type StyleFunctionConfig, type TastyValidationConfig, createValidationConfig, eslint_plugin_default as default, recommended, strict, validationConfig };
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import eslint_plugin_default, { recommended, strict, validationConfig } from "@tenphi/starlight/eslint-plugin";
3
- export { eslint_plugin_default as default, recommended, strict, validationConfig };
2
+ import eslint_plugin_default, { createValidationConfig, recommended, strict, validationConfig } from "@tenphi/starlight/eslint-plugin";
3
+ export { createValidationConfig, eslint_plugin_default as default, recommended, strict, validationConfig };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
 
2
2
  import starlight_default, { CookbookOptions, DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS } from "@tenphi/starlight";
3
- import { BrandConfig, BrandDeclaration, BuildConfig, COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, ComponentStyleConfig, ComponentStyles, ComponentStylesConfig, ComponentsConfig, ContentConfig, CookbookComponentName, CookbookComponentStyles, CookbookComponentSubElementName, DocsAsset, DocsConfig, DocsDiagnostic, DocsEntry, DocsFrontmatter, DocsGraph, DocsRoute, DocsSource, EditLinkConfig, HeadConfig, LocaleConfig, MarkdownConfig, NavigationConfig, NavigationItem, NavigationPlacement, NavigationTab, SearchConfig, SiteConfig, SiteIconConfig, SiteVersion, ThemeConfig, ThemeFont, ThemeFontFile, ThemeFonts, ThemePaletteColor, ThemePaletteConfig, ThemeTokenValue, ThemeTokens, TypographyPreset, TypographyPresets, defineDocsConfig, mergeDocsConfig } from "@tenphi/docs";
4
- export { type BrandConfig, type BrandDeclaration, type BuildConfig, COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, type ComponentStyleConfig, type ComponentStyles, type ComponentStylesConfig, type ComponentsConfig, type ContentConfig, type CookbookComponentName, type CookbookComponentStyles, type CookbookComponentSubElementName, type CookbookOptions, DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS, type DocsAsset, type DocsConfig, type DocsDiagnostic, type DocsEntry, type DocsFrontmatter, type DocsGraph, type DocsRoute, type DocsSource, type EditLinkConfig, type HeadConfig, type LocaleConfig, type MarkdownConfig, type NavigationConfig, type NavigationItem, type NavigationPlacement, type NavigationTab, type SearchConfig, type SiteConfig, type SiteIconConfig, type SiteVersion, type ThemeConfig, type ThemeFont, type ThemeFontFile, type ThemeFonts, type ThemePaletteColor, type ThemePaletteConfig, type ThemeTokenValue, type ThemeTokens, type TypographyPreset, type TypographyPresets, starlight_default as default, defineDocsConfig, mergeDocsConfig };
3
+ import { BrandConfig, BrandDeclaration, BreadcrumbItem, BuildConfig, COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, COOKBOOK_PALETTE_NAMES, ComponentStyleConfig, ComponentStyles, ComponentStylesConfig, ComponentsConfig, ContentConfig, CookbookComponentName, CookbookComponentStyles, CookbookComponentSubElementName, DocsAsset, DocsConfig, DocsDiagnostic, DocsEntry, DocsFrontmatter, DocsGraph, DocsRoute, DocsSource, EditLinkConfig, FontLoadingConfig, HeadConfig, LocaleConfig, MarkdownConfig, NavigationConfig, NavigationItem, NavigationPlacement, NavigationTab, PageSeoConfig, SearchConfig, SiteConfig, SiteIconConfig, SiteLogoConfig, SiteSeoConfig, SiteVersion, SocialImage, TableOfContentsConfig, ThemeConfig, ThemeFont, ThemeFontFile, ThemeFonts, ThemePaletteColor, ThemePaletteConfig, ThemeTokenValue, ThemeTokens, TypographyPreset, TypographyPresets, defineDocsConfig, mergeDocsConfig } from "@tenphi/docs";
4
+ export { type BrandConfig, type BrandDeclaration, type BreadcrumbItem, type BuildConfig, COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, COOKBOOK_PALETTE_NAMES, type ComponentStyleConfig, type ComponentStyles, type ComponentStylesConfig, type ComponentsConfig, type ContentConfig, type CookbookComponentName, type CookbookComponentStyles, type CookbookComponentSubElementName, type CookbookOptions, DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS, type DocsAsset, type DocsConfig, type DocsDiagnostic, type DocsEntry, type DocsFrontmatter, type DocsGraph, type DocsRoute, type DocsSource, type EditLinkConfig, type FontLoadingConfig, type HeadConfig, type LocaleConfig, type MarkdownConfig, type NavigationConfig, type NavigationItem, type NavigationPlacement, type NavigationTab, type PageSeoConfig, type SearchConfig, type SiteConfig, type SiteIconConfig, type SiteLogoConfig, type SiteSeoConfig, type SiteVersion, type SocialImage, type TableOfContentsConfig, type ThemeConfig, type ThemeFont, type ThemeFontFile, type ThemeFonts, type ThemePaletteColor, type ThemePaletteConfig, type ThemeTokenValue, type ThemeTokens, type TypographyPreset, type TypographyPresets, starlight_default as default, defineDocsConfig, mergeDocsConfig };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import starlight_default, { DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS } from "@tenphi/starlight";
3
- import { COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, defineDocsConfig, mergeDocsConfig } from "@tenphi/docs";
4
- export { COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS, starlight_default as default, defineDocsConfig, mergeDocsConfig };
3
+ import { COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, COOKBOOK_PALETTE_NAMES, defineDocsConfig, mergeDocsConfig } from "@tenphi/docs";
4
+ export { COOKBOOK_COMPONENT_NAMES, COOKBOOK_COMPONENT_SUB_ELEMENTS, COOKBOOK_PALETTE_NAMES, DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS, starlight_default as default, defineDocsConfig, mergeDocsConfig };
package/docs/ai-agents.md CHANGED
@@ -14,15 +14,14 @@ concrete task, for example:
14
14
  > Create a Cookbook site for this repository in `docs-site/`. Use the existing
15
15
  > README and `docs/` pages as sources. Set the public site URL to
16
16
  > `https://docs.example.com`, add a getting started page if one is missing,
17
- > and run the doctor and production build.
17
+ > and run the complete validation command.
18
18
 
19
19
  From the repository root, the agent can run:
20
20
 
21
21
  ```sh
22
22
  npm create @tenphi/cookbook@latest docs-site -- --source . --site https://docs.example.com --yes
23
23
  cd docs-site
24
- npm run doctor
25
- npm run build
24
+ npm run validate
26
25
  ```
27
26
 
28
27
  For a new project without existing documentation, omit `--source .`. To document
@@ -53,9 +52,21 @@ The HTML pages remain the primary source of information. Their text, headings,
53
52
  links, and page metadata are present in the static response, so an agent can
54
53
  read them without running client JavaScript. Write descriptive headings and
55
54
  link text, keep instructions current, and verify the production output with
56
- `npm run build` and `npm run preview`.
55
+ `npm run validate` and `npm run preview`.
57
56
 
58
57
  Google's [AI Search guidance](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)
59
58
  prioritizes ordinary crawlability and useful content. It says `llms.txt` is
60
59
  not used by Google Search; Cookbook provides it for other agents that use the
61
60
  [llms.txt proposal](https://llmstxt.org/).
61
+
62
+ ## Read a published page
63
+
64
+ Follow the page’s `rel="alternate" type="text/markdown"` link or its **Download Markdown**
65
+ control for a clean reading representation with canonical/source/language/version
66
+ metadata. Use the HTML for interactive component output omitted from Markdown.
67
+ See [Publishing metadata](./publishing.md) for discovery and indexing controls.
68
+
69
+ For maintenance, follow the [upgrade guide](./migration.md), then check the
70
+ [deployed output](./deployment.md#validate-the-published-result). The
71
+ [recipes](./recipes.md) provide complete starting configurations;
72
+ [troubleshooting](./troubleshooting.md) explains common failures.
@@ -51,8 +51,9 @@ and contrast modes.
51
51
  ## Code groups
52
52
 
53
53
  Pass labeled source strings to create an accessible tabbed code group. `language`
54
- is an optional visible label; these programmatic snippets render as escaped plain
55
- text. Ordinary fenced Markdown provides syntax highlighting.
54
+ selects the Shiki grammar (defaults to plain text). Groups share semantic syntax
55
+ colors, configured grammars/transformers, and copy controls with Markdown fences.
56
+ Each panel retains keyboard-accessible tabs and readable fallback content.
56
57
 
57
58
  ```mdx
58
59
  <CodeGroup
@@ -74,13 +75,46 @@ text. Ordinary fenced Markdown provides syntax highlighting.
74
75
 
75
76
  ## Other components
76
77
 
77
- | Component | Props and behavior |
78
- | --------- | ----------------------------------------------------------------------------------------------------------------- |
79
- | `Card` | Optional `title` and `href`; renders an article or link with slot content |
80
- | `Steps` | Ordered list container; supply `li` elements |
81
- | `Logo` | Optional `label`, `decorative`, and `class`; renders the Cookbook mark |
82
- | `Preview` | Required `title`; optional `html`, `css`, and `javascript`; isolates examples in Shadow DOM or a sandboxed iframe |
78
+ | Component | Props and behavior |
79
+ | --------- | --------------------------------------------------------------------------------------------------- |
80
+ | `Card` | Optional `title` and `href`; renders an article or link with slot content |
81
+ | `Steps` | Ordered list container; supply `li` elements |
82
+ | `Logo` | Optional `label`, `decorative`, and `class`; renders the Cookbook mark |
83
+ | `Preview` | Required `title`; optional `html`, `css`, and `javascript`; isolates examples in a sandboxed iframe |
83
84
 
84
85
  The public package exports each component's props type, such as `TabProps`,
85
86
  `CalloutProps`, and `CodeGroupProps`. Styles and all named sub-elements are
86
87
  listed in [Theme and components](./theme-and-components.md#style-customization).
88
+
89
+ ## On-page navigation
90
+
91
+ Desktop pages show a table of contents. Opt into a compact disclosure below the
92
+ header on smaller screens, globally in `docs.config.ts`:
93
+
94
+ ```ts
95
+ tableOfContents: { mobile: true, minHeadingLevel: 2, maxHeadingLevel: 3 }
96
+ ```
97
+
98
+ The same frontmatter field can change heading levels or set `mobile: false` on
99
+ one page. `tableOfContents: false` disables both versions. Selecting a mobile
100
+ contents link closes the disclosure and moves keyboard focus to the heading.
101
+ Customize `theme.styles.MobileTableOfContents` and `TableOfContentsLayout`.
102
+
103
+ ## Hero images
104
+
105
+ Use a relative local image in frontmatter. Cookbook reads its intrinsic dimensions
106
+ and reserves the correct aspect ratio as the layout responds to screen width:
107
+
108
+ ```yaml
109
+ hero:
110
+ tagline: Build something useful.
111
+ image:
112
+ file: ./assets/hero.png
113
+ alt: A preview of the documentation
114
+ ```
115
+
116
+ Use `light` and `dark` instead of `file` for appearance variants; both must have
117
+ the same aspect ratio. Remote or public-path images require explicit `width` and
118
+ `height`. Hero images load eagerly with high fetch priority; use ordinary Markdown
119
+ images for content farther down the page. `theme.styles.Hero.Visual` controls
120
+ responsive sizing without changing the intrinsic dimensions.
package/docs/cli.md CHANGED
@@ -20,19 +20,61 @@ npx @tenphi/cookbook doctor --config ./config/manual.ts --json
20
20
 
21
21
  Doctor checks the content graph, configuration, semantic theme, links, assets,
22
22
  navigation, and redirects without starting Astro. It exits non-zero for errors.
23
- It does not compile MDX or replace a full Astro build.
23
+ It validates configured local font files, logo/favicon sources, public social
24
+ images, and component override paths. `--public-dir ./static` supports a custom
25
+ Astro public directory; the default is `public/` in the app directory.
26
+
27
+ Doctor reports **preflight** health. It does not compile MDX or components,
28
+ download Google fonts, check image decoding, inspect Astro plugins, or verify
29
+ the emitted site. These checks happen during the build and output validation.
24
30
 
25
31
  JSON results use a stable envelope, including configuration failures:
26
32
 
27
33
  ```json
28
34
  {
29
35
  "ok": true,
36
+ "scope": "preflight",
37
+ "buildVerified": false,
30
38
  "pages": 8,
31
39
  "assets": 3,
32
40
  "diagnostics": []
33
41
  }
34
42
  ```
35
43
 
44
+ ## Check the built site and deployment
45
+
46
+ ```sh
47
+ npm run validate
48
+ npx @tenphi/cookbook check-build --out dist --json
49
+ npx @tenphi/cookbook check-build --out dist --url https://docs.example.com/manual/
50
+ ```
51
+
52
+ New projects include `validate`, which runs `cookbook doctor && astro build &&
53
+ cookbook check-build`. Existing projects can add that script to `package.json`.
54
+ Use the same environment variables and configuration for building and checking.
55
+ `--out` is relative to the selected app directory, even when `docs.config.ts`
56
+ points at a different content root.
57
+
58
+ `check-build` reads actual HTML and CSS plus Cookbook's publishing manifest. It
59
+ checks local page/asset URLs, heading fragments, referenced fonts, social images,
60
+ canonical and indexing metadata, sitemap coverage, and discovery files. Direct
61
+ links to drafts are valid; drafts must still be built and carry `noindex`.
62
+ External links and remote font availability are outside this local check.
63
+
64
+ With `--url`, it also fetches the built pages and referenced local assets from
65
+ that deployment, checks status codes, rejects HTML fallbacks for assets, and
66
+ compares page title/canonical/indexing metadata. Include the deployed path prefix;
67
+ it may differ from the production prefix in the build. Production canonicals can
68
+ remain unchanged on a preview host. An unexpected `X-Robots-Tag: noindex` on an
69
+ indexable page is reported. Network failures exit non-zero; rerun after the host
70
+ has finished deploying.
71
+
72
+ JSON adds `scope` (`built-output` or `deployment`) and `checkedUrls`. A missing
73
+ production site URL is a warning: configure `site.url` or Astro's `site` before
74
+ relying on canonical or sitemap checks. Warnings do not fail the command.
75
+ This checks output and hosting; browser accessibility and interaction testing
76
+ remain separate steps.
77
+
36
78
  ## Update package content
37
79
 
38
80
  ```sh
@@ -20,6 +20,10 @@ export default defineDocsConfig({
20
20
  Cookbook validates the configuration when it loads. Unknown top-level and
21
21
  section keys are errors.
22
22
 
23
+ Start with a complete [configuration recipe](./recipes.md) for common tasks.
24
+ This page documents the available options; [working examples](./examples.md)
25
+ covers repository layouts and shared presets.
26
+
23
27
  ## Project resolution and presets
24
28
 
25
29
  `cookbook()` and CLI commands discover the same `docs.config.ts`. Set `root`
@@ -92,10 +96,19 @@ content: {
92
96
  },
93
97
  ```
94
98
 
95
- Each `routeBase` must have a page, and version roots must be distinct. A
99
+ Each `routeBase` must have a home page (a localized home also qualifies), and version roots must be distinct. A
96
100
  single-version site can continue using `site.version` for a simple label.
97
101
 
98
- ### Site icons
102
+ ### Site logo
103
+
104
+ `site.logo` accepts a local image path, `false` to hide the mark, or an object
105
+ with `src` (one image) or `light` and `dark` (appearance variants). Optional
106
+ `alt`, `decorative`, `href`, `width`, and `height` control accessible text,
107
+ destination, and intrinsic sizing. It applies to the header and mobile drawer.
108
+ See the [shared logo guide](./theme-and-components.md#a-shared-site-logo).
109
+ The logo and favicon are independent.
110
+
111
+ ## Site icons
99
112
 
100
113
  Cookbook ships its book mark as the default favicon. Set `site.favicon` to a
101
114
  local SVG, PNG, JPEG, WebP, AVIF, or GIF when the documentation should use the
@@ -204,6 +217,14 @@ renderer frontmatter. Use `content.frontmatter: "reject"` to reject unrelated
204
217
  fields. Other supported fields include `aliases`, `template`, `hero`, `editUrl`,
205
218
  `lastUpdated`, `prev`, `next`, `head`, `draft`, and `slug`.
206
219
 
220
+ ### Draft pages
221
+
222
+ Set `draft: true` to publish a page for review at its normal URL. Drafts remain
223
+ available in development and production, and authored links to them are valid.
224
+ They are excluded from automatic navigation, pagination, search, sitemaps, and
225
+ `llms.txt`, and receive `noindex` metadata. Removing `draft` makes the page
226
+ discoverable on the next build. A draft URL is public; this is not access control.
227
+
207
228
  ## Languages
208
229
 
209
230
  Expose Starlight's multilingual routing and language picker directly:
@@ -218,10 +239,17 @@ defaultLocale: "root",
218
239
  ```
219
240
 
220
241
  Locale keys other than `root` are URL prefixes. A source routed to `/fr/guide`
221
- is the French counterpart of `/guide`; `defaultLocale` provides fallback
222
- content when a translated route is missing. See Starlight's
223
- [internationalization guide](https://starlight.astro.build/guides/i18n/) for
224
- the shared routing and fallback behavior.
242
+ is the French counterpart of `/guide`. Navigation and pagination include the
243
+ current locale's published routes. Locale prefixes are added once, after the
244
+ site's deployment base.
245
+
246
+ Cookbook does not create duplicate fallback pages. When the language picker
247
+ cannot find a translation, it links to the equivalent page in `defaultLocale`,
248
+ then an existing locale home or default-language home. The picker labels
249
+ cross-language fallbacks. Locales with no usable target are omitted. Header home
250
+ links use the same policy, so a missing `/fr/` never creates a broken home link.
251
+ Alternate-language metadata lists only existing published translations. Drafts
252
+ remain directly accessible but are excluded from locale discovery.
225
253
 
226
254
  ## Content
227
255
 
@@ -350,13 +378,19 @@ theme: {
350
378
  surface: { tone: 98, saturation: 0.05 },
351
379
  text: {
352
380
  base: "surface",
353
- tone: "-10",
381
+ tone: 0,
382
+ saturation: 0,
383
+ contrast: { wcag: [7, 10] }
384
+ },
385
+ heading: {
386
+ base: "surface",
387
+ tone: [4, 0],
354
388
  saturation: 0,
355
389
  contrast: { wcag: [7, 10] }
356
390
  },
357
391
  textSoft: {
358
392
  base: "surface",
359
- tone: "-10",
393
+ tone: [25, 10],
360
394
  saturation: 0.05,
361
395
  contrast: { wcag: [4.5, 7] }
362
396
  }
@@ -404,8 +438,11 @@ flows. The default layout is capped at `87.5rem` (1400px), matching the
404
438
  Tasty site, with a `58rem` reading column and a `17.5rem` sidebar. `brand.from`
405
439
  supplies the color seed. `palette` supplies semantic Glaze declarations that
406
440
  inherit its hue and saturation unless a role supplies its own `from`. `tone` and
407
- a saturation factor set the surface; `base` and `contrast` express reading-color
408
- requirements against that surface. Glaze resolves light, dark, and both
441
+ a saturation factor set the surface. Reading text uses absolute tones: body
442
+ text starts at 0, and headings at 4. Glaze applies normal-mode tone boundaries
443
+ and adapts the values for dark mode; `base` and `contrast` provide minimum
444
+ contrast safeguards. `palette.heading` follows an explicit `palette.text`
445
+ declaration unless customized separately. Glaze resolves light, dark, and both
409
446
  high-contrast modes independently. Literal color shorthand and structured
410
447
  brand hue/saturation/tone remain supported. A requested APCA floor below 45 requires the explicit
411
448
  `unsafeContrast: true` escape hatch. Learn more in
@@ -456,11 +493,9 @@ export default defineConfig({
456
493
  });
457
494
  ```
458
495
 
459
- Cookbook forwards these plugins to Starlight. Plugins that add stylesheets or
460
- style tags fail the production build because Cookbook ships only Tasty styles.
461
- Plugins that replace
462
- Cookbook-owned components or change routes can conflict with the content graph.
463
- Use `theme.styles` for visual changes.
496
+ Cookbook preserves custom metadata for route middleware and supports custom schemas.
497
+ See the [plugin compatibility guide](./plugins.md) for the tested ecosystem matrix,
498
+ content query API, Tasty adapters, and collection/style limitations.
464
499
 
465
500
  Cookbook renders fenced `mermaid` blocks as responsive, theme-aware SVG during
466
501
  the static build. Flowcharts, state, sequence, class, and entity-relationship
@@ -585,3 +620,30 @@ contrast modes. Adjust blur and other header styling via `theme.styles.HeaderFra
585
620
  `theme.palette.overlay` controls the fixed underlay color, defaulting to black at
586
621
  50% opacity in every scheme. The mobile drawer uses the Glaze shadow token and
587
622
  a 120ms slide transition; customize these through `theme.styles.Sidebar`.
623
+
624
+ ### Translate Cookbook controls
625
+
626
+ Cookbook includes English and French interface messages. Other languages use
627
+ English for missing Cookbook messages while Starlight supplies its own built-in
628
+ translations. Override messages by language code; regional codes fall back to the
629
+ base language and then English:
630
+
631
+ ```ts
632
+ translations: {
633
+ de: {
634
+ appearance: "Darstellung",
635
+ closeNavigation: "Navigation schließen",
636
+ "navigation.Guide": "Anleitung",
637
+ },
638
+ },
639
+ ```
640
+
641
+ `navigation.<configured label>` translates named tabs and navigation groups.
642
+ Automatically generated links use the title from the translated page.
643
+ Cookbook message keys are `appearance`, `colorScheme`, `contrast`,
644
+ `normalContrast`, `highContrast`, `auto`, `closeNavigation`, `primary`, `sections`,
645
+ `chooseSection`, `headerLinks`, `more`, `closeMore`, `moreLinks`, `versions`,
646
+ `documentationVersion`, `documentationVersions`, `version`, `home`,
647
+ `generatedWith`, `viewSource`, `copyCode`, `codeCopied`, `codeCopyError`,
648
+ `copyHeading`, `linkCopied`, `linkCopyError`, `notFound`, `notFoundDescription`,
649
+ and `fallback`. Preserve `{heading}` in the `copyHeading` message.
@@ -120,6 +120,32 @@ into one local document before the build. Cookbook checks the spec size against
120
120
  `build.maxAssetBytes` and reports invalid or duplicate operation routes.
121
121
  Generated pages are static references; they do not make requests to the API.
122
122
 
123
+ ### Supported OpenAPI details
124
+
125
+ Cookbook renders the core OpenAPI 3.0/3.1 operation model:
126
+
127
+ - Operation parameters replace path parameters with the same **name and location**.
128
+ The same name in `query` and `path` remains two parameters.
129
+ - Parameters and request/response media types render singular `example` values
130
+ (including `false`, `0`, and `null`) and named `examples`. Local Example Object
131
+ references resolve to their summaries, descriptions, and values. HTTP(S)
132
+ `externalValue` examples are linked and never fetched by the build.
133
+ - Operations inherit document `security`. An explicit array replaces it;
134
+ `security: []` declares anonymous access. Empty objects allow an anonymous
135
+ alternative. Schemes within an object are required together; array entries
136
+ are alternatives. Scheme details and required scopes appear on each page.
137
+ - Local JSON Pointer references support escaped names and reference chains.
138
+ Reference-only cycles and unknown security schemes produce diagnostics.
139
+ Recursive schema definitions remain visible as JSON references.
140
+
141
+ These follow the [OpenAPI parameter and security rules](https://spec.openapis.org/oas/v3.1.1.html#operation-object).
142
+ Cookbook is not a complete OpenAPI validator or an interactive API client.
143
+ Callbacks, webhooks, link execution, OAuth login flows, multipart editors,
144
+ request serialization, and interactive schema expansion are not implemented.
145
+ Schema JSON is displayed as authored; externally split specs must be bundled
146
+ first. Use an OpenAPI validator alongside `cookbook doctor` for specification
147
+ validation beyond the rendered feature set.
148
+
123
149
  ## npm package sources
124
150
 
125
151
  Package sources read documentation from the actual npm artifact:
@@ -48,6 +48,14 @@ export default defineDocsConfig({
48
48
  });
49
49
  ```
50
50
 
51
+ ## Keep styling on the server
52
+
53
+ Tasty and Glaze run during build/server rendering. The browser receives extracted
54
+ CSS; never initialize their configuration or ship their runtime in client scripts
55
+ or hydrated islands. Use server-rendered styled components with small client
56
+ scripts for behavior. Cookbook rejects browser imports of its styling API,
57
+ Tasty, and Glaze, including transitive dependencies.
58
+
51
59
  ## Write styles through Cookbook
52
60
 
53
61
  Import `defineComponent`, `tasty`, `useGlobalStyles`, `resolveComponentStyles`,
@@ -12,12 +12,13 @@ plain file server.
12
12
  ## Build locally
13
13
 
14
14
  ```sh
15
- npm run build
15
+ npm run validate
16
16
  npm run preview
17
17
  ```
18
18
 
19
- Run [`cookbook doctor`](./cli.md#validate-with-doctor) in CI before building
20
- when you want a focused content diagnostic step.
19
+ New projects include `validate`: preflight, production build, then built-output
20
+ checks. Existing projects can add this script using the [upgrade guide](./migration.md).
21
+ Run [`cookbook doctor`](./cli.md#validate-with-doctor) for a focused preflight.
21
22
 
22
23
  ## GitHub Pages
23
24
 
@@ -93,3 +94,19 @@ to the sitemap. Host these files with the rest of `dist/`; custom files in
93
94
 
94
95
  Check the deployed HTML, `sitemap-index.xml`, and `llms.txt` through their public
95
96
  URLs. See [AI agents](./ai-agents.md) for the setup and reading workflow.
97
+
98
+ For title templates, social images, preview noindex, older-version policies,
99
+ breadcrumbs, and copy-page downloads, see [Publishing metadata](./publishing.md).
100
+
101
+ ## Validate the published result
102
+
103
+ New projects include `npm run validate` for preflight, build, and output checks.
104
+ After the host finishes deploying, run:
105
+
106
+ ```sh
107
+ npx @tenphi/cookbook check-build --url https://docs.example.com/manual/
108
+ ```
109
+
110
+ Use your actual deployment URL, including any base path, and the same local
111
+ `dist/` output that was deployed. See [CLI checks](./cli.md#check-the-built-site-and-deployment)
112
+ for the exact coverage, warnings, JSON output, and custom output directories.
package/docs/examples.md CHANGED
@@ -19,7 +19,7 @@ export default defineConfig({ integrations: [cookbook()] });
19
19
  Put this in `docs.config.ts` at the repository root. README/docs discovery is
20
20
  automatic; add `content.sources` only when you need different routes or files.
21
21
 
22
- ```ts
22
+ ```ts cookbook-verify=minimal
23
23
  import { defineDocsConfig } from "@tenphi/cookbook/config";
24
24
 
25
25
  export default defineDocsConfig({ site: { title: "My library" } });
@@ -75,7 +75,7 @@ is the npm public registry. Keep credentials out of committed configuration.
75
75
 
76
76
  A preset is an ordinary exported `DocsConfig` object:
77
77
 
78
- ```ts
78
+ ```ts cookbook-verify=shared-theme
79
79
  import { defineDocsConfig, mergeDocsConfig } from "@tenphi/cookbook/config";
80
80
 
81
81
  const company = defineDocsConfig({
@@ -99,3 +99,6 @@ Objects merge recursively, arrays replace, and inputs are not mutated. Tasty
99
99
  styles preserve their default values when adding conditional overrides. A preset
100
100
  can live in a shared workspace package. Component overrides remain partial Tasty
101
101
  objects and merge into Cookbook's complete base styles inside the renderer.
102
+
103
+ For complete branding, font, locale, and preview configurations, see
104
+ [configuration recipes](./recipes.md).