All three lines render in the DOM; CSS drives the fade via data-active.
Reduced-motion users see the first line only (no interval, no fade).
Rotation pauses on hover and keyboard focus. aria-live=off on the
rotator so AT does not announce every swap; aria-hidden flips per-swap
to avoid duplicate heading announcements.
Also set vite.build.assetsInlineLimit=0 in astro.config.mjs so Astro
emits the rotator script as a same-origin external file (dist/assets/)
rather than inlining it — required for CSP script-src 'self' compliance.
Camel + cameleer figure on compass rose, amber on transparent. Imported
from design-system/assets. Replaces the placeholder topographic-wave icon
across favicon chain, header, and OG assets (subsequent tasks).
13 tasks, each self-contained with exact file paths, final copy,
verification steps, and a commit at the end. Covers: logo + favicon
import (tasks 1-3), SEO meta (task 4), Hero static + rotating H1
(tasks 5-6), all five section copy rewrites (tasks 7-11), OG image
redesign (task 12), end-to-end verification (task 13). CSP-safe
rotation via Astro <script> bundling (script-src 'self' respected).
Hetzner Webhosting L runs Apache with AllowOverride None on the
user docroot, so file-based .htaccess is silently ignored — directives
in public/.htaccess never applied. Confirmed via direct-origin tests:
neither Header, Rewrite, nor FilesMatch fired regardless of the file
being present and readable.
The only origin-side override path on this tier is konsoleH's per-
directory Serverkonfiguration UI, which writes to a separate Apache
config file outside the user's filesystem (and thus outside any
deploy pipeline).
Make the architecture honest:
- Delete public/.htaccess (dead code Apache never reads).
- Remove the "Copy .htaccess into dist" CI step (now a no-op).
- Update deploy.yml header comment to point at Cloudflare for headers.
- Update OPERATOR-CHECKLIST.md §1 with the three Webhosting-L gotchas:
port 222 for SSH, SFTP_PATH must match the actual vhost docroot
(default is bare public_html/), and AllowOverride None.
- Update §5 to reflect manual workflow_dispatch (no auto-deploy on
push) and 5-header expectation.
- Update README.md deploy section likewise.
Headers (HSTS, CSP, XFO, X-Content-Type-Options, Referrer-Policy,
Permissions-Policy) are now owned by Cloudflare Transform Rules,
documented in OPERATOR-CHECKLIST.md §2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes:
1. Merge build and deploy jobs into a single 'deploy' job. This
eliminates the actions/upload-artifact@v3 round-trip, which was
silently stripping dotfiles (.htaccess) from the artifact and
leaving the deployed origin without security headers. The built
dist/ (including .htaccess) now flows directly into rsync in the
same workspace.
2. Remove the 'push: branches: [main]' trigger so deploy runs only
on workflow_dispatch (manual click in Gitea Actions UI).
Merges to main no longer auto-deploy — production promotion is
an explicit user action.
The concurrency group at workflow level still prevents overlapping
deploys. All secrets remain unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy public/.htaccess into dist after Astro build (Astro/Vite drops
dotfiles from public/ otherwise, leaving the origin without HSTS).
# Conflicts:
# .gitea/workflows/deploy.yml
Astro/Vite drops dotfiles from public/ during build, so .htaccess
never makes it into dist/. The deployed Apache origin then has no
header rules to apply, leaving the site without HSTS, X-Frame-Options,
Referrer-Policy, etc. — caught today by the post-deploy smoke test
("HSTS missing").
Copy the file explicitly after build. test -f makes the step fail
loudly if public/.htaccess goes missing, rather than silently
shipping a header-less site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hetzner Webhosting exposes SSH on TWO ports:
port 22 — SFTP only, refuses remote command exec
port 222 — full SSH with shell, supports rsync
Previous deploys hit "exec request failed on channel 0" because we
were using port 22. Switch back from lftp to plain rsync, but route
it through port 222 with --rsync-path=/usr/bin/rsync (Hetzner's
locked-down PATH doesn't include rsync by default) and BatchMode=yes
to disable interactive prompts.
Mirrors the working local command:
rsync -avz --rsync-path=/usr/bin/rsync \
-e "ssh -p 222 -i ~/.ssh/id_ed25519_gitea -o BatchMode=yes" \
./ apibny@www691.your-server.de:/usr/www/users/apibny/www.cameleer.io
Keeps host-key pinning (StrictHostKeyChecking + UserKnownHostsFile)
which the local command omits because the user's personal known_hosts
already trusts the host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues from the previous lftp run:
- "GetPass() failed -- assume anonymous login" / "Password required":
without `-u USER,` (trailing comma = empty password), lftp tries
to prompt for a password instead of relying on the ssh key passed
via sftp:connect-program.
- Heredoc body was indented with leading whitespace; lftp can mis-
parse leading-whitespace lines as command continuations.
Also bump verbosity (`debug 3`) so the ssh command lftp launches
is logged — makes the next failure easier to read — and bound
retries to 1 so we fail fast in CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hetzner Webhosting accepts SSH for file transfer but refuses remote
command exec, failing rsync with:
exec request failed on channel 0
rsync error: error in rsync protocol data stream (code 12)
rsync over SSH requires spawning a remote rsync binary, which isn't
possible on SFTP-only tiers. Switch the mirror to lftp, which speaks
SFTP end-to-end. Same semantics (upload + delete removed files), same
key + known_hosts pinning via sftp:connect-program.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in the CI infrastructure fixes:
- ci.yml: probe Chromium binary; fall back to Playwright (95977c8)
- tailwind: lift text-faint to meet WCAG AA contrast (2fde385)
- deploy.yml: pin artifact actions to v3 for Gitea (bbd68ec)
actions/upload-artifact@v4 and download-artifact@v4 use the
@actions/artifact v2+ client, which targets a github.com-only
backend and fails on Gitea / Forgejo / GHES with:
GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
and download-artifact@v4+ are not currently supported on GHES.
Pin both to v3, which uses the older artifact protocol that Gitea
Actions implements.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
text-faint #6b7280 on bg #060a13 measures ~4.06:1 contrast — under the
4.5:1 normal-text threshold — which fails Lighthouse's color-contrast
audit and drops the accessibility score to 0.90 on /pricing and
/privacy (the only pages currently using this token).
#828b9b yields ~5.66:1, clears AA with margin, and stays visually
distinct from text-muted (#9aa3b2, ~7.8:1) so the design hierarchy
between text / text-muted / text-faint is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Ubuntu runner image ships /usr/bin/chromium-browser as a snap
forwarder stub that exits with "install via snap" when invoked but
is found on PATH. The previous detection used `command -v` only, so
it accepted the stub, set CHROME_PATH to it, and Lighthouse later
failed to launch Chrome (ECONNREFUSED on the debug port).
Probe each candidate with `--version` to confirm it actually runs.
When no working system binary exists, install Playwright's bundled
Chromium (supports linux/arm64) with --with-deps for system libs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Switch ci.yml + deploy.yml env bindings from ${{ vars.* }} to
${{ secrets.* }}. Gitea lets you put non-sensitive Actions values in
either tab, and the secrets tab was used in practice — workflow was
reading the wrong context and getting empty strings.
- Broaden the "no TODO markers ship" guard to accept both TODO: and
legacy TBD: prefixes, matching the imprint/privacy page markers that
were recently renamed.
- Document the secret-vs-variable choice in OPERATOR-CHECKLIST so the
next operator doesn't get tripped up by the same thing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- .gitea/workflows/ci.yml: builds, tests, lints, and runs Lighthouse on
every push and PR to main. Runs on arm64 self-hosted Gitea runner.
- .gitea/workflows/deploy.yml: deploys to Hetzner on push to main or
manual workflow_dispatch from Gitea UI. No Lighthouse (that's CI's
job). Keeps the TBD-marker guard as a last-line safety check.
Both workflows live on the same concurrency group so no two deploys
race. On main push, CI and deploy run in parallel; CI is independent
and non-blocking for the deploy step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runner: self-hosted arm64. Deploy target: amd64 (Hetzner). Cross-arch is
safe because Astro output is plain static HTML/CSS/JS — nothing in the
bundle is arch-specific.
Changes:
- runs-on: ubuntu-latest (most portable act_runner label — override per your
runner's registered labels if needed).
- Install Chromium from apt at workflow time (Google Chrome has no Linux/arm64
stable build; Chromium does). Handles both chromium and chromium-browser
package names, sudo-less runners, and idempotently skips if already present.
- Export CHROME_PATH so LHCI picks the right binary.
- Add chromeFlags to lighthouserc.cjs: --no-sandbox --headless=new
--disable-gpu --disable-dev-shm-usage (required in containerized/root
Chromium on CI runners).
- timeout-minutes on both jobs.
- Defense-in-depth install of rsync + openssh in deploy job if the runner
image doesn't ship them.
- Null-guard SFTP_KEY and SFTP_KNOWN_HOSTS secrets.
- Switch echo to printf for deterministic newline handling when writing key
material to ~/.ssh files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove Sitemap line from robots.txt (no @astrojs/sitemap installed; was
pointing to a 404 that would trip Google Search Console).
- Align Permissions-Policy across all three enforcement layers (middleware,
.htaccess, Cloudflare Transform Rule in OPERATOR-CHECKLIST) by dropping the
stray fullscreen=(self) from the middleware.
- Bump Lighthouse CI numberOfRuns from 1 to 3 to dampen CI-runner noise.
- Add CI guard that fails the build if any <TBD:...> marker survives into
dist/ — prevents a legally incomplete imprint from shipping by accident.
- Add SFTP_* secret null-guard before the rsync --delete step so a missing
secret fails loudly instead of targeting the SSH user's home root.
- Document the set:html compile-time-constant invariant in DualValueProps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- .htmlvalidate.json with relaxed rules for design-system inline styles
- linkinator.config.json skipping mail, external auth/platform origins
- Fix lint:html npm script quoting for Windows-shell compatibility
- Switch astro build.format to 'directory' so /pricing resolves without MultiViews
- trailingSlash: 'ignore' lets both /pricing and /pricing/ work naturally
- Add aria-label to both <nav> landmarks (Primary, Footer) to satisfy html-validate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>