# 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.yml` — `workflow_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 70–88.
  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 107–109.
  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`)

```css
--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.
