docs: own public documentation metadata (#1713)
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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"
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: "CLI"
|
||||
description: "Install, start, and operate Kandev from the command line."
|
||||
---
|
||||
|
||||
# Kandev CLI
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string[]>} 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;
|
||||
});
|
||||
}
|
||||
@@ -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<string, string>} files Fixture files keyed by relative path.
|
||||
* @param {{pages: string[]}} meta Navigation metadata.
|
||||
* @returns {Promise<string>} 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/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user