Cachety is a static export (out/). It can be hosted anywhere that serves files. Two free options fit well:
| Cloudflare Pages (recommended, configured) | GitHub Pages | |
|---|---|---|
| Price | Free tier: unlimited bandwidth and requests, 500 builds/month | Free for public repositories |
| Custom domain + HTTPS | Yes | Yes |
HTTP security headers (_headers) |
Yes: per-page CSP, HSTS, frame-ancestors, COOP/COEP, Permissions-Policy |
No: GitHub Pages ignores _headers |
Network lock (CSP connect-src 'self') |
Header and <meta> |
<meta> only (still enforced) |
Clean URLs (/fr → fr.html) |
Yes | Yes |
Recommendation: Cloudflare Pages. The <meta> CSP keeps the "nothing can be uploaded" guarantee on both hosts. But only Cloudflare applies the headers that are impossible in a <meta> tag: clickjacking protection (frame-ancestors), HSTS, cross-origin isolation (COOP/COEP/CORP) and Permissions-Policy. The privacy page also names Cloudflare as the host.
.github/workflows/ci.yml runs on every push and pull request:
testjob: dependency audit, unit tests, static export, export security tests, then the Chromium browser suite. On a push tomain, it uploads the testedout/as an artifact.browsersjob: the same browser suite on WebKit (blocking) and Firefox (informational).deployjob: only on a push tomain, and only iftestandbrowserspassed. It downloads the exactout/that was tested and publishes it with Wrangler, which is pinned inbun.lock.
Pull requests and the weekly audit run never deploy.
Manual publish. To force a deploy without a new commit (for example after adding the secrets, or after a Cloudflare setting change): GitHub → Actions → CI → Run workflow, pick the main branch, keep Publish checked, and run. The full test suite runs first, as for a push; untick Publish to only run the tests. Manual runs on other branches never deploy.
-
Create the Pages project (once, from your machine):
bunx wrangler login bunx wrangler pages project create cachety --production-branch main
Do not connect the project to Git in the Cloudflare dashboard. GitHub Actions does the deploying, after the tests.
-
Create an API token: Cloudflare dashboard → My Profile → API Tokens → Create Token → Custom token.
- Permissions: Account → Cloudflare Pages → Edit. Nothing else.
- Account resources: your account only.
-
Add two repository secrets: GitHub → Settings → Secrets and variables → Actions.
CLOUDFLARE_API_TOKEN: the token from step 2.CLOUDFLARE_ACCOUNT_ID: shown in the right-hand sidebar of the Cloudflare dashboard (Workers & Pages overview).
-
Optional but recommended: GitHub → Settings → Environments →
production. Restrict deployments to themainbranch, and add yourself as a required reviewer if you want to approve each release. -
Custom domain: Cloudflare → Workers & Pages →
cachety→ Custom domains → addcachety.com, thenwww.cachety.com. If the domain's DNS is already on Cloudflare, the records are created for you. Then redirectwwwto the apex: Rules → Redirect Rules →www.cachety.com/*→https://cachety.com/${1}, 301. Do not skip it: without it,wwwserves a second copy of the site (kept out of search by anX-Robots-Tag: noindexrule in_headers, as iscachety.pages.dev), and in Google Search Console use a Domain property (cachety.com) or thehttps://cachety.com/prefix, nothttps://www.cachety.com/, or the sitemap is rejected because its URLs are on another host. -
Keep it tracker-free: do not enable Web Analytics or Zaraz on the zone or the Pages project. They inject a third-party script, which the CSP would block, and they would contradict the privacy page. Leave Email obfuscation and Rocket Loader off too, since they rewrite HTML and break the CSP hashes.
From then on, every push to main that passes the tests is live within a few minutes.
bun run deploy # build, then wrangler pages deploycurl -sI https://cachety.com/en | grep -i -E "content-security|strict-transport|x-frame"The response should show a CSP with sha256 hashes, HSTS and X-Frame-Options: DENY.
_headers allows at most 100 rules and 2,000 characters per line. The build groups the CSP rules by language: one rule per language for every page under /fr/, /ja/… plus one exact rule for each home page (66 rules for 333 pages in 11 languages on 2026-10-17, including the shared /embed/* rule of the partner embed; de, es, id and pt already use the finer grouping below, 9 rules each). If a language's rule would exceed the line limit, that language automatically falls back to one rule per guide or comparison section and one per other page. The build fails if either limit is still exceeded, so you will know before deploying. A new language adds 2 rules; new pages add none until their language falls back to the finer grouping (about 7 more rules per language). With 7 languages still grouped, that leaves room for every language to fall back only if the finer grouping gets coarser first: watch the count the build prints.
GitHub Pages works, with the header limitations described above. Replace the deploy job in .github/workflows/ci.yml with the following (SHAs pinned as of 2026-10):
deploy:
name: Deploy (GitHub Pages)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [test, browsers]
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: site
path: out/
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: out/
- id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1Then: GitHub → Settings → Pages → Source: GitHub Actions, and add cachety.com as the custom domain (DNS: A/AAAA records to GitHub Pages, or a CNAME for www).
Serve out/ with "try $uri, $uri.html, $uri/index.html" resolution. Reproduce the headers from out/_headers: the /* block applies to everything, and each path-specific block adds that page's CSP. The <meta> CSP in each page already keeps the network lock, so the headers mainly add frame-ancestors, HSTS and cross-origin isolation.