Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 15 additions & 27 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,8 @@ This repo is the **realunit.app** website — public, static. See the
`public/` ships verbatim to Cloudflare Pages — what you commit is what gets
served. The one exception is the invite/promo HTML: `functions/_middleware.js`
rewrites those bytes on the way out so crawlers see the code in
`apple-itunes-app`, `og:*` and the App Links before any script runs. It also
reports a rewritten landing as `200`: Pages resolves `/invite/<code>` to the
code-less shell through the `_redirects` rewrite but keeps the not-found
status of the path that was asked for, and a crawler drops a `404` before it
reads the tags. The promotion is guarded on the two landing marks — `id="state-loading"` and `aria-busy="true"`, which both landings carry together in one tag and no other page the site ships carries together — so the site's
own 404 page keeps saying 404, and HEAD answers with the same status as GET.
Both methods are resolved internally as one full GET, without `Range` /
`If-Range` and without the conditional request headers, because the whole document
is rewritten and the status is decided from its body. The conditional request headers are dropped rather than evaluated, which is a deliberate deviation from RFC 9110 §13.1: a rewritten landing emits no validator to condition on, so the answer is always the current representation and never a 304 or a 412. A pass-through answer keeps the origin's own headers, validator included. `scripts/dev-server.mjs` shares the injection and answers HEAD without a body, but has no promotion, no marker guard and no header stripping: its own routing serves the landings as `200` and never produces the not-found status the promotion exists to correct. Nothing else is transformed, and there is no server-side
rendering. The dev dependencies exist **only** for the quality gates below
`apple-itunes-app`, `og:*` and the App Links before any script runs. Nothing
else is transformed, and there is no server-side rendering. The dev dependencies exist **only** for the quality gates below
(formatting, HTML validation, unit tests, screenshots); nothing compiles or
bundles the site.
- **Invite/promo HTML rewrite is banner, canonical, and store handoff.** Safari,
Expand Down Expand Up @@ -53,9 +45,8 @@ This repo is the **realunit.app** website — public, static. See the
adding any other host to that allowlist needs a reason in the PR.
- Inline `style="…"` attributes and `<style>` blocks are fine (`style-src`
allows `'unsafe-inline'`).
- **Put the reusable, side-effect-free JS in `public/js/lib/`.** It carries a
unit-coverage gate (see below), as do `functions/lib/**` and the Function
entry point `functions/_middleware.js`; DOM/network glue stays in the
- **Put the reusable, side-effect-free JS in `public/js/lib/`.** That is the only
code with a unit-coverage gate (see below); DOM/network glue stays in the
page-level scripts and is covered by the Playwright suite.
- **Don't put mutable files under `public/assets/`.** That path has an immutable,
one-year cache header — only content-hashed or otherwise stable-named assets
Expand All @@ -82,14 +73,14 @@ sanity-check anything touching scripts/images in the dev deployment.
Every pull request must pass the gates below; CI runs them as required status
checks.

| Gate | Command | What it enforces |
| ----------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Formatting | `npm run format:check` | Prettier formatting of the maintained code (the hand-written HTML pages are validated, not reformatted) |
| HTML validity | `npm run validate:html` | Valid markup on every page under `public/` |
| Site completeness | `npm run check:site` | Every `<html lang>`, every internal link/asset resolves, and each glue script loads its `js/lib` core first |
| Unit coverage | `npm run test:coverage` | 100% line/branch/function/statement coverage of `public/js/lib/**` and `functions/_middleware.js`; `functions/lib/**` at its ratchet in `vitest.config.mjs` |
| Functional | `npm run test:e2e` | Playwright smoke + behavior suite (every page loads, platform detection, the full confirm flow) |
| Visual regression | `npm run e2e:docker` | Every view in the visual matrix (page × viewport × language × state) matches its committed baseline, then `check:visual` |
| Gate | Command | What it enforces |
| ----------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Formatting | `npm run format:check` | Prettier formatting of the maintained code (the hand-written HTML pages are validated, not reformatted) |
| HTML validity | `npm run validate:html` | Valid markup on every page under `public/` |
| Site completeness | `npm run check:site` | Every `<html lang>`, every internal link/asset resolves, and each glue script loads its `js/lib` core first |
| Unit coverage | `npm run test:coverage` | 100% line/branch/function/statement coverage of the extracted browser logic (`public/js/lib/**`) |
| Functional | `npm run test:e2e` | Playwright smoke + behavior suite (every page loads, platform detection, the full confirm flow) |
| Visual regression | `npm run e2e:docker` | Every view in the visual matrix (page × viewport × language × state) matches its committed baseline, then `check:visual` |

`npm run check` runs the first four locally in one go. The Playwright suites run
against a local dev server (`test:e2e`); the visual gate runs in a pinned
Expand All @@ -110,12 +101,9 @@ The shipped page scripts (`public/platform.js`,
than chase 100% coverage through the DOM, the **pure** logic — platform
detection, language resolution, API-base derivation, response→state mapping, and
the i18n copy — is extracted into `public/js/lib/` (side-effect free, exposed on
a `window.*` global) and unit-tested to 100% with Vitest + jsdom. The rest of
those page scripts is covered end-to-end by the Playwright functional suite
(`tests/behavior.spec.mjs`). Two more surfaces carry their own unit-coverage
gate and are not page scripts at all: `functions/lib/**` at the ratchet set in
`vitest.config.mjs`, and the Pages Function entry point
`functions/_middleware.js` at 100% — see the quality-gates table above.
a `window.*` global) and unit-tested to 100% with Vitest + jsdom. Everything else
is covered end-to-end by the Playwright functional suite
(`tests/behavior.spec.mjs`).

If you add a file under `public/js/lib/`, it must reach 100% coverage or the
Quality gate fails (the threshold reports every matched file, tested or not). A
Expand Down
16 changes: 2 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,7 @@ uploaded to Cloudflare Pages.
custom scheme; `twitter:app:country` is CH) are injected
into the HTML bytes from the request URL (`functions/_middleware.js` on
Cloudflare Pages, and the local dev-server) so Safari, Play, WhatsApp, X,
and share crawlers can snapshot them before JS. On Cloudflare Pages, and only
there, `functions/_middleware.js` also reports a rewritten landing as `200`:
Pages resolves `/invite/<code>` to the code-less shell through the
`_redirects` rewrite but keeps the not-found status of the path that was asked
for, and a crawler drops a `404` before it reads the tags. The promotion is
guarded on two marks the landings carry and the site's 404 page does not, and
HEAD answers with the same status as GET. Both methods are resolved internally
as one full GET, without `Range` / `If-Range` and without the conditional
request headers, because the whole document is rewritten and the status is decided
from its body. The conditional request headers are dropped rather than evaluated, which is a deliberate deviation from RFC 9110 §13.1: a rewritten landing emits no validator to condition on, so the answer is always the current representation and never a 304 or a 412. A pass-through answer keeps the origin's own headers, validator included. `scripts/dev-server.mjs` shares the injection and answers HEAD without a body, but has no promotion, no marker guard and no header stripping: its own routing serves the landings as `200` and never produces the not-found status the promotion exists to correct.
`og:title`, `og:description`,
and share crawlers can snapshot them before JS. `og:title`, `og:description`,
and image alt name the campaign code; `?lang=en` sets English copy and `og:locale=en_GB`;
invitee names wait for lookup JS. `/js/invite-banner.js` in `<head>`
is the CSP-safe JS fallback — Cloudflare Pages CSP blocks inline `<script>`.
Expand Down Expand Up @@ -102,9 +92,7 @@ From v2 a build toolchain (Astro) is introduced; the plain-image landing stays t
## Testing

The site still ships verbatim — the tooling is dev-only. Pure browser logic lives
in `public/js/lib/**` and is unit-tested to 100% (Vitest + jsdom), as is the
Pages Function entry point `functions/_middleware.js`; `functions/lib/**` is
measured at its ratchet in `vitest.config.mjs`. The pages,
in `public/js/lib/**` and is unit-tested to 100% (Vitest + jsdom); the pages,
platform detection and the full confirm flow are covered by Playwright
(functional + screenshot regression). See [CONTRIBUTING](CONTRIBUTING.md#quality-gates)
for the gate list and commands (`npm run check`, `npm run test:e2e`,
Expand Down
Loading
Loading