Docs / Packages / @zudojs/docs
v1.0.2

@zudojs/docs

A small toolkit for describing documentation pages in code, checking them for mistakes, and turning them into Markdown or JSON.

DOCUMENT MODEL REGISTRY VALIDATION NAVIGATION GENERATORS

OVERVIEW

Documentation usually lives in loose Markdown files. Nothing checks that two pages do not share a name, that every link points somewhere real, or that the sidebar only lists pages that exist. Problems show up when a reader clicks a dead link.

@zudojs/docs gives each page a stable ID and a plain object shape. You put those objects in a registry (a lookup table keyed by ID), run the validators over them, and then generate Markdown or a JSON index for whatever site tool you use.

The package has no opinion about how you render pages. It does not include a web server, a Markdown-to-HTML converter, or a search engine. It only models, checks, and exports.

WHEN YOU NEED IT
  • → You generate docs from code and want them checked before publishing.
  • → You have many pages that link to each other by ID.
  • → You need a sidebar tree with breadcrumbs and previous/next links.
  • → You want to keep some pages server-only and out of a public index.
WHEN YOU DON'T
  • → You have a handful of Markdown files and a static site generator already checks links.
  • → You need Markdown rendered to HTML. Use a Markdown library for that.
  • → You need full-text search. This package only defines the SearchDocument type; it ships no search.

INSTALLATION

Install the package. It pulls in @zudojs/errors on its own; nothing else is required.

$ npm install @zudojs/docs
These docs follow the framework source. If an export shown here is missing from the version you installed, update to the latest @zudojs release.

QUICK START

This example creates two pages, stores them in a registry, checks them, and prints a JSON index.

import { createDocumentRegistry, createMarkdownDocument, validateAll, generateIndex, } from "@zudojs/docs"; const registry = createDocumentRegistry(); registry.register( createMarkdownDocument( "intro", "Introduction", "# Introduction\n\nWelcome. Next: [Setup](setup)", { category: "introduction", tags: ["start"] }, ), ); registry.register( createMarkdownDocument( "setup", "Setup", "# Setup\n\nRun npm install.", { category: "guide", tags: ["start"] }, ), ); const result = validateAll(registry.getAll()); console.log(result.valid); // true console.log(result.issues); // [] const index = generateIndex(registry.getAll()); console.log(index[0]); // { id: "intro", title: "Introduction", category: "introduction", tags: ["start"] }

Nothing is written to disk. generateIndex returns plain objects; you decide where they go (a file, an HTTP response, a search tool).

DOCUMENTS

A document is one page of documentation described as a plain object. Every document has three required fields: an id, a title, and content. Everything else is optional.

IDs are dot-separated, like guides.http.routing. Each segment may contain letters, digits, _ and -. Titles can change later; IDs should not, because links and navigation point at them.

Three ways to build one

createDocument takes the full options object. The other two are shortcuts for the most common content types.

import { createDocument, createMarkdownDocument, createStructuredDocument, } from "@zudojs/docs"; // 1. Full form: you choose the content type yourself const routing = createDocument({ id: "guides.http.routing", title: "HTTP Routing", description: "How to define routes", content: { type: "markdown", value: "# Routing\n\nDefine routes here." }, category: "guide", tags: ["http"], status: "stable", }); // 2. Markdown shortcut: (id, title, markdown, extras?) const config = createMarkdownDocument( "guides.config", "Configuration", "# Configuration\n\nUse layered sources.", { category: "guide" }, ); // 3. Structured shortcut: content is a list of typed nodes const container = createStructuredDocument( "api.container", "Container API", [ { type: "heading", level: 1, value: "Container" }, { type: "paragraph", value: "The container manages dependencies." }, { type: "code", language: "typescript", value: "const c = createContainer();" }, { type: "callout", kind: "tip", value: "Register once, resolve anywhere." }, ], ); console.log(routing.id); // "guides.http.routing" console.log(Object.isFrozen(routing)); // true

The returned object is a deep-frozen copy. Changing your original options afterwards does not change the document, and trying to assign to the document throws in strict mode.

Content types

The content field is an object with a type. Four types are accepted.

typeShapeNotes
"markdown"{ type, value: string }Links are validated; the title can be extracted from the first # heading.
"structured"{ type, nodes: DocumentationNode[] }Node types: heading, paragraph, code, list, link, table, quote, callout. Rendered to Markdown by the generator.
"html"{ type, value: string }Passed through unchanged. Links are not validated.
"mdx"{ type, value: string }Same as html: stored and emitted as-is.

Optional fields

FieldAllowed valuesNotes
categoryintroduction, guide, tutorial, reference, api, architecture, configuration, deployment, security, migration, examplesAnything else is an INVALID_CATEGORY error in validation.
statusstable, experimental, beta, deprecated, internalFree to leave out.
tagsstring[]Used by registry.byTag.
visibility"SERVER" or "CLIENT"Only an unset or exactly "CLIENT" visibility is client-visible; any other value (including "server") is treated as server-only and left out of generateIndex by default.
deprecated, deprecatedMessageboolean, stringSet both together. The generator prints a DEPRECATED banner.
description, version, metadatastring, string, objectCarried through to the generators.
Watch out: the builders throw DocumentValidationError right away if id, title or content is missing or the ID has a bad shape (spaces, slashes, empty segments). They do not check category or status; that is the validator's job.

REGISTRY

A registry is a lookup table that holds documents by ID. It refuses duplicates, so two pages can never claim the same ID. It also stores a frozen copy of each document, so editing your original object later has no effect on what is stored.

This example registers two pages, tries a duplicate, and then queries the registry.

import { createDocumentRegistry, createMarkdownDocument, DuplicateDocumentError, } from "@zudojs/docs"; const registry = createDocumentRegistry(); registry.registerAll([ createMarkdownDocument("intro", "Introduction", "# Intro", { category: "introduction", tags: ["start"] }), createMarkdownDocument("setup", "Setup", "# Setup", { category: "guide", tags: ["start"] }), ]); try { registry.register(createMarkdownDocument("intro", "Again", "# Again")); } catch (error) { if (error instanceof DuplicateDocumentError) { console.log(error.message); // Document "intro" is already registered. } } console.log(registry.get("intro")?.title); // "Introduction" console.log(registry.has("nope")); // false console.log(registry.size); // 2 console.log(registry.ids()); // ["intro", "setup"] console.log(registry.byCategory("guide").map((d) => d.id)); // ["setup"] console.log(registry.byTag("start").length); // 2

Methods

MethodWhat it doesNotes
register(doc)Stores one document.Throws DuplicateDocumentError if the ID exists.
registerAll(docs)Stores several documents in order.Stops at the first duplicate; earlier ones stay registered.
get(id)Returns the document or undefined.Never throws.
getAll(options?)Returns every document.{ visibility: "CLIENT" | "SERVER" | "ALL" }, default "ALL".
has(id), delete(id), clear()Check, remove one, remove all.delete returns true if something was removed.
ids(), idSet()All IDs as an array or a Set.idSet() is what validateLinks and validateNavigation expect.
byCategory(c), byTag(t)Filter documents.Return frozen arrays.
sizeNumber of documents.A property, not a method.
Tip: DocumentRegistry implements the DocumentationProvider interface (get and getAll). If you load documents from somewhere else, implement that same interface and the rest of your code does not need to change.

FRONTMATTER

Frontmatter is a block of key: value lines at the top of a Markdown file, fenced by --- on its own line above and below. It is how Markdown files usually carry a title, tags and similar metadata.

parseFrontmatter splits a raw file into the metadata and the rest of the text. serializeFrontmatter does the reverse.

import { parseFrontmatter, serializeFrontmatter } from "@zudojs/docs"; const raw = `--- title: HTTP Routing category: guide tags: - http - routing version: 1.0 --- # Routing Define routes here.`; const { metadata, content } = parseFrontmatter(raw); console.log(metadata.title); // "HTTP Routing" console.log(metadata.tags); // ["http", "routing"] console.log(metadata.version); // "1.0" (a string, not the number 1) console.log(content); // "# Routing\n\nDefine routes here." const text = serializeFrontmatter({ title: "Notes: draft", tags: ["a"] }, "# Body"); console.log(text); // --- // title: "Notes: draft" // tags: // - a // --- // // # Body

The serializer added quotes around Notes: draft because a bare colon would confuse the parser. Anything you serialize parses back to the same values.

What the parser understands

  • key: value where the value is a string, a number (10, -1, 1.5), true/false, or null.
  • → Quoted strings, in double or single quotes. A quoted value is never turned into a number or a boolean.
  • → A list: tags: followed by indented - item lines.
  • → One level of nesting: author: followed by indented name: value lines.
  • # comments outside quotes are dropped.

The named keys title, description, category, version, status, deprecatedMessage and visibility always come back as strings, and tags is always an array of strings (tags: http["http"], tags:[]). Unknown keys get the parser's best guess.

Watch out: if the closing --- is missing, parseFrontmatter does not throw. It returns { metadata: {}, content: raw } with the whole input untouched. Check metadata.title if you need to know parsing worked.

VALIDATION

Validators look at documents and report problems without throwing. Each one returns a ValidationResult: { valid, issues }. Every issue has a severity of "error" or "warning", a short code, a message, and usually a documentId.

valid is false only when at least one issue is an error. Warnings never make a result invalid, so read issues if you want to see them.

validateAll runs every check in one call. Pass a navigation tree as the second argument to have it checked too.

import { createDocumentRegistry, createMarkdownDocument, validateAll, } from "@zudojs/docs"; import type { DocumentationNavigationItem } from "@zudojs/docs"; const registry = createDocumentRegistry(); registry.registerAll([ createMarkdownDocument("intro", "Introduction", "See [Setup](setup)."), createMarkdownDocument("setup", "Setup", "See [Deploy](deploy)."), ]); const navigation: DocumentationNavigationItem[] = [ { title: "Start", children: [ { title: "Introduction", documentId: "intro" }, { title: "Setup", documentId: "setup" }, { title: "Missing", documentId: "missing" }, ], }, ]; const result = validateAll(registry.getAll(), navigation); console.log(result.valid); // false for (const issue of result.issues) { console.log(issue.severity, issue.code, issue.message); } // warning BROKEN_LINK Document "setup" links to "deploy" which is not registered. // error NAVIGATION_UNKNOWN_DOCUMENT Navigation item "Missing" references unknown document "missing".

The broken link is only a warning, so on its own it would not have made valid false. The navigation entry pointing at a page that does not exist is an error.

The individual validators

FunctionWhat it checksIssue codes
validateDocument(doc)ID shape, required fields, content shape, allowed category/status/visibility values, structured node types.MISSING_ID, INVALID_ID, MISSING_TITLE, MISSING_CONTENT, INVALID_CONTENT_TYPE, INVALID_CATEGORY, INVALID_STATUS, INVALID_VISIBILITY, INVALID_TAGS, INVALID_NODE (errors); DEPRECATED_WITHOUT_MESSAGE, DEPRECATION_MISMATCH (warnings)
validateNoDuplicateIds(docs)Two documents in the array with the same ID.DUPLICATE_ID (error)
validateLinks(doc, ids, options?)Markdown [text](target) links and structured link nodes point at a registered ID. Links whose scheme is not http, https, mailto, tel, ftp or ftps are UNSAFE_LINK errors; other external URLs, #anchors, images and links inside code are skipped.UNSAFE_LINK (error); BROKEN_LINK, LINK_VALIDATION_SKIPPED (warnings)
validateNavigation(items, ids)Every documentId in the tree is registered; no cycles; no empty items.NAVIGATION_UNKNOWN_DOCUMENT, NAVIGATION_CYCLE (errors); NAVIGATION_DUPLICATE_DOCUMENT, NAVIGATION_EMPTY_ITEM (warnings)
validateAll(docs, navigation?, options?)All of the above, plus pages that are registered but not in the navigation.NAVIGATION_ORPHAN_DOCUMENT (warning; turn off with { reportOrphans: false })
In plain words: the ids argument is a Set of every document ID you know about. registry.idSet() builds it for you. A link counts as valid when its target, resolved relative to the current document, is in that set.

GENERATORS

Generators turn documents into output you can hand to another tool. There are three: generateMarkdown for one page, generateJSON for one page as a plain object, and generateIndex for a list of page summaries without content.

This example generates Markdown with frontmatter, then builds an index that hides a server-only page.

import { createMarkdownDocument, generateMarkdown, generateIndex, } from "@zudojs/docs"; const setup = createMarkdownDocument( "setup", "Setup", "# Setup\n\nRun npm install.", { category: "guide", tags: ["start"], version: "1.0" }, ); console.log(generateMarkdown(setup)); // --- // title: Setup // category: guide // tags: // - start // version: "1.0" // --- // // # Setup // // Run npm install. const docs = [ setup, createMarkdownDocument("ops.secrets", "Secrets", "# Secrets", { visibility: "SERVER" }), ]; console.log(generateIndex(docs).map((d) => d.id)); // ["setup"] (SERVER pages are left out by default) console.log(generateIndex(docs, { visibility: "ALL" }).map((d) => d.id)); // ["setup", "ops.secrets"]

Options

FunctionOptionEffect
generateMarkdown(doc, options?)includeFrontmatterDefault true. Set false to get only the body.
includeMetaDefault false. Appends **Owner:** and **Updated:** lines from metadata.
sanitizerAn object with a sanitize(string) method. Applied to html and mdx content only.
generateIndex(docs, options?)visibility"CLIENT" (default), "SERVER" or "ALL".
generateJSON(doc)noneReturns all document fields including content as a plain object.
In plain words: structured content becomes Markdown too. Headings become # lines, code nodes become fenced blocks, callouts become > **TIP:** quotes. The generator escapes values so a stray backtick or pipe inside a node cannot break the output.

CODE EXAMPLES

A DocumentationExample is a code snippet with an id, a language and the code itself, plus an optional title and description. You can check one is complete and render it as a Markdown code block.

import { validateExample, renderExampleMarkdown } from "@zudojs/docs"; import type { DocumentationExample } from "@zudojs/docs"; const example: DocumentationExample = { id: "hello", title: "Hello", language: "ts", code: 'console.log("hi");', description: "Prints hi.", }; console.log(validateExample(example)); // { valid: true, errors: [] } console.log(validateExample({ id: "", language: "", code: "" }).errors); // ["Example ID is required.", "Example requires a language.", "Example requires code content."] console.log(renderExampleMarkdown(example)); // ### Hello // // Prints hi. // // ```ts // console.log("hi"); // ```

validateExample returns { valid, errors } with plain strings, not ValidationIssue objects. exampleToJSON(example) gives you a plain object with the same five fields.

UTILITIES

Small pure functions for IDs, links and Markdown text. They are the same helpers the validators use internally.

import { documentIdFromPath, resolveDocumentLink, isValidDocumentId, normalizeDocumentId, extractTitleFromMarkdown, extractHeadings, stripMarkdown, } from "@zudojs/docs"; console.log(documentIdFromPath("guides/http/routing.md")); // "guides.http.routing" console.log(resolveDocumentLink("guides.http.routing", "../auth")); // "guides.auth" console.log(resolveDocumentLink("guides.http.routing", "./middleware")); // "guides.http.middleware" console.log(isValidDocumentId("guides.http")); // true console.log(isValidDocumentId("guides/http")); // false console.log(normalizeDocumentId(" .a..b. ")); // "a.b" const md = "# Setup\n\n## Install\n\n**bold** and [a link](x)"; console.log(extractTitleFromMarkdown(md)); // "Setup" console.log(extractHeadings(md)); // [{ level: 1, text: "Setup" }, { level: 2, text: "Install" }] console.log(stripMarkdown(md)); // "Setup\n\nInstall\n\nbold and a link"

Headings inside fenced code blocks are ignored by extractTitleFromMarkdown and extractHeadings, so a # comment in a shell example does not become a title.

API REFERENCE

Everything below is exported from @zudojs/docs.

Functions

NameWhat it doesNotes
createDocument(options)Builds a frozen document from an options object.Throws DocumentValidationError on missing id/title/content or a bad ID.
createMarkdownDocument(id, title, markdown, extras?)Shortcut for markdown content.extras takes every optional document field.
createStructuredDocument(id, title, nodes, extras?)Shortcut for structured content.Node types are checked by validateDocument, not here.
createDocumentRegistry()Returns a new, empty DocumentRegistry.Same as new DocumentRegistry().
matchesVisibility(doc, filter?)True when a document passes a "CLIENT" | "SERVER" | "ALL" filter.Used by getAll and generateIndex.
parseFrontmatter(raw)Splits a Markdown string into { metadata, content }.Never throws; returns empty metadata when there is no block.
serializeFrontmatter(metadata, content)Writes a frontmatter block followed by the content.Quotes values that could be misread.
validateDocument, validateNoDuplicateIds, validateLinks, validateNavigation, validateAllReturn a ValidationResult.See Validation.
toValidationResult(issues)Builds { valid, issues } from an issue list.Handy when writing your own checks.
getBreadcrumbs, flattenNavigation, findNavigationItem, getSiblings, getAdjacentRead a navigation tree.All take (documentId, items) except flattenNavigation(items).
generateMarkdown(doc, options?)One document to a Markdown string.Options: includeFrontmatter, includeMeta, sanitizer.
generateJSON(doc)One document to a plain object.Includes content.
generateIndex(docs, options?)Array of summaries without content.Default visibility: "CLIENT".
nodesToMarkdown(nodes)Structured nodes to Markdown.Throws TypeError on an unknown node type.
validateExample, renderExampleMarkdown, exampleToJSONWork with a DocumentationExample.See Code Examples.
isValidDocumentId, normalizeDocumentId, documentIdFromPath, resolveDocumentLink, stripLinkDecorationsID and link helpers.Pure functions on strings.
extractTitleFromMarkdown, extractHeadings, stripMarkdown, stripFencedCodeBlocksMarkdown text helpers.Ignore fenced code where relevant.
deepFreeze(value), deepFreezeClone(value)Freeze an object tree, in place or as a copy.The builders and registry use deepFreezeClone.
createDocumentationError(message, options?), isDocumentationError(value)Create or detect a DocumentationError.

Classes

NameWhat it doesNotes
DocumentRegistryStores documents by ID.See the Registry method table.

Types

NameWhat it doesNotes
DocumentationDocumentThe document shape.See Documents.
DocumentationContent, DocumentationNodeContent union and structured node union.Node interfaces: HeadingNode, ParagraphNode, CodeNode, ListNode, LinkNode, TableNode, QuoteNode, CalloutNode.
DocumentationCategory, DocumentationStatusString unions for category and status.
DocumentBuilderOptions, DocumentBuilderExtrasArguments to createDocument and the shortcut builders.
DocumentationNavigationItem, DocumentationBreadcrumbNavigation tree item and breadcrumb entry.
ValidationResult, ValidationIssue, ValidateAllOptions, ValidateLinksOptionsValidator output and options.maxContentLength, reportOrphans.
ParsedFrontmatter, FrontmatterMetadata, FrontmatterValueFrontmatter parser output.
MarkdownGeneratorOptions, IndexGeneratorOptions, DocumentVisibilityFilter, GetAllOptionsGenerator and registry options.
DocumentationExample, ExampleValidationResultCode example shape and its check result.
DocumentationProvider, DocumentationSanitizer, DocumentationSourceLoaderInterfaces you can implement.The registry implements DocumentationProvider. The package ships no loader.
APISymbol, APIExample, APIParameter, SearchDocument, SearchResult, DocumentationVersionType contracts for tools built on top of this package.Types only. There is no extractor, search index or version manager in this package.

Errors

All error classes are defined in @zudojs/errors and re-exported here so one import is enough.

NameWhat it doesNotes
DocumentationErrorBase class for every error below.Use isDocumentationError(err) to test for it.
DocumentValidationErrorThrown by the three builders.Missing id/title/content or an invalid ID.
DuplicateDocumentErrorThrown by register and registerAll.Message: Document "id" is already registered.
DocumentNotFoundError, BrokenDocumentationLinkError, InvalidFrontmatterError, InvalidNavigationError, ExampleValidationError, DocumentParseError, GenerationError, DocumentationVersionErrorExported for your own code.Nothing in this package throws them. Validators report issues instead of throwing.

Constants

NameWhat it doesNotes
DEFAULT_MAX_LINK_SCAN_LENGTHLongest Markdown (in characters) that validateLinks scans.100000. Longer content gets a LINK_VALIDATION_SKIPPED warning.

Navigation walkers stop at a depth of 64 and report deeper trees as NAVIGATION_CYCLE; that limit is internal and not exported.

COMMON MISTAKES

  • Using a file path as an IDcreateDocument({ id: "guides/http" }) throws DocumentValidationError because / is not allowed. → Convert it first: documentIdFromPath("guides/http.md") gives "guides.http".
  • Treating valid: true as "no issues" → Broken links and orphan pages are warnings, so the result stays valid and they go unnoticed. → Log or fail on result.issues.length > 0 when you want a strict build.
  • Passing an array of documents to validateLinks → It expects a Set of IDs as the second argument, so every link is reported as broken. → Use registry.idSet() or new Set(docs.map((d) => d.id)).
  • Expecting generateIndex to list every page → Pages with visibility: "SERVER" are missing because the default filter is "CLIENT". → Pass { visibility: "ALL" } when you want all of them.
  • Editing a document after registering it → The registry stores a frozen copy, so the edit is ignored or throws in strict mode. → Build a new document and call delete(id) then register(doc).
  • Registering the same ID twiceDuplicateDocumentError is thrown and registerAll stops part-way. → Check registry.has(id) first, or run validateNoDuplicateIds on the array before registering.

COMPLETE EXPORT INDEX

Every name @zudojs/docs exports from its package root at v1.0.3 — 103 in total, generated from the package’s own entry point rather than written by hand. The sections above explain the ones you reach for most; this is the exhaustive list, so nothing shipped is undocumented. Names not covered above are typically internal helpers and supporting types.

Show all 103 exports
Classes (12)
BrokenDocumentationLinkError DocumentationError DocumentationVersionError DocumentNotFoundError DocumentParseError DocumentRegistry DocumentValidationError DuplicateDocumentError ExampleValidationError GenerationError InvalidFrontmatterError InvalidNavigationError
Functions (42)
clampHeadingLevel createDocument createDocumentationError createDocumentRegistry createMarkdownDocument createStructuredDocument deepFreeze deepFreezeClone documentIdFromPath exampleToJSON extractHeadings extractTitleFromMarkdown fenceFor findNavigationItem flattenNavigation generateIndex generateJSON generateMarkdown getAdjacent getBreadcrumbs getSiblings isDocumentationError isValidDocumentId matchesVisibility nodesToMarkdown normalizeDocumentId parseFrontmatter renderExampleMarkdown resolveDocumentLink sanitizeLanguage serializeFrontmatter stripFencedCodeBlocks stripLinkDecorations stripMarkdown tableCell toValidationResult validateAll validateDocument validateExample validateLinks validateNavigation validateNoDuplicateIds
Interfaces (39)
APIExample APIParameter APISymbol CalloutNode CodeNode DocumentationBreadcrumb DocumentationDocument DocumentationErrorOptions DocumentationExample DocumentationMetadata DocumentationNavigationItem DocumentationProvider DocumentationSanitizer DocumentationSourceLoader DocumentationVersion DocumentBuilderOptions ExampleValidationResult FrontmatterMetadata GetAllOptions HeadingNode HTMLContent IndexGeneratorOptions LinkNode ListNode MarkdownContent MarkdownGeneratorOptions MDXContent ParagraphNode ParsedFrontmatter QuoteNode SearchDocument SearchResult SourceLocation StructuredContent TableNode ValidateAllOptions ValidateLinksOptions ValidationIssue ValidationResult
Type aliases (9)
APISymbolKind DocumentationCategory DocumentationContent DocumentationNode DocumentationStatus DocumentBuilderExtras DocumentVisibilityFilter FrontmatterScalar FrontmatterValue
Constants (1)
DEFAULT_MAX_LINK_SCAN_LENGTH