---
title: Docs Site
description: >-
  How this documentation site is built with Blume and deployed to Cloudflare
  Pages behind Cloudflare Access.
---
This site is the repository's own `docs/` folder, published by
[Blume](https://useblume.dev) and deployed to **Cloudflare Pages** on every push
to `main`. It sits behind **Cloudflare Access** (Zero Trust), so only people the
Access policy allows can read it.

## How it is put together

| Piece | Where |
|-------|-------|
| Site config | `blume.config.ts` (root) |
| Content | `docs/` — an Obsidian-style vault; `obsidian()` resolves the `[[wikilinks]]` |
| Landing page | `docs/index.mdx` (MDX, so it can use Blume components) |
| Build | `bun run docs:build` → static site in `dist/` |
| Deploy | `.github/workflows/docs.yml` → `wrangler pages deploy dist` |
| CI | `.github/workflows/ci.yml` → typecheck, unit + integration tests, worker bundle |

Blume reads `docs/` in place: the Markdown in this repository is the single
source of truth, and the site adds nothing to it except `index.mdx`. A `.mdx`
page uses Blume components with no imports; the `.md` pages are plain Markdown,
which is also why the vault source and the MDX source are configured separately.

## Local preview

```bash
bun install                # repository root
bun run docs:dev           # dev server with hot reload
bun run docs:build         # production build into dist/
bun run docs:preview       # serve the built site exactly as a static host would
bun run docs:validate      # check internal, anchor, and asset links
bunx blume doctor          # diagnose configuration and content problems
```

## Deploying

`.github/workflows/docs.yml` builds the site and uploads it to the Pages project
`chat-agent-docs` on every push to `main`. Nothing about this runs on the app's
VPS: the pipeline is GitHub Actions → Cloudflare, so the runtime host needs no
new environment variables.

### Repository secrets

Settings → Secrets and variables → Actions → **Secrets**:

| Secret | Value |
|--------|-------|
| `CLOUDFLARE_API_TOKEN` | An API token scoped to the account that owns the Pages project (policies below) |
| `CLOUDFLARE_ACCOUNT_ID` | That account's ID (Cloudflare dashboard → **Workers & Pages** → *Account ID* in the right sidebar, or `wrangler whoami`) |

### Token policies

Create the token at **My Profile → API Tokens → Create Token → Create Custom
Token** and give it exactly these permissions, with **Account Resources** set to
the one account:

| Permission | Scope | Why |
|------------|-------|-----|
| **Cloudflare Pages** — *Edit* (API name: `Pages Write`) | Account | Create the project and upload deployments |
| **Account Settings** — *Read* | Account | Let Wrangler resolve the account behind the token |
| **Access: Apps and Policies** — *Edit* (API name: `Access: Apps and Policies Write`) | Account | Only needed if you want the Access application created by API instead of by hand |

An account API token (rather than a user token) with the first two rows is enough
for the workflow. The token is stored only as a GitHub secret — never in this
repository.

### Repository variables (optional)

Settings → Secrets and variables → Actions → **Variables**:

| Variable | Default | Meaning |
|----------|---------|---------|
| `PAGES_PROJECT` | `chat-agent-docs` | Pages project name, if you want a different one |
| `DOCS_SITE_URL` | `https://chat-agent-docs.pages.dev` | Canonical origin — set it once the site has a custom domain, so canonicals, the sitemap, and Open Graph images point at the real host |

The workflow creates the Pages project if it does not exist yet
(`wrangler pages project create`), then deploys with
`wrangler pages deploy dist --project-name chat-agent-docs`. Every deploy is a
production deploy on `main`; pull requests only build the site, so a broken page
fails the PR instead of the site. Until both secrets exist the deploy steps skip
and say so in the run summary.

:::note
Cloudflare exposes only the *current* deployment's URL (`CF_PAGES_URL`) on Pages,
which changes on every deploy — hence the pinned `deployment.site` in
`blume.config.ts` and the `DOCS_SITE_URL` override.
:::

## Cloudflare Access (Zero Trust)

Access is what makes the site private; it is enforced at Cloudflare's edge before
any file is served, and it covers the whole site — pages, the search index, the
`.md` mirrors, `llms.txt`, and the Open Graph images.

The Access **application** has to exist for the hostname the site answers on:

1. Cloudflare dashboard → **Zero Trust** → **Access** → **Applications** →
   **Add an application** → **Self-hosted**.
2. Application domain: the Pages hostname (`chat-agent-docs.pages.dev`, and the
   custom domain too if one is attached).
3. Session duration: whatever suits the team.

From the API (needs the **Access: Apps and Policies — Edit** policy above):

```bash
curl -X POST \
  "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps" \
  -H "authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "content-type: application/json" \
  --data '{
    "name": "chatAgent docs",
    "domain": "chat-agent-docs.pages.dev",
    "type": "self_hosted",
    "session_duration": "24h"
  }'
```

### The policy

An Access application with **no** policy denies everyone, so the site is
unreachable until a policy is added — the safe state while it is being set up.
Add one at **Zero Trust → Access → Applications → chatAgent docs → Policies**:

| Field | Value |
|-------|-------|
| Action | **Allow** |
| Selector | `Emails` (explicit addresses), `Emails ending in` (a domain), or `Cloudflare account member` for everyone in the Cloudflare account |
| Value | The addresses, domain, or — for the account-member rule — nothing |
| Session duration | Inherits the application's; shorter means more re-auth |

Add a second policy with the selector **Service Token**
(`any_valid_service_token`) if a machine needs to fetch the site — CI jobs,
monitoring, or an agent. Create the token under **Zero Trust → Access → Service
Auth**, then send its `CF-Access-Client-Id` / `CF-Access-Client-Secret` headers.

A few consequences worth knowing before the policy goes on:

- Agents and link previews stop working: `llms.txt`, the `.md` mirrors, and the
  MCP surface all live behind the same protection. Slack unfurls and X cards
  will show nothing.
- To keep some pages public later, publish them as a separate site — Access
  covers the whole hostname, not per-page.

## What the build emits

Alongside the HTML pages, Blume writes the agent-facing surface at the site root:

| File | Purpose |
|------|---------|
| `llms.txt`, `llms-full.txt` | Index and full corpus for coding agents |
| `<route>.md`, `<route>.mdx` | A Markdown twin of every page |
| `sitemap.xml`, `robots.txt` | Crawler files (built from `deployment.site`) |
| `agent-readability.json`, `.well-known/ai-catalog.json` | Agent discovery manifests |
| `_headers`, `vercel.json` | Host configuration files — Pages reads `_headers`, and ignores the rest |

---

Back to [Architecture](/architecture) · [Turn Router Middleware](/turn-router-middleware)
