---
title: Publish docs
description: Keep docs beside your code as Markdown; ops stores every version and the docs site shows it.
order: 2
---

Docs live in a `docs` folder beside the code they describe. Each version of a package or
each production deploy of a service publishes the folder as a new version of its **doc
set**, and the docs site shows it within seconds. Nothing is rebuilt for it.

Doc sets are public: anyone can read them without logging in. Never put anything private
in a docs folder.

## The folder

Pages are Markdown, and the folder's own layout is the navigation. `index.md` is the first
page of a folder.

```
docs/
  index.md
  install.md
  guides/
    index.md
    streaming.md
    images/flow.png
```

Every page starts with front matter. `title` is required; `description` and `order` are
optional, and nothing else is allowed:

```md
---
title: Streaming bodies
description: Read and write objects without buffering them.
order: 2
---
```

`order` is a whole number and sorts the pages of a folder. Write each value on one line.

## What is checked

Nothing is published when a check fails, and the answer names every page that broke it.

- Every page has a `title`, and its front matter uses only the three keys.
- Every relative link and image points at a file in the same folder; a link may leave out
  `.md` or point at a folder.
- Images are PNG, JPEG, WebP or SVG, 2 MB at most, and an SVG holds no script.
- Every other file is refused. Hidden files, such as `.DS_Store`, are left out.
- The folder fits the size limit, 64 MB unless your ops sets another.

Links to other sites, `#anchors` and absolute paths are not checked.

## Publishing

**A package** publishes its docs in its publish run. A module's `docs` folder, beside its
`crate` and `layer`, becomes the version the tag published. The `docs` folder beside a
crate or an npm package that is no module, such as `crates/billing/docs` or
`packages/ui/docs`, becomes a version of that package's set, named like the package
without its scope, at the package's own version. A broken folder refuses the tag before
anything publishes. A package without a folder publishes without docs, and the run says
so. Pages changed under a version that has its docs already wait for the next version.

**Guides that are no package's** go in the folder [the project file](../concepts/project-file.md)
names. A tag's publish run publishes it at the tag's version:

```ts
export default defineProject({
  services: {},
  docs: { set: 'rungholt-guides', path: 'docs' },
})
```

**A service** names its folder the same way:

```ts
export default defineProject({
  services: { api: { dockerfile: 'Dockerfile', port: 8080 } },
  docs: { set: 'rungholt-api', path: 'docs' },
})
```

Every production deploy then publishes the folder after the service is healthy, as the
version named by the deployed commit. A broken folder fails that `docs` step and notifies
you; the deploy itself stands.

**Anything else** pushes with the CLI and a token that holds `package.publish` in the
project. The Project admin and Developer roles hold it:

```sh
ops docs push ./docs --set rungholt-api --version 1.4.0
```

A set name is lowercase letters, digits and dashes. A set named like a module or a package
is that package's set, and its versions must be versions the package has published. Any other
set is a service set and names its project the first time: pass `--project rungholt`, or
`--kind service` to use the project chosen with `ops use`.

## Versions

A version is published once. The same folder pushed again is accepted and changes
nothing; other content under the same version is refused, so publish a new version
instead.

**Yanking** a version hides it from readers and keeps its archive. The newest version that
is not yanked is the one the docs site shows first.

```sh
ops docs                          # every doc set, with its latest version
ops docs rungholt-api             # one set's versions, newest first
ops docs yank rungholt-api 1.4.0
```

Yanking needs `package.publish` in the set's project, like a push. Reading needs no login.

## In the console

**Library**, **Docs** lists every doc set with its latest version, and each reader with
the revision it serves and how long it took to pick it up. A set's page lists its
versions, with **Yank from the site** in each version's menu, and the pages of the latest.
A project's own sets are under **Docs** in its sidebar, and a deployment or publish run
shows its `docs` step.

## Showing doc sets on your own site

A project that renders doc sets declares itself a reader. Ops then calls it on every new
version or yank, so it never has to rebuild:

```ts
docsReader: { service: 'web', path: '/api/docs/refresh', secret: 'OPS_DOCS_SECRET' },
```

Ops sends `POST <path>` to the production service with `{"revision": n}` and the value of
`OPS_DOCS_SECRET` from the service's secret group in the `x-ops-docs-secret` header. The
service answers within 60 seconds, with a 2xx status and `{"revision": n}`, the revision
it now serves.

A call that fails is not repeated, so the service also asks on a timer:
`GET /api/v1/docs/revision?since=<n>` answers what changed, and the service sends
`x-ops-docs-reader` (the project slug), `x-ops-docs-secret` and `x-ops-docs-served` (the
revision it serves) with it, so ops always knows what each reader shows and how long it
took to catch up.
