From 66e03d5287fab0bdfedf5dda6846d02ac4bcd6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:32:40 +0100 Subject: [PATCH] docs: own public documentation metadata (#1713) --- .agents/skills/docs-maintainer/SKILL.md | 9 +- .github/workflows/notify-docs.yml | 65 +++++++++++ CONTRIBUTING.md | 2 +- Makefile | 1 + docs/public/README.md | 47 ++++++++ docs/public/add-agent-cli.md | 5 + docs/public/cli.md | 5 + docs/public/configuration.md | 5 + docs/public/desktop-app.md | 5 + docs/public/docker.md | 5 + docs/public/git-operations.md | 5 + docs/public/index.md | 5 + docs/public/k8s.md | 5 + docs/public/meta.json | 23 ++++ docs/public/remote-cloud-environment.md | 5 + docs/public/run-as-a-service.md | 5 + docs/public/websocket-api.md | 5 + docs/public/windows-support.md | 5 + docs/public/workflow-import-export.md | 5 + docs/public/workflow-tips.md | 5 + scripts/validate-public-docs.mjs | 148 ++++++++++++++++++++++++ scripts/validate-public-docs.test.mjs | 147 +++++++++++++++++++++++ 22 files changed, 509 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/notify-docs.yml create mode 100644 docs/public/README.md create mode 100644 docs/public/meta.json create mode 100644 scripts/validate-public-docs.mjs create mode 100644 scripts/validate-public-docs.test.mjs diff --git a/.agents/skills/docs-maintainer/SKILL.md b/.agents/skills/docs-maintainer/SKILL.md index b2aa7f237e..27b4b29438 100644 --- a/.agents/skills/docs-maintainer/SKILL.md +++ b/.agents/skills/docs-maintainer/SKILL.md @@ -14,7 +14,8 @@ Use this skill to decide whether public docs need updates and to make those upda - Implementation plans stay under `docs/plans/**`. - Architecture decisions stay under `docs/decisions/**`. - Raw supporting notes can remain under `docs/**` outside `docs/public/**`, but do not publish them unless rewritten for users. -- The landing/docs website consumes public docs through its manifest and generated content. Do not hand-edit generated website docs. +- `docs/public/meta.json` owns published-page order and navigation groups. Page paths own routes, and page frontmatter owns titles and descriptions. +- The landing/docs website generates its content from this directory. Do not hand-edit generated files in the landing repository. ## When Docs Need Updates @@ -39,6 +40,7 @@ Skip public docs when the change is: 2. Search `docs/public/**` first for affected terms and commands. 3. If public docs exist, update them with the same PR as the behavior change. 4. If no public docs exist but the behavior is user-facing, add or propose the smallest useful public page/section. + When adding a page, include `title` and `description` frontmatter and list its page slug or path without the `.md` extension in `docs/public/meta.json` exactly once, for example `cli`. See `docs/public/README.md`. 5. If the change only updates implementation intent or architectural history, update specs/plans/ADRs instead. 6. Keep public docs task-oriented: prerequisites, commands, expected result, troubleshooting, and links to reference. 7. Preserve internal links inside `docs/public/**` where possible. Link to source-only raw docs only when the raw note is intentionally not published. @@ -51,13 +53,16 @@ Run the checks relevant to your change: ```bash # Replace SEARCH_TERM with the command, config key, or terminology that changed. rg -n "SEARCH_TERM" docs/public docs/specs docs/decisions +node --test scripts/validate-public-docs.test.mjs +node scripts/validate-public-docs.mjs ``` For website docs publishing changes, also run from the landing repo: ```bash +pnpm install --frozen-lockfile pnpm --filter @kandev/docs fetch-docs -pnpm exec vitest run apps/docs/lib/docs-processing.test.ts +pnpm exec vitest run apps/docs/lib/docs-processing.test.ts apps/docs/lib/public-docs.test.ts pnpm --filter @kandev/docs build ``` diff --git a/.github/workflows/notify-docs.yml b/.github/workflows/notify-docs.yml new file mode 100644 index 0000000000..b8eeb5688d --- /dev/null +++ b/.github/workflows/notify-docs.yml @@ -0,0 +1,65 @@ +name: Published Docs + +on: + push: + branches: [main] + paths: + - "docs/public/**" + pull_request: + branches: [main] + paths: + - "docs/public/**" + - "scripts/validate-public-docs.mjs" + - "scripts/validate-public-docs.test.mjs" + - ".github/workflows/notify-docs.yml" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + validate: + name: Validate public docs + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + + - name: Test public docs validator + run: node --test scripts/validate-public-docs.test.mjs + + - name: Validate public docs + run: node scripts/validate-public-docs.mjs + + rebuild: + name: Trigger Cloudflare Pages rebuild + needs: validate + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 4 + + steps: + - name: Trigger Cloudflare Pages deploy hook + env: + DEPLOY_HOOK_URL: ${{ secrets.CLOUDFLARE_PAGES_DEPLOY_HOOK_KANDEV_AI }} + run: | + if [[ -z "$DEPLOY_HOOK_URL" ]]; then + echo "::error::CLOUDFLARE_PAGES_DEPLOY_HOOK_KANDEV_AI is not configured" + exit 1 + fi + + curl --fail --silent --show-error \ + --max-time 30 \ + --retry 3 \ + --retry-all-errors \ + --retry-max-time 90 \ + --request POST \ + "$DEPLOY_HOOK_URL" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa0ab4dc66..9fca9b04e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ You must understand the code you submit. You're welcome to use AI tools to help 1. **Fork and branch.** Create a feature branch from `main`. 2. **Keep PRs focused.** One logical change per PR. Small PRs get reviewed faster. -3. **Update public docs when behavior changes.** User-facing docs live in `docs/public/**`. If your change affects CLI commands, config keys, install/deploy flows, workflows, executors, APIs, screenshots, or user-facing terminology, update the relevant public docs in the same PR. +3. **Update public docs when behavior changes.** User-facing docs live in `docs/public/**`. If your change affects CLI commands, config keys, install/deploy flows, workflows, executors, APIs, screenshots, or user-facing terminology, update the relevant public docs in the same PR. See the [public docs contribution guide](docs/public/README.md) when editing navigation or adding a page. 4. **Test your changes.** Run `make fmt` first, then `make typecheck test lint` before submitting. Manually verify that your feature works end-to-end, and add screenshots or recordings to the PR if it has a UI component. **If your change touches any UI files (anything under `apps/web/`), you must add or update Playwright e2e tests in `apps/web/e2e/` to prevent regressions.** Run them with `make test-e2e`. See [docs/test_e2e_web.md](docs/test_e2e_web.md) for patterns and fixtures. ## Bug Reports diff --git a/Makefile b/Makefile index 3e7af05ea8..72eb662ce7 100644 --- a/Makefile +++ b/Makefile @@ -427,6 +427,7 @@ test-scripts: @python3 scripts/lint-harness-files.test.py @bash scripts/release-desktop.test.sh @node --test apps/desktop/e2e/desktop-launch-smoke.test.mjs + @node --test scripts/validate-public-docs.test.mjs .PHONY: test-e2e test-e2e: build-backend build-web diff --git a/docs/public/README.md b/docs/public/README.md new file mode 100644 index 0000000000..a0e3bc2367 --- /dev/null +++ b/docs/public/README.md @@ -0,0 +1,47 @@ +# Contributing to the Public Docs + +Files in this directory are the source for [kandev.ai/docs](https://kandev.ai/docs). A merged change under `docs/public/**` triggers a Cloudflare Pages rebuild automatically. + +## Update an Existing Page + +Edit the relevant Markdown file directly. Keep commands, configuration keys, API names, and screenshots aligned with the implementation in the same pull request. + +Use relative links between published pages, for example `[CLI](cli.md)`. The website build rewrites those links to `/docs/...` routes. Links to repository files outside `docs/public` are rewritten to their GitHub source URLs. + +## Add a Page + +1. Create a lowercase, kebab-case Markdown file in this directory, such as `custom-executors.md`. Its filename becomes its route: `custom-executors.md` is published at `/docs/custom-executors`. +2. Start the file with `title` and `description` frontmatter, followed by one level-one heading: + + ```markdown + --- + title: "Custom Executors" + description: "Configure a custom executor for Kandev tasks." + --- + + # Custom Executors + + Page content starts here. + ``` + +3. Add the filename without its extension to `meta.json` exactly once. Its position controls sidebar order. Entries such as `---Execution---` create section labels. +4. Link the new page from related documentation where useful. + +Pages omitted from `meta.json`, duplicate entries, unknown entries, and missing frontmatter fail validation. + +## Validate + +Run the dependency-free checks from the Kandev repository root: + +```bash +node --test scripts/validate-public-docs.test.mjs +node scripts/validate-public-docs.mjs +``` + +To build the complete docs site, clone `kdlbs/landing` beside this repository and run: + +```bash +cd ../landing +pnpm install --frozen-lockfile +KANDEV_DOCS_SOURCE_PATH=../kandev/docs pnpm --filter @kandev/docs build +``` diff --git a/docs/public/add-agent-cli.md b/docs/public/add-agent-cli.md index 2672042eab..6f46a0dd18 100644 --- a/docs/public/add-agent-cli.md +++ b/docs/public/add-agent-cli.md @@ -1,3 +1,8 @@ +--- +title: "Add an Agent CLI" +description: "Add a new ACP adapter or agent CLI integration to Kandev." +--- + # Adding a New Agent CLI Integration This guide explains how to add support for a new coding agent CLI to Kandev. diff --git a/docs/public/cli.md b/docs/public/cli.md index 81867ae42e..c366aaee6a 100644 --- a/docs/public/cli.md +++ b/docs/public/cli.md @@ -1,3 +1,8 @@ +--- +title: "CLI" +description: "Install, start, and operate Kandev from the command line." +--- + # Kandev CLI ## Architecture diff --git a/docs/public/configuration.md b/docs/public/configuration.md index 9893d03ec0..2357b03ca8 100644 --- a/docs/public/configuration.md +++ b/docs/public/configuration.md @@ -1,3 +1,8 @@ +--- +title: "Configuration" +description: "Configure Kandev workspaces, runtimes, settings, and environment behavior." +--- + # Configuration Kandev's backend reads configuration from three sources, in this order of precedence (later sources override earlier ones): diff --git a/docs/public/desktop-app.md b/docs/public/desktop-app.md index 856a3455ab..418efd84fa 100644 --- a/docs/public/desktop-app.md +++ b/docs/public/desktop-app.md @@ -1,3 +1,8 @@ +--- +title: "Desktop App" +description: "Install and run the Tauri desktop app with the native Kandev runtime." +--- + # Kandev Desktop App Kandev desktop is a Tauri app that starts the native Kandev runtime locally and shows the existing Kandev UI in a system WebView. It does not require Node.js at runtime. Node.js, pnpm, Rust, and the Tauri CLI are build-time tools only. diff --git a/docs/public/docker.md b/docs/public/docker.md index e7d8afc643..59b924ed36 100644 --- a/docs/public/docker.md +++ b/docs/public/docker.md @@ -1,3 +1,8 @@ +--- +title: "Docker" +description: "Run Kandev and agent execution environments with Docker." +--- + # Docker Guide Run Kandev in a Docker container. For Kubernetes deployment, see [k8s.md](k8s.md). diff --git a/docs/public/git-operations.md b/docs/public/git-operations.md index ff0345ce42..08f29ddeba 100644 --- a/docs/public/git-operations.md +++ b/docs/public/git-operations.md @@ -1,3 +1,8 @@ +--- +title: "Git Operations" +description: "Understand how Kandev manages repositories, worktrees, branches, and review surfaces." +--- + # Git Operations for Worktrees This document describes the architecture for Git operations (Pull, Push, Rebase, Merge, and Abort) on worktrees in Kandev. diff --git a/docs/public/index.md b/docs/public/index.md index 03d2212ba3..3ca8b0d90d 100644 --- a/docs/public/index.md +++ b/docs/public/index.md @@ -1,3 +1,8 @@ +--- +title: "Overview" +description: "Start building with Kandev agentic development workflows." +--- + # Kandev Feature Guide This page expands the short feature list in the README without turning the README into a catalog. diff --git a/docs/public/k8s.md b/docs/public/k8s.md index 1dd49453a2..087246fefe 100644 --- a/docs/public/k8s.md +++ b/docs/public/k8s.md @@ -1,3 +1,8 @@ +--- +title: "Kubernetes" +description: "Deploy Kandev in Kubernetes environments." +--- + # Kubernetes Deployment Guide This guide covers building the Kandev Docker image and deploying it to a Kubernetes cluster. diff --git a/docs/public/meta.json b/docs/public/meta.json new file mode 100644 index 0000000000..f33c238f25 --- /dev/null +++ b/docs/public/meta.json @@ -0,0 +1,23 @@ +{ + "title": "Kandev Docs", + "pages": [ + "---Start---", + "index", + "cli", + "configuration", + "desktop-app", + "---Execution---", + "docker", + "k8s", + "run-as-a-service", + "remote-cloud-environment", + "windows-support", + "---Workflows---", + "workflow-tips", + "workflow-import-export", + "git-operations", + "add-agent-cli", + "---Reference---", + "websocket-api" + ] +} diff --git a/docs/public/remote-cloud-environment.md b/docs/public/remote-cloud-environment.md index ac14507638..63737ca5e0 100644 --- a/docs/public/remote-cloud-environment.md +++ b/docs/public/remote-cloud-environment.md @@ -1,3 +1,8 @@ +--- +title: "Remote Cloud Environment" +description: "Use remote execution environments for agent work." +--- + # Remote Cloud Environment Instructions Setup notes and caveats for developing Kandev in ephemeral cloud VMs (Cursor Cloud, Codex, GitHub Codespaces, or similar sandboxed environments). diff --git a/docs/public/run-as-a-service.md b/docs/public/run-as-a-service.md index 6ddcbdde86..775e92211c 100644 --- a/docs/public/run-as-a-service.md +++ b/docs/public/run-as-a-service.md @@ -1,3 +1,8 @@ +--- +title: "Run as a Service" +description: "Run Kandev as a persistent service on your own machine or infrastructure." +--- + # Run Kandev as a Service Install Kandev as an OS-managed service (systemd on Linux, launchd on macOS) so it auto-starts and stays running. User-mode services installed by `kandev service install` can self-update from the System → Updates page. Non-service installs and `--system` services still update manually: `npm i -g kandev@latest` or `brew upgrade kandev`, then re-run `kandev service install`. diff --git a/docs/public/websocket-api.md b/docs/public/websocket-api.md index 7048526449..1c9d9fb07e 100644 --- a/docs/public/websocket-api.md +++ b/docs/public/websocket-api.md @@ -1,3 +1,8 @@ +--- +title: "WebSocket API" +description: "Reference for Kandev WebSocket messages and API behavior." +--- + # Kandev WebSocket API This page documents the WebSocket envelope, current domain model, and the request actions used by the main task and agent workflow. It is not a substitute for the Go request types: payloads evolve with the application. diff --git a/docs/public/windows-support.md b/docs/public/windows-support.md index 002fd0f234..5767833c61 100644 --- a/docs/public/windows-support.md +++ b/docs/public/windows-support.md @@ -1,3 +1,8 @@ +--- +title: "Windows Support" +description: "Run Kandev on Windows through native and WSL workflows." +--- + # Windows Support (WSL) Kandev runs on Windows through WSL (Windows Subsystem for Linux). diff --git a/docs/public/workflow-import-export.md b/docs/public/workflow-import-export.md index 76d3c1e0c2..54146f2786 100644 --- a/docs/public/workflow-import-export.md +++ b/docs/public/workflow-import-export.md @@ -1,3 +1,8 @@ +--- +title: "Workflow Import and Export" +description: "Move workflows between workspaces or Kandev installs with portable YAML." +--- + # Workflow Import / Export — Portable YAML Format Kandev workflows can be exported to a portable YAML file and imported into diff --git a/docs/public/workflow-tips.md b/docs/public/workflow-tips.md index 0c0117222d..7b875b84e4 100644 --- a/docs/public/workflow-tips.md +++ b/docs/public/workflow-tips.md @@ -1,3 +1,8 @@ +--- +title: "Workflow Tips" +description: "Practical guidance for designing and operating Kandev workflows." +--- + # Workflows Kandev ships with five workflow templates. Each defines a sequence of steps with automated transitions - agents start, stop, and move between steps based on events like entering a step, sending a message, or an agent completing its turn. diff --git a/scripts/validate-public-docs.mjs b/scripts/validate-public-docs.mjs new file mode 100644 index 0000000000..7d930c55c5 --- /dev/null +++ b/scripts/validate-public-docs.mjs @@ -0,0 +1,148 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); + +/** + * Validate that every published Markdown page has frontmatter and appears + * exactly once in the public navigation metadata. + * + * @param {string} [docsDir] Directory containing published docs and meta.json. + * @returns {Promise<{pageCount: number}>} Number of validated published pages. + */ +export async function validatePublicDocs( + docsDir = path.join(repoRoot, "docs/public"), +) { + const meta = await readMeta(docsDir); + const files = await collectMarkdownFiles(docsDir); + const pagesBySlug = new Map(); + + for (const file of files) { + if (path.posix.basename(file).toLowerCase() === "readme.md") continue; + + const markdown = await fs.readFile(path.join(docsDir, file), "utf8"); + assertFrontmatter(file, markdown); + + const slug = file.replace(/\.mdx?$/, "").replace(/\/index$/, ""); + const existing = pagesBySlug.get(slug); + if (existing) { + throw new Error( + `multiple published files resolve to slug ${slug}: ${existing}, ${file}`, + ); + } + pagesBySlug.set(slug, file); + } + + const listed = new Set(); + for (const entry of meta.pages) { + if (isNavigationDecoration(entry)) continue; + if (listed.has(entry)) { + throw new Error(`meta.json lists page more than once: ${entry}`); + } + if (!pagesBySlug.has(entry)) { + throw new Error(`meta.json references unknown page: ${entry}`); + } + + listed.add(entry); + } + + for (const slug of pagesBySlug.keys()) { + if (!listed.has(slug)) { + throw new Error(`meta.json is missing published page: ${slug}`); + } + } + + return { pageCount: pagesBySlug.size }; +} + +/** + * Read and validate the shape of public navigation metadata. + * + * @param {string} docsDir Directory containing meta.json. + * @returns {Promise<{pages: string[]}>} Parsed navigation metadata. + */ +async function readMeta(docsDir) { + const raw = await fs.readFile(path.join(docsDir, "meta.json"), "utf8"); + const meta = JSON.parse(raw); + if (!meta || typeof meta !== "object" || Array.isArray(meta)) { + throw new Error("meta.json must contain a JSON object"); + } + if ( + !Array.isArray(meta.pages) || + !meta.pages.every((entry) => typeof entry === "string") + ) { + throw new Error("meta.json pages must be an array of strings"); + } + + return meta; +} + +/** + * Recursively collect Markdown paths relative to the published docs root. + * + * @param {string} dir Published docs root. + * @param {string} [relativeDir] Directory relative to the docs root. + * @returns {Promise} Sorted relative Markdown paths. + */ +async function collectMarkdownFiles(dir, relativeDir = "") { + const entries = await fs.readdir(path.join(dir, relativeDir), { + withFileTypes: true, + }); + const files = await Promise.all( + entries.map(async (entry) => { + const relativePath = path.posix.join(relativeDir, entry.name); + if (entry.isDirectory()) { + return collectMarkdownFiles(dir, relativePath); + } + + return /\.mdx?$/.test(entry.name) ? [relativePath] : []; + }), + ); + + return files.flat().sort(); +} + +/** + * Require non-empty title and description fields in leading frontmatter. + * + * @param {string} file Relative page path used in validation errors. + * @param {string} markdown Page source. + * @returns {void} + */ +function assertFrontmatter(file, markdown) { + const block = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1]; + if ( + !block || + !/^title:\s*\S.*$/m.test(block) || + !/^description:\s*\S.*$/m.test(block) + ) { + throw new Error( + `${file} must start with YAML frontmatter containing title and description`, + ); + } +} + +/** + * Return whether a metadata entry is a navigation heading. + * + * @param {string} entry Navigation metadata entry. + * @returns {boolean} Whether the entry is a heading decoration. + */ +function isNavigationDecoration(entry) { + return /^---.*---$/.test(entry); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + validatePublicDocs() + .then(({ pageCount }) => + console.log(`Validated ${pageCount} published docs pages.`), + ) + .catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/scripts/validate-public-docs.test.mjs b/scripts/validate-public-docs.test.mjs new file mode 100644 index 0000000000..89e9c97526 --- /dev/null +++ b/scripts/validate-public-docs.test.mjs @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test, { after } from "node:test"; +import { validatePublicDocs } from "./validate-public-docs.mjs"; + +const tempDirs = []; + +/** + * Create an isolated published-docs fixture. + * + * @param {Record} files Fixture files keyed by relative path. + * @param {{pages: string[]}} meta Navigation metadata. + * @returns {Promise} Temporary fixture directory. + */ +async function createDocs(files, meta) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "kandev-public-docs-")); + tempDirs.push(dir); + await Promise.all( + Object.entries(files).map(async ([file, content]) => { + const target = path.join(dir, file); + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.writeFile(target, content); + }), + ); + await fs.writeFile(path.join(dir, "meta.json"), JSON.stringify(meta)); + return dir; +} + +after(async () => { + await Promise.all( + tempDirs.map((dir) => fs.rm(dir, { recursive: true, force: true })), + ); +}); + +const validPage = `--- +title: "Overview" +description: "Start using Kandev." +--- + +# Kandev + +Page body. +`; + +test("accepts explicitly ordered pages with required frontmatter", async () => { + const dir = await createDocs( + { + "README.md": "# Contributing", + "index.md": validPage, + "cli.md": validPage, + }, + { title: "Kandev Docs", pages: ["---Start---", "index", "cli"] }, + ); + + await assert.doesNotReject(validatePublicDocs(dir)); +}); + +test("rejects published pages omitted from meta.json", async () => { + const dir = await createDocs( + { "index.md": validPage, "cli.md": validPage }, + { pages: ["index"] }, + ); + + await assert.rejects( + validatePublicDocs(dir), + /meta.json is missing published page: cli/, + ); +}); + +test("rejects meta.json entries without a matching file", async () => { + const dir = await createDocs( + { "index.md": validPage }, + { pages: ["index", "nonexistent"] }, + ); + + await assert.rejects( + validatePublicDocs(dir), + /meta.json references unknown page: nonexistent/, + ); +}); + +test("rejects unsupported link decorations as unknown pages", async () => { + const dir = await createDocs( + { "index.md": validPage }, + { pages: ["index", "external:[Support](https://example.com)"] }, + ); + + await assert.rejects( + validatePublicDocs(dir), + /meta.json references unknown page: external:\[Support\]\(https:\/\/example.com\)/, + ); +}); + +test("rejects duplicate entries in meta.json", async () => { + const dir = await createDocs( + { "index.md": validPage }, + { pages: ["index", "index"] }, + ); + + await assert.rejects( + validatePublicDocs(dir), + /meta.json lists page more than once: index/, + ); +}); + +test("rejects files that resolve to the same published slug", async () => { + const dir = await createDocs( + { "foo.md": validPage, "foo/index.md": validPage }, + { pages: ["foo"] }, + ); + + await assert.rejects( + validatePublicDocs(dir), + /multiple published files resolve to slug foo: foo.md, foo\/index.md/, + ); +}); + +test("accepts single-character frontmatter values", async () => { + const dir = await createDocs( + { + "index.md": `--- +title: x +description: y +--- + +# X +`, + }, + { pages: ["index"] }, + ); + + await assert.doesNotReject(validatePublicDocs(dir)); +}); + +test("rejects published pages without title and description frontmatter", async () => { + const dir = await createDocs( + { "index.md": "# Kandev\n" }, + { pages: ["index"] }, + ); + + await assert.rejects( + validatePublicDocs(dir), + /index.md must start with YAML frontmatter containing title and description/, + ); +});