@zudojs/docs
A small toolkit for describing documentation pages in code, checking them for mistakes, and turning them into Markdown or JSON.
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.
- → 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.
- → 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
SearchDocumenttype; it ships no search.
INSTALLATION
Install the package. It pulls in @zudojs/errors on its own; nothing else is required.
QUICK START
This example creates two pages, stores them in a registry, checks them, and prints a JSON index.
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.
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.
| type | Shape | Notes |
|---|---|---|
"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
| Field | Allowed values | Notes |
|---|---|---|
category | introduction, guide, tutorial, reference, api, architecture, configuration, deployment, security, migration, examples | Anything else is an INVALID_CATEGORY error in validation. |
status | stable, experimental, beta, deprecated, internal | Free to leave out. |
tags | string[] | 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, deprecatedMessage | boolean, string | Set both together. The generator prints a DEPRECATED banner. |
description, version, metadata | string, string, object | Carried through to the generators. |
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.
Methods
| Method | What it does | Notes |
|---|---|---|
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. |
size | Number of documents. | A property, not a method. |
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.
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: valuewhere the value is a string, a number (10,-1,1.5),true/false, ornull. - → 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- itemlines. - → One level of nesting:
author:followed by indentedname: valuelines. - →
# commentsoutside 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.
--- 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.
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
| Function | What it checks | Issue 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 }) |
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.
Options
| Function | Option | Effect |
|---|---|---|
generateMarkdown(doc, options?) | includeFrontmatter | Default true. Set false to get only the body. |
includeMeta | Default false. Appends **Owner:** and **Updated:** lines from metadata. | |
sanitizer | An object with a sanitize(string) method. Applied to html and mdx content only. | |
generateIndex(docs, options?) | visibility | "CLIENT" (default), "SERVER" or "ALL". |
generateJSON(doc) | none | Returns all document fields including content as a plain object. |
# 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.
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.
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
| Name | What it does | Notes |
|---|---|---|
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, validateAll | Return a ValidationResult. | See Validation. |
toValidationResult(issues) | Builds { valid, issues } from an issue list. | Handy when writing your own checks. |
getBreadcrumbs, flattenNavigation, findNavigationItem, getSiblings, getAdjacent | Read 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, exampleToJSON | Work with a DocumentationExample. | See Code Examples. |
isValidDocumentId, normalizeDocumentId, documentIdFromPath, resolveDocumentLink, stripLinkDecorations | ID and link helpers. | Pure functions on strings. |
extractTitleFromMarkdown, extractHeadings, stripMarkdown, stripFencedCodeBlocks | Markdown 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
| Name | What it does | Notes |
|---|---|---|
DocumentRegistry | Stores documents by ID. | See the Registry method table. |
Types
| Name | What it does | Notes |
|---|---|---|
DocumentationDocument | The document shape. | See Documents. |
DocumentationContent, DocumentationNode | Content union and structured node union. | Node interfaces: HeadingNode, ParagraphNode, CodeNode, ListNode, LinkNode, TableNode, QuoteNode, CalloutNode. |
DocumentationCategory, DocumentationStatus | String unions for category and status. | |
DocumentBuilderOptions, DocumentBuilderExtras | Arguments to createDocument and the shortcut builders. | |
DocumentationNavigationItem, DocumentationBreadcrumb | Navigation tree item and breadcrumb entry. | |
ValidationResult, ValidationIssue, ValidateAllOptions, ValidateLinksOptions | Validator output and options. | maxContentLength, reportOrphans. |
ParsedFrontmatter, FrontmatterMetadata, FrontmatterValue | Frontmatter parser output. | |
MarkdownGeneratorOptions, IndexGeneratorOptions, DocumentVisibilityFilter, GetAllOptions | Generator and registry options. | |
DocumentationExample, ExampleValidationResult | Code example shape and its check result. | |
DocumentationProvider, DocumentationSanitizer, DocumentationSourceLoader | Interfaces you can implement. | The registry implements DocumentationProvider. The package ships no loader. |
APISymbol, APIExample, APIParameter, SearchDocument, SearchResult, DocumentationVersion | Type 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.
| Name | What it does | Notes |
|---|---|---|
DocumentationError | Base class for every error below. | Use isDocumentationError(err) to test for it. |
DocumentValidationError | Thrown by the three builders. | Missing id/title/content or an invalid ID. |
DuplicateDocumentError | Thrown by register and registerAll. | Message: Document "id" is already registered. |
DocumentNotFoundError, BrokenDocumentationLinkError, InvalidFrontmatterError, InvalidNavigationError, ExampleValidationError, DocumentParseError, GenerationError, DocumentationVersionError | Exported for your own code. | Nothing in this package throws them. Validators report issues instead of throwing. |
Constants
| Name | What it does | Notes |
|---|---|---|
DEFAULT_MAX_LINK_SCAN_LENGTH | Longest 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 ID →
createDocument({ id: "guides/http" })throwsDocumentValidationErrorbecause/is not allowed. → Convert it first:documentIdFromPath("guides/http.md")gives"guides.http". -
Treating
valid: trueas "no issues" → Broken links and orphan pages are warnings, so the result stays valid and they go unnoticed. → Log or fail onresult.issues.length > 0when you want a strict build. -
Passing an array of documents to
validateLinks→ It expects aSetof IDs as the second argument, so every link is reported as broken. → Useregistry.idSet()ornew Set(docs.map((d) => d.id)). -
Expecting
generateIndexto list every page → Pages withvisibility: "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)thenregister(doc). -
Registering the same ID twice →
DuplicateDocumentErroris thrown andregisterAllstops part-way. → Checkregistry.has(id)first, or runvalidateNoDuplicateIdson 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
BrokenDocumentationLinkError DocumentationError DocumentationVersionError DocumentNotFoundError DocumentParseError DocumentRegistry DocumentValidationError DuplicateDocumentError ExampleValidationError GenerationError InvalidFrontmatterError InvalidNavigationErrorclampHeadingLevel 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 validateNoDuplicateIdsAPIExample 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 ValidationResultAPISymbolKind DocumentationCategory DocumentationContent DocumentationNode DocumentationStatus DocumentBuilderExtras DocumentVisibilityFilter FrontmatterScalar FrontmatterValueDEFAULT_MAX_LINK_SCAN_LENGTH