Docs Site
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 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
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:
- Cloudflare dashboard → Zero Trust → Access → Applications → Add an application → Self-hosted.
- Application domain: the Pages hostname (
chat-agent-docs.pages.dev, and the custom domain too if one is attached). - Session duration: whatever suits the team.
From the API (needs the Access: Apps and Policies — Edit policy above):
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.mdmirrors, 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 · Turn Router Middleware