# Publishing sites

> Publish a space, folder or note as a static documentation site, from the web app, the terminal or CI

A space, a folder or a single note can be published as a static documentation site. Every note becomes a page, folders become sections of the sidebar, and the site gets search, a dark mode, a table of contents on each page and files that AI agents can read.

A site is a snapshot: it is built from the notes as they are when you publish or rebuild it. When the notes change, rebuild the site to show the changes.

:::callout{type="info"}
Publishing, rebuilding and unpublishing need admin rights in the space. A small globe next to a space, folder or note in the sidebar shows that it is published.
:::

## Publish from the web app

:::steps
::step
Open the publish dialog

---
In the sidebar, right-click the space, folder or note (or press and hold it on a touch screen) and choose **Publish as site**.
::
::step
Pick where it goes

---
Under **Publish to**, choose a target. **Kribble (served by this server)** is always there. The other choices appear only if the server's operator set them up (see Targets below). If this server is the only target, the dialog doesn't ask.
::
::step
Set the address or directory

---
For this server, edit **Site URL**: the part after `/p/<organisation>/` is the slug, made from the name. Use lowercase letters, numbers and hyphens. For a local directory or SSH server, set **Subdirectory** instead.
::
::step
Choose the layout (folders and spaces)

---
Turn on **Group root folders as top-bar tabs** to show each top-level folder as a tab in the site's header. Open **Language & versioning** if top-level folders hold translations or older versions (see Tabs, versions and languages below).
::
::step
Choose colours and access

---
**Appearance** sets the site's colours (see Colours below). **Access** decides who can read it: **Public** or **Space members only**. Sites on other targets are always public.
::
::step
Publish

---
Click **Publish site**. The build runs on the server in the background; the dialog shows its status and a message when the site is up.
::
:::

### Targets

| Target | Where the site ends up | Who can read it |
| --- | --- | --- |
| **Kribble (served by this server)** | `https://<server>/p/<organisation>/<slug>/`; for a personal space, `https://<server>/p/~<username>/<slug>/` | Public, or space members only |
| **Local copy** | A directory on the server, under a base directory the operator chose, in the **Subdirectory** you give. Another web server serves it. | Public |
| **SSH upload** | A directory on another machine, uploaded over SFTP, in the **Subdirectory** you give | Public |
| **Cloudflare Pages** | A Pages project named after the space, folder or note, at `https://<project>.pages.dev/` | Public |

For local and SSH targets, the subdirectory is emptied and filled again on every build, so don't point it at a directory that holds anything else. Nested paths such as `products/sdk/v2` work. Kribble doesn't know the public address of a site on a local or SSH target: ask the operator for it.

The operator sets up targets in the server's configuration: see [Publish targets](../running-a-server/publish-targets.md).

### Tabs, versions and languages

These options are for spaces and folders, and are set when you first publish.

- **Group root folders as top-bar tabs**: each top-level folder becomes a tab in the header, and the sidebar shows only the active tab's pages. Good for docs with distinct parts, such as Guides and API reference.
- **Version folders**: top-level folders that hold older versions of the docs, for example `v1` and `v2`. The site gets a version switcher; the rest of the content is the latest version. Add a folder named `latest` to the list to make that folder the version shown at the site's root.
- **Language folders**: top-level folders that hold translations, for example `de` and `ja`. The site gets a language switcher. **Default language label** names the language of the rest of the content (`en` if you leave it empty).

With tabs on, version and language folders are looked for inside each tab's folder. A language folder can also sit inside a version folder.

When folder names look like versions (`v1`, `2.0`, `latest`, `beta`…) or language codes (`de`, `ja`, `pt-br`…), the dialog offers **Auto-detect from folder names**, which fills both lists for you.

### Colours

**Appearance** has three colours: **Brand** (links, highlights), **Light BG** and **Dark BG** (the page background in light and dark mode). Left empty, the site uses the organisation's colours from its **Theme** settings. A change to the organisation's theme reaches a site at its next rebuild.

## Manage a published site

Open **Publish as site** again on the published space, folder or note. The dialog now shows:

- The status: **queued**, **building**, **published**, **failed**, or **transfer failed** when the site was built but couldn't be copied to its target. A transfer error shows the message from the target; fix the target and click **Retry transfer**.
- **Site URL** with **Copy** and an open button, for sites on this server. For a Cloudflare Pages site, its `pages.dev` address; for local and SSH targets, the target and subdirectory.
- **Access** (sites on this server) and **Theme**. Changes to these apply at the next rebuild.
- **Last built** and the time.
- **Rebuild**: builds the site again from the current notes, with the access and theme shown.
- **Unpublish**: takes the site down and removes its files from the target.

The slug, the target, the tabs and the version and language folders can't be changed on a published site. To change them, unpublish it and publish it again.

## What a published site offers

Readers get:

- **Search** across all pages, with the search button in the header or Ctrl+K (Cmd+K on a Mac). It works like the app's search, in the reader's browser:
  - Titles match as you type, even from a few letters (`dsk ap` finds "The desktop app"); other words forgive a typo and complete the word being typed.
  - Each page's language is detected when the site is built, and words also find their other forms in it (`deployments` finds "deploying", `beállításaid` finds "Beállítások"), in all the languages the site's pages are in, for the same languages as the app's search. Longer words (six letters or more) also match inside longer words, which helps with compounds: `korlátozás` finds "sebességkorlátozásokat". Pages found these ways come after the pages that have the words as typed. Typed accents count as in the app: `kör` ranks a page with *kör* before pages with *kor*. The stemmers are small scripts published with the site, loaded only for the languages it uses.
  - Each result names the section that matched, and opens the page at that heading. A preview of the section shows on wide screens.
  - With nothing typed, it lists the pages the reader opened lately.
  - Operators narrow it down: `in:Guides` (a folder or tab, `in:Guides/Setup` for a path; Tab completes the name), `title:word`, `updated:<30d` (also `today`, `>90d`, `2026-03`, `2026-01..2026-03`), `sort:recent`, `oldest` or `title`, `"exact phrase"` and `-without`. An operator that can't match says so, and finds nothing.
  - ↑ and ↓ move, Enter opens, Ctrl+Enter (Cmd+Enter on a Mac) opens in a new tab, Esc closes.

  Sites of a single note have no search.
- **Dark mode**, from the reader's system setting or the toggle in the header.
- **A table of contents** of the page's headings next to the text, following the reader as they scroll.
- The sidebar with every page (a menu button on phones), and the version and language switchers if the site has them.
- The organisation's icon in the header, if it has one.
- A copy button on every code block.

### Page actions

Above each page's title, **Copy page** copies the page as markdown, ready to paste into a chat with an AI. The arrow next to it opens more:

- **Copy as markdown** and **View as markdown** (the page as plain text).
- **Use with your agent**: the command that gives Claude Code, Codex or Claude Desktop these docs, with a copy button:

  ```bash
  kribble mcp install --docs https://kribble.example.com/p/acme/sdk/
  ```

  On a public site it also links the site's `llms.txt`. On a private site it says to sign the CLI in to the server first.

### Files for agents

Every build writes, next to the HTML pages:

| File | What it holds |
| --- | --- |
| `PAGE.md` next to every `PAGE.html` | The page as markdown; links to other pages point at their `.md` copies |
| `llms.txt` | The list of pages, linked as markdown ([llmstxt.org](https://llmstxt.org)) |
| `llms-full.txt` | Every page in one file; one per version and language |
| `kribble-docs.json` | The pages, versions, languages and search indexes, for tools such as `kribble mcp docs` |

They are plain static files, so they work wherever the site is hosted: this server, your own web server, Cloudflare Pages.

### Let readers' AI tools search the docs

Anyone with the [kribble CLI](../using-kribble/the-kribble-cli.md) can give their AI tools a published site, without an account on your server:

```bash
kribble mcp install --docs https://docs.example.com
```

This adds an MCP server to Claude Code, Claude Desktop and Codex (those it finds; name one to pick it, for example `kribble mcp install codex --docs URL`). Their agents can then search the docs, list the pages and read a page or one section of it. The server is named after the site (for example `acme-sdk-docs`); remove it with `kribble mcp uninstall --name acme-sdk-docs`.

To run the same server by hand, for another MCP client, use `kribble mcp docs URL`. It speaks MCP over standard input and output; `--version v1` and `--lang de` pick the version and language it reads by default.

It reads only the site's public files, so it works with any host, and with sites built by other tools that have an `llms.txt`. For a private site, the reader must be a member of the space and have signed the CLI in to the same server with `kribble login`; the CLI sends that login only to that server.

## Publish from the terminal

The [kribble CLI](../using-kribble/the-kribble-cli.md) publishes and rebuilds sites with your login.

```bash
kribble targets                              # where sites can go, and the flags each takes
kribble deploy "Engineering/SDK"             # publish; in a terminal it asks for the target
kribble sites                                # every site you can reach, with URL and status
kribble sites "Engineering/SDK"              # one site
kribble redeploy "Engineering/SDK" --wait    # build it again and wait for the result
```

`kribble deploy PATH` publishes a space (`"Space"`), folder (`"Space/Folder"`) or note. If it is published already, it builds it again with its current settings and ignores the other flags. In a terminal, without `--target`, it asks where the site goes, then for the slug and access (this server) or the subdirectory (local and SSH). Its flags:

| Flag | Meaning |
| --- | --- |
| `--target kribble\|local\|ssh\|cloudflare_pages` | The target type; `kribble` (this server) by default |
| `--target-name NAME` | Which local, SSH or Cloudflare Pages target, when there are several |
| `--slug S` | This server: the URL is `<server>/p/<organisation>/<slug>/` (default: from the name) |
| `--access public\|space_members` | This server: who can read it (default `public`) |
| `--subdir DIR` | Local and SSH: the directory under the target's base (default: from the name); emptied on every build |
| `--tabs` | Show the top-level folders as tabs |
| `--versions "v1,v2"` | Top-level folders that are older versions |
| `--langs "de,ja"` | Top-level folders that are translations |
| `--default-lang en` | The language of the rest |
| `--wait` | Wait until the build is done; exit with an error if it failed |
| `-y` | Don't ask; use the defaults |
| `--json` | Print the result as JSON |

`kribble redeploy` takes the published path or a site id (from `kribble sites --json`). Without `--wait`, both commands return as soon as the build is queued; `kribble sites PATH` shows when it is done.

## Publish from CI

`kribble deploy` never asks anything in CI: it asks for the target only in an interactive terminal, and not when the `CI` environment variable is set (GitHub Actions, GitLab CI and most others set it) or with `-y`. Give it what it needs as flags, and the server's address and an access token in `KRIBBLE_URL` and `KRIBBLE_TOKEN`. It publishes the first time and rebuilds every time after, so the same command works on every run:

```bash
kribble deploy "Engineering/SDK" --target ssh --target-name docs-host --subdir sdk --wait
```

`--wait` follows the build and exits with an error if it fails, so the job fails too. `--json` prints the result (with the site's URL) for later steps.

To keep a site in step with docs that live in a repository, push the docs and rebuild the site in the same job:

```bash
kribble push docs
kribble redeploy "Engineering/SDK" --wait
```

Which token to use:

- **Publish only** is enough for `kribble deploy`, `kribble redeploy`, `kribble sites` and `kribble targets`.
- `kribble push` also reads and writes notes, so a job that pushes needs **Read + Write (MCP agents)** or **All scopes**.

Either way, the person who created the token needs admin rights in the space. Access tokens and their scopes are explained in [AI agents](../using-kribble/ai-agents.md).

A GitHub Actions job, with the token saved as the repository secret `KRIBBLE_TOKEN`:

```yaml
name: Docs
on:
  push:
    branches: [main]
    paths: ["docs/**"]
jobs:
  docs:
    runs-on: ubuntu-latest
    env:
      KRIBBLE_URL: https://kribble.example.com
      KRIBBLE_TOKEN: ${{ secrets.KRIBBLE_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - run: |
          curl -fsSL "$KRIBBLE_URL/install.sh" | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - run: kribble push docs
      - run: kribble deploy "Engineering/SDK" --wait
```

Without the CLI, an HTTP request rebuilds the site of a whole space, with its settings:

```bash
curl -X POST "$KRIBBLE_URL/api/v1/spaces/$SPACE_ID/publish" \
  -H "Authorization: Bearer $KRIBBLE_TOKEN"
```

This endpoint needs a token with the `publish` scope (**Publish only** or **Publish + Import** are enough). It only rebuilds a space that is published already; publish it once first. `kribble ls --json` shows a space's id.
