Skip to content

POC (#68): git-backed handbook on Astro + Cloudflare — GitHub auth, review workflow, no CMS/DB - #72

Open
VibratingKoala wants to merge 94 commits into
mainfrom
i68-handbook-poc
Open

POC (#68): git-backed handbook on Astro + Cloudflare — GitHub auth, review workflow, no CMS/DB#72
VibratingKoala wants to merge 94 commits into
mainfrom
i68-handbook-poc

Conversation

@VibratingKoala

@VibratingKoala VibratingKoala commented Jul 1, 2026

Copy link
Copy Markdown

Custom Astro SSR app on Cloudflare Workers replacing VitePress/GitHub Pages, so the handbook can gate per-page reader access — which a static host can't do. No CMS, no database: content, access control, drafts, and publishing all live in git/GitHub.

Architecture

Browser → Astro SSR Worker (stateless, no datastore)
  CONTENT  = markdown in doc/*.md — the SAME files the legacy VitePress site
             builds from (single source, nothing ported). POC frontmatter
             (visibility/section/parent/sort) lives there; VitePress ignores it.
             Everything in the build is PUBLISHED; drafts/pending edits live on
             handbook/<slug> branches + PRs
  ACCESS   = GitHub itself: repo collaborators ARE the allow-list (nothing committed here)
  IDENTITY = GitHub username (no emails anywhere; a CI guard fails the build on any)
  SESSION  = AES-GCM encrypted cookie (+ the user's own GitHub token, auto-refreshed)

Sign in with GitHub (org GitHub App, live since 07-09). Each user's own token maps repo permission → role (resolveRoleSelf), re-verified every ~10 min; no bot token in the App path (a classic-OAuth fallback with a bot token still exists but is slated for removal).

Access model — reading vs capability are separate axes

Reading (2 tiers, roles play no part): public → anyone; internal → any signed-in person. There is deliberately no finer tier — every signed-in user is a repo collaborator who could read the markdown source on GitHub anyway. Forbidden == not-found (404, no existence signal); fail-closed everywhere (unknown visibility → internal; malformed page files → tightest tier).

Capability (from GitHub repo permission):

GitHub permission handbook role can
admin / maintain admin + review dashboard: approve & publish / reject
write editor + edit: save drafts, submit for approval
explicit collaborator reader read internal pages
everyone else anonymous public pages only

(The gate is the explicit-collaborator check (204/404), never permission=="read" — GitHub reports read for any user while the repo is public; regression-tested.)

Publishing is git-native — no status field

Save draft            commit on handbook/<slug>, NO PR — private WIP
                      (reopening the page RESUMES the draft; listing shows a
                       "draft in progress" chip; new pages get their own section)
Submit for approval   the branch gets its ONE PR per page — pending, unpublished
Approve & publish     admin merges (in-app dashboard or GitHub) → deploy → live

Every commit is authored by the signed-in person (their GitHub App user token — no bot; GitHub refuses writes the moment push access is revoked). Concurrent edits 409 via blob sha; a branch holding unmerged commits is never force-reset (data-loss regression-tested); deletes are always review requests. The dashboard honors the main ruleset fully — no self-approval, checks must pass, stale branches auto-refresh.

Agent surface (llms.txt)

/llms.txt (index), /llms-full.txt (all readable content) and per-page /<slug>.md (raw markdown) — llmstxt.org endpoints for AI tooling (the standard-repository code-quality hook points every org AI session at the handbook). Dynamic, through the same canRead gate as HTML: anonymous sees public pages only, fail closed, forbidden == 404. Supersedes the old site's build-time vitepress-plugin-llms output at cutover — same URLs, now ACL-aware. Note: the five style-guide pages must flip to visibility: public before cutover (the hook fetches anonymously) — pending sign-off.

Test locally (no GitHub setup needed)

cd app && pnpm install
cp .dev.vars.example .dev.vars   # set COOKIE_ENCRYPTION_KEY (≥32 chars), DEV_LOGIN=1
pnpm dev:edit                    # dev server + local write agent (real file+git commits)
                                 # content = ../doc/*.md directly; no seed step
# personas without OAuth:  /api/auth/dev-login?user=alice&role=editor   (reader, admin)

Real GitHub login is locally testable too: personal OAuth App (callback http://localhost:4321/api/auth/callback), DEV_LOGIN=0 — verified live.

Staging & production

  • ONE worker (osbr-handbook); [env.staging] is inert with the Cloudflare adapter — see POC.md "Deploy to staging" for the mechanics (keep_vars, dashboard-managed vars, GITHUB_BRANCH POC targeting). Non-prod hosts serve an environment ribbon + X-Robots-Tag: noindex; both retire automatically on the real domain (host-based).
  • The org GitHub App is live (installed on the repo since 07-09) — login, per-person commits and the full draft→submit→approve→merge loop work on the deployed staging URL.
  • Deploys are currently manual (pnpm build && npx wrangler deploy). deploy-worker.yml auto-deploys pushes to this branch / main (incl. content-only doc/** merges) once a repo admin adds the CLOUDFLARE_API_TOKEN secret — required for "approve & publish" to be self-serve.
  • CI: run-tests (required check) also fires on handbook/* PRs — without that, editorial merges could never satisfy the ruleset. A docs-build job keeps the legacy VitePress site green until cutover.

Verified

  • 94 unit tests: ACL truth table, session crypto tamper/expiry, GitHub role mapping, PR-mode driver (branch create/reuse/force-reset guard, sha conflicts, draft vs submit), review approve→merge ordering, frontmatter parser round-trip incl. the on-disk # H1 boundary, llms.txt builders, sanitizer XSS. astro check 0/0/0, build, biome, both guards (incl. no-personal-emails).
  • Live: persona×visibility matrix, editor RBAC + admin-only dashboard gating, CSRF, stored-XSS stripping, ACL-gated search, real GitHub OAuth round-trip, editor save/delete flows driven in a real browser, llms.txt/.md endpoints anon vs signed-in (2 vs 15 pages, cache headers flip). The legacy VitePress build (incl. vitepress-plugin-llms) was run against the frontmattered doc/ files — no breakage, no frontmatter leakage.
  • Full multi-agent code review (8 angles → adversarial verification) — all 8 findings fixed, incl. a data-loss bug and a revocation fail-open.

After this PR merges — the roadmap

At merge time

  • Merge needs one approval from another editor (no self-approval) + green run-tests (add the ci-testing label to trigger it on this branch)
  • Delete the branch on merge — that deletion is what auto-flips editor submissions from targeting this branch to main (resolveBase() self-retires; GitHub retargets the existing open handbook/* PRs too)

One-time setup (parallel, any order)

  • [org admin] Org GitHub App created + installed on this repo (done 2026-07-09; residual cleanup — drop GITHUB_TOKEN, GITHUB_WRITE_ENABLED, revoke the personal demo PAT/OAuth app — tracked in POC.md)
  • [repo admin] Add the CLOUDFLARE_API_TOKEN repo secret (company account; Workers Scripts:Edit + Workers KV Storage:Edit) so merges auto-deploy — until then every publish is a manual deploy
  • [infra] DNS Phase 0: move the osbrjp.com zone from Route 53 to the company Cloudflare account (records copied exactly — MX/email double-checked; full runbook in POC.md). Blocks only the final domain flip
  • [content] Flip the five style guides to visibility: public (required by the standard-repository hook's anonymous fetch; pending sign-off)

Production

GITHUB_WRITE_ENABLED stays wired as the editing kill switch in prod — one secret change disables editing instantly, readers unaffected.

Closes #68.

🤖 Generated with Claude Code

VibratingKoala and others added 30 commits June 30, 2026 15:26
From-scratch (no CMS) handbook on a single Astro SSR Worker over Cloudflare D1.

- Auth: hand-written Google OAuth (AES-GCM session cookie, state-nonce CSRF,
  verified_email); DEV_LOGIN shim for local testing without Google.
- ACL: middleware resolves role+groups from D1 per request (no identity bleed);
  one readableWhere predicate gates content; fail-closed; restricted hidden.
- Sidebar shows the full handbook structure (collapsible, persisted); internal
  pages show a sign-in prompt; content stays access-controlled.
- Editor at /admin (editor-role only): create/edit, draft/publish, live preview,
  double-submit CSRF, slug auto-generated from title, save confirmation banner.
- Single VitePress-style design with light/dark toggle, nav bar, sidebar +
  on-this-page, OSBR logo.
- D1 schema + migrate-doc seed from doc/*.md (generic personas, no real emails).

Verified locally: pnpm check / lint / test (26) / build / guard; reader-ACL
matrix, editor RBAC, CSRF, stored-XSS all confirmed against local D1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Sidebar: collapse sections by default, auto-expand current page's section;
  anon sees only public pages, logged-in users see all with "no access" badges
- Right-side "On this page" TOC: collapsible h3 groups under each h2
- On-page pencil edit button for editors
- In-content [[TOC]] now builds a nested list so h3 sub-items indent
  structurally; widen sanitize schema to keep class on ul/ol/li

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- "Back without saving" link returns to the admin page list
- Delete button (own form, editor + CSRF gated) with a JS confirm;
  deletePage() removes the page and its group grants atomically
- /admin/delete redirects to /admin?deleted=1 with a confirmation banner

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tions

- Move Back (left) and Delete (right) into a top bar above the form
- Wrap page metadata in a collapsible "Page settings" <details> (open by default)
- Right-align Save draft / Publish at the bottom

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Style the editor-page <h1> (it was unstyled outside .prose); edit page
  heading now reads "Edit: <title>"
- Replace the native confirm() with a styled <dialog>: "Delete this page?
  Are you sure… This cannot be undone." with Cancel / Delete actions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the markdown body + live preview grid in a collapsible <details> with
one summary title, replacing the two separate per-pane titles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop the leadership group, the leader@/editor-leader@ personas, and their
  group membership from the seed generator
- security-policy is now 'internal' instead of restricted-to-leadership
- Regenerate seed (10 pages: 2 public, 8 internal; 0 groups), update POC.md

The restricted/group-gated visibility level remains supported by the schema
and ACL; it's just unused by the default seed now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pins the primary actions while editing so they're reachable without
scrolling whether the sections are collapsed or expanded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Back now uses history.back() to return to wherever the editor was opened
from (the page or the list), falling back to /admin for direct loads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Group pages under collapsible <details> per section with a count badge
  (drops the now-redundant Section column)
- Style the admin-page <h1> (was unstyled outside .prose)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the create action onto the heading row as a primary button (the
conventional spot), replacing the easy-to-miss text link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- table-layout:fixed with shared column widths so every section's columns
  line up; right-align Visibility/Status/Actions
- Add a Delete action per row that opens the same confirm modal as the
  editor and posts to /admin/delete (CSRF token now issued on the list page)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch from localStorage to sessionStorage and detect reloads via the
Performance Navigation API: a refresh resets to the default (only the
active page's section open); normal navigation restores opened sections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously two button systems (homepage/.btn pill vs editor/modal padding
buttons) rendered at different heights and fonts. Now every button shares
one base; Publish is primary (brand), Save draft secondary (alt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Returns to the previous page (history.back), falling back to the handbook
home for direct loads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- searchPages() in db/pages.ts: case-insensitive LIKE on title/body, ANDed
  with the existing readableWhere(visitor) ACL so results never leak a page
  the caller can't read; title matches rank above body matches; LIKE
  wildcards escaped
- GET /api/search returns { results } JSON, fails closed (503 + empty) on
  DB error, sets Cache-Control private/no-store + Vary: Cookie
- Navbar search box with debounced fetch; results rendered via
  createElement/textContent (no innerHTML on API data); plain-text snippets
- 6 new tests covering ACL (incl. draft visibility), ranking, empty query,
  and bind ordering

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clearer for non-technical staff. Renames src/pages/admin -> edit-pages and
updates every reference (navbar Editor link, reader edit pencil, New page,
Edit/Delete links, save/delete/preview form actions + redirects, Back link).
No redirect from the old /admin (clean break; nothing external links to it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Edit route keys on the page slug: /edit-pages/edit/<slug> (was numeric id).
  Adds getEditablePageBySlug(); Edit links, the reader edit pencil, and the
  save redirect all use the slug.
- Rename the create route to /edit-pages/create (clearer than /new); heading
  now "Create page".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native <datalist> popup is browser-rendered and can't be themed, so it
looked out of place. Swap it for a custom dropdown styled to match the dark
UI; still lets you pick an existing section or type a new one (free text).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove dead getPageVisibility() (never wired up) from db/pages.ts
- Remove orphaned .pane-title CSS (panes lost their titles when the Content
  section became collapsible)
- Style .nav-edit (the navbar "Editor" link had markup but no CSS)
- Replace the "Back to handbook" inline style with an .admin-back class

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visible label + help text only; the form field name and route slug are
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both editor sections (Page settings, Content) now start collapsed. Open-state
is remembered per tab (sessionStorage): a reload resets to collapsed, but
navigating back restores whatever you had open — same model as the sidebar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The identical history.back()/same-origin-referrer handler lived in both the
editor and the Pages list. Move it to lib/ui/backLink.ts (wireBackLink).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sessionStorage reload-reset / navigation-restore logic for <details>
sections was duplicated (inline) in the sidebar and the editor. Move it to
CollapsePersistence.astro, which renders the inline script (define:vars, so
no flash) parameterised by selector + storageKey.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce src/content.config.ts (glob collection, fail-closed schema:
missing visibility -> restricted, missing status -> draft) and generate
src/content/pages/<slug>.md for all 10 pages via scripts/seed-content.mjs.
Additive — nothing reads the collection yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/content/acl.ts: pure canRead() predicate (mirror of readableWhere,
  fail-closed) + search + snippet helpers — unit-testable without Astro
- lib/content/pages.ts: getPageBySlug/getNavPages/getSidebarPages/
  listAllPages/getEditablePageBySlug/searchPages over the collection
- Visitor gains groupKeys (stable keys for the git ACL); groupIds kept until
  db/pages.ts is removed. Middleware resolves both via a groups JOIN.

Not yet wired — readers still use db/pages.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point the reader page, search API, sitemap, sidebar, and access.ts at
lib/content/pages (git-backed) instead of lib/db/pages (D1). ACL verified
live from frontmatter: anon sees only public, reader +internal, search +
sitemap gated correctly. Editor pages still use D1 (Phase 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VibratingKoala and others added 16 commits July 6, 2026 17:16
…g everyone out)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…me="action" buttons)

form.action is shadowed by the draft/submit buttons (name="action"), so
fetch(form.action) hit /edit-pages/[object RadioNodeList] -> 404, shown as a
misleading "saving isn't enabled" popup. Use getAttribute("action") instead,
and give 404 its own error message so a routing failure can't masquerade as
an environment restriction again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Section lists start collapsed on every viewport/load (was: open on desktop,
  mobile-only strip, persistence resetting on reload — three behaviors)
- One user-facing name: navbar, dropdown, h1, tab title and back-link all say
  "Manage pages" (URL stays /edit-pages — stable identifier, not a label)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uired-legend

- "New pages in progress" gets a brand-tinted card (matches the draft badge hue)
  so active work reads differently from plain content sections
- Resuming a draft shows ONE merged banner below Back/Delete instead of two
  green notices sandwiching it (draftResume prop on EditorForm)
- "* required" legend removed — the asterisk convention needs no explanation
  (labels keep the tooltip, inputs keep the required attribute)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- One helper owns the delete-dialog rules both call sites shared (environment
  check FIRST on open and confirm; fetch-submit so failures pop up); the Pages
  listing injects row slug/title via onOpen
- Remove .notice-muted / .preview-title (defined, referenced nowhere)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete was built for published pages (deletion PR -> review -> merge); a
draft-only page 404'd and left its edit branch behind. Now delete.ts detects
the draft-only case and calls discardDraft(): delete the edit branch (GitHub
auto-closes any PR from it) — no review step, nothing was ever public. The
listing shows a 'Draft discarded' banner; the client 404 message no longer
misdiagnoses this as a session problem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- submitForm shows the clicked button's data-busy label (Saving…/Submitting…/
  Deleting…) and guards re-entry — a submit spans several GitHub API calls and
  a silent button invites double-clicks
- save.ts redirects back to the EDIT page with the outcome banner (draft saved /
  submitted with PR link) instead of teleporting to the listing; the stale
  'new page would 404' rationale predates resumable drafts
- Listing keeps deleted/discarded/submitted banners (delete PRs still land there)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- prLinkFromParam (reviews.ts): one place validates ?pr/?submitted params and
  builds the GitHub link — was copy-pasted regex + URL template in two pages
- Reviews page approve/reject: disable clicked button + data-busy label
  (Approving…/Rejecting…) — same dead-click/double-merge exposure the editor
  buttons had; runs after the reject-confirm so a cancel doesn't freeze it
- pageshow reset: Back to a page frozen mid-submit reloads instead of showing
  a permanently disabled Submitting… button

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the handbook

- /edit-pages/reviews/{n}: the proposed version rendered like a real page,
  the currently-published version collapsible below for comparison, deletion
  reviews called out, approve/reject in place (GitHub link kept as audit trail)
- getReview (reviews.ts): one open edit-PR, validated to our base + edit-branch
  prefix — anything else 404s
- readPageAtRef (store.github.ts): page content at an arbitrary ref; readDraft
  now reuses it (dedupe) with its unique-commits gate on top
- Reviews table: finally styled (.admin-table had NO css — browser-default
  soup); Change title links to the internal page, GitHub demoted to a side link;
  Page/Checks columns drop on mobile

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eriod)

The workers.dev deployment is a proof-of-concept under evaluation, not the
blessed staging of a production system. The label self-retires: the real
domain renders no ribbon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two action buttons + title + author can't share table columns at 375px, and a
table's min content width drags the whole page horizontal. A review list is a
list: text left, actions right, wrapping below on narrow screens. Removes the
hour-old .admin-table styles it replaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Each review renders as a bordered card (same language as Manage pages
  sections) instead of bare divider lines
- reviewLabel() strips our own PR-title scaffolding ('Submit "X" (slug)') so
  the list and review page show just X; hand-made PR titles pass through

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pushes to the deployed branch (i68-handbook-poc now, main post-POC) that touch
app/** test, build and deploy the Worker — this is what makes an approved
submission actually appear on the site. Without the repo secret the run skips
with a notice instead of failing every merge. keep_vars + the pinned SESSION
KV id make CI deploys safe for dashboard config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Snapshot of #73 at 23cc079 (doc/style-guide*.md), converted to
content-collection format: frontmatter added (section Guideline, sorts 61-65,
visibility internal — flip per page later if any should be public), leading h1
dropped (title renders from frontmatter). Slugs match the VitePress paths so
Alex's cross-page links work unchanged. One deliberate divergence: the
sk_live_-prefixed example string is replaced with an obvious placeholder so it
can't trip secret scanners on this public repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VibratingKoala and others added 4 commits July 10, 2026 15:14
… tree)

New optional 'parent' (slug) threads through schema, parse, serialize, the
editor save round-trip (hidden field — a save must never strip nesting) and a
recursive SidebarTree component (depth-capped so a frontmatter cycle can't
hang the render; children of an ACL-hidden parent surface at top level rather
than vanish). Style guides now nest: Development Guide > Style Guide > four
languages, indented with the rule line like the VitePress original.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… open)

Parents render as <details> nodes: the label link navigates (an <a> inside
<summary> is its own activation target — no toggle), the chevron/row toggles,
collapsed by default except the branch containing the current page. Joins the
existing CollapsePersistence via data-section (n: prefix). Animated chevron,
hover affordance, and the level rule line brightens under the pointer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lumns)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arent nesting, discard-draft

The TEMPORARY-demo-auth section described a state that ended when the org
GitHub App went live; replaced with current auth state + residual cleanup
list. Owner checklist step marked done. Feature list gains the internal
review page, draft-discard delete, and the parent frontmatter field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VibratingKoala and others added 7 commits July 13, 2026 09:58
# Conflicts:
#	.github/workflows/run-tests.yml
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The POC no longer keeps hand-ported copies of the handbook content.
The Astro collection now globs ../doc (the same files VitePress builds
from); POC frontmatter (title/section/visibility/parent/sort) lives in
doc/*.md where VitePress ignores it. Bodies keep their '# H1' on disk
for VitePress — stripLeadingH1 on load, re-added on save, both sides
in serialize.ts. Editor writes (GitHub store + local agent) target
doc/. seed-content.mjs and its test retire with the copies. The one
content edit: the HTML/CSS guide's fake-but-realistic api-key example
string stays sanitized (public repo; secret scanners trip on sk_live_).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
llmstxt.org surface for AI agents (the standard-repository hook sends
every org Claude session to the handbook). Unlike the old site's
vitepress-plugin-llms (static, all-public — fine while everything is
public), these are dynamic and run through the same canRead gate as
the HTML pages: anonymous sees public only, fail closed, 404 for
missing AND forbidden alike. Index links point at /<slug>.md so agents
get raw markdown instead of scraping HTML. Verified live both ways:
anon = 2 public entries, signed-in = all 15; cache flips to
private,no-store + Vary: Cookie once a session exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-source refactor

Without this, an approved editor merge (touches only doc/<slug>.md) would
never redeploy the Worker and the published change would never appear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…equisite

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ext H1

Four-agent review (reuse/simplification/efficiency/altitude), fixes:
- One excerpt rule (acl.excerpt) shared by search snippets + llms summaries;
  one H1-restore rule (serialize.withTitleH1) shared by saves + .md endpoints;
  one nav-grouping rule (acl.groupBySection) shared by sidebar + llms index;
  one site title (lib/site.ts) shared by header brand, hero, llms header;
  one CONTENT_DIR (serialize.ts) shared by the GitHub + local write drivers.
- llms.txt/llms-full.txt: shared llmsTextEndpoint factory; anonymous output
  memoized per isolate (Workers don't edge-cache off Cache-Control alone, and
  the anonymous result is a deploy-time constant). Dev exempt.
- run-tests.yml: the 6-line trigger condition now lives once, in a gate job.
- doc/security-policy.md led with a SETEXT H1 the strip regex can't see —
  double title on the page, and a save would have prepended a second heading.
  Converted to ATX, title aligned; new content.test.mjs enforces the
  '# {title}' invariant over the whole doc/ corpus so this can't recur.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

POC: move handbook off VitePress to a headless CMS with reader RBAC + Google Workspace auth

1 participant