# The API Reference Belongs Next to the Guides

Most projects publish their documentation twice. The guides live on one site,
written by hand. The API reference lives on another, generated by a separate
tool with its own theme, its own search and its own idea of what a link is. A
guide that says "see `parse()`" points at a URL nobody checks, and when the
function is renamed the link quietly dies.

ssg 1.8.69 builds both in one pass.

## A library, from its code

```yaml
api_docs:
  - root: packages/core
check_api: warn
```

Take a function with an ordinary doc comment:

```js
/**
 * Splits text into words, numbers and punctuation.
 *
 * @param {string} text The text to split.
 * @param {{ keepSpace?: boolean }} [options]
 * @returns {Token[]} Every token, in order.
 * @example
 * ```js
 * import { tokenize } from "textkit";
 * console.log(tokenize("Hello, world!"));
 * ```
 */
export function tokenize(text, options = {}) { /* ... */ }
```

ssg reads `package.json`, follows the entry points and re-exports, and turns
what the package exports into pages: one for the package with its README, one
per module, one per class and interface. JavaScript is read with its doc
comments, in Go, without Node. A package that ships `.d.ts` files is read from
those, because that is what its users import.

The pages are ordinary pages. They show up in the site search, the sitemap,
`llms.txt` and the Markdown copies agents read. `check_links` checks a
`{@link Lexer}` in a comment the same way it checks a link in a guide.
`check_api` adds what a reference tends to miss: exports nobody described,
parameters with no words, `@deprecated` with no replacement.

## Go, PHP and Python too

The same `api_docs` entry reads three more languages, and there is still
nothing to install. Go is read with the standard library's own parser.
PHP and Python have scanners built into ssg that read declarations and
comments and skip everything else. Python is never run, and neither is PHP.

```yaml
api_docs:
  - root: services/billing     # go.mod: read as Go
  - root: plugins/textkit      # composer.json: read as PHP
  - root: tools/textkit        # pyproject.toml: read as Python
```

Each language keeps its own habits. Go's `[Name]` links and `Example`
functions, PHPDoc's `@param Type $name`, and Python's Google, NumPy and reST
docstrings are all understood. Declarations are shown the way the language
writes them:

```go
func Tokenize(text string, keepSpace bool) []lexer.Token
```

```php
public function next(): ?Token
```

```python
def tokenize(text: str, *, keep_space: bool = False) -> list[Token]
```

Each scanner has limits. A PHP function declared inside an `if`, or a
Python import under `if TYPE_CHECKING:`, is not seen.
[API docs: languages](/api_languages/) lists every one.

## Examples you can run

An example in a comment is usually a promise that has not been tested since it
was written. Give ssg the package as an ES module:

```yaml
    playground: https://cdn.example.com/npm/core@1/+esm
```

Every JavaScript `@example` becomes an editor with a Run button. The code runs
in a sandboxed frame with its own origin. It can import the package, print to
the console and throw, and it cannot see the page, its cookies or its storage.
A reader who wants to know what `tokenize("Hello, world!")` returns finds out
in one click instead of installing anything:

![The live example: an editor holding four lines that import tokenize, a Run and a Reset button, and the output "4 tokens" followed by the array Hello, comma, world, exclamation mark](/img/blog/api-docs-playground.webp)

## A REST API, from its OpenAPI file

```yaml
api_docs:
  - openapi: api/openapi.yaml
```

OpenAPI 3.0 or 3.1, YAML or JSON. A fragment of the example in the
repository:

```yaml
paths:
  /tokens:
    post:
      tags: [Text]
      operationId: tokenize
      summary: Split text into tokens
      parameters:
        - name: keepSpace
          in: query
          schema: {type: boolean, default: false}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/TextInput'}
            example: {text: "Hello, world"}
components:
  securitySchemes:
    apiKey: {type: apiKey, in: header, name: X-API-Key}
```

From a file like that you get a front page with servers,
authentication and every endpoint, a page per tag, and a page of schemas.
Parameters show their types, limits and defaults. Request and response bodies
link to their schemas, with the file's examples or, when it has none, an
example built from the schema.

Under each operation there is a **Try it** console. Pick the server, fill in
the parameters and the body, paste an API key or a token, and send. It shows the
status, the time, the headers and the body, and the same request as a `curl`
command. The key stays in the page's memory. It is never saved, and the `curl`
line shows `<X-API-Key>` instead of the key, so a screenshot of it is safe to
share.

![The Try it console for POST /tokens: a server list, a keepSpace choice, the JSON body, a masked API key, the Send request button, then 200 OK in 14 ms with the response body, headers and a curl command whose key reads X-API-Key in angle brackets](/img/blog/api-docs-tryit.webp)

The request goes from the reader's browser to your API, which therefore has to
allow your docs site in CORS. When it does not, the console says so and still
hands over the `curl` command.

## A theme made for this

`template: apidoc` is a new built-in theme for documentation sites. Guides and
references share one sidebar, and the current package opens its tree there.
There is search on `/`, an "On this page" column, breadcrumbs, and a light and a
dark scheme. Every colour pair is measured against WCAG 2.2 AA and the numbers
are in the theme's README.

It carries your brand, not ours. The logo, favicon and social profiles come
from the `marketing:` settings every theme reads, and brand colours come from
`colors:`. The header takes your own links, with an icon where you name one,
and an icon for the repository. The footer says "Built with SSG" unless you set
`ssg_credit: false`.

```yaml
template: apidoc
marketing:
  logo: /logo.svg
  social_profiles:
    github: https://github.com/example/textkit
    discord: https://discord.example.com/textkit
colors:
  primary: "#1967d2"
variables:
  nav:
    - {label: Guide, url: /getting-started/}
    - {label: npm, url: "https://npm.example.com/package/textkit", icon: npm}
  repository_url: https://github.com/example/textkit
```

![The apidoc theme on a REST reference page: the sidebar with guides and the open textkit service tree, a POST label beside /tokens, parameter and response tables, and an On this page column](/img/themes/apidoc.webp)

The other themes are not left out. The live examples and the console are added
by the build to any page that needs them, so ssgtheme, simple or your own theme
show them too.

## Try it locally

The repository has a working example: a small text library in JavaScript, Go,
PHP and Python, and the REST service built on it, all documented side by side
in one site.

```bash
ssg --config examples/api-docs/ssg.yaml --http --watch
```

The guides are [API docs from code](/api_docs/) and
[REST API docs from OpenAPI](/rest_api_docs/).
