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.
  • @param and @returns take 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} Options with @property lines, and @callback, define types that exist only in comments. @template T makes a symbol generic.
  • @deprecated says what to use instead. @see, @since and @example are shown as written. @example text that is not already a code block is shown as one.
  • @internal hides a symbol unless visibility asks for it. @hidden or @ignore hides it always. @beta, @alpha and @experimental show a badge, and stability can leave those levels out.
  • Any other tag is kept in api.json for a theme to show.

{@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:

  1. as a full ID;
  2. in the comment's own module, so a module's own Token wins over another's;
  3. 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;
  • @deprecated that does not say what to use instead;
  • an @example that 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.html for the package page;
  • api-module.html for module pages;
  • api-symbol.html for 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.json is the model, described below.
  • Every API page is also published as Markdown when markdown_publish is on, and is listed in llms.txt.
  • ssg mcp adds api_search, api_symbol and api_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 register findSymbol and getSymbol in the browser, reading api.json the 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.