agent-pages lets coding agents publish HTML pages to your own Cloudflare account. Each page has versions, a library lists every page, and a share link opens one page with no login.
The repo has four parts:
| Folder | What it holds |
|---|---|
skills/explain/ |
The explain skill creates HTML explanations, lessons, documents, and presentations. It guides content, emphasis, and the choice of prose, tables, charts, or diagrams within each page. diagrams/ holds 64 diagram templates from diagram-design. |
kit/ |
explain.css and explain.js: the look and the runtime that every page shares. |
brand/ |
The mark. mark.svg is the master, mark-small.svg is the cut for 16 to 32 px with thicker parts and a bolder heading line (the favicon), and mark-square.svg is for platforms that round the corners. favicon.ico and apple-touch-icon.png are rendered from them. |
tools/ |
port-diagram-design.mjs copies the diagram-design templates into skills/explain/diagrams/. Run it again to take a newer version. |
cli/ |
agent-pages: new, check, pack, serve, publish and list. Node 18 or later, no dependencies. |
host/ |
A Cloudflare Worker: the library, the viewer, the API and the share links. |
- Link the skill into your agent's skill folder:
ln -s "$PWD/skills/explain" ~/.claude/skills/explain ln -s "$PWD/skills/explain" ~/.codex/skills/explain
- Put the CLI on your
PATH. Install it from npm, or link it from the clone:npm install -g agent-pages # or ln -s "$PWD/cli/agent-pages.mjs" ~/.local/bin/agent-pages
- Try it:
agent-pages check skills/explain/examples/gallery.html
Without a host, the skill still works. Pages go to ~/.agent/explain/<repo>/, and the agent shows them inline or in a local preview.
- R2 stores the HTML of each version at
pages/<group>/<name>/<version>.html. D1 stores the page list, the versions and the share links. - At publish time, the Worker indexes the text of the page in a D1 FTS5 table, with one row for each claim. Browser Rendering takes a light and a dark screenshot of the top of the page, and R2 stores them as
pages/<group>/<name>/<version>.<theme>.webp. - The raw route gives each
.k-claimsection with no id the idclaim-<n>. A search result links to/p/<group>/<name>#claim-<n>, and the viewer passes the hash to the frame. - The viewer shows each page in an
<iframe>with thesandboxattribute. The page HTML comes from a route that sendsContent-Security-Policy: sandbox. So the script in a page runs with no origin, and it cannot read the host's cookies or call its API. - Cloudflare Access protects the whole hostname. A person signs in with Access. The CLI sends an Access service token.
- The Worker checks the Access JWT on each request too. Access at the edge is the first check, and the Worker is the second.
- A page whose
<main>has the classk-deckis a deck, and D1 storeskind = 'deck'for it. At publish time, Browser Rendering opens the deck at 1920×1080, and the kit reports the slides that overflow. The publish response returns that audit, and the CLI prints it. The deck thumbnails show the first slide. - A second Access application bypasses Access for
/s/*. On that path the Worker checks only the share token, which is 32 random bytes.
| Route | Auth | Does |
|---|---|---|
GET / |
Access | The library: a grid or a list of thumbnails, pinned pages first, with a filter for each repo and full-text search |
GET /p/<group>/<name> |
Access | The viewer for the latest version |
GET /p/<group>/<name>/v/<n> |
Access | The viewer for one version |
GET /raw/<group>/<name>/<n> |
Access | The page HTML, with the sandbox CSP |
GET /t/<group>/<name>/<n>/<light or dark>.webp |
Access | A thumbnail |
GET /pdf/<group>/<name>/<n> |
Access | A deck as a PDF, one page for each slide. The host caches it in R2. |
GET /s/_/favicon.svg, favicon.ico, apple-touch-icon.png |
None | The site icons, which share links show too |
GET /api/pages |
Access | The page list as JSON |
GET /api/search?q=<words> |
Access | The best match on each page, best page first |
PUT, DELETE /api/pages/<group>/<name>/pin |
Access | Pin or unpin a page |
POST /api/pages/<group>/<name>/refresh |
Access | Index the latest version and take its thumbnails again |
PUT /api/pages/<group>/<name> |
Access | Store a new version. The body is the HTML. |
DELETE /api/pages/<group>/<name> |
Access | Delete the page, its versions and its share links |
GET, POST /api/pages/<group>/<name>/shares |
Access | List or make share links. POST body: { "version": n } or { "version": "latest" } |
DELETE /api/shares/<token> |
Access | Revoke a share link |
GET /s/<token> |
Token | The shared page |
GET /s/<token>/pdf |
Token | The shared deck as a PDF |
You need a Cloudflare account, a zone for the hostname, and Cloudflare Zero Trust (the free plan is enough). The host uses the cf CLI and Bun.
- Install and test:
cd host bun install bun run test
- Copy
deploy.example.jsontodeploy.local.json, and sethostnameandaccessTeam. LeaveaccessAudempty for now. Git ignoresdeploy.local.json. An optionalnamesets the name of the Worker, the D1 database and the R2 bucket. The default isagent-pages. - Create the storage and apply the migrations:
cf auth whoami # confirm the account cf d1 create --name agent-pages cf r2 buckets create --name agent-pages bunx cf d1 migrations apply <database id> --dir migrations
- Build and deploy:
With no
bunx vite build --mode production bunx cf deploy --prebuilt --mode production
accessAud, the Worker refuses every request except share links. - In Cloudflare Zero Trust:
- Make a service token for the CLI.
- Make a self-hosted application for the hostname. Give it an Allow policy for your email and a Service Auth policy for the token.
- Make a second self-hosted application for
<hostname>/s/*with a Bypass policy for everyone.
- Put the AUD tag of the first application in
deploy.local.jsonasaccessAud, then build and deploy again. - Set these variables in your shell profile:
export AGENT_PAGES_URL=https://<hostname> export AGENT_PAGES_CLIENT_ID=<service token client ID> export AGENT_PAGES_CLIENT_SECRET=<service token client secret>
- Publish a page:
agent-pages publish skills/explain/examples/gallery.html --slug example/gallery
To run the host locally, apply the migrations to the local database and start Vite. Local mode has no Access check. The local browser simulator cannot take screenshots, so local mode uses the real Browser Rendering service of the account that cf signs in to.
bunx cf d1 migrations apply 00000000-0000-4000-8000-00000000a271 --local --persist-to .cloudflare/state --dir migrations
bunx vite --port 8787
AGENT_PAGES_URL=http://localhost:8787 agent-pages publish <page>MIT. NOTICE.md lists the code and design values that come from other projects.