This repo is the realunit.app website — public, static. See the
README for the architecture and the list of files under public/.
- No build toolchain for the site. Plain HTML/CSS/JS only. Everything in
public/ships verbatim to Cloudflare Pages — what you commit is what gets served. The one exception is the invite/promo HTML:functions/_middleware.jsrewrites those bytes on the way out so crawlers see the code inapple-itunes-app,og:*and the App Links before any script runs. The landing for a code-bearing path is served by that Function too, not by_redirects:_routes.jsonhands/invite,/invite/*,/promoand/promo/*to it first, so the 200-rewrites never run and the asset lookup behindcontext.next()answers the path as asked —404, with the site's own 404 page. The platform is therefore asked first and only its404is replaced: the shell is read from the asset binding by name (env.ASSETS.fetch), checked against the two landing marksid="state-loading"andaria-busy="true", injected, and answered200, forHEADas well asGET. Every other answer stands —/invite/and/promo/are real files served200, rewritten in place for aGET, and/inviteand/invite/index.htmlkeep the308that canonicalises them. AHEADcarries no body to recognise the shell in, so it reads the shell from the binding like the404case does and answers as theGETwould, without a body. Without the binding, or when the file under that name is not the shell, the platform's answer stands rather than an invented one. 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,
Play, and share crawlers snapshot
apple-itunes-app,og:url,rel=canonical,twitter:url,og:title,twitter:title,og:description,twitter:description,og:image:alt,twitter:image:alt,og:locale,og:site_name, htmllang, the Play referrer, android-app / ios-app alternate links, Facebook App Links (al:ios:url/al:android:urlarerealunit-wallet://…;al:android:classisswiss.realunit.app.MainActivity;al:web:urlis the HTTPS landing), and Twitter App Cardtwitter:app:url:iphone/twitter:app:url:ipad/twitter:app:url:googleplay(same custom scheme) from the HTML bytes before/js/invite-banner.js/invite.jsrun, sofunctions/_middleware.js(repo-rootfunctions/, picked up bywrangler pages deploy public) andscripts/dev-server.mjsinject those from the request URL (www folded onto the apex). The campaign code is inog:title/twitter:title/og:description/og:image:alt/twitter:image:alt;?lang=ensets English title/description/alt andog:locale; invitee names wait for lookup JS. The committedpublic/inviteandpublic/promoHTML stay generic (app-idonly, og:url / twitter:url/invite/or/promo/,og:site_nameRealUnit, generic titles and descriptions, Play href without referrer, noal:*, notwitter:app:*,format-detectiontelephone=no, date=noso iOS does not turn the code or Aktionstext date into a link,x-apple-data-detectors="false"on#ok-code/#ok-bodybecause Safari re-scans JS-inserted text, no inline<script>). Do not add other Pages Functions or server-side rendering. - Keep the page self-contained.
public/_headerssets a strict CSP:- No inline
<script>and no third-party resources (scripts, styles, images, fonts). Load JS from same-origin files instead. The single permitted network call is the code lookup against the DFX API, which is whyconnect-srcnamesapi.dfx.swissanddev.api.dfx.swissexplicitly — adding any other host to that allowlist needs a reason in the PR. - Inline
style="…"attributes and<style>blocks are fine (style-srcallows'unsafe-inline').
- No inline
- Put the reusable, side-effect-free JS in
public/js/lib/. That is the only browser code with a unit-coverage gate (see below);functions/_middleware.jsandfunctions/lib/**have their own. 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 belong there. If a file's bytes may change under the same name, keep it out. - Public repo — never commit secrets or personal data.
- Node 22 (
engines.node >= 22) - Docker — only for the visual regression gate
npm install
npm run serve # preview at http://127.0.0.1:4173 (Cloudflare-Pages-like routing)npm run serve mirrors the Cloudflare Pages routing (serves public/ at the
root, custom 404). Note that _headers — CSP, cache-control — is applied by
Cloudflare Pages only, so CSP violations do not show up locally;
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 the extracted browser logic (public/js/lib/**) and of functions/_middleware.js; functions/lib/** is ratcheted at the level it reaches today |
| 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
container (see below).
- Quality (
.github/workflows/quality.yml) — the first four gates. - Screenshots (
.github/workflows/visual.yml) — the visual gate, in the pinned Playwright container.
Both run on every pull request and on pushes to develop/main.
The shipped page scripts (public/platform.js,
public/confirm-aktionariat/confirm.js) are classic, DOM-coupled IIFEs. Rather
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. 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
glue script must load its core first — check:site enforces the ordering.
Screenshots are only reproducible when they render against the exact browsers the baselines were generated with. Therefore:
- Baselines are generated and compared inside the pinned container
(
mcr.microsoft.com/playwright:v<version>-noble). @playwright/testis pinned to an exact version equal to that image tag. A guard step invisual.ymlfails the build if the two drift apart.- Never generate baselines on macOS/Windows — they would not match the Linux CI render.
npm run e2e:docker # run the suite + compare against baselines + check:visual
npm run e2e:docker:update # regenerate baselines after an intentional UI changeThe visual matrix lives in tests/pages.mjs (VIEWS), which is the single
source of truth — do not maintain a second list here. It currently covers six
families: the invite and promo landings (each in their loading, resolved,
invalid, missing-code, platform-matched and JS-less variants), the confirm-page states,
the account-merge pages, the home landing in its equal-badge and
platform-matched layouts, and the 404 page — across desktop-chromium,
tablet-chromium and mobile-safari. check:visual enforces that every view × applicable viewport
has exactly one committed baseline, nothing is orphaned, and the report ran them
all. When you intentionally change a page's look, run e2e:docker:update and
commit the updated PNGs under tests/__screenshots__/.
- Feature branch → PR into
develop(the default branch). - Merging to
developauto-deploys to dev.realunit.app. - The
develop → mainrelease PR is opened automatically — no need to create it. - Merging that PR to
maindeploys production, realunit.app.
- Small, focused diffs.
- English commit messages and code comments; German for user-facing page text.
- Run
npm run checkand the Playwright suites before requesting review. - Confirm no CSP violations in the dev deployment for anything touching scripts, images, or external resources.