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>
9.2 KiB
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 liveindex.htmland 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_dispatchfrom 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.ymlon anymaincommit 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 (seeOPERATOR-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_dispatchonly. - Concurrency:
group: deploy-production,cancel-in-progress: false— same group asdeploy.yml. Gitea will queue, never overlap. - Runner:
ubuntu-latest(matchesdeploy.yml). - Secrets used:
SFTP_HOST,SFTP_USER,SFTP_PATH,SFTP_KEY,SFTP_KNOWN_HOSTS,PUBLIC_SALES_EMAIL. - Steps:
actions/checkout@v4- Configure SSH (key + known_hosts; install rsync/openssh if missing) — same logic as
deploy.ymllines 70–88. - Inject
PUBLIC_SALES_EMAILinto 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. rsync -avz --delete --rsync-path=/usr/bin/rsyncoverssh -p 222ofplaceholder/→$SFTP_PATH/— same flags and SSH options asdeploy.ymllines 107–109.- 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 fromdeploy.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:
-
<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">(matchesBaseLayout.astro)<link rel="icon" type="image/png" sizes="32x32" href="./favicon.png">— the 32 px PNG that ships inplaceholder/. 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.
-
<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 inplaceholder/. Relative path so it resolves against the docroot root regardless of whatrsync --deletecleared.- Eyebrow:
<p>with✦ Routes are remapping.— italic, accent color, small. - Heading:
<h1>withWe'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.astroline 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>withcameleer.io · <a href="mailto:__SALES_EMAIL__">__SALES_EMAIL__</a>— JetBrains Mono, faint color. The token is replaced at deploy time.
- Centered
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-h1rule. - Subhead: DM Sans 400, 1.125rem,
--text-muted, max-width ~42rem (matchesmaxWidth.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, triggerActions → deploy → Run workflowon the desiredmaincommit. Because both workflows share thedeploy-productionconcurrency group, they can never run simultaneously.
6. Verification
After implementation:
- Local visual check: open
placeholder/index.htmlin 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. - Run a dry rsync against an alternate path (e.g. a throwaway docroot folder) before flipping cameleer.io.
- First real run: trigger
deploy-placeholder, confirm sales email substituted (curl -s https://www.cameleer.io/ | grep -F 'mailto:'), confirm sentinel string present, confirmcurl -sI https://www.cameleer.io/cameleer-logo.pngreturns HTTP 200. Then triggerdeploy.ymlto 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_EMAILsecret. - Smoke test grep is in.