Files
Corey PettyandClaude Opus 4.8 dc89328b56 Migrate site to Quartz 5
Follows the official v5 migration: branch from upstream/v5, run
`quartz create` (non-interactively, via -t/-X/-s/-b/-l), then restore this
site's customisations on top.

Config moves from TypeScript to YAML. The port was small - the only
deviations from upstream defaults were pageTitle and baseUrl, so
quartz.config.yaml is the generated default with those two changed and
markdownLinkResolution kept at "shortest". quartz.layout.ts is gone
entirely; layout position is now a per-plugin property, and 44 built-in
components resolve as community plugins via quartz.lock.json.

Carried over from v4: content (verified byte-identical, 194 files, no
additions and no losses), serverless/inline-comments-worker, docs-internal,
custom static HTML and images, AGENTS.md/CLAUDE.md, and the .gitnexus /
.dev.vars / .wrangler ignore rules.

Deliberately not carried over:
- quartz/components/InlineComments.* - becomes a standalone v5 plugin, see
  docs-internal/inline-comments-v5-plugin-migration.md
- quartz/plugins/transformers/tikz.ts - custom transformer, used by exactly
  one content file; needs its own port and should not block this
- quartz.config.ts / quartz.layout.ts - superseded by quartz.config.yaml

The deploy workflow gains the mandatory `npx quartz plugin install` step and
a plugin cache keyed on quartz.lock.json, but is deliberately left on
workflow_dispatch only. GitHub Pages has a single production deployment, so
a push trigger here would immediately replace the live v4 site; the push
trigger is commented out until cutover.

Verified: build succeeds, 194 files parsed, 360 pages emitted, no content
from ignored paths leaked into the output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:15:05 -04:00

6.4 KiB

Inline Comments Worker

The one serverless piece behind Quartz inline comments. It does three things and holds the two secrets that can't live in the browser:

Route Purpose
GET /api/auth/login Redirect to GitHub's authorize URL
GET /api/auth/callback Exchange code → user session, postMessage it back to the opener
POST /api/auth/refresh Exchange a refresh token for a fresh user token
GET /api/comments Anonymous read proxy (server token) so logged-out visitors see highlights

Writes never touch this worker — the browser posts comments straight to GitHub's GraphQL API with the signed-in user's own token.

Auth is a GitHub App, not an OAuth App. That matters: an OAuth App would have to request the public_repo scope, which grants write access to every public repo the commenter owns. A GitHub App's permissions are fixed by the App definition, so commenters grant only Discussions: write on this one repo. No scope is sent on the authorize URL as a result.

Order matters. The App's callback URL must contain the worker's URL, and the worker's URL doesn't exist until it's deployed — so deploy first. Deploying without secrets is fine; those endpoints simply error until you add them.

1. Deploy the worker (Cloudflare Workers)

cd serverless/inline-comments-worker
npm install
npx wrangler login     # first run also prompts you to pick your workers.dev subdomain
npx wrangler deploy    # prints the URL

The printed URL is https://<name>.<your-subdomain>.workers.dev, where <name> is name in wrangler.toml. For this repo it is:

https://inline-comments.inline-assembly.workers.dev

2. Create a GitHub App

Create it under the logos-co org so ownership isn't tied to one person:

https://github.com/organizations/logos-co/settings/appsNew GitHub App

Setting Value
GitHub App name Assembly Inline Comments
Homepage URL https://logos-co.github.io/assembly/
Callback URL https://inline-comments.inline-assembly.workers.dev/api/auth/callback
Request user authorization (OAuth) during installation check this
Expire user authorization tokens leave checked (see below)
Webhook → Active uncheck — we don't use webhooks
Repository permissions → Discussions Read and write
Where can this GitHub App be installed? Only on this account

Everything else can stay at its default. Then:

  1. Create GitHub App.
  2. Copy the Client ID (Iv23li…) and Generate a new client secret — copy it immediately, it's shown once.
  3. Install App → install it on logos-co/assembly (choosing "Only select repositories" and picking just that repo). Without this install step the App can't touch the repo's Discussions.

Add a second callback URL on the same App for local dev — http://localhost:8787/api/auth/callback. Unlike OAuth Apps, a GitHub App accepts multiple callback URLs, so one App covers both prod and dev.

On token expiration. With "Expire user authorization tokens" enabled, user tokens last 8 hours and come with a ~6-month refresh token; the client refreshes silently via POST /api/auth/refresh. If you disable expiration, GitHub omits those fields and the client treats the token as non-expiring — both paths work, so keep the secure default.

A GitHub App is not subject to the org's OAuth App access restrictions; it's governed by installation instead. That's why the previous "org must approve the OAuth App" step is gone — installing it (step 3) is the equivalent, and an org owner does it once.

3. Create the server read token

A fine-grained PAT (https://github.com/settings/tokens?type=beta) scoped to logos-co/assembly with Discussions: read. This lets anonymous visitors load existing comments. Store it as GITHUB_TOKEN.

4. Set secrets and origins

npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put GITHUB_TOKEN

Secrets apply immediately. ALLOWED_ORIGINS lives in wrangler.toml [vars] and requires a redeploy to take effect:

npx wrangler deploy

Local dev

cp .dev.vars.example .dev.vars   # fill in the three secrets (git-ignored)
npm run dev                      # serves on http://localhost:8787

ALLOWED_ORIGINS already includes http://localhost:8080 (Quartz's dev server).

5. Point Quartz at the worker

In quartz.layout.ts:

Component.InlineComments({
  provider: "github",
  options: {
    repo: "logos-co/assembly",
    repoId: "R_kgDOQUhKqA",
    category: "Announcements",
    categoryId: "DIC_kwDOQUhKqM4Cxur2",
    apiBase: "https://inline-comments.inline-assembly.workers.dev",
    mapping: "url", // must match giscus's mapping to share a discussion
  },
})

If apiBase is empty the component no-ops, so it's safe to land before the worker exists.

Notes / limits

  • Comments + replies are fetched 100-at-a-time (no pagination yet).
  • mapping must match on read and write. To share the same discussion as the existing giscus widget, use the mapping giscus is configured with.
  • Ports to Vercel/Netlify functions are straightforward — the handler is a single fetch(request, env); only the deploy wrapper changes.