Skip to main content

Cloudflare

This site runs on Cloudflare as a Void app. Void builds one Worker that serves the Docusaurus output as static assets, plus the site's API routes and Durable Objects. The Worker is its own package, packages/mochinori-website-worker/ (@mochinori/website-worker), separate from the Docusaurus site in packages/mochinori-website/. Void treats any package that depends on @docusaurus/core as a static site and refuses to deploy it with a backend, even with inference.appType: 'void' (true of Void 0.22.0).

In packages/mochinori-website-worker/:

  • void.config.ts sets the Worker name and compatibility date, and serves it at mochinori.com (a custom domain, so the Worker must deploy to the Cloudflare account that holds the mochinori.com zone) as well as on workers.dev. The account comes from CLOUDFLARE_ACCOUNT_ID or your Void sign-in, not from the repo. It also tells Void to serve Docusaurus's own 404.html for unknown URLs.
  • vite.config.ts adds voidPlugin() and uses ../mochinori-website/build (the Docusaurus output) as the static assets. void deploy only runs vite build, so the Docusaurus site has to be built first. The build and deploy scripts do that, and vite build stops with an error when there is no site build to serve.
  • routes/ holds API routes. Each file exports Hono handlers per method, wrapped in defineHandler, and [name] is a path parameter. routes/api/health.ts serves GET /api/health, and routes/api/counter/[name].ts serves GET and POST /api/counter/<name>.
  • durable-objects/ holds Durable Objects. Each module default-exports a defineDurableState definition, and Void derives the binding and class from the filename. counter.ts becomes the COUNTER binding and the CounterDurableObject class. Routes call it with Counter.get(name).increment(1).
  • routes/api/images/ stores demo image uploads in R2 (the STORAGE binding, bucket mochinori-demo-uploads). POST /api/images checks the file's magic bytes (PNG, JPEG, GIF, WebP or AVIF; never SVG), caps it at 5 MB and stores it under a random name. Posting JSON {"url": ...} instead mirrors a remote image: the Worker fetches it (public http(s) hosts only, each redirect checked the same way, 10 second timeout) and applies the same checks. GET /api/images/<name> serves it with nosniff, a sandboxing CSP and same-origin CORP. Uploads expire after an hour: reads of an expired object delete it, and crons/expire-demo-images.ts sweeps the bucket every 15 minutes. The rules live in server/imageUploads.ts, which has unit tests.
  • void.lock.json records the generated bindings and Durable Object migrations. Commit it whenever it changes: Cloudflare migrations are append-only, and the lock keeps their order.

Run it locally​

pnpm run dev:website

This builds the site, then serves it with the API routes and Durable Objects in Miniflare at http://localhost:5173. Use pnpm run start:website instead for live-reloading docs work; it runs Docusaurus alone, without the API.

To check a production build:

pnpm run build-website
pnpm --filter @mochinori/website-worker run preview

Deploy​

Every push to main deploys the site through the Deploy workflow. You can also run that workflow from the Actions tab, or deploy from your machine:

pnpm run deploy:website

The script builds the Docusaurus site, then runs void deploy --platform cloudflare, which creates the mochinori-website Worker on the first deploy and updates it afterwards. Without --platform, Void offers to link an existing Worker, and that fails when the Worker doesn't exist yet. If an earlier run saved such a link, delete packages/mochinori-website-worker/.void/cloudflare-link.json and deploy again.

The first local deploy signs you in to Cloudflare and saves the account in packages/mochinori-website-worker/.void/project.json. The workflow needs the CLOUDFLARE_API_TOKEN secret, from a token that can edit Workers Scripts and Workers R2 Storage. Set CLOUDFLARE_ACCOUNT_ID too when the token can reach more than one account, and CLOUDFLARE_WORKERS_SUBDOMAIN (a variable or a secret) to the account's workers.dev subdomain (the part before .workers.dev).

Pull request previews​

The Preview workflow deploys each pull request as its own Worker, mochinori-website-pr-<number>, and comments its URL on the pull request: https://mochinori-website-pr-<number>.<subdomain>.workers.dev. Each push redeploys it, and closing the pull request deletes it. It uses the same secrets as the Deploy workflow, and skips pull requests from forks, which get no secrets.

A preview is a separate Worker rather than a preview version of mochinori-website, because Cloudflare gives no preview URL to versions of a Worker that defines Durable Objects. So each preview has its own Durable Objects and applies its own migrations. It shares production's R2 bucket (the workflow creates any bucket the pull request adds) and has no cron triggers; production's cron expires uploads for both.

Add a Cloudflare feature​

  • API route: add a file under routes/. Routes under /api reach the Worker before static assets.
  • Durable Object: add a module under durable-objects/, run pnpm --filter @mochinori/website-worker run prepare to regenerate types, and commit the updated void.lock.json.
  • KV, R2, D1, queues and crons are inferred from how the code uses them. See the Void docs for each one.