Files
cameleer-website/docs/superpowers/specs/2026-04-25-under-construction-placeholder-design.md
hsiegeln 9b0c36b5e0 docs(spec): add design for under-construction placeholder
Branded "back shortly" page deployed via a new manual-trigger
deploy-placeholder.yml workflow. Standalone HTML + two PNG
siblings, no Astro build dependency, shares the deploy-production
concurrency group with deploy.yml so the two can never race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:57:41 +02:00

9.2 KiB
Raw Blame History

Under-construction placeholder — design

Date: 2026-04-25 Status: approved (pending user spec review)

1. Purpose

A branded "back shortly" page for cameleer.io that the operator can swap into the live origin on demand from Gitea Actions. Used during planned maintenance, incident fallback, or any moment the real site needs to come down without leaving visitors on a broken page.

2. Constraints & non-goals

  • Same target as the real site. Deploys to the same Hetzner Webhosting L docroot used by deploy.yml. Replaces the live index.html and assets at the docroot root.
  • Must work when the main build is broken. This is the worst-case in which a placeholder is needed. Therefore the placeholder MUST NOT depend on npm ci, astro build, or any other step that could fail along with the main site's build.
  • Manual trigger only. Same pattern as deploy.ymlworkflow_dispatch from the Gitea UI. No push/auto-deploy.
  • Cannot race the main deploy. Both workflows write to the same docroot via rsync --delete; concurrent runs would clobber each other.
  • Recovery is the regular deploy. Triggering deploy.yml on any main commit restores the site. No bespoke "un-placeholder" workflow.
  • Origin-side headers are not in scope. Hetzner Webhosting L runs AllowOverride None; all response headers are owned by Cloudflare Transform Rules (see OPERATOR-CHECKLIST.md §2). The placeholder workflow does NOT need to assert HSTS/CSP/XFO — those headers are origin-agnostic.

Out of scope:

  • Per-environment placeholder variants (staging, etc.). Same target, same content.
  • A status page, ETA, or live incident feed.
  • Cookie banner, analytics, or any third-party JS.

3. Architecture

cameleer-website/
├── placeholder/
│   ├── index.html              # standalone HTML, inlined CSS
│   ├── cameleer-logo.png       # copy of public/icons/cameleer-192.png (~36 KB)
│   └── favicon.png             # copy of public/icons/cameleer-32.png (~2.4 KB)
├── .gitea/workflows/
│   ├── ci.yml                  # unchanged
│   ├── deploy.yml              # unchanged
│   └── deploy-placeholder.yml  # NEW
└── README.md                   # NEW section: "Placeholder mode"

Why standalone HTML, not Astro

The placeholder lives outside src/, is not picked up by astro build, and never enters dist/. It is a single self-contained index.html with inlined CSS. Two PNG assets ship alongside the HTML in placeholder/ and are referenced by relative paths (./cameleer-logo.png, ./favicon.png) so the page renders correctly after rsync --delete clears the docroot. The repo's full-resolution public/cameleer-logo.svg is 1.5 MB (embedded raster data) and is therefore not used here; the 192 px PNG is the right size and weight for a placeholder hero.

Trade-off accepted: brand tokens (colors, fonts) are hand-mirrored from tailwind.config.mjs rather than imported. If those tokens change, the placeholder may visibly drift. Acceptable because (a) the placeholder is rarely shown, (b) the file is short enough to re-sync in two minutes, (c) the alternative — coupling the placeholder to the Astro build — defeats the placeholder's whole reason for existing.

Workflow shape

deploy-placeholder.yml:

  • Trigger: workflow_dispatch only.
  • Concurrency: group: deploy-production, cancel-in-progress: false — same group as deploy.yml. Gitea will queue, never overlap.
  • Runner: ubuntu-latest (matches deploy.yml).
  • Secrets used: SFTP_HOST, SFTP_USER, SFTP_PATH, SFTP_KEY, SFTP_KNOWN_HOSTS, PUBLIC_SALES_EMAIL.
  • Steps:
    1. actions/checkout@v4
    2. Configure SSH (key + known_hosts; install rsync/openssh if missing) — same logic as deploy.yml lines 7088.
    3. Inject PUBLIC_SALES_EMAIL into the placeholder. The HTML contains a single literal token __SALES_EMAIL__ (no hyphens, no other instances anywhere); sed -i "s|__SALES_EMAIL__|$PUBLIC_SALES_EMAIL|g" placeholder/index.html. Fail loudly with : "${PUBLIC_SALES_EMAIL:?...}" first. Verify replacement by grepping that the token no longer appears.
    4. rsync -avz --delete --rsync-path=/usr/bin/rsync over ssh -p 222 of placeholder/$SFTP_PATH/ — same flags and SSH options as deploy.yml lines 107109.
    5. Smoke test: curl -s https://www.cameleer.io/ | grep -q 'Routes are remapping' — placeholder-unique sentinel. Fail the workflow if absent. (Skip the security-headers grep from deploy.yml; those headers come from Cloudflare and apply equally to placeholder responses, so they're already covered.)

Why rsync --delete

Matches deploy.yml behaviour. The docroot reflects exactly what the placeholder ships, with no leftover assets from a previous real-site deploy lingering and being indexed.

4. Placeholder content

Markup outline

Single <!doctype html> document. Sections, in order:

  1. <head>:

    • <title>Cameleer — Back shortly</title>
    • <meta name="description" content="Cameleer is briefly offline. We'll be back on the trail in a moment.">
    • <meta name="robots" content="noindex">
    • <meta name="color-scheme" content="dark"> and <meta name="theme-color" content="#060a13"> (matches BaseLayout.astro)
    • <link rel="icon" type="image/png" sizes="32x32" href="./favicon.png"> — the 32 px PNG that ships in placeholder/. Relative path so it resolves against the docroot root after rsync.
    • Google Fonts <link> for DM Sans (400, 700) and JetBrains Mono (400). Single preconnect.
    • Inlined <style> block with the design tokens below.
  2. <body>:

    • Centered <main> (flex, full viewport height, items/justify center).
    • <img src="./cameleer-logo.png" alt="Cameleer" width="96" height="96"> — references the sibling PNG that ships in placeholder/. Relative path so it resolves against the docroot root regardless of what rsync --delete cleared.
    • Eyebrow: <p> with ✦ Routes are remapping. — italic, accent color, small.
    • Heading: <h1> with We're back on the trail<br>in a moment. — display size, tight tracking. Two-line cadence echoes the live hero's "Ship Camel integrations. Sleep through the night."
    • Subhead: lifted verbatim from src/components/sections/Hero.astro line 42 — Cameleer is the hosted runtime and observability platform for Apache Camel — auto-traced, replay-ready, cross-service correlated. The 3 AM page becomes a 30-second answer.
    • Mono microcopy: <p> with cameleer.io · <a href="mailto:__SALES_EMAIL__">__SALES_EMAIL__</a> — JetBrains Mono, faint color. The token is replaced at deploy time.

Design tokens (mirrored from tailwind.config.mjs)

--bg:           #060a13;
--bg-elevated:  #0c111a;
--border:       #1e2535;
--accent:       #f0b429;
--text:         #e8eaed;
--text-muted:   #9aa3b2;
--text-faint:   #828b9b;

Background: solid --bg with a single radial-gradient(60% 60% at 50% 50%, rgba(240,180,41,0.10), transparent 70%) overlay to echo the hero's amber glow. No topographic SVG — too much weight for a fallback page.

Typography:

  • Eyebrow: DM Sans italic, 14px, --accent, letter-spacing 0.
  • H1: DM Sans 700, clamp(2.25rem, 4.5vw, 4rem), line-height 1.05, letter-spacing: -0.02em — same numbers as the hero .hero-h1 rule.
  • Subhead: DM Sans 400, 1.125rem, --text-muted, max-width ~42rem (matches maxWidth.prose).
  • Microcopy: JetBrains Mono 400, 12px, --text-faint. Underline on hover only.

@media (prefers-reduced-motion: reduce) is not relevant because the page has no animations.

File size budget

Target ≤ 6 KB for index.html itself (markup + inlined CSS, no inlined image data). The two PNG siblings (cameleer-logo.png ~36 KB, favicon.png ~2.4 KB) ship as separate files. No JS, no external CSS, no fonts other than the Google Fonts CSS link (the actual font files are fetched lazily by the browser).

5. README update

Append a "Placeholder mode" section under "Deployment":

Placeholder mode. To put the site into "back shortly" mode, trigger Actions → deploy-placeholder → Run workflow. To bring the real site back, trigger Actions → deploy → Run workflow on the desired main commit. Because both workflows share the deploy-production concurrency group, they can never run simultaneously.

6. Verification

After implementation:

  1. Local visual check: open placeholder/index.html in a browser (the __SALES_EMAIL__ token will be visible, that is expected) and confirm centered layout, brand colors, logo render, and copy render correctly at 360px / 768px / 1440px viewport widths.
  2. Run a dry rsync against an alternate path (e.g. a throwaway docroot folder) before flipping cameleer.io.
  3. First real run: trigger deploy-placeholder, confirm sales email substituted (curl -s https://www.cameleer.io/ | grep -F 'mailto:'), confirm sentinel string present, confirm curl -sI https://www.cameleer.io/cameleer-logo.png returns HTTP 200. Then trigger deploy.yml to restore.

7. Open questions

None. All clarifying questions answered during brainstorming:

  • Same target as real site (Hetzner cameleer.io docroot).
  • Branded teaser using existing hero subhead.
  • Contact line uses PUBLIC_SALES_EMAIL secret.
  • Smoke test grep is in.