Guide
API documentation from code
ssg reads JavaScript and TypeScript, together with their documentation
comments, and publishes the API as ordinary pages of your site. Those pages use
the same theme, search, sitemap, llms.txt, MCP tools and link checking as the
guides you write by hand. A guide can link to a symbol and a symbol to a guide;
one build checks both.
Go, PHP and Python packages are read the same way, without installing anything: see API_LANGUAGES.
A REST API described in OpenAPI gets the same treatment, with a "Try it" console on every operation: see REST_API_DOCS.
Quick start
1api_docs:
2 - root: packages/core # holds package.json and the code
3check_api: warn # report undocumented exports and dead links
ssg reads package.json to find the entry points (exports, then module,
then main) and publishes:
| URL | What |
|---|---|
/api/core/ |
the package: its README and its modules |
/api/core/src/lex/ |
a module: functions, types, enums and variables, each with an anchor |
/api/core/src/lex/Lexer/ |
a class or interface: its constructor, properties and methods |
/api.json |
the whole model, for tools and agents (below) |
No Node.js is needed. JavaScript is read with its doc comments. A package
that ships TypeScript declarations (types in package.json, a types
condition in exports, or an index.d.ts) is read from those instead, since
they are exactly what its users import. For a TypeScript project, build its
declarations first, as npm publish would (tsc --declaration), or point
entry at the .d.ts files. A types path that does not exist yet does not
count: ssg then reads the JavaScript.
Configuration
1api_docs:
2 - name: core # default: "name" in package.json
3 root: packages/core
4 language: javascript # default: detected; or typescript, go, php, python
5 entry: [src/index.js] # default: from package.json
6 include: ["src/**"] # which files may be read (globs, relative to root)
7 exclude: ["**/*.test.*"]
8 url: /api/core/ # default: /api/<name>/
9 visibility: public # public (default) | internal | all
10 stability: [beta] # which non-stable levels to show; empty = all
11 readme: true # the package README on its front page
12 source_url: https://example.com/repo/blob/{ref}/{path}#L{line}
13 source_ref: auto # a tag or branch; auto = the commit git reports
14 playground: https://cdn.example.com/npm/core@1/+esm # live examples (below)
15check_api: warn # off (default) | warn | strict
Several entries document several packages, for example in a monorepo. Every package gets its own URL prefix, and they share search and cross-links.
Writing documentation comments
A comment directly above a declaration documents it:
1/**
2 * Splits source text into tokens.
3 *
4 * The lexer is incremental: call {@link Lexer.next} until it returns `null`.
5 *
6 * @param {string} source - the text to read
7 * @param {{strict?: boolean}} [options] - reading options
8 * @returns {Lexer} a lexer positioned at the start
9 * @throws {SyntaxError} on an unterminated string
10 * @example
11 * const lex = createLexer("a b")
12 * lex.next() // → { kind: "word", text: "a" }
13 * @since 1.2
14 */
15export function createLexer(source, options) { … }
- The first paragraph is the summary. It is shown in lists, in search results and in the page description.
@paramand@returnstake a type in braces, written in TypeScript syntax ({Array<string>},{string | null},{(x: number) => void}).[name]marks a parameter as optional and[name=default]gives its default.@typedef {Object} Optionswith@propertylines, and@callback, define types that exist only in comments.@template Tmakes a symbol generic.@deprecatedsays what to use instead.@see,@sinceand@exampleare shown as written.@exampletext that is not already a code block is shown as one.@internalhides a symbol unlessvisibilityasks for it.@hiddenor@ignorehides it always.@beta,@alphaand@experimentalshow a badge, andstabilitycan leave those levels out.- Any other tag is kept in
api.jsonfor a theme to show.
Links between symbols
{@link Name} links to a symbol. {@link Name | text} uses your own link
text, and {@linkcode Name} shows the text as code. A name is looked up in
this order:
- as a full ID;
- in the comment's own module, so a module's own
Tokenwins over another's; - as the only symbol with that name in the whole API.
Lexer.next reaches a member, and src/lex#Lexer a symbol in another module.
A name nobody has, or that two modules both have, is not guessed. It is shown
as text and reported by check_api.
Live examples
With playground: set to the package as an ES module, every @example that is
a single JavaScript code block (```js) gets an editor with Run and
Reset, and Ctrl+Enter runs it:
1/**
2 * @example
3 * ```js
4 * import { tokenize } from "textkit";
5 * console.log(tokenize("Hello, world"));
6 * ```
7 */
The code runs in a sandboxed frame. It can run scripts, but it has its own
opaque origin, so it cannot read the page, its cookies or its storage. An
import map points the package name at the playground URL, so import … from "textkit" works, and the package's exports are also available as plain names.
console output and uncaught errors appear under the editor. A run that takes
longer than 5 seconds is stopped. Without JavaScript the example stays an
ordinary code block.
The URL has to answer cross-origin requests. A CDN build of an npm package
(jsDelivr's +esm, esm.sh, unpkg with ?module) does. A file the site
publishes itself needs Access-Control-Allow-Origin: *. The script and its
styles are added only to pages that have an example, in every theme.
Checking the documentation
check_api: warn lists, and strict (or a strict build) fails on:
- an exported symbol with no description;
- a parameter with no description, when its function has one;
@deprecatedthat does not say what to use instead;- an
@examplethat is not code; - a
{@link}that leads nowhere; - code the extractor could not read: a syntax error, or a missing entry point.
Themes
apidoc is a theme made for documentation only. It has a guides and
reference sidebar that opens the current package's tree, a search box over
search-index.json (press /), an "On this page" column, breadcrumbs, and
light and dark schemes. It ships inside the binary:
1template: apidoc
2search_index: true
3variables:
4 title: textkit # name in the header and titles
5 tagline: Tokenize and format text. # the front page's lead
6 start: {url: /getting-started/, label: Get started}
7 nav: # your header links; icon shows a mark
8 - {url: /changelog/, label: Changelog}
9 - {url: "https://npm.example.com/package/core", label: npm, icon: npm}
10 repository_url: https://github.com/example/core # repo icon in the header
11 docs_nav_order: [getting-started, configuration] # guide slugs, in order
12 gtm_id: GTM-XXXXXXX # optional Google Tag Manager
13 ssg_credit: false # hide "Built with SSG" in the footer
The logo, favicon and social profiles come from marketing: (logo,
favicon, social_profiles), and brand colours from colors: (primary,
primary_dark). Its colours, type, contrast ratios and every branding setting
are in
templates/apidoc/README.
Every API page is a normal page whose body is the reference written as
Markdown, so any theme shows it through page.html. A theme can draw API pages
its own way with three layouts:
api-index.htmlfor the package page;api-module.htmlfor module pages;api-symbol.htmlfor class and interface pages.
apidoc and ssgtheme ship all three. They put the package tree beside the page and a breadcrumb trail above it. They read:
| Data | What |
|---|---|
.Page.Content |
the reference body |
.Page.Extra.api.package, .module, .symbol |
the model of this page (a REST API: package.Name and rest: true) |
.Page.Extra.api.nav |
the package tree: Title, URL, Kind, Children |
.Page.Extra.api.crumbs |
the trail: package › module › symbol |
{{ apiHref "core/src/lex#Lexer" }} and {{ apiType .Type }} (a type with every
documented name linked) are there for a theme that draws signatures itself.
Use them in your own theme only: a theme in the ssg repository is also built by
older releases, which do not have them.
The module and class pages carry hide_from_lists: true, so a theme's guide
list shows each package once, by its front page.
For agents
api.jsonis the model, described below.- Every API page is also published as Markdown when
markdown_publishis on, and is listed inllms.txt. ssg mcpaddsapi_search,api_symbolandapi_module. A class answers with its members as a list of IDs; an agent asks for one member by its ID.- With
webmcp: true, the site's pages registerfindSymbolandgetSymbolin the browser, readingapi.jsonthe first time they are used.
In --watch
Every source file of a documented package (.js, .ts, .json, .md and so
on, outside node_modules) is an input of the build. A change makes the next
build full, so the reference never shows code that is no longer there.
The API model (api.json)
A build that documents code also publishes api.json. It is the whole API as
data, for tools and agents that would rather not read HTML. Its shape is
described by docs/api-model.schema.json (JSON Schema
2020-12) and versioned by its schema field. A change a reader could notice
raises the number, and ssg refuses a document with a number it does not know.
1{
2 "schema": 1,
3 "packages": [{
4 "name": "core",
5 "modules": [{
6 "id": "core/src/lex",
7 "path": "src/lex",
8 "symbols": [{
9 "id": "core/src/lex#Lexer",
10 "name": "Lexer",
11 "kind": "class",
12 "members": [{
13 "id": "core/src/lex#Lexer.next",
14 "name": "next",
15 "kind": "method",
16 "signatures": [{ "returns": { "kind": "name", "name": "Token", "ref": "core/src/lex#Token" } }]
17 }]
18 }]
19 }]
20 }]
21}
IDs
IDs come from where a symbol lives and what it is called, never from its position in a file. Moving code around inside a file changes no URL and no link.
| What | ID | Anchor on the module page |
|---|---|---|
| module | core/src/lex |
— |
| symbol | core/src/lex#Lexer |
#Lexer |
| member | core/src/lex#Lexer.next |
#Lexer.next |
A module path loses its extension (.js, .mjs, .d.ts and so on) and any
leading ./. A JavaScript file and its declaration file therefore describe the
same module.
Kinds
namespace, class, interface, function, method, constructor,
property, accessor, type, enum, enumMember, variable.
Types are trees
A type is never a string to be parsed again. Promise<Token[]> is a name
node with one array argument, whose element is a name node carrying ref:
the ID of the documented Token. A theme can link every part, and an agent can
follow ref without guessing. The shapes are name, union, intersection,
array, tuple, literal, function and object. A type the model does not
take apart, such as a conditional or mapped type, is verbatim, with its source
text in name.
Order
api.json is written in a fixed order: packages by name, modules and symbols by
ID. Two builds of the same sources produce identical bytes, so a diff of the
file is a diff of the API. Overloads keep the order they were declared in.