59 Commits

Author SHA1 Message Date
hsiegeln
183a92123c perf(homepage): serve product screenshots as resized WebP to lift Lighthouse perf above 0.95
All checks were successful
ci / build-test (push) Successful in 4m56s
The homepage Lighthouse perf score dropped to 0.94 in CI (threshold 0.95)
because Hero and ThreeAmWalkthrough each load a 1920×945 PNG screenshot
straight out of public/product/ — together ~1.2 MiB and the LCP element.
Other pages have no product imagery and pass with 1.0.

Adds a sharp-based generator (scripts/optimize-product-images.mjs, run via
`npm run optimize:images`) that emits 1280w and 1920w WebP variants beside
each source PNG. Lightbox.astro now wraps the trigger in a <picture> with
a WebP <source srcset> auto-derived from the PNG path (PNG kept as
fallback for the ~2% of clients without WebP), exposes a fetchpriority
prop, and points the dialog modal at the 1920w WebP. The Hero passes
fetchpriority="high" on the LCP image; index.astro injects a matching
<link rel="preload" as="image" imagesrcset> via a new BaseLayout head
slot so the WebP is discovered before the body parses.

Effect on the LCP image: 551 KiB PNG → 46 KiB WebP at 1280w (-92%).
Local Lighthouse perf: 0.95 → 1.00 across 3 runs on /index.html;
pricing/imprint stay 1.00; privacy unchanged at 0.98 (pre-existing CLS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:20:29 +02:00
hsiegeln
2ec4a86e3d ci: skip app.cameleer.io in linkinator (auth host)
Some checks failed
ci / build-test (push) Failing after 5m24s
CI's lint:links step started failing while app.cameleer.io is in
maintenance — linkinator follows the sign-in/sign-up CTAs out to
the Logto host and surfaces transient 502s as build failures.

The skip list already covered auth.cameleer.io but missed the
app.cameleer.io rename from commit fa12df8. Add it alongside
the other internal hosts.

Trade-off: CI no longer catches a real breakage of the auth URLs.
Acceptable because (a) the auth host has its own deploy gates,
(b) maintenance windows there should not red-bar marketing-site
deploys, (c) the site's middleware/header tests still verify the
URL shape via the auth.test.ts schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:52:37 +02:00
hsiegeln
147a813119 Merge branch 'relaunch-2026-04-25' into main
Some checks failed
ci / build-test (push) Failing after 1m40s
Brings in:
- chore: remove all nJAMS references from the live site
- under-construction placeholder feature (spec, plan, tests, page, workflow, README)
2026-04-25 18:44:50 +02:00
hsiegeln
db337ed9c6 docs(readme): note Cloudflare cache caveat for placeholder mode
Some checks failed
ci / build-test (push) Failing after 1m35s
The placeholder serves HTTP 200, so Cloudflare's edge will cache
it normally. Document the operator action (purge cache or set a
short-TTL Cache Rule) needed before recovery for longer outages,
so the edge doesn't keep serving the placeholder past recovery.

Surfaced by final code review of the feature branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:29:02 +02:00
hsiegeln
3773dcc1f8 docs(readme): add placeholder mode section
Documents the deploy-placeholder workflow trigger and the recovery
path back to the real site via deploy.yml.
2026-04-25 18:25:30 +02:00
hsiegeln
07abb101d4 ci(deploy): cache-bust smoke test against Cloudflare edge
The post-deploy smoke test was fetching www.cameleer.io/ directly,
which Cloudflare may serve from edge cache regardless of what rsync
just shipped — falsely greenlighting a failed deploy or red-flagging
a successful one. Append ?cb=$GITHUB_RUN_ID and Cache-Control:
no-cache to both fetches so the edge revalidates per run.

Surfaced by code review of the prior commit (4c98caab).
2026-04-25 18:23:18 +02:00
hsiegeln
4c98caabc8 ci(deploy): add deploy-placeholder workflow
Manual-trigger workflow that substitutes PUBLIC_SALES_EMAIL into
placeholder/index.html, rsyncs placeholder/ to the Hetzner docroot
over SSH:222, then smoke-tests the live origin for the sentinel
string, mailto link, and logo URL.

Shares the deploy-production concurrency group with deploy.yml so
the two workflows can never race on the same docroot. Recovery is
the regular deploy.yml — no separate un-placeholder workflow.
2026-04-25 18:17:01 +02:00
hsiegeln
d0bacdd622 feat(placeholder): add under-construction page with branded teaser
Standalone HTML + two sibling PNGs, no Astro build dependency.
Carries __SALES_EMAIL__ substitution tokens that the new deploy
workflow replaces at deploy time.
2026-04-25 18:11:19 +02:00
hsiegeln
49fdd96f4f test(placeholder): add static-content tests for under-construction page 2026-04-25 18:05:52 +02:00
hsiegeln
c3511a4c1b docs(plan): implementation plan for under-construction placeholder
Four-task plan: tests → page+assets → workflow → README. TDD via
src/placeholder.test.ts (vitest only discovers src/**/*.test.ts).
Atomic commits per task; the plan is wired off the spec at commit
9b0c36b.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:03:27 +02:00
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
hsiegeln
37897f07c3 chore: remove all nJAMS references from the live site
All checks were successful
ci / build-test (push) Successful in 4m12s
Per Hendrik's direction, no nJAMS references on the website. The
founder pedigree claim now stands on the years and the customer
segment ("15 years building integration monitoring for banks,
insurers, and logistics operators") without naming a prior product.

Changes:
- src/components/sections/SocialProofStrip.astro: drop the
  ' · ex-nJAMS' suffix from the founder attribution; collapse the
  multi-step PENDING comment into a single-line founder-name TODO.
- src/components/sections/WhyUs.astro: drop the trademark-review
  comment (no longer relevant — the body text never named the
  prior product, only the comment did).
- OPERATOR-CHECKLIST.md: remove the 'Why us / nJAMS wording review'
  pre-publish task.  Also rename the adjacent 'MID-tier retention'
  TODO to 'Starter-tier retention' to match the relaunched tier
  taxonomy.
- docs/superpowers/specs/2026-04-25-cameleer-website-relaunch-design.md:
  update §4, §6.2, §12 to reflect the removed wording.  Only the
  founder-name placeholder remains as a pre-publish blocker.

Historical specs / plans under docs/superpowers/{specs,plans}/
keep their original wording — they're records of past decisions
and are not on the website.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:41:59 +02:00
8f5e84523e Merge pull request 'chore(auth): redirect sign-in/sign-up to app.cameleer.io' (#5) from relaunch-2026-04-25 into main
All checks were successful
ci / build-test (push) Successful in 4m17s
Reviewed-on: #5
2026-04-25 09:33:23 +02:00
hsiegeln
fa12df8ec6 chore(auth): redirect sign-in/sign-up to app.cameleer.io
All checks were successful
ci / build-test (push) Successful in 3m41s
ci / build-test (pull_request) Successful in 4m12s
Both auth flows now navigate to the app domain rather than the
auth.cameleer.io subdomain:

  PUBLIC_AUTH_SIGNIN_URL → https://app.cameleer.io/sign-in
  PUBLIC_AUTH_SIGNUP_URL → https://app.cameleer.io/sign-in?first_screen=register

Updated:
- .env.example (the canonical reference values)
- OPERATOR-CHECKLIST.md (deploy-time secret values)
- src/config/auth.test.ts (test fixtures)
- src/middleware.ts (CSP-comment about <a> navigation target)
- src/pages/privacy.astro (visitor-facing external-links section
  in §6 of the privacy policy)

The auth.ts validator stays strict-https — the new URLs are still
absolute https URLs, just on a different host.  Logto itself may
still run at auth.cameleer.io as the OIDC backend; only the
visitor-facing /sign-in entry point moved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:28:02 +02:00
3b184488bb Merge pull request 'relaunch-2026-04-25' (#4) from relaunch-2026-04-25 into main
All checks were successful
ci / build-test (push) Successful in 4m21s
Reviewed-on: #4
2026-04-25 08:08:54 +02:00
hsiegeln
203e4bfb41 perf: replace 1.5 MB cameleer-logo.svg refs with optimised PNGs
All checks were successful
ci / build-test (push) Successful in 3m44s
ci / build-test (pull_request) Successful in 4m17s
The Inkscape-exported cameleer-logo.svg in public/ is 1.5 MB —
loaded eagerly in the SiteHeader (32×32) and Hero (64×64), it was
the dominant hit on the homepage's largest-contentful-paint. The
relaunch's added above-the-fold DOM nudged Lighthouse perf from
0.95 to 0.92 and tipped CI's >=0.95 threshold red.

Switch all four SVG references to the pre-optimised PNG icons that
already ship in public/icons/:
  - SiteHeader (32-displayed): /icons/cameleer-48.png   (4.4 KB)
  - Hero       (64-displayed): /icons/cameleer-192.png  (36 KB)
  - SiteFooter (24-displayed): /icons/cameleer-32.png   (2.4 KB)
  - BaseLayout favicon link: drop the SVG, keep the existing
    32 PNG fallback (already declared on the next line).

Local Lighthouse (http-server, no gzip) before: perf 0.72,
LCP 10.0s. After: perf 0.94, LCP 1.6s. CI on Linux + LH static
server should comfortably clear the 0.95 gate.

The SVG file itself is left in place — unreferenced, but kept in
case any external link still points at it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 07:59:30 +02:00
hsiegeln
8dec3e792a chore(sections): delete retired DualValueProps + ProductShowcase
Some checks failed
ci / build-test (push) Failing after 3m44s
These two sections were collapsed into ThreeAmWalkthrough.astro
(Task 3). No remaining consumers — removed from the tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:37:33 +02:00
hsiegeln
9cded54ce3 feat(homepage): wire SocialProofStrip + ThreeAmWalkthrough
Updates the homepage section order to the proof-first arc:
  Hero → SocialProofStrip → ThreeAmWalkthrough → HowItWorks
  → WhyUs → PricingTeaser → FinalCTA

The retired DualValueProps and ProductShowcase imports are dropped
here; the unused .astro files themselves are deleted in Task 11.

Page <title> + <meta description> updated to lead with the new H1.

Verified: dist/index.html contains the #walkthrough anchor target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:37:17 +02:00
hsiegeln
21c1122369 refactor(pricing-page): rename tiers Trial/Starter/Scale/Enterprise
Customer-facing tier names replace the internally-coded
MID/HIGH/BUSINESS labels. Pricing structure (envs, apps,
retention, features) is unchanged. CTA labels updated to match.

The 'Everything in X' feature lines reference the new neighbour
names. Footer note 'HIGH and BUSINESS' updated to 'Scale and
Enterprise'.

Verified: dist/ contains no remaining MID/HIGH/BUSINESS strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:36:43 +02:00
hsiegeln
5f06e5ccad refactor(pricing-teaser): rename tiers, show only 2 cards
- Tier rename: MID → Starter (Scale and Enterprise live on /pricing).
- Homepage shows Trial + Starter only. Starter retains the
  ★ MOST POPULAR ribbon.
- 'See full comparison →' inline link replaced by a clearer
  'See all plans (Scale, Enterprise) →' line below the cards.
- Trial card price stays 'Free'; the tier name stays honest about
  the 14-day cap.

The full /pricing.astro page still shows all four renamed tiers —
updated separately in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:36:07 +02:00
hsiegeln
04b930de62 refactor(final-cta): bookend the hero, drop camel pun
H2 now echoes the Hero (intentional bookend pattern). Sub line
loses the 'No camels harmed' aside. Single primary CTA — no
secondary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:35:41 +02:00
hsiegeln
b1b6b52f3f refactor(why-us): drop 03:00 watermark, reword card 2
The decorative giant '03:00' watermark on card 2 plus its 'live'
ops-desk timestamp gimmick was the third repetition of the 3 AM
metaphor on the homepage — the post-launch review flagged that
five hits turns a sharp pain point into a slogan.

Card 2 reworded to lead with 'operated integration in production
for 15 years' — same pedigree claim, no second 3 AM reference.
The walkthrough section already does the 3 AM beat in full.

Card 1 unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:35:24 +02:00
hsiegeln
0ad067847c refactor(how-it-works): slim repetitive 'no SDK' boilerplate
The 'no code changes / no SDK / no rewrite' line is already said
clearly in the Hero subhead and the WhyUs cards. Repeating it on
step 1 of HowItWorks adds nothing. Step 3's tail 'Nothing to
instrument. Nothing to maintain.' is two sentences saying the
same thing — both removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:34:58 +02:00
hsiegeln
d67a89bacb feat(hero): single H1, annotation pins, microline, anchor CTA
- Drop the rotating headline and its <script> block
- Replace with single category-defining H1: 'Ship Camel integrations.
  Sleep through the night.'
- Add price microline under the CTAs (14-day trial · from €20/mo)
- Replace 'Sign in' secondary CTA with 'See it in action ↓' anchor
  to #walkthrough
- Add three numbered annotation pins overlaid on the screenshot,
  with a 3-up legend below the image (correlation ID, failure
  context, error pinned)

The eyebrow pill is retained — the only surviving camel pun on
the homepage per the pun-budget decision in the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:34:37 +02:00
hsiegeln
54bbb46755 feat(sections): add ThreeAmWalkthrough component
Replaces DualValueProps + ProductShowcase with a single before/after
split: a styled <pre> block (the 'without' state) next to the
existing /product/error-detail.png screenshot (the 'with' state).
Three short callouts below.

Section anchor #walkthrough is the target for the Hero's
'See it in action ↓' secondary CTA (added in Task 4).

The 'without' panel is implemented as a styled <pre> per the spec —
no asset production required. A future phase may swap to a recorded
terminal screencap; that swap is a one-component change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:33:50 +02:00
hsiegeln
29c2d13776 feat(sections): add SocialProofStrip component
Founder pedigree quote plus design-partner mailto CTA.
Uses auth.salesEmail (not auth.salesMailto) so we can pass a subject.

Two PENDING gates documented in the component:
  - [Founder Name] placeholder
  - 'ex-nJAMS' wording subject to trademark clearance

Component is created but not yet wired into index.astro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:33:07 +02:00
hsiegeln
ce314adf2d feat(styles): enable smooth in-page anchor scrolling
Adds html { scroll-behavior: smooth } with a prefers-reduced-motion
override. Required for the relaunch hero's 'See it in action' anchor
CTA to feel natural.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:32:34 +02:00
hsiegeln
ad4288c3ed docs(plans): cameleer website relaunch implementation plan
Twelve atomic tasks, each ending with its own commit:
  1. Add scroll-behavior:smooth to global.css
  2. Create SocialProofStrip.astro
  3. Create ThreeAmWalkthrough.astro
  4. Rebuild Hero.astro
  5. Slim HowItWorks.astro
  6. Refresh WhyUs.astro
  7. Rebuild FinalCTA.astro
  8. Refresh PricingTeaser.astro
  9. Refresh /pricing.astro tier names
 10. Wire new sections into index.astro
 11. Delete DualValueProps + ProductShowcase
 12. Run full quality gates

Order is build-safe: each task leaves the build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:31:33 +02:00
hsiegeln
e3383471d1 docs(specs): cameleer website relaunch design (2026-04-25)
Structural relaunch of the homepage to address the conversion gaps
identified in the post-launch design review:

- Single H1 (kill the rotating one)
- New social proof strip (founder quote + design-partner CTA)
- 3 AM walkthrough section with before/after split (replaces
  DualValueProps + ProductShowcase)
- Pricing tier rename: Trial / Starter / Scale / Enterprise
- Pricing teaser slimmed to 2 cards on the homepage
- Pun budget cut from 5+ to 1 (eyebrow pill survives)

Static stack, hosting, and security posture unchanged from the
2026-04-24 spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:26:46 +02:00
hsiegeln
b7b58dd948 feat(design): click-to-enlarge on product screenshots
Some checks failed
ci / build-test (push) Failing after 3m41s
Lightbox.astro — reusable native HTMLDialogElement wrapper:
- Trigger: <button> wrapping the <img>, cursor: zoom-in, amber zoom-pill
  badge fades in on hover/focus.
- Dialog: showModal() opens a full-viewport modal (≤1800x1200 cap) with
  blurred amber-tinted backdrop.
- Close paths: native form[method=dialog] submit (Escape + close button),
  click on backdrop, click on the image itself.
- Accessibility: aria-labelledby + visually-hidden heading avoids both
  aria-label-misuse and no-redundant-role validator conflicts. Focus
  returns to trigger on close (native HTMLDialogElement behavior).
- Motion: 220ms fade+scale open, disabled under prefers-reduced-motion.
- CSP: <script> is Astro-bundled to an external file — script-src 'self'
  respected.

Hero and ProductShowcase now use <Lightbox> instead of a raw <img> for
the product screenshots. The existing frame styling (border, glow, ring
overlay) is untouched — the lightbox trigger is a block-level button
that fills the frame.
2026-04-25 00:31:48 +02:00
hsiegeln
4d4c072834 feat(design): atmosphere + WhyUs editorial 3-AM treatment
TopographicBg now actually reads:
- Per-line stroke width varies (triangle wave — contour-interval feel)
- Per-line opacity varies by vertical depth (darker mid-section, lighter
  edges)
- One line in four rendered in cyan (echo of cross-route correlation)
- Radial-mask soft edge fade so lines dissolve into the section boundary
- Default opacity bumped from 0.12 to 0.35; section callers still scale it
  down via the opacity prop, but the new internal variation makes the
  atmosphere visible where before it was invisible

WhyUs second tile: 3-AM storytelling moment now lands typographically:
- Decorative 03:00 glyph (amber/4% alpha) in the top-right corner
- Eyebrow log-entry treatment: pulsing amber dot + mono 03:00:47.218
  timestamp + OPS DESK label — reads like a product UI log row
- The rest of the tile unchanged

ProductShowcase figure: figcaption moved to last child (HTML spec
requires figcaption to be first or last in a figure; a div after it was
a validation error).
2026-04-25 00:26:16 +02:00
hsiegeln
c4395eb245 feat(design): card motion + Pricing MID tier hierarchy
- DualValueProps: 110ms staggered rise-in on load (cubic-bezier ease),
  reduced-motion users see cards pre-populated, no animation.
- All card sections (DualValueProps, HowItWorks, WhyUs, Pricing) gain a
  subtle hover lift: -translate-y-0.5, amber/40 border, soft amber drop
  shadow. 200ms ease-out — tactile but not noisy.
- Pricing MID tier now looks like the highlighted option: ring-2 accent,
  amber-tinted drop shadow, lg:-translate-y-2 (sits above the others),
  and a 'MOST POPULAR' ribbon pill. The 1px border swap was invisible.
2026-04-25 00:23:54 +02:00
hsiegeln
073ff2ad48 feat(design): new ProductShowcase section — 'When something breaks'
Editorial section between DualValueProps and HowItWorks. Breaks the
identical-rectangle cascade with an asymmetric 8/4 grid: large
error-detail screenshot with subtle cyan/amber backlight on the left,
three numbered callout captions on the right.

The screenshot (cross-route correlation chain + circuit breaker +
fallback + Java stack trace) makes the 'deep tracing, replay, live
control' claims concrete in a way the abstract RouteDiagram never did.

Cyan kicker on this section (vs. amber elsewhere) signals 'this one is
different' and echoes the cross-route correlation color in the product.
2026-04-25 00:22:28 +02:00
hsiegeln
ad8312b7f0 chore: gitignore .claude/ session state
Accidentally committed .claude/scheduled_tasks.lock in the previous
commit. Untrack it and add .claude/ to .gitignore so local Claude Code
session state does not leak into the repo.
2026-04-25 00:21:07 +02:00
hsiegeln
8c77db02ac feat(design): Hero asymmetric layout with real product UI + bug fixes
- Hero restructured from stacked to 2-col grid on lg+ (copy left, product
  screenshot right). Replaces the abstract RouteDiagram with the actual
  exchange-detail view — the product doing the thing the copy promises.
- Kicker broken out of the shared uppercase-mono pattern: italic pill with
  a soft amber fill/border, scaled up to 14px. The humor now wears a
  different costume from the other section kickers.
- Hero brand mark scaled to 64px and given a slow 7s sway (reduced-motion
  guarded) — living atmosphere, not ambient animation.
- H1 min-height raised to 2.5em to absorb the 2-line wrap of line 1 at
  mobile sizes without layout shift on rotation.
- Amber radial glow behind the product shot + subtle bevel + frame ring.
- Footer placeholder 3-wavy-lines SVG replaced with real camel logo
  (spec gap from earlier refresh — header got swapped, footer didn't).
- Screenshot assets imported under public/product/.
2026-04-25 00:20:39 +02:00
hsiegeln
af7c61c203 feat(brand): redesign OG image around new thesis
All checks were successful
ci / build-test (push) Successful in 3m30s
1200x630, solid #060a13 ground, amber topographic lines, circle-C mark,
headline 'Run Apache Camel without running Apache Camel.' plus short
subhead. The full camel-compass product logo is not embedded — raster
inside SVG at OG size would bloat the asset. Circle-C wordmark reads
cleanly at the 600x315 thumbnail render.
2026-04-24 23:54:14 +02:00
hsiegeln
0f02a62e6f feat(copy): FinalCTA — 'Your camels called. Time to ride.'
Second humor pop (pairing with the Hero kicker). Subline adds the
tasteful-absurd 'No camels harmed.' closer.
2026-04-24 23:53:10 +02:00
hsiegeln
47142051c4 feat(copy): PricingTeaser — 'no credit card, no sales call'
Subline slightly warmer and more concrete ('a working trial in ten
minutes'). Tier cards untouched.
2026-04-24 23:52:19 +02:00
hsiegeln
135a6246d9 feat(copy): WhyUs — warmer language, 3-AM framing
Tile 2 headline now 'Built by people who know what 3 AM looks like.'
Bodies soften 'bidirectional protocol / signed config' into plain-value
language. nJAMS-legacy trademark review note preserved.
2026-04-24 23:51:30 +02:00
hsiegeln
3bb71942dc feat(copy): HowItWorks — plain-language steps, bash snippet gone
Step 1 no longer shows java -javaagent:... on the marketing page; that
detail belongs in docs/install. Subtitle tightened to 'Nothing to
maintain.'
2026-04-24 23:49:42 +02:00
hsiegeln
62c77a8dc5 feat(copy): DualValueProps — outcome-led, plain language
Three tiles: ship-then-sleep, debug-in-daylight, keep-what-you-chose.
Drops -javaagent, nanosecond, and 45+ EIP node types from the landing
copy — those belong in docs.
2026-04-24 23:48:32 +02:00
hsiegeln
77bf0bfa74 feat(hero): rotate three positioning lines on a 10s cycle
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.
2026-04-24 23:46:21 +02:00
hsiegeln
518d7a8afc feat(copy): Hero static rewrite — new kicker, thesis H1, subline
Drops -javaagent from the hero. Adds the 48px product mark next to the
kicker. Rotation is added in the next commit.
2026-04-24 23:42:09 +02:00
hsiegeln
84ff83303a feat(copy): update homepage title + meta description
Thesis: 'Run Apache Camel without running Apache Camel.' Reframes the
page from observability tool to hosted runtime platform for Camel.
2026-04-24 23:40:17 +02:00
hsiegeln
eff1ba6b8e feat(brand): swap header icon for real Cameleer logo
Placeholder 3-wavy-lines SVG replaced with the product logo
(camel + cameleer figure + compass rose).
2026-04-24 23:39:13 +02:00
hsiegeln
03573b2ac1 feat(brand): wire favicon chain to real product logo
SVG primary + 32px PNG fallback + Apple touch icon. Removes the
placeholder 3-wavy-lines favicon.
2026-04-24 23:38:12 +02:00
hsiegeln
6f0268ebea feat(brand): import real cameleer product logo + favicon set
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).
2026-04-24 23:37:04 +02:00
hsiegeln
2526b1f0fc docs: implementation plan for copy + brand refresh
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).
2026-04-24 23:34:38 +02:00
hsiegeln
01cf23f2f6 docs: copy + brand refresh spec — reposition as hosted Camel runtime
Reframe the homepage from "observability tool" to "hosted runtime platform
for Apache Camel with observability baked in" — matching what the product
actually is across cameleer, cameleer-server, and cameleer-saas.

- Thesis: "Run Apache Camel without running Apache Camel."
- Three rotating Hero positioning lines (slow fade, reduced-motion aware).
- Two strategic humor pops (Hero kicker + FinalCTA), rest stays serious.
- Drop implementation jargon (-javaagent, Docker, ByteBuddy, etc.) from
  the landing page; keep Camel-audience vocabulary (routes, processors,
  exchanges, replay).
- Replace placeholder topographic-wave icon with the real Cameleer product
  logo (camel + cameleer figure + compass rose) from design-system/assets.
- Section-by-section copy changes for Hero, DualValueProps, HowItWorks,
  WhyUs, PricingTeaser, FinalCTA.

No architecture changes; content + asset refresh only.
2026-04-24 23:28:10 +02:00
hsiegeln
3a1fe5f2c7 docs+ci: own security headers at Cloudflare, drop dead .htaccess path
All checks were successful
ci / build-test (push) Successful in 3m33s
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>
2026-04-24 23:04:09 +02:00
hsiegeln
d6851cd5aa Merge branch 'feat/initial-build' into main
All checks were successful
ci / build-test (push) Successful in 4m2s
Merge build+deploy jobs, switch to manual trigger only.
2026-04-24 21:24:44 +02:00
hsiegeln
ca2a725953 ci(deploy): merge build+deploy into one job, manual trigger only
All checks were successful
ci / build-test (push) Successful in 4m0s
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>
2026-04-24 21:24:42 +02:00
hsiegeln
fdb0411c35 Sync main into feat/initial-build before merge-jobs refactor 2026-04-24 21:23:58 +02:00
hsiegeln
461b5e0cd6 Merge branch 'feat/initial-build' into main
Some checks failed
deploy / build (push) Successful in 36s
deploy / deploy (push) Failing after 14s
ci / build-test (push) Successful in 3m54s
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
2026-04-24 21:09:35 +02:00
hsiegeln
0d743402ac ci(deploy): copy public/.htaccess into dist after build
All checks were successful
ci / build-test (push) Successful in 3m47s
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>
2026-04-24 21:08:51 +02:00
hsiegeln
28fcaf16c5 Merge branch 'feat/initial-build' into main
Some checks failed
ci / build-test (push) Successful in 4m2s
deploy / build (push) Successful in 30s
deploy / deploy (push) Failing after 13s
Revert to rsync, route through Hetzner's SSH port 222 (the shell port,
as opposed to port 22 which is SFTP-only).
2026-04-24 20:24:33 +02:00
hsiegeln
e3fbbbada7 ci(deploy): revert to rsync via SSH port 222 (Hetzner shell port)
All checks were successful
ci / build-test (push) Successful in 3m57s
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>
2026-04-24 20:24:27 +02:00
hsiegeln
cb21be71f0 Merge branch 'feat/initial-build' into main
Some checks failed
ci / build-test (push) Successful in 3m55s
deploy / build (push) Successful in 28s
deploy / deploy (push) Failing after 12s
Fix lftp auth (explicit -u USER, + unindented heredoc body).
2026-04-24 20:08:29 +02:00
hsiegeln
5417565e34 ci(deploy): fix lftp auth — explicit empty password + unindented script
All checks were successful
ci / build-test (push) Successful in 4m0s
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>
2026-04-24 20:08:22 +02:00
56 changed files with 4734 additions and 298 deletions

View File

@@ -1,5 +1,5 @@
# Logto auth endpoints — the marketing site only performs <a href> navigations to these. # Logto auth endpoints — the marketing site only performs <a href> navigations to these.
# No tokens, no cookies, no XHR — these are plain hyperlinks. # No tokens, no cookies, no XHR — these are plain hyperlinks.
PUBLIC_AUTH_SIGNIN_URL=https://auth.cameleer.io/sign-in PUBLIC_AUTH_SIGNIN_URL=https://app.cameleer.io/sign-in
PUBLIC_AUTH_SIGNUP_URL=https://auth.cameleer.io/sign-in?first_screen=register PUBLIC_AUTH_SIGNUP_URL=https://app.cameleer.io/sign-in?first_screen=register
PUBLIC_SALES_EMAIL=sales@cameleer.io PUBLIC_SALES_EMAIL=sales@cameleer.io

View File

@@ -0,0 +1,103 @@
# -----------------------------------------------------------------------------
# cameleer-website — Deploy under-construction placeholder
#
# MANUAL TRIGGER ONLY. Replaces the live cameleer.io docroot with a static
# "back shortly" page. Recovery: trigger Actions → deploy → Run workflow on
# the desired main commit.
#
# Shares the deploy-production concurrency group with deploy.yml so the two
# workflows queue rather than race on the same docroot.
#
# This workflow does NOT run npm/astro. The placeholder is hand-authored
# static HTML in placeholder/, deliberately decoupled from the main build so
# it can ship even when the main build is broken (which is the worst case in
# which a placeholder is needed).
#
# Required secrets (repo settings → Actions → Secrets):
# SFTP_HOST, SFTP_USER, SFTP_PATH, SFTP_KEY, SFTP_KNOWN_HOSTS
# PUBLIC_SALES_EMAIL
# -----------------------------------------------------------------------------
name: deploy-placeholder
on:
workflow_dispatch:
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Substitute sales email into placeholder
env:
PUBLIC_SALES_EMAIL: ${{ secrets.PUBLIC_SALES_EMAIL }}
run: |
set -e
: "${PUBLIC_SALES_EMAIL:?PUBLIC_SALES_EMAIL secret must be set}"
sed -i "s|__SALES_EMAIL__|${PUBLIC_SALES_EMAIL}|g" placeholder/index.html
if grep -q '__SALES_EMAIL__' placeholder/index.html; then
echo "Token __SALES_EMAIL__ still present after substitution — refusing to ship."
exit 1
fi
- name: Configure SSH
env:
SFTP_KEY: ${{ secrets.SFTP_KEY }}
SFTP_KNOWN_HOSTS: ${{ secrets.SFTP_KNOWN_HOSTS }}
run: |
set -e
: "${SFTP_KEY:?SFTP_KEY secret must be set}"
: "${SFTP_KNOWN_HOSTS:?SFTP_KNOWN_HOSTS secret must be set}"
mkdir -p ~/.ssh
printf '%s\n' "$SFTP_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$SFTP_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
if ! command -v rsync >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi
$SUDO apt-get update -qq
$SUDO apt-get install -y --no-install-recommends rsync openssh-client
fi
- name: Deploy via rsync
env:
SFTP_USER: ${{ secrets.SFTP_USER }}
SFTP_HOST: ${{ secrets.SFTP_HOST }}
SFTP_PATH: ${{ secrets.SFTP_PATH }}
run: |
: "${SFTP_USER:?SFTP_USER secret must be set}"
: "${SFTP_HOST:?SFTP_HOST secret must be set}"
: "${SFTP_PATH:?SFTP_PATH secret must be set}"
# Hetzner Webhosting splits SSH into two ports:
# port 22 — SFTP only, no remote command exec
# port 222 — full SSH with shell exec (rsync needs this)
# `--rsync-path=/usr/bin/rsync` tells the local rsync where to find
# the remote binary on Hetzner's locked-down PATH.
# `BatchMode=yes` disables interactive prompts.
rsync -avz --delete --rsync-path=/usr/bin/rsync \
-e "ssh -p 222 -i $HOME/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts" \
placeholder/ "$SFTP_USER@$SFTP_HOST:$SFTP_PATH/"
- name: Post-deploy smoke test
run: |
set -e
echo "Confirming the placeholder is live on www.cameleer.io..."
# Cache-bust per run so Cloudflare's edge can't serve a stale response
# that masks a failed deploy. ?cb=$GITHUB_RUN_ID forces a fresh cache key;
# the no-cache request header tells any well-behaved cache to revalidate.
CB="$GITHUB_RUN_ID"
BODY=$(curl -sf -H 'Cache-Control: no-cache' "https://www.cameleer.io/?cb=$CB")
echo "$BODY" | grep -qF 'Routes are remapping' \
|| { echo "Sentinel string missing — placeholder did not land."; exit 1; }
echo "$BODY" | grep -qF 'mailto:' \
|| { echo "mailto: link missing — sales email substitution may have failed."; exit 1; }
curl -sfI -H 'Cache-Control: no-cache' "https://www.cameleer.io/cameleer-logo.png?cb=$CB" > /dev/null \
|| { echo "cameleer-logo.png not reachable on the live origin."; exit 1; }
echo "Placeholder is live."

View File

@@ -1,10 +1,19 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# cameleer-website — Deploy to Hetzner Webhosting L # cameleer-website — Deploy to Hetzner Webhosting L
# #
# Runs ONLY on pushes to `main` and on manual dispatch from the Gitea UI. # MANUAL TRIGGER ONLY. Runs exclusively on workflow_dispatch from the Gitea UI
# Does NOT run Lighthouse CI (that's in ci.yml — assume any commit that reached # (Actions → deploy → Run workflow). Does NOT auto-deploy on push to main —
# main already passed the full gate). Rebuilds fresh, runs the TBD guard, and # merges to main must be explicitly promoted to production.
# rsyncs `dist/` to the origin over SSH with host-key pinning. #
# Build and deploy run in a single job; rsync uploads dist/ directly. No
# upload-artifact round-trip (v3 strips dotfiles, v4 isn't supported on Gitea).
#
# Security headers (HSTS, CSP, X-Frame-Options, etc.) are NOT set by this
# deploy. Hetzner Webhosting L runs Apache with AllowOverride None on the
# user docroot, so file-based .htaccess is silently ignored. All response
# headers are owned by Cloudflare Transform Rules — see OPERATOR-CHECKLIST.md
# §2 "Cloudflare". Apache config exposed via konsoleH UI is the only origin-
# side override path and is not managed from this repo.
# #
# Runner: self-hosted arm64 Gitea runner. Adjust `runs-on` if your runner's # Runner: self-hosted arm64 Gitea runner. Adjust `runs-on` if your runner's
# labels differ. Deploy target is Hetzner amd64 — arch mismatch is a non-issue # labels differ. Deploy target is Hetzner amd64 — arch mismatch is a non-issue
@@ -12,15 +21,12 @@
# #
# Required secrets (repo settings → Actions → Secrets): # Required secrets (repo settings → Actions → Secrets):
# SFTP_HOST, SFTP_USER, SFTP_PATH, SFTP_KEY, SFTP_KNOWN_HOSTS # SFTP_HOST, SFTP_USER, SFTP_PATH, SFTP_KEY, SFTP_KNOWN_HOSTS
# Required variables (repo settings → Actions → Variables):
# PUBLIC_AUTH_SIGNIN_URL, PUBLIC_AUTH_SIGNUP_URL, PUBLIC_SALES_EMAIL # PUBLIC_AUTH_SIGNIN_URL, PUBLIC_AUTH_SIGNUP_URL, PUBLIC_SALES_EMAIL
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
name: deploy name: deploy
on: on:
push:
branches: [main]
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@@ -28,9 +34,9 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
build: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 25
env: env:
PUBLIC_AUTH_SIGNIN_URL: ${{ secrets.PUBLIC_AUTH_SIGNIN_URL }} PUBLIC_AUTH_SIGNIN_URL: ${{ secrets.PUBLIC_AUTH_SIGNIN_URL }}
PUBLIC_AUTH_SIGNUP_URL: ${{ secrets.PUBLIC_AUTH_SIGNUP_URL }} PUBLIC_AUTH_SIGNUP_URL: ${{ secrets.PUBLIC_AUTH_SIGNUP_URL }}
@@ -61,28 +67,6 @@ jobs:
exit 1 exit 1
fi fi
# Pin to v3 — Gitea Actions implements the v3 artifact protocol.
# upload/download-artifact@v4 talk to a github.com-only backend and
# fail with GHESNotSupportedError on Gitea / Forgejo / GHES.
- name: Upload dist artifact
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Download dist artifact
uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Configure SSH - name: Configure SSH
env: env:
SFTP_KEY: ${{ secrets.SFTP_KEY }} SFTP_KEY: ${{ secrets.SFTP_KEY }}
@@ -96,35 +80,33 @@ jobs:
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$SFTP_KNOWN_HOSTS" > ~/.ssh/known_hosts printf '%s\n' "$SFTP_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts
# Hetzner Webhosting accounts are SFTP-only — they accept SSH for file # Ensure rsync + openssh are present even on a minimal runner image.
# transfer but refuse remote command exec ("exec request failed on if ! command -v rsync >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
# channel 0"). rsync over SSH needs to spawn a remote rsync binary,
# so it cannot work here. Use lftp's mirror instead, which speaks
# SFTP end-to-end with the same key + known_hosts pinning.
if ! command -v lftp >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi
$SUDO apt-get update -qq $SUDO apt-get update -qq
$SUDO apt-get install -y --no-install-recommends lftp openssh-client $SUDO apt-get install -y --no-install-recommends rsync openssh-client
fi fi
- name: Deploy via lftp (mirror over SFTP) - name: Deploy via rsync
env: env:
SFTP_USER: ${{ secrets.SFTP_USER }} SFTP_USER: ${{ secrets.SFTP_USER }}
SFTP_HOST: ${{ secrets.SFTP_HOST }} SFTP_HOST: ${{ secrets.SFTP_HOST }}
SFTP_PATH: ${{ secrets.SFTP_PATH }} SFTP_PATH: ${{ secrets.SFTP_PATH }}
run: | run: |
# Fail loudly if any secret is missing — otherwise mirror --delete # Fail loudly if any secret is missing — otherwise rsync --delete
# could be directed at the SSH user's home root. # could be directed at the SSH user's home root.
: "${SFTP_USER:?SFTP_USER secret must be set}" : "${SFTP_USER:?SFTP_USER secret must be set}"
: "${SFTP_HOST:?SFTP_HOST secret must be set}" : "${SFTP_HOST:?SFTP_HOST secret must be set}"
: "${SFTP_PATH:?SFTP_PATH secret must be set}" : "${SFTP_PATH:?SFTP_PATH secret must be set}"
lftp <<LFTP # Hetzner Webhosting splits SSH into two ports:
set cmd:fail-exit yes # port 22 — SFTP only, no remote command exec
set sftp:connect-program 'ssh -a -x -i $HOME/.ssh/id_ed25519 -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts' # port 222 — full SSH with shell exec (rsync needs this)
open sftp://$SFTP_USER@$SFTP_HOST # `--rsync-path=/usr/bin/rsync` tells the local rsync where to find
mirror --reverse --delete --verbose --parallel=4 dist/ $SFTP_PATH/ # the remote binary on Hetzner's locked-down PATH.
bye # `BatchMode=yes` disables interactive prompts.
LFTP rsync -avz --delete --rsync-path=/usr/bin/rsync \
-e "ssh -p 222 -i $HOME/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts" \
dist/ "$SFTP_USER@$SFTP_HOST:$SFTP_PATH/"
- name: Post-deploy smoke test - name: Post-deploy smoke test
run: | run: |

4
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Build output # Build output
dist/ dist/
.astro/ .astro/
.lighthouseci/
# Dependencies # Dependencies
node_modules/ node_modules/
@@ -22,6 +23,9 @@ Thumbs.db
# Brainstorming / visual companion previews # Brainstorming / visual companion previews
.superpowers/ .superpowers/
# Claude Code session state (local tooling)
.claude/
# Logs # Logs
*.log *.log
npm-debug.log* npm-debug.log*

View File

@@ -4,20 +4,25 @@ One-time setup that lives outside code. Do these before the first `main` merge t
## 1. Hetzner Webhosting L ## 1. Hetzner Webhosting L
- [ ] Provision Webhosting L plan. Note the SSH hostname and SFTP path. - [ ] Provision Webhosting L plan. Note the SSH hostname (e.g. `wwwNNN.your-server.de`) and the user login.
- [ ] In the Hetzner control panel, **enable SSH access** for the main user. - [ ] In konsoleH, **enable SSH access** for the user.
- [ ] In konsoleH → Domainverwaltung, register the production domain (`www.cameleer.io`) on this hosting and confirm what document root Apache uses for it. On Webhosting L, the Apache vhost docroot for the addon domain is typically the bare `~/public_html/` (NOT a subdirectory). The `SFTP_PATH` secret must match this exactly — wrong path = 404 from origin.
- [ ] Generate an ed25519 SSH key pair locally (once): - [ ] Generate an ed25519 SSH key pair locally (once):
```bash ```bash
ssh-keygen -t ed25519 -f ~/.ssh/cameleer-website-deploy -C "cameleer-website CI" ssh-keygen -t ed25519 -f ~/.ssh/cameleer-website-deploy -C "cameleer-website CI"
``` ```
- [ ] Add the **public** key to `~/.ssh/authorized_keys` on the Hetzner account. - [ ] Add the **public** key to `~/.ssh/authorized_keys` on the Hetzner account (or via konsoleH SSH-Schlüssel UI).
- [ ] Test SSH: `ssh -i ~/.ssh/cameleer-website-deploy user@hetzner-host "ls -la"`. - [ ] Test SSH on **port 222** (Hetzner Webhosting splits SFTP=22 / SSH-shell=222; rsync needs 222):
- [ ] Create a subdirectory for the site (typical path: `public_html/www.cameleer.io/`).
- [ ] Grab the SSH host key for pinning:
```bash ```bash
ssh-keyscan -t ed25519 hetzner-host > hetzner-known-hosts.txt ssh -p 222 -i ~/.ssh/cameleer-website-deploy user@wwwNNN.your-server.de "ls -la"
``` ```
- [ ] Install Let's Encrypt (or use Hetzner's built-in) for the origin hostname. Cloudflare Full (strict) requires a valid origin cert. - [ ] Grab the SSH host key for pinning, also on **port 222**:
```bash
ssh-keyscan -p 222 -t rsa,ed25519,ecdsa wwwNNN.your-server.de > hetzner-known-hosts.txt
```
Verify the fingerprint against what your manual SSH session displayed before saving the secret — `ssh-keyscan` doesn't authenticate.
- [ ] **Origin TLS:** Cloudflare Full (strict) requires a valid origin cert. Hetzner Webhosting auto-issues Let's Encrypt — confirm the cert is active in konsoleH → SSL → SSL-Zertifikate.
- [ ] **`.htaccess` caveat (important):** Hetzner Webhosting L runs Apache with `AllowOverride None` on the user docroot, so any `.htaccess` file you `rsync` is **silently ignored** by Apache. The only way to set Apache directives on this tier is via konsoleH → Einstellungen → Serverkonfiguration (per-directory wrench panel). This repo therefore owns no `.htaccess`; all response headers live in Cloudflare (see §2). The konsoleH `.htaccess` panel is left empty by default; defense-in-depth header copies there are optional and survive rsync deploys (different storage location).
## 2. Cloudflare (zone: cameleer.io) ## 2. Cloudflare (zone: cameleer.io)
@@ -67,11 +72,11 @@ Add these under Repository settings → Actions → Secrets (or variables):
|------|------|-------| |------|------|-------|
| `SFTP_HOST` | secret | Hetzner SSH hostname | | `SFTP_HOST` | secret | Hetzner SSH hostname |
| `SFTP_USER` | secret | Hetzner SSH user | | `SFTP_USER` | secret | Hetzner SSH user |
| `SFTP_PATH` | secret | Absolute path to document root (e.g., `/usr/home/cameleer/public_html/www.cameleer.io`) | | `SFTP_PATH` | secret | Absolute path to the Apache vhost docroot configured in konsoleH (typically `/usr/www/users/<login>/public_html`). Mismatch → 404 on origin. |
| `SFTP_KEY` | secret | Contents of `~/.ssh/cameleer-website-deploy` (private key, PEM) | | `SFTP_KEY` | secret | Contents of `~/.ssh/cameleer-website-deploy` (private key, PEM) |
| `SFTP_KNOWN_HOSTS` | secret | Contents of `hetzner-known-hosts.txt` (captured via `ssh-keyscan`) | | `SFTP_KNOWN_HOSTS` | secret | Contents of `hetzner-known-hosts.txt` (captured via `ssh-keyscan`) |
| `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://auth.cameleer.io/sign-in` | | `PUBLIC_AUTH_SIGNIN_URL` | secret | `https://app.cameleer.io/sign-in` |
| `PUBLIC_AUTH_SIGNUP_URL` | secret | `https://auth.cameleer.io/sign-in?first_screen=register` | | `PUBLIC_AUTH_SIGNUP_URL` | secret | `https://app.cameleer.io/sign-in?first_screen=register` |
| `PUBLIC_SALES_EMAIL` | secret | `sales@cameleer.io` (or whatever sales alias you set up) | | `PUBLIC_SALES_EMAIL` | secret | `sales@cameleer.io` (or whatever sales alias you set up) |
These three are not actually secret (they end up in the built HTML), but Gitea's These three are not actually secret (they end up in the built HTML), but Gitea's
@@ -82,15 +87,17 @@ workflows read them via the `${{ secrets.* }}` context.
- [ ] Fill in `src/pages/imprint.astro` `operator` object with real legal details. - [ ] Fill in `src/pages/imprint.astro` `operator` object with real legal details.
- [ ] Fill in `operatorContact` in `src/pages/privacy.astro`. - [ ] Fill in `operatorContact` in `src/pages/privacy.astro`.
- [ ] Review the "Why us" / nJAMS wording in `src/components/sections/WhyUs.astro` for trademark safety. - [ ] Confirm Starter-tier retention: spec says **7 days**; `cameleer-saas/HOWTO.md` says **30 days**. Reconcile one side or the other.
- [ ] Confirm MID-tier retention: spec says **7 days**; `cameleer-saas/HOWTO.md` says **30 days**. Reconcile one side or the other.
## 5. First deploy ## 5. First deploy
The `deploy` workflow is **manual-only** — it does NOT auto-fire on push to `main`. After merging, trigger it explicitly.
1. Merge a PR to `main`. 1. Merge a PR to `main`.
2. Watch the Gitea Actions run: `build`, then `deploy`. 2. In Gitea: **Actions → deploy → Run workflow** on `main`.
3. The workflow includes a post-deploy smoke check — if HSTS / CSP / XFO are missing from the live response, the deploy fails and must be debugged at the Cloudflare Transform Rule layer. 3. Watch the single `deploy` job (build + tests + rsync + smoke test in one step).
4. Manually verify: 4. The workflow's post-deploy smoke check verifies HSTS / CSP / X-Frame-Options on the live response. If any fail, the deploy step exits non-zero — debug at the **Cloudflare Transform Rule** layer (§2 above), since headers no longer come from the origin.
- `curl -sI https://www.cameleer.io/` returns all six security headers. 5. Manually verify:
- `curl -sI https://www.cameleer.io/` returns all 5 security headers (HSTS, CSP, XFO, X-Content-Type-Options, Referrer-Policy, Permissions-Policy).
- `https://cameleer.io/` → `https://www.cameleer.io/` 301 redirect. - `https://cameleer.io/` → `https://www.cameleer.io/` 301 redirect.
- Open the site in an incognito window on desktop + mobile. - Open the site in an incognito window on desktop + mobile.

View File

@@ -34,7 +34,19 @@ See `.env.example`. All are `PUBLIC_*` (build-time, embedded in HTML).
## Deployment ## Deployment
Push to `main` Gitea Actions runs tests, builds, lints, then `rsync`s `dist/` to Hetzner over SSH (ed25519 key, host-key-pinned). Rollback is `git revert && git push`. **Manual trigger only.** Merging to `main` does NOT auto-deploy. To ship: Gitea **Actions → deploy → Run workflow** on `main`. The workflow runs tests, builds, then `rsync`s `dist/` to Hetzner over SSH (ed25519 key on port 222, host-key-pinned), and post-deploy curls the live site to verify security headers.
Rollback: trigger the deploy workflow on the previous `main` commit (Actions UI lets you pick a ref).
### Placeholder mode
To put the site into "back shortly" mode, trigger Gitea → **Actions → deploy-placeholder → Run workflow**. To bring the real site back, trigger **Actions → deploy → Run workflow** on the desired `main` commit. Both workflows share the `deploy-production` concurrency group, so they can never run simultaneously.
The placeholder is hand-authored static HTML in `placeholder/` and does NOT depend on `npm`/`astro build` — it is deliberately decoupled from the main build so it can ship even when that build is broken.
**Scope note.** The placeholder serves HTTP 200 (not 503), so Cloudflare's edge will cache it normally. This is fine for short planned maintenance windows. For longer outages or incident fallback, purge Cloudflare's cache (or set a short-TTL Cache Rule for the maintenance window) before triggering recovery via `deploy.yml`, otherwise the edge may serve the placeholder past recovery until TTL expires.
**Security headers** (HSTS, CSP, X-Frame-Options, etc.) are owned by **Cloudflare Transform Rules**, not by anything in this repo. Hetzner Webhosting L ignores file-based `.htaccess` (`AllowOverride None`), so origin-side header config is impossible from code. See `OPERATOR-CHECKLIST.md` §2.
See [`OPERATOR-CHECKLIST.md`](./OPERATOR-CHECKLIST.md) for the one-time Hetzner + Cloudflare setup. See [`OPERATOR-CHECKLIST.md`](./OPERATOR-CHECKLIST.md) for the one-time Hetzner + Cloudflare setup.

View File

@@ -19,6 +19,11 @@ export default defineConfig({
vite: { vite: {
build: { build: {
cssMinify: 'lightningcss', cssMinify: 'lightningcss',
// Prevent Astro from inlining small scripts into the HTML.
// Without this, the hero rotator script (< 4 KB) gets inlined as a
// <script type="module"> tag, which violates CSP script-src 'self'
// (no 'unsafe-inline'). Setting 0 forces all scripts to external files.
assetsInlineLimit: 0,
}, },
}, },
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,518 @@
# Under-construction placeholder Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ship a branded "back shortly" page for cameleer.io plus a manual-trigger Gitea workflow that swaps it onto the live origin on demand, recoverable by re-running the existing `deploy.yml`.
**Architecture:** Standalone HTML in a top-level `placeholder/` directory, plus two PNG asset copies. A new `.gitea/workflows/deploy-placeholder.yml` rsyncs that directory to the same Hetzner docroot used by `deploy.yml` (`--delete` enabled, `deploy-production` concurrency group shared so the two workflows queue rather than race). No Astro build dependency, so the placeholder still ships when the main build is broken — which is the worst case where one is needed.
**Tech Stack:** Plain HTML5 + inlined CSS, Google Fonts (DM Sans, JetBrains Mono), bash + rsync over SSH:222, Vitest 1 for static-content assertions.
**Spec:** `docs/superpowers/specs/2026-04-25-under-construction-placeholder-design.md` (commit `9b0c36b`).
---
## File Structure
| File | Status | Responsibility |
|---|---|---|
| `placeholder/index.html` | Create | Static under-construction page. Single self-contained file, references the two sibling PNGs by relative path. Contains `__SALES_EMAIL__` substitution token (used twice — `mailto:` href and link text). |
| `placeholder/cameleer-logo.png` | Create (copy) | Hero logo. Copy of `public/icons/cameleer-192.png` (~36 KB). |
| `placeholder/favicon.png` | Create (copy) | Browser tab icon. Copy of `public/icons/cameleer-32.png` (~2.4 KB). |
| `src/placeholder.test.ts` | Create | Static assertions that the placeholder HTML has the contract the deploy workflow depends on (sentinel string, token, references, no JS, etc.). Lives in `src/` because `vitest.config.ts` only discovers `src/**/*.test.ts`. |
| `.gitea/workflows/deploy-placeholder.yml` | Create | Manual-dispatch workflow: substitute sales email → rsync `placeholder/` to docroot → smoke-test the live origin. |
| `README.md` | Modify | Append a "Placeholder mode" subsection under "Deployment". |
---
## Task 1: Add tests for the placeholder HTML
**Why first:** Establishes the contract the workflow depends on — sentinel string, substitution token, asset references — before any markup is written, so we can't accidentally drop one of them later.
**Files:**
- Create: `src/placeholder.test.ts`
- [ ] **Step 1: Write the failing test file**
Create `src/placeholder.test.ts` with this exact content:
```typescript
import { describe, it, expect } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const placeholderDir = join(process.cwd(), 'placeholder');
const indexPath = join(placeholderDir, 'index.html');
describe('placeholder/index.html', () => {
const html = readFileSync(indexPath, 'utf8');
it('starts with the HTML5 doctype', () => {
expect(html.toLowerCase().trimStart()).toMatch(/^<!doctype html>/);
});
it('has the back-shortly title', () => {
expect(html).toContain('<title>Cameleer — Back shortly</title>');
});
it('is not indexable by search engines', () => {
expect(html).toContain('<meta name="robots" content="noindex">');
});
it('declares the dark color-scheme matching the live site', () => {
expect(html).toContain('<meta name="color-scheme" content="dark">');
expect(html).toContain('<meta name="theme-color" content="#060a13">');
});
it('contains the sentinel string the deploy workflow greps for', () => {
// The workflow's post-deploy smoke test fails if this string is missing.
expect(html).toContain('Routes are remapping');
});
it('uses the live hero subhead verbatim', () => {
expect(html).toContain(
'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.'
);
});
it('contains __SALES_EMAIL__ tokens at both the mailto href and the link text', () => {
const matches = html.match(/__SALES_EMAIL__/g) ?? [];
expect(matches.length).toBeGreaterThanOrEqual(2);
});
it('contains no other __TOKEN__ style placeholders', () => {
// Guard against a forgotten token that would survive the sed substitution.
const allTokens = html.match(/__[A-Z][A-Z0-9_]+__/g) ?? [];
const nonSales = allTokens.filter((t) => t !== '__SALES_EMAIL__');
expect(nonSales).toEqual([]);
});
it('references the sibling cameleer-logo.png by relative path', () => {
expect(html).toContain('src="./cameleer-logo.png"');
});
it('references the sibling favicon.png by relative path', () => {
expect(html).toContain('href="./favicon.png"');
});
it('has no <script> tags (placeholder must work without JS)', () => {
expect(html).not.toMatch(/<script[\s>]/i);
});
});
describe('placeholder/ asset siblings', () => {
it('cameleer-logo.png exists on disk', () => {
expect(existsSync(join(placeholderDir, 'cameleer-logo.png'))).toBe(true);
});
it('favicon.png exists on disk', () => {
expect(existsSync(join(placeholderDir, 'favicon.png'))).toBe(true);
});
});
```
- [ ] **Step 2: Run the test suite to verify the new tests fail**
Run: `npm test`
Expected: vitest fails when loading `src/placeholder.test.ts` because `readFileSync` throws `ENOENT` on the missing `placeholder/index.html`. The pre-existing `src/middleware.test.ts` suite must still pass.
- [ ] **Step 3: Commit the failing tests**
```bash
git add src/placeholder.test.ts
git commit -m "test(placeholder): add static-content tests for under-construction page"
```
Note: this commit is intentionally a red bar. Task 2 turns it green in a single follow-up commit. If you prefer a green-only history, fold this commit into Task 2's commit at the end.
---
## Task 2: Create the placeholder page and copy assets
**Files:**
- Create: `placeholder/index.html`
- Create: `placeholder/cameleer-logo.png` (copy of `public/icons/cameleer-192.png`)
- Create: `placeholder/favicon.png` (copy of `public/icons/cameleer-32.png`)
- [ ] **Step 1: Copy the PNG assets into `placeholder/`**
The repo's full-resolution `public/cameleer-logo.svg` is 1.5 MB (embedded raster data) and is not used here. The 192 px PNG is the correct size and weight for the placeholder hero.
```bash
mkdir -p placeholder
cp public/icons/cameleer-192.png placeholder/cameleer-logo.png
cp public/icons/cameleer-32.png placeholder/favicon.png
ls -la placeholder/
```
Expected: both files present. `cameleer-logo.png` ~36 KB; `favicon.png` ~2.4 KB.
- [ ] **Step 2: Write `placeholder/index.html`**
Create `placeholder/index.html` with this exact content. Note both occurrences of `__SALES_EMAIL__` — the deploy workflow substitutes them via `sed ... -g`.
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#060a13">
<meta name="robots" content="noindex">
<meta name="description" content="Cameleer is briefly offline. We'll be back on the trail in a moment.">
<title>Cameleer — Back shortly</title>
<link rel="icon" type="image/png" sizes="32x32" href="./favicon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono&display=swap">
<style>
:root {
--bg: #060a13;
--accent: #f0b429;
--text: #e8eaed;
--text-muted: #9aa3b2;
--text-faint: #828b9b;
}
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background-color: var(--bg);
background-image: radial-gradient(60% 60% at 50% 50%, rgba(240, 180, 41, 0.10), transparent 70%);
color: var(--text);
font-family: 'DM Sans', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
main {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2.5rem 1.5rem;
text-align: center;
}
.logo {
width: 96px;
height: 96px;
margin: 0 0 1.75rem;
}
.eyebrow {
display: inline-block;
margin: 0 0 1.5rem;
font-size: 14px;
font-style: italic;
color: var(--accent);
}
h1 {
margin: 0 0 1.5rem;
font-weight: 700;
font-size: clamp(2.25rem, 4.5vw, 4rem);
line-height: 1.05;
letter-spacing: -0.02em;
max-width: 18ch;
}
.subhead {
margin: 0 0 2rem;
max-width: 42rem;
font-size: 1.125rem;
line-height: 1.55;
color: var(--text-muted);
}
.micro {
margin: 0;
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
font-size: 12px;
color: var(--text-faint);
}
.micro a { color: inherit; text-decoration: none; }
.micro a:hover, .micro a:focus { text-decoration: underline; }
</style>
</head>
<body>
<main>
<img class="logo" src="./cameleer-logo.png" alt="Cameleer" width="96" height="96">
<p class="eyebrow">✦ Routes are remapping.</p>
<h1>We're back on the trail<br>in a moment.</h1>
<p class="subhead">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.</p>
<p class="micro">cameleer.io · <a href="mailto:__SALES_EMAIL__">__SALES_EMAIL__</a></p>
</main>
</body>
</html>
```
- [ ] **Step 3: Run the placeholder tests to verify they pass**
Run: `npm test -- placeholder`
Expected: all tests in `src/placeholder.test.ts` pass. (`-- placeholder` is a vitest test-name filter that runs only the new file's `describe` blocks for fast feedback.)
- [ ] **Step 4: Run the full test suite to verify no regressions**
Run: `npm test`
Expected: every existing test still passes (the middleware/CSP suite is the only other one).
- [ ] **Step 5: Visual verification in a browser**
The `__SALES_EMAIL__` token will be visible in the rendered page — that is expected; it's substituted at deploy time. Confirm visual treatment.
```bash
npx serve placeholder -l 4322
# then open http://localhost:4322 in a browser
```
Confirm by eye:
1. Centered single-column layout, logo on top.
2. Dark background (#060a13) with a faint amber radial glow centered.
3. Italic amber eyebrow `✦ Routes are remapping.`.
4. Bold display heading wraps onto two lines on desktop ("We're back on the trail" / "in a moment.").
5. Subhead reads as muted body text below.
6. Mono microcopy at the bottom shows `cameleer.io · __SALES_EMAIL__` in faint grey, with the token rendered as a `mailto:` link.
7. Resize the window to ~360 px wide — layout stays centered, heading scales down via `clamp()`, no horizontal scroll.
Stop the server with Ctrl-C when done.
- [ ] **Step 6: Commit the placeholder page and assets**
```bash
git add placeholder/index.html placeholder/cameleer-logo.png placeholder/favicon.png
git commit -m "feat(placeholder): add under-construction page with branded teaser
Standalone HTML + two sibling PNGs, no Astro build dependency.
Carries __SALES_EMAIL__ substitution tokens that the new deploy
workflow replaces at deploy time."
```
---
## Task 3: Add the deploy-placeholder workflow
**Files:**
- Create: `.gitea/workflows/deploy-placeholder.yml`
- [ ] **Step 1: Write `.gitea/workflows/deploy-placeholder.yml`**
Create with this exact content. Mirrors `deploy.yml`'s SSH/rsync pattern but skips `npm ci`/`astro build`/Lighthouse — the placeholder is hand-authored static and must be deployable when the main build is broken.
```yaml
# -----------------------------------------------------------------------------
# cameleer-website — Deploy under-construction placeholder
#
# MANUAL TRIGGER ONLY. Replaces the live cameleer.io docroot with a static
# "back shortly" page. Recovery: trigger Actions → deploy → Run workflow on
# the desired main commit.
#
# Shares the deploy-production concurrency group with deploy.yml so the two
# workflows queue rather than race on the same docroot.
#
# This workflow does NOT run npm/astro. The placeholder is hand-authored
# static HTML in placeholder/, deliberately decoupled from the main build so
# it can ship even when the main build is broken (which is the worst case in
# which a placeholder is needed).
#
# Required secrets (repo settings → Actions → Secrets):
# SFTP_HOST, SFTP_USER, SFTP_PATH, SFTP_KEY, SFTP_KNOWN_HOSTS
# PUBLIC_SALES_EMAIL
# -----------------------------------------------------------------------------
name: deploy-placeholder
on:
workflow_dispatch:
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Substitute sales email into placeholder
env:
PUBLIC_SALES_EMAIL: ${{ secrets.PUBLIC_SALES_EMAIL }}
run: |
set -e
: "${PUBLIC_SALES_EMAIL:?PUBLIC_SALES_EMAIL secret must be set}"
sed -i "s|__SALES_EMAIL__|${PUBLIC_SALES_EMAIL}|g" placeholder/index.html
if grep -q '__SALES_EMAIL__' placeholder/index.html; then
echo "Token __SALES_EMAIL__ still present after substitution — refusing to ship."
exit 1
fi
- name: Configure SSH
env:
SFTP_KEY: ${{ secrets.SFTP_KEY }}
SFTP_KNOWN_HOSTS: ${{ secrets.SFTP_KNOWN_HOSTS }}
run: |
set -e
: "${SFTP_KEY:?SFTP_KEY secret must be set}"
: "${SFTP_KNOWN_HOSTS:?SFTP_KNOWN_HOSTS secret must be set}"
mkdir -p ~/.ssh
printf '%s\n' "$SFTP_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$SFTP_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
if ! command -v rsync >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi
$SUDO apt-get update -qq
$SUDO apt-get install -y --no-install-recommends rsync openssh-client
fi
- name: Deploy via rsync
env:
SFTP_USER: ${{ secrets.SFTP_USER }}
SFTP_HOST: ${{ secrets.SFTP_HOST }}
SFTP_PATH: ${{ secrets.SFTP_PATH }}
run: |
: "${SFTP_USER:?SFTP_USER secret must be set}"
: "${SFTP_HOST:?SFTP_HOST secret must be set}"
: "${SFTP_PATH:?SFTP_PATH secret must be set}"
# Hetzner Webhosting splits SSH into two ports:
# port 22 — SFTP only, no remote command exec
# port 222 — full SSH with shell exec (rsync needs this)
# `--rsync-path=/usr/bin/rsync` tells the local rsync where to find
# the remote binary on Hetzner's locked-down PATH.
# `BatchMode=yes` disables interactive prompts.
rsync -avz --delete --rsync-path=/usr/bin/rsync \
-e "ssh -p 222 -i $HOME/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts" \
placeholder/ "$SFTP_USER@$SFTP_HOST:$SFTP_PATH/"
- name: Post-deploy smoke test
run: |
set -e
echo "Confirming the placeholder is live on www.cameleer.io..."
BODY=$(curl -sf https://www.cameleer.io/)
echo "$BODY" | grep -qF 'Routes are remapping' \
|| { echo "Sentinel string missing — placeholder did not land."; exit 1; }
echo "$BODY" | grep -qF 'mailto:' \
|| { echo "mailto: link missing — sales email substitution may have failed."; exit 1; }
curl -sfI https://www.cameleer.io/cameleer-logo.png > /dev/null \
|| { echo "cameleer-logo.png not reachable on the live origin."; exit 1; }
echo "Placeholder is live."
```
- [ ] **Step 2: Verify the YAML parses**
Run a quick Node-based parse check (no extra dep needed; Node ships with no YAML parser, so use a one-off `npx`):
```bash
npx --yes js-yaml .gitea/workflows/deploy-placeholder.yml > /dev/null && echo "YAML OK"
```
Expected: `YAML OK`. If `js-yaml` errors, re-read the file for stray tabs or unbalanced quoting.
- [ ] **Step 3: Verify the existing deploy.yml is unchanged**
Run: `git diff .gitea/workflows/deploy.yml`
Expected: empty output (the new workflow is additive only).
- [ ] **Step 4: Commit the workflow**
```bash
git add .gitea/workflows/deploy-placeholder.yml
git commit -m "ci(deploy): add deploy-placeholder workflow
Manual-trigger workflow that substitutes PUBLIC_SALES_EMAIL into
placeholder/index.html, rsyncs placeholder/ to the Hetzner docroot
over SSH:222, then smoke-tests the live origin for the sentinel
string, mailto link, and logo URL.
Shares the deploy-production concurrency group with deploy.yml so
the two workflows can never race on the same docroot. Recovery is
the regular deploy.yml — no separate un-placeholder workflow."
```
---
## Task 4: Document the placeholder mode in the README
**Files:**
- Modify: `README.md` (append a subsection under "## Deployment", after the existing "Rollback" paragraph and before "**Security headers**")
- [ ] **Step 1: Read the current README to locate the insertion point**
Run: `cat README.md`
Locate the line `Rollback: trigger the deploy workflow on the previous \`main\` commit (Actions UI lets you pick a ref).`. The new subsection goes immediately after it, separated by a blank line, before the `**Security headers**` paragraph.
- [ ] **Step 2: Insert the placeholder-mode subsection**
Use the `Edit` tool with these exact arguments. `old_string` is the existing two-paragraph boundary; `new_string` reproduces it with the new `### Placeholder mode` subsection wedged in between.
`file_path`: `README.md`
`old_string`:
````
Rollback: trigger the deploy workflow on the previous `main` commit (Actions UI lets you pick a ref).
**Security headers** (HSTS, CSP, X-Frame-Options, etc.) are owned by **Cloudflare Transform Rules**, not by anything in this repo.
````
`new_string`:
````
Rollback: trigger the deploy workflow on the previous `main` commit (Actions UI lets you pick a ref).
### Placeholder mode
To put the site into "back shortly" mode, trigger Gitea → **Actions → deploy-placeholder → Run workflow**. To bring the real site back, trigger **Actions → deploy → Run workflow** on the desired `main` commit. Both workflows share the `deploy-production` concurrency group, so they can never run simultaneously.
The placeholder is hand-authored static HTML in `placeholder/` and does NOT depend on `npm`/`astro build` — it is deliberately decoupled from the main build so it can ship even when that build is broken.
**Security headers** (HSTS, CSP, X-Frame-Options, etc.) are owned by **Cloudflare Transform Rules**, not by anything in this repo.
````
- [ ] **Step 3: Verify the README still renders cleanly**
Run: `cat README.md | head -60`
Confirm by eye that the new subsection appears under "Deployment", the surrounding paragraphs are intact, and there is exactly one blank line between adjacent blocks.
- [ ] **Step 4: Commit the README update**
```bash
git add README.md
git commit -m "docs(readme): add placeholder mode section
Documents the deploy-placeholder workflow trigger and the recovery
path back to the real site via deploy.yml."
```
---
## Final verification
- [ ] **Run the full test suite one more time**
Run: `npm test`
Expected: all tests pass — both `src/middleware.test.ts` and `src/placeholder.test.ts`.
- [ ] **Confirm the four commits are in place**
Run: `git log --oneline -5`
Expected (top-down): README docs commit, deploy-placeholder.yml commit, placeholder feat commit, placeholder test commit, then the spec commit (`9b0c36b docs(spec): ...`).
- [ ] **Sanity-check the placeholder directory ships only what it should**
Run: `ls -la placeholder/`
Expected: exactly three files — `index.html`, `cameleer-logo.png`, `favicon.png`. No stray `.test.ts`, `.DS_Store`, etc. (If anything else appears, remove it before merging — `rsync --delete` would otherwise push it to the live origin.)
- [ ] **Push and trigger the first real run** (operator step, not part of the implementation)
Push the branch, merge to `main` once reviewed, then in Gitea: **Actions → deploy-placeholder → Run workflow** on `main`. Verify by visiting `https://www.cameleer.io/` that the placeholder renders, then trigger **Actions → deploy → Run workflow** to restore the real site.

View File

@@ -0,0 +1,244 @@
# Cameleer website — copy + brand refresh spec
**Date**: 2026-04-24
**Status**: Approved for implementation planning
**Project**: `cameleer-website`
**Supersedes sections of**: `2026-04-24-cameleer-website-design.md` — copy, logo, and tone only. Architecture, auth flow, hosting, and tech stack from the original spec remain in force.
---
## 1. Overview
The current marketing site sells Cameleer as **"zero-code observability for Apache Camel"**. This underrepresents the product: Cameleer is a **hosted runtime platform for Apache Camel** — the place you actually run your integrations, with deep tracing, replay, and live control baked in. Comparable to Mulesoft CloudHub in category, for teams who chose Apache Camel specifically to stay open and avoid vendor lock-in.
This spec refreshes the homepage copy, tone, and brand mark to match that positioning. Three goals:
1. Reposition from "observability tool" to "hosted Camel runtime with observability baked in".
2. Lighten the tone — less technical jargon, more outcome-focused language, two strategic humor pops.
3. Replace the placeholder topographic-wave icon with the real Cameleer product logo (camel + cameleer figure + compass rose on desert waves, amber).
No architecture changes. No new pages. No new dependencies. Pure content + asset refresh.
---
## 2. Positioning
### 2.1 New thesis
**Run Apache Camel without running Apache Camel.**
Cameleer is the hosted home for your Camel integrations — with deep tracing, replay, and live control built in. Because you chose Camel to stay free, not to stay up all night.
### 2.2 Rotating Hero positioning lines
Three lines rotate slowly in the Hero subline area:
1. **Run Apache Camel without running Apache Camel.**
2. **Camel integrations, minus the baggage.**
3. **Your camels, our caravan. You just ride.**
**Rotation behavior:**
- Fade swap every ~10 seconds (8s static + 2s fade).
- Paused on hover and on focus within the Hero.
- If `prefers-reduced-motion: reduce` is set — render the first line only, no rotation, no JS.
- Initial render shows line 1 so the critical content is meaningful without JS.
- Implementation: small inline `<script type="module">` in the Hero component. No third-party animation library.
**Accessibility:**
- The rotating container is the H1 — it is the primary page heading and the accessibility name of the document. Use `aria-live="off"` on the rotation wrapper so assistive technology does not announce every swap (announcing a different H1 every 10 seconds is hostile). All three lines render in the DOM; only one is visible. Visually-hidden siblings should have `aria-hidden="true"` to avoid duplicate heading announcements.
- Tab-focus anywhere inside the Hero pauses rotation (same pause rule as hover).
### 2.3 Terms to avoid on landing page
Remove from all landing page copy:
- `-javaagent`, `java agent`, `Docker`, `ByteBuddy`, `Spring Boot`, `Quarkus`, `JAR`
- `Prometheus`, `OpenTelemetry`, `CDI`, `JMX`
- "nanosecond", "45+ EIP node types" (acceptable in headings-off detail, not in headlines/H2s)
- "zero-code instrumentation" → replace with "no code changes"
Keep (audience vocabulary):
- Apache Camel, routes, processors, exchanges
- Replay, trace, correlation ID
- EIP — allowed once as a credibility signal, not a feature
Out-of-scope for this spec: the `/install` or `/docs` routes where `-javaagent` snippets belong. The existing homepage's bash snippet moves off the homepage — see §4.3.
---
## 3. Brand mark refresh
### 3.1 Assets to import
From `C:/Users/Hendrik/Documents/projects/design-system/assets/` into `cameleer-website/public/`:
- `cameleer-16.png``public/icons/cameleer-16.png`
- `cameleer-32.png``public/icons/cameleer-32.png`
- `cameleer-48.png``public/icons/cameleer-48.png`
- `cameleer-180.png``public/icons/cameleer-180.png` (Apple touch)
- `cameleer-192.png``public/icons/cameleer-192.png` (PWA/Android)
- `cameleer-512.png``public/icons/cameleer-512.png` (PWA/Android)
- `cameleer-logo.svg``public/cameleer-logo.svg` (vector, used in header)
- `cameleer-logo.png``public/cameleer-logo.png` (raster fallback for OG)
The SVG is 1.5 MB (not SVGO'd). It's used only in the header at ~32px. If the unminified size becomes a concern, run it through SVGO at build time — not in this spec's scope. For now, ship as-is and measure.
### 3.2 Header icon swap
`src/components/SiteHeader.astro`:
- Remove the inline 3-wavy-lines SVG.
- Replace with `<img src="/cameleer-logo.svg" width="32" height="32" alt="" decoding="async">`.
- Keep the existing `<a href="/" aria-label="Cameleer home">` wrapper and the "Cameleer" wordmark beside it.
- Keep the `group-hover:text-accent` transition on the wordmark only.
### 3.3 Favicon chain
Update `src/layouts/BaseLayout.astro` `<head>`:
- Primary: `<link rel="icon" href="/cameleer-logo.svg" type="image/svg+xml">`
- Fallback: `<link rel="icon" href="/icons/cameleer-32.png" sizes="32x32" type="image/png">`
- Apple: `<link rel="apple-touch-icon" href="/icons/cameleer-180.png">`
- Remove the existing `public/favicon.svg` (placeholder topographic lines).
### 3.4 Hero brand mark
In the Hero section, add a distinct small mark (4864px) next to or just above the rotating positioning line. Amber on transparent. Tasteful, not a watermark. The mark anchors the camel/desert/GPS humor visually — the humor lands harder when the camel is literally present.
### 3.5 OG image
Replace `public/og-image.svg` content with a new design built around the real logo + the new thesis "Run Apache Camel without running Apache Camel." Same dimensions (1200×630), same solid dark background. Exact layout is a creative execution task, not spec'd here.
---
## 4. Section-by-section copy changes
Each section below lists the current headline → new headline and notes on body copy changes. Specific body copy wording is implementation detail — the writer should follow the tone and term-use rules in §2.
### 4.1 Hero (`src/components/sections/Hero.astro`)
**Kicker** (small uppercase line above headline):
- Current: `Observability · Apache Camel`
- New: `Your camels called. They want a GPS.`
**Headline** (H1):
- Current: `See every route. / Reach into every flow.`
- New: `Run Apache Camel without running Apache Camel.` (the primary of three rotating lines)
- The H1 is the rotation target — it's the single most important piece of copy on the page and what gets indexed/shared.
**Subline**:
- Current: "Zero-code tracing, processor-level detail, and live control for Apache Camel — from a single `-javaagent` flag."
- New: "The hosted home for your Camel integrations — with deep tracing, replay, and live control built in. Because you chose Camel to stay free, not to stay up all night."
- Static — does not rotate.
**`-javaagent` code inline**: removed from Hero.
**CTAButtons**: unchanged.
**RouteDiagram**: unchanged (visual, not copy).
### 4.2 DualValueProps (`src/components/sections/DualValueProps.astro`)
Current three tiles lead with outcomes but bodies are tech-heavy. Rewrite bodies to be outcome- and feeling-led. No humor in this section.
Tile 1 — **Ship integrations, then sleep.**
Body reframe: you can see every route, every processor, every exchange, without writing a single line of tracing code. When something breaks, you know where and why.
Tile 2 — **Debug in daylight, not at 3 AM.**
Body reframe: replay failed exchanges, follow a single request across services, capture exactly what went wrong — with the pieces ops actually needs at 3 AM already captured so you don't need to be up at 3 AM.
Tile 3 — **Own your stack. No lock-in.**
Body reframe: you chose Apache Camel on purpose — open, portable, standard. Cameleer runs and observes your Camel apps as they are. No SDK, no code changes, no rewrite.
All three tile bodies: strip `-javaagent`, `45+ EIP`, "nanosecond". Keep them plain, outcome-first, ~2-3 sentences each.
### 4.3 HowItWorks (`src/components/sections/HowItWorks.astro`)
Section kicker unchanged: `For engineers`. Section H2 unchanged: `How it works`.
Three steps:
01 — **Point us at your Camel app**
Body: drop your app in, or connect an existing one. No code changes, no SDK, nothing to rewrite.
**Remove** the `java -javaagent:...` bash snippet entirely. The install detail belongs in docs, not on the landing page.
02 — **We take it from there**
Body: every route, processor, exchange, and dependency is discovered and traced automatically. Sensitive fields are masked by default.
03 — **Watch it run**
Body: browse executions, tap live traffic, replay failed exchanges, follow flows across services. Nothing to instrument, nothing to maintain.
Subtitle under "How it works" — current: "Three steps. No code changes. Works across Camel 4.x." — softens to: "Three steps. No code changes. Nothing to maintain."
### 4.4 WhyUs (`src/components/sections/WhyUs.astro`)
Section unchanged structurally. Kicker `Why Cameleer` and H2 stay. Body softening only:
Tile 1 — headline stays: "Generic APMs do not understand Camel. Cameleer does."
Body: rewrite to drop "45+ Apache Camel EIP node types", "bidirectional protocol", "signed config changes". Translate to plain value: "Most monitoring tools see your app as a Java process and a pile of HTTP calls. Cameleer understands that you're running a Camel app — it speaks choices, splits, multicasts, error handlers, and every EIP pattern as first-class citizens. When you ask 'why did this exchange fail?', you get an answer, not a log tail."
Tile 2 — headline tweak: "Built by people who know what 3 AM looks like."
Body: keep the nJAMS-legacy story but warmer. "The Cameleer team spent years building integration monitoring for banks, insurers, and logistics operators — the kind of shops where a stuck exchange at 3 AM means someone's phone is ringing. We know what integration teams actually need then, and what they never use. Cameleer is what we'd build today, purpose-built for Apache Camel."
Retain the trademark review note (nJAMS legacy).
### 4.5 PricingTeaser (`src/components/sections/PricingTeaser.astro`)
Kicker `Pricing` unchanged. H2 `Start free. Grow when you need to.` unchanged.
Subline current: "No credit card for the trial. See full comparison →"
Subline new: "No credit card. No sales call. Just a working trial in ten minutes."
Tier cards and prices unchanged.
### 4.6 FinalCTA (`src/components/sections/FinalCTA.astro`)
**Headline:**
- Current: `Start seeing your routes.`
- New: `Your camels called. Time to ride.`
**Subline:**
- Current: "14-day free trial. Your first app, instrumented and live in under 10 minutes."
- New: "14-day free trial. Your first Camel app, hosted, traced, and running in under ten minutes. No code changes. No camels harmed."
**CTAButtons**: unchanged.
---
## 5. Files touched
- `src/components/SiteHeader.astro` — logo swap (§3.2)
- `src/layouts/BaseLayout.astro` — favicon chain + OG image reference (§3.3)
- `src/components/sections/Hero.astro` — kicker, rotating H1, subline, brand mark (§4.1, §3.4, §2.2)
- `src/components/sections/DualValueProps.astro` — three tile copy rewrites (§4.2)
- `src/components/sections/HowItWorks.astro` — step copy rewrites + bash snippet removal (§4.3)
- `src/components/sections/WhyUs.astro` — tile body softening (§4.4)
- `src/components/sections/PricingTeaser.astro` — subline tweak (§4.5)
- `src/components/sections/FinalCTA.astro` — headline + subline (§4.6)
- `public/cameleer-logo.svg`, `public/cameleer-logo.png` — new assets (§3.1)
- `public/icons/cameleer-{16,32,48,180,192,512}.png` — new assets (§3.1)
- `public/favicon.svg` — deleted (§3.3)
- `public/og-image.svg` — redesigned around new thesis (§3.5)
---
## 6. Out of scope
- Docs/install page creation — the `-javaagent` snippet moves off the homepage but this spec does not create its new home. Follow-up.
- Logo SVG optimization (1.5 MB ship-as-is; measure before optimizing).
- A/B testing the positioning lines. Rotation serves all three; we read analytics separately if/when added.
- Pricing page (`/pricing`), imprint, privacy copy changes — this spec is homepage-only.
- i18n — English only. No translation work.
---
## 7. Open questions / risks
**Trademark — nJAMS legacy wording**: §4.4 Tile 2 mentions "the kind of shops where a stuck exchange at 3 AM means someone's phone is ringing." This is legacy nJAMS muscle memory, not new IP. Hendrik's trademark review gate from the original spec's §10 still applies before go-live.
**Positioning tension with docs**: once the landing page stops saying "`-javaagent`", SEO signals around that specific technical term weaken. Acceptable trade-off for the target audience (SMB+ Camel-chooser decision-makers), but if engineer-skewed traffic matters for discovery, consider a technical sub-page (`/how-it-runs`) or docs page carrying those keywords.
**Logo SVG weight**: 1.5 MB is large for a ~32px header render. If Lighthouse performance regresses, swap the header to reference `cameleer-logo.png` or an SVGO-minified copy.
**SEO title/description**: the BaseLayout `<title>` + meta description currently read "Zero-code observability for Apache Camel." These should update to match the new thesis ("Run Apache Camel without running Apache Camel."). Include this in the Hero section implementation task.

View File

@@ -0,0 +1,413 @@
# Cameleer website — relaunch design spec
**Date**: 2026-04-25
**Status**: Approved for implementation planning
**Project**: `cameleer-website`
**Supersedes (homepage composition only)**: `2026-04-24-cameleer-website-design.md` §6 (homepage layout) and `2026-04-24-cameleer-website-copy-refresh-design.md` §2 (rotating hero)
**Preserved from prior specs**: architecture, hosting, security posture, auth flow, audience model, tech stack, brand mark, design tokens
---
## 1. Overview
The current marketing site, while well-crafted at the component level, is strategically under-powered: a rotating H1 fails the 5-second test, the page leans 5+ camel puns deep, "3 AM" is repeated to slogan-status, there's no social proof anywhere, the screenshot is unannotated, and the homepage shows four pricing tiers (two of which say "Contact"). This spec rebuilds the homepage composition and copy to address those gaps.
**Three goals:**
1. Pass the 5-second test for two audiences in parallel (DevOps Engineers + IT Managers).
2. Carry the page on credible trust anchors despite having zero customer logos to ship.
3. Ship within the same static-only Astro 5 stack — no new dependencies, no backend, no forms, no analytics.
The pricing page (`/pricing.astro`) gets a tier-naming refresh; everything else outside the homepage stays as-is.
---
## 2. Non-goals (preserved from prior spec)
These remain explicitly **out of scope** — same list as `2026-04-24-cameleer-website-design.md` §3:
- Blog, docs, features page, about page, changelog, case studies (Docs/Changelog nav stubs deferred to a future phase — see §10)
- Contact forms, newsletter signup, lead-capture, email automation
- Interactive product demos, video players
- Analytics, tracking pixels, cookie consent banners
- Custom sign-up or sign-in forms (all auth redirects to Logto)
- Backend code on the marketing host
- Customer logos, attributed customer quotes (no customers ready to publish yet — see §4)
---
## 3. Audiences (unchanged)
Same two-audience model as the prior spec. Every section continues to label its primary audience lean:
| Audience | Role | What they need from the page |
|----------|------|------------------------------|
| **Managers** (amber) | Integration leads, architects who sign the check | Business outcomes, risk reduction, pedigree, pricing clarity |
| **Engineers** (cyan) | Camel developers, DevOps, SREs | Capability, mechanism, coverage, zero-friction mechanics |
The relaunch's H1 leans Manager-outcome on purpose — the IT Manager is the check-signer, "sleep through the night" speaks to both audiences (one runs the pager, one carries the cost of a bad pager night).
---
## 4. Trust anchors (the social proof problem)
The original site shipped with no social proof. This relaunch can't fix that with logos or attributed customer quotes — **none are ready to publish**. The two anchors we *can* lean on:
1. **Founder pedigree** — "15 years building integration monitoring for banks, insurers, and logistics operators." No prior-product name is used. The pedigree claim stands on the years and the customer-segment, not on a brand reference.
2. **Design-partner program**. Reframes the pre-customer state as a feature ("hand-picked early partners"), with a `mailto:` CTA to `PUBLIC_SALES_EMAIL` that the visitor can use to apply.
Both anchors live in a new dedicated **Social Proof Strip** section (§6.2) immediately below the hero.
---
## 5. Page structure — final 7 sections
| # | Section | Status | Source files |
|---|---------|--------|--------------|
| 1 | **Hero** | Rebuilt-in-place | `Hero.astro` |
| 2 | **Social proof strip** | NEW | `SocialProofStrip.astro` (new) |
| 3 | **3 AM walkthrough** | Replaces DualValueProps + ProductShowcase | `ThreeAmWalkthrough.astro` (new); old files deleted |
| 4 | **How it works** | Rebuilt-in-place | `HowItWorks.astro` |
| 5 | **Why Cameleer** | Rebuilt-in-place | `WhyUs.astro` |
| 6 | **Pricing teaser** | Rebuilt-in-place | `PricingTeaser.astro` |
| 7 | **Final CTA** | Rebuilt-in-place | `FinalCTA.astro` |
Order is deliberate — see §11 for the rationale (proof-first arc: hero → who built it → product walkthrough → how → why → pricing → close).
---
## 6. Section content
### 6.1 Hero (rebuilt)
**Audience lean**: Manager-outcome, with engineer-readable proof on the right.
**Layout**: existing 5/7 grid retained — left column copy, right column screenshot.
**Left column copy:**
- **Eyebrow pill** (mono, accent, italic — kept from current): `✦ Your camels called. They want a GPS.` — this is the *only* surviving camel pun on the homepage.
- **H1** (single, no rotator): **Ship Camel integrations. Sleep through the night.**
- **Sub** (~18px, muted): "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."
- **Primary CTA**: `Start free trial` (existing `CTAButtons` primary, no changes).
- **Secondary CTA**: `See it in action ↓` — anchor link to `#walkthrough`. New, replaces the existing `Sign in` button which moves to top-right nav only.
- **Microline** (mono, muted, ~12px) under CTAs: `14-day trial · from €20/mo · no credit card`.
**Right column**:
- Existing `Lightbox` of `/product/exchange-detail.png` is retained.
- Three numbered annotation pins overlaid on the image via positioned absolute spans (no PNG re-bake). Pins coordinate with three short callout labels rendered *below* the image (not on it):
1. **Correlation ID** — "Click to follow one exchange across services."
2. **Failure in context** — "Circuit breaker tripped. Fallback ran. Tried `backend:80`."
3. **Full error pinned** — "Exception, stack trace, headers, payload — all here."
- The pin-to-callout mapping is achieved with `aria-describedby` so screen-readers reach the explanation.
- The hero-mark sway and topographic background are retained.
**Hero rotator removed.** The three rotating lines are not rehomed — the eyebrow pill carries the brand voice, the H1 carries the product claim. This is a deliberate reduction in pun count from 5+ to 1.
### 6.2 Social proof strip (NEW)
**Audience lean**: Both — managers see pedigree, engineers see honest framing.
**Component**: `SocialProofStrip.astro` (new, in `src/components/sections/`).
**Layout**: full-width section, dark, no card-style border. Visually quieter than the surrounding sections so it reads as a *trust line*, not a feature block. Vertical padding ~py-16.
**Content** (single content column, `max-w-3xl mx-auto px-6`):
- **Eyebrow** (mono, amber, ~12px): `// Built by people who've done this before`
- **Quote block** (italic, ~17px, max 62ch, accent-colored 3px left border, padding-left ~20px):
> *"We spent 15 years building integration monitoring for banks that couldn't afford downtime. Cameleer is what we'd build today — purpose-built for Apache Camel, no retrofit."*
- **Attribution** (~13px, muted, mono): `— [Founder Name], co-founder`
- **Below attribution** (~24px gap, then a single `mailto:`-styled CTA in mono+cyan): `Apply to the design-partner program →`
**`<!-- PENDING -->` gates** (do not ship without resolving):
- `[Founder Name]` is a placeholder. Must be filled in pre-publish.
**Design-partner CTA target**: built inline in `SocialProofStrip.astro` using `auth.salesEmail` (not `auth.salesMailto`, which has no subject helper):
```astro
href={`mailto:${auth.salesEmail}?subject=${encodeURIComponent('Design partner enquiry — Cameleer')}`}
```
No body — let the applicant introduce themselves.
### 6.3 3 AM walkthrough (replaces DualValueProps + ProductShowcase)
**Audience lean**: Engineer-pain on the left, Engineer-resolution on the right; Manager reads the contrast.
**Component**: `ThreeAmWalkthrough.astro` (new). Old `DualValueProps.astro` and `ProductShowcase.astro` are deleted.
**Section id**: `walkthrough` (anchor target for the hero secondary CTA).
**Layout**: a two-column responsive split that collapses to stacked at `md` and below. Below the split, a single row of three short callouts.
**Header block**:
- **Eyebrow** (mono, cyan): `// When something breaks`
- **H2**: **The 3 AM page. With and without Cameleer.**
- **Sub** (one line): "Same Camel app. Same failed exchange. Different night."
**Split — left column ("Without Cameleer")**:
- Container: rounded card, `bg-bg`, dashed border, mono font.
- Top tag (`text-text-faint`, mono, uppercase, tracking): `Without Cameleer · 03:12 AM`.
- Body: a styled `<pre>` block with realistic Camel-ish log content. Approximate content (final wording in implementation):
```
$ kubectl logs camel-router-7d4f8c
... [stack-trace excerpt — Camel-style line refs] ...
$ grep "order-842" *.log
router-3.log: WARN exchange order-842 stuck in saga-fulfillment
router-3.log: ERROR processor backend:80 → connect timeout
$ ssh prod-integration-3
prod-integration-3 $ kubectl logs ...
> slack #integration-team
"anyone know why order-842 is stuck??"
[3 of 4 reactions]
~47 min later: someone wakes up an SRE.
```
- Tone: muted/red-tinged for the stack trace lines, neutral muted for the rest, accent (sad amber) on the "47 min" closer.
**Split — right column ("With Cameleer")**:
- Container: rounded card, `bg-bg-elevated`, accent-tinted border, retains the same `box-shadow` glow as the existing showcase.
- Top tag (mono, accent): `With Cameleer · 30 sec`.
- Body: existing `Lightbox` of `/product/error-detail.png` (or a tighter crop — implementation may use the existing image). No annotation pins here — the hero already does the annotated-image pattern; this section is about the *contrast*, not the deep-dive.
- Below the image, a single line: `▸ Open exchange order-842 → see the failure pinned → click Replay after fix.`
**Below the split — three short callouts** (3-up grid at `lg`, stacks below):
1. **Cross-service correlation** — "Every exchange carries its correlation ID forward. One click jumps to what the downstream route did with the same message."
2. **Runtime detail, not guesswork** — "Circuit breaker tripped. Fallback path ran. Request tried `backend:80`. The pieces a 3 AM page actually needs — already captured."
3. **The whole story of a failure** — "Exception class, message, stack trace, headers, payload — all pinned to the exchange. No log-grepping tour."
(These three are the existing `ProductShowcase.astro` callouts, kept verbatim. They earn their place after the dramatic split — they explain *what* you saw on the right.)
**Asset note**: the "Without Cameleer" content is implemented as a styled `<pre>` block within the Astro component — no PNG asset required, no Photoshop, no licensing. A future phase may swap to a recorded terminal screenshot if it tests better; the swap is a one-component change.
### 6.4 How it works (rebuilt-in-place)
**Audience lean**: Engineer.
Same 3-step layout, same component. Slim the copy:
| Step | Title | Body (final) |
|------|-------|--------------|
| 01 | Point us at your Camel app | Drop it in, or connect one you already run. No code changes. |
| 02 | We take it from there | Every route, every processor, every exchange — discovered and traced automatically. Sensitive fields are masked by default. |
| 03 | Watch it run | Browse executions, tap live traffic, replay failed exchanges, follow flows across services. |
**Cuts**: the redundant "No SDK. Nothing to rewrite." line on step 1 (already said in Hero), and the duplicate "Nothing to instrument. Nothing to maintain." at the end of step 3 (two consecutive sentences saying the same thing).
### 6.5 Why Cameleer (rebuilt-in-place)
**Audience lean**: Manager.
Same 2-card layout. Two changes:
1. **Cut the giant `03:00` decorative watermark** on card 2. The 3 AM beat is now told in the walkthrough; repeating it as a wall-decal here is the "five hits on one metaphor" the roast flagged.
2. **Reword card 2** to remove the second 3 AM reference. New body (replacing both paragraphs):
- **H3**: **Built by people who've operated integration in production for 15 years.**
- **P1**: "We spent over a decade building integration monitoring for banks, insurers, and logistics operators — the kind of shops where a stuck exchange is a regulatory event, not just an inconvenience."
- **P2**: "Cameleer is what we'd build today, purpose-built for Apache Camel. No legacy, no retrofit, no assumptions about a generic middleware platform."
The "3 AM" phrasing is dropped; the pedigree claim is now told once in §6.2 (with the founder face) and again here (with the philosophy). Two distinct beats, not the same beat twice.
Card 1 (Generic APMs) stays as-is.
### 6.6 Pricing teaser (rebuilt-in-place)
**Audience lean**: Both.
**Tier rename across both `index.astro` and `/pricing.astro`**:
| Old name | New name | Price (unchanged) | Homepage teaser? |
|----------|----------|--------------------|-------------------|
| Trial | **Trial** | Free · 14 days | ✓ |
| MID | **Starter** | 20 € /mo | ✓ (highlight) |
| HIGH | **Scale** | Contact | — link only |
| BUSINESS | **Enterprise** | Contact | — link only |
The first tier keeps the name **Trial** (not "Free") because it's time-limited; the price column shows "Free" but the tier name stays honest about the 14-day cap.
**Homepage teaser layout**: 2 cards (Trial + Starter), Starter retains the `★ MOST POPULAR` ribbon.
**CTA labels** on the two cards:
- Trial card: `Start free trial` → `auth.signUpUrl`
- Starter card: `Start on Starter` → `auth.signUpUrl` (was: `Start on MID`)
**Below the cards**: a single line link: `See all plans (Scale, Enterprise) →` to `/pricing`. Replaces the inline "See full comparison →" link in the heading area.
**`/pricing.astro` page**: identical structure to today, but tier names updated to the renamed taxonomy and CTA labels updated accordingly.
### 6.7 Final CTA (rebuilt-in-place)
- **H2**: **Ship integrations. Sleep through the night.** (echoes H1 — bookend pattern, intentional repetition).
- **Sub**: "14-day free trial. Your first Camel app, hosted, traced, and running in under ten minutes. No code changes."
- **CTA**: `Start free trial` (single, primary). `CTAButtons` rendered with `showSecondary={false}`.
**Cut**: the entire `Your camels called. Time to ride.` line and the `No camels harmed.` aside.
---
## 7. Navigation (unchanged)
`SiteHeader.astro` keeps its current structure: Logo · Pricing · `[Sign in]` · `[Start free trial]`.
Docs / Changelog nav stubs are deferred to a follow-up phase. A "coming soon" stub page is worse than no nav entry. When real Docs or a real Changelog exists, this nav grows.
---
## 8. Voice and pun budget
**One camel pun on the homepage.** The eyebrow pill `Your camels called. They want a GPS.` is retained — it sits under the H1 (doesn't compete), it's witty without being a punchline, and it gives the brand voice one signature beat.
**Removed from the homepage:**
- `Camel integrations, minus the baggage.` (rotator line 2)
- `Your camels, our caravan. You just ride.` (rotator line 3)
- `Your camels called. Time to ride.` (FinalCTA H2 — replaced)
- `No camels harmed.` (FinalCTA sub — removed)
**3 AM mentions** are now told as one beat in §6.3 (the walkthrough) and one micro-mention in the H1 sub. The `03:00` decorative watermark on `WhyUs.astro` card 2 is cut. Three references → two. Slogan-status → narrative status.
---
## 9. Component-level changes
**New files:**
- `src/components/sections/SocialProofStrip.astro`
- `src/components/sections/ThreeAmWalkthrough.astro`
**Deleted files:**
- `src/components/sections/DualValueProps.astro`
- `src/components/sections/ProductShowcase.astro`
**Modified files:**
- `src/components/sections/Hero.astro` — rotator removed, eyebrow + H1 + sub + secondary CTA + microline + annotation pins
- `src/components/sections/HowItWorks.astro` — body copy slimmed (steps 1, 3)
- `src/components/sections/WhyUs.astro` — `03:00` watermark removed, card 2 reworded, founder/3-AM language refactored
- `src/components/sections/PricingTeaser.astro` — tier renaming + 2-card homepage layout + "See all plans" link
- `src/components/sections/FinalCTA.astro` — H2 + sub + single-CTA reworked
- `src/components/CTAButtons.astro` — accept new optional `secondaryLabel`/`secondaryHref` overrides for hero's `See it in action ↓` (already supported by existing prop interface — no API change required)
- `src/pages/index.astro` — section order updated, deleted/new components wired
- `src/pages/pricing.astro` — tier renaming applied to the full table
**Unchanged:**
- `src/components/SiteHeader.astro`
- `src/components/SiteFooter.astro`
- `src/components/Lightbox.astro`
- `src/components/RouteDiagram.astro`
- `src/components/TopographicBg.astro`
- `src/config/auth.ts`
- `src/middleware.ts`
- `tailwind.config.mjs` (no new tokens needed)
- `astro.config.mjs`
**Also modified:**
- `src/styles/global.css` — add `html { scroll-behavior: smooth; }` plus a `@media (prefers-reduced-motion: reduce)` override resetting it to `auto`. Required for the hero secondary CTA's anchor scroll to feel natural.
---
## 10. Asset deliverables
**No new image assets required for v1 of the relaunch.** Specifically:
- The "Without Cameleer" panel in §6.3 is a styled `<pre>` block. No screenshot needed.
- The "With Cameleer" panel reuses the existing `/product/error-detail.png`.
- Hero annotation pins are HTML/CSS overlays on the existing `/product/exchange-detail.png` — no PNG re-bake.
**Future enhancement candidates** (not blocking this relaunch):
- Replace the `<pre>` "without" panel with a recorded terminal screencap (PNG/SVG).
- Re-bake `/product/exchange-detail.png` with annotations baked in if HTML overlays prove brittle on responsive breakpoints.
---
## 11. Why this order works (proof-first arc)
The H1 makes a strong claim ("30-second answer at 3 AM"). The page below has to deliver proof in order:
1. **Hero** — claim + annotated screenshot (claim + a thumbnail of the proof).
2. **Social proof strip** — *who* is making this claim, why you should believe them.
3. **3 AM walkthrough** — *show* the 30-second answer side-by-side with the alternative.
4. **How it works** — by now the visitor wants to know "ok how do I get this".
5. **Why Cameleer** — manager-voiced reassurance: "purpose-built, not generic."
6. **Pricing teaser** — clarity, two cards, no contact-sales wall.
7. **Final CTA** — the bookend.
A traditional SaaS layout (features → benefits → how → pricing) would put proof in third-party logos before the product walkthrough. We don't have those, so the walkthrough *is* the proof, and the strip + Why Cameleer carry the credibility load.
---
## 12. Validation
**CI gates that must pass on the branch before merge** (existing infrastructure):
- `npm run test` — vitest passes (auth config + middleware tests; the relaunch shouldn't touch these).
- `npm run build` — Astro static build completes with no errors.
- `npm run lint:html` — html-validate passes on `dist/`.
- `npm run lint:links` — linkinator passes on `dist/` (the new `#walkthrough` anchor is internal — link-checker should accept it).
- `npm run lh` — Lighthouse CI ≥ 0.95 on all 4 categories.
**Manual QA checklist** (operator runs before publish):
- [ ] Hero secondary CTA "See it in action ↓" scrolls smoothly to `#walkthrough` (requires adding `html { scroll-behavior: smooth; }` to `src/styles/global.css`, with a `@media (prefers-reduced-motion: reduce)` override that sets it back to `auto` — see §9).
- [ ] At `<= md` breakpoint, the walkthrough split stacks with no horizontal scroll.
- [ ] Annotation pins on the hero screenshot remain positioned correctly across the breakpoints we currently support.
- [ ] `prefers-reduced-motion: reduce` disables the hero-mark sway and any tile-rise animations (existing handling preserved).
- [ ] Tab focus order on the homepage is: nav → hero primary → hero secondary → social-proof CTA → walkthrough CTA targets … → final CTA.
- [ ] All `mailto:` links open with the correct subject (design-partner CTA + sales contacts).
- [ ] Founder name placeholder is filled in `SocialProofStrip.astro` before publish.
**Pre-publish blockers** (recorded in code as `<!-- PENDING -->` HTML comments):
1. `[Founder Name]` placeholder in `SocialProofStrip.astro` — must be replaced with a real name.
This is deliberately surfaced as a code-level TODO rather than a spec-level open question so the operator can't accidentally publish with the placeholder intact.
---
## 13. Out-of-scope future enhancements
Listed here so they aren't lost — none are part of this spec:
- Customer logo strip in the social-proof section once at least one named pilot agrees to publish.
- Attributed customer quote + concrete metric (replaces the founder quote when available).
- Competitive comparison table (cut from this relaunch — revisit after a clear competitive narrative emerges with real customers).
- Docs and Changelog nav entries (gated on real content existing).
- Cloudflare Web Analytics opt-in (pending DSGVO review and a need that isn't met by Cloudflare's own zone analytics).
- Recorded terminal screencap for the "Without Cameleer" panel.
- A second variant of the H1 (or different secondary CTA) once enough traffic exists for an honest A/B test.
---
## 14. Decision log (for future reference)
Decisions made during the brainstorming session that aren't otherwise visible in the document:
| Decision | Choice | Why |
|----------|--------|-----|
| Relaunch ambition | Structural relaunch (composition + copy, not full reposition) | Static stack works; the gap is composition, not infrastructure |
| H1 voice | Manager-outcome ("Sleep through the night") | Check-signer audience; "sleep" works for both engineers and managers |
| Hero rotator | Killed | Two of three rotator lines were vibe-only — coin-flip on the 5-second test |
| Trust anchor | Founder quote + design-partner CTA | No customer logos available; pedigree + honest pre-customer framing |
| Comparison table | Cut | Not enough competitive narrative to build it honestly today |
| Tier names | Trial / Starter / Scale / Enterprise | Procurement-friendly, no internally-coded labels |
| Hero secondary CTA | `See it in action ↓` to `#walkthrough` | Low-commitment escape hatch; keeps visitor on-page |
| 3 AM walkthrough | Before/after split with real-feeling content | Strongest dramatic contrast; no competitor exposure |
| "Without Cameleer" image format | Styled `<pre>` block | No asset production blocking the relaunch |
| Nav additions (Docs/Changelog) | Deferred | Stub pages worse than no entry; revisit when real content exists |
| Pun budget | 1 (eyebrow pill) | Cut from 5+ to 1; voice survives, novelty tax doesn't |

View File

@@ -0,0 +1,133 @@
# 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 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`)
```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.

View File

@@ -2,6 +2,7 @@
"recurse": true, "recurse": true,
"silent": true, "silent": true,
"skip": [ "skip": [
"^https://app\\.cameleer\\.io",
"^https://auth\\.cameleer\\.io", "^https://auth\\.cameleer\\.io",
"^https://platform\\.cameleer\\.io", "^https://platform\\.cameleer\\.io",
"^https://www\\.cameleer\\.io", "^https://www\\.cameleer\\.io",

View File

@@ -13,6 +13,7 @@
"test:watch": "vitest", "test:watch": "vitest",
"lint:html": "html-validate \"dist/**/*.html\"", "lint:html": "html-validate \"dist/**/*.html\"",
"lint:links": "linkinator dist --recurse --silent", "lint:links": "linkinator dist --recurse --silent",
"optimize:images": "node scripts/optimize-product-images.mjs",
"lh": "lhci autorun" "lh": "lhci autorun"
}, },
"dependencies": { "dependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
placeholder/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

93
placeholder/index.html Normal file
View File

@@ -0,0 +1,93 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#060a13">
<meta name="robots" content="noindex">
<meta name="description" content="Cameleer is briefly offline. We'll be back on the trail in a moment.">
<title>Cameleer — Back shortly</title>
<link rel="icon" type="image/png" sizes="32x32" href="./favicon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono&display=swap">
<style>
:root {
--bg: #060a13;
--accent: #f0b429;
--text: #e8eaed;
--text-muted: #9aa3b2;
--text-faint: #828b9b;
}
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background-color: var(--bg);
background-image: radial-gradient(60% 60% at 50% 50%, rgba(240, 180, 41, 0.10), transparent 70%);
color: var(--text);
font-family: 'DM Sans', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
main {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2.5rem 1.5rem;
text-align: center;
}
.logo {
width: 96px;
height: 96px;
margin: 0 0 1.75rem;
}
.eyebrow {
display: inline-block;
margin: 0 0 1.5rem;
font-size: 14px;
font-style: italic;
color: var(--accent);
}
h1 {
margin: 0 0 1.5rem;
font-weight: 700;
font-size: clamp(2.25rem, 4.5vw, 4rem);
line-height: 1.05;
letter-spacing: -0.02em;
max-width: 18ch;
}
.subhead {
margin: 0 0 2rem;
max-width: 42rem;
font-size: 1.125rem;
line-height: 1.55;
color: var(--text-muted);
}
.micro {
margin: 0;
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
font-size: 12px;
color: var(--text-faint);
}
.micro a { color: inherit; text-decoration: none; }
.micro a:hover, .micro a:focus { text-decoration: underline; }
</style>
</head>
<body>
<main>
<img class="logo" src="./cameleer-logo.png" alt="Cameleer" width="96" height="96">
<p class="eyebrow">✦ Routes are remapping.</p>
<h1>We're back on the trail<br>in a moment.</h1>
<p class="subhead">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.</p>
<p class="micro">cameleer.io · <a href="mailto:__SALES_EMAIL__">__SALES_EMAIL__</a></p>
</main>
</body>
</html>

View File

@@ -1,55 +0,0 @@
# ---------------------------------------------------------------
# www.cameleer.io — Apache config at the Hetzner origin.
# Defense in depth: Cloudflare handles most of this at the edge;
# these rules make sure the origin is hardened even without the CDN.
# ---------------------------------------------------------------
# Enable rewriting
RewriteEngine On
# Force HTTPS — redundant with Cloudflare but belts-and-braces.
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Redirect apex -> www.
RewriteCond %{HTTP_HOST} ^cameleer\.io$ [NC]
RewriteRule ^(.*)$ https://www.cameleer.io/$1 [L,R=301]
# Disable directory listings.
Options -Indexes
# Block access to dotfiles and sensitive extensions that should never be here.
<FilesMatch "^\.|\.(env|ini|log|sh|bak|sql|git)$">
Require all denied
</FilesMatch>
# Prevent MIME sniffing, clickjacking, etc. (primary copy also comes from Astro middleware
# and Cloudflare Transform Rules — these apply if either layer is bypassed).
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Cache hashed build assets aggressively; HTML must be revalidated.
<FilesMatch "\.(css|js|woff2|svg|png|jpg|jpeg|webp|ico)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.html$">
Header set Cache-Control "public, max-age=3600, must-revalidate"
</FilesMatch>
# Remove Server header leak where possible.
Header unset X-Powered-By
</IfModule>
# Compression (Hetzner supports mod_deflate).
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml text/plain
</IfModule>
# Custom error pages (optional — fall back to default if not present).
ErrorDocument 404 /404.html
ErrorDocument 403 /404.html

BIN
public/cameleer-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

112
public/cameleer-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#060a13"/>
<g fill="none" stroke="#f0b429" stroke-width="1.4" stroke-linecap="round">
<path d="M4 10 Q10 6 16 12 T28 10"/>
<path d="M4 16 Q10 12 16 18 T28 16"/>
<path d="M4 22 Q10 18 16 24 T28 22"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -1,13 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
<rect width="1200" height="630" fill="#060a13"/> <rect width="1200" height="630" fill="#060a13"/>
<g fill="none" stroke="#f0b429" stroke-width="1" opacity="0.2"> <!-- Subtle topographic lines -->
<path d="M0,120 Q300,60 600,150 T1200,120"/> <g fill="none" stroke="#f0b429" stroke-width="1.2" stroke-linecap="round" opacity="0.10">
<path d="M0,240 Q300,180 600,270 T1200,240"/> <path d="M0 140 Q200 80 400 160 T800 140 T1200 160"/>
<path d="M0,360 Q300,300 600,390 T1200,360"/> <path d="M0 250 Q200 190 400 270 T800 250 T1200 270"/>
<path d="M0,480 Q300,420 600,510 T1200,480"/> <path d="M0 360 Q200 300 400 380 T800 360 T1200 380"/>
<path d="M0 470 Q200 410 400 490 T800 470 T1200 490"/>
</g> </g>
<text x="80" y="260" fill="#f0b429" font-family="'DM Sans', sans-serif" font-size="22" letter-spacing="6">OBSERVABILITY · APACHE CAMEL</text> <!-- Product mark (fallback: amber circle w/ wordmark — real PNG logo embedded would exceed OG size budget) -->
<text x="80" y="360" fill="#e8eaed" font-family="'DM Sans', sans-serif" font-size="72" font-weight="700">See every route.</text> <g transform="translate(90, 110)">
<text x="80" y="440" fill="#e8eaed" font-family="'DM Sans', sans-serif" font-size="72" font-weight="700">Reach into every flow.</text> <circle cx="60" cy="60" r="58" fill="none" stroke="#f0b429" stroke-width="2"/>
<text x="80" y="540" fill="#9aa3b2" font-family="'JetBrains Mono', monospace" font-size="26">cameleer.io</text> <text x="60" y="74" text-anchor="middle" font-family="DM Sans, system-ui, sans-serif" font-size="40" font-weight="700" fill="#f0b429">C</text>
</g>
<text x="220" y="190" font-family="DM Sans, system-ui, sans-serif" font-size="28" font-weight="600" fill="#f0b429" letter-spacing="3">CAMELEER</text>
<text x="90" y="340" font-family="DM Sans, system-ui, sans-serif" font-size="64" font-weight="700" fill="#f4f5f7">Run Apache Camel</text>
<text x="90" y="420" font-family="DM Sans, system-ui, sans-serif" font-size="64" font-weight="700" fill="#f4f5f7">without running Apache Camel.</text>
<text x="90" y="500" font-family="DM Sans, system-ui, sans-serif" font-size="28" font-weight="400" fill="#a0a8b8">The hosted home for your Camel integrations.</text>
<text x="90" y="540" font-family="DM Sans, system-ui, sans-serif" font-size="28" font-weight="400" fill="#a0a8b8">Deep tracing, replay, and live control — built in.</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 921 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View File

@@ -0,0 +1,48 @@
// Generate WebP variants of source PNGs in public/product/.
// Run after replacing/adding a source PNG; outputs are committed.
//
// For each <name>.png we emit:
// <name>-1280.webp (q=82, used as inline srcset for desktop ≤ ~1280 px)
// <name>-1920.webp (q=80, used as inline srcset for retina/wide viewports
// and as the lightbox-modal full-size source)
//
// The original .png is kept as a <picture> fallback for the rare browser
// without WebP support (~2 % globally).
import { readdir, stat } from 'node:fs/promises';
import { join, parse } from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
const SRC_DIR = fileURLToPath(new URL('../public/product/', import.meta.url));
const VARIANTS = [
{ width: 1280, quality: 82, suffix: '-1280' },
{ width: 1920, quality: 80, suffix: '-1920' },
];
const entries = await readdir(SRC_DIR);
const pngs = entries.filter((f) => f.toLowerCase().endsWith('.png'));
if (pngs.length === 0) {
console.error(`No PNGs found in ${SRC_DIR}`);
process.exit(1);
}
for (const file of pngs) {
const { name } = parse(file);
const inputPath = join(SRC_DIR, file);
const inputBytes = (await stat(inputPath)).size;
console.log(`\n${file} (${(inputBytes / 1024).toFixed(0)} KiB)`);
for (const v of VARIANTS) {
const outName = `${name}${v.suffix}.webp`;
const outPath = join(SRC_DIR, outName);
const info = await sharp(inputPath)
.resize({ width: v.width, withoutEnlargement: true })
.webp({ quality: v.quality, effort: 6 })
.toFile(outPath);
const pct = ((1 - info.size / inputBytes) * 100).toFixed(0);
console.log(`${outName} ${(info.size / 1024).toFixed(0)} KiB (-${pct}%)`);
}
}

View File

@@ -0,0 +1,264 @@
---
interface Props {
src: string;
alt: string;
width: number;
height: number;
loading?: 'eager' | 'lazy';
fetchpriority?: 'high' | 'low' | 'auto';
caption?: string;
triggerClass?: string;
imgClass?: string;
/** Set to false for sources without sibling .webp variants. */
optimized?: boolean;
/** CSS sizes attribute for the WebP srcset. Defaults to a mobile-first guess. */
sizes?: string;
}
const {
src,
alt,
width,
height,
loading = 'lazy',
fetchpriority,
caption,
triggerClass = '',
imgClass = 'block w-full h-auto',
optimized = true,
sizes = '(min-width: 1024px) 56vw, 100vw',
} = Astro.props;
// Auto-derive WebP variant paths from the PNG src. The optimize-product-images
// script emits <name>-1280.webp and <name>-1920.webp next to each <name>.png.
const webpBase = optimized && /\.png$/i.test(src) ? src.replace(/\.png$/i, '') : null;
const webpSrcset = webpBase ? `${webpBase}-1280.webp 1280w, ${webpBase}-1920.webp 1920w` : null;
const webpDialogSrc = webpBase ? `${webpBase}-1920.webp` : src;
// Unique per-instance id so multiple lightboxes on a page do not collide.
const dialogId = `lb-${Math.random().toString(36).slice(2, 10)}`;
---
<button
type="button"
class={`lightbox-trigger group ${triggerClass}`.trim()}
data-lightbox-open={dialogId}
aria-label={`Enlarge: ${alt}`}
>
{webpSrcset ? (
<picture>
<source type="image/webp" srcset={webpSrcset} sizes={sizes} />
<img
src={src}
alt={alt}
width={width}
height={height}
loading={loading}
decoding="async"
fetchpriority={fetchpriority}
class={imgClass}
/>
</picture>
) : (
<img
src={src}
alt={alt}
width={width}
height={height}
loading={loading}
decoding="async"
fetchpriority={fetchpriority}
class={imgClass}
/>
)}
<span aria-hidden="true" class="lightbox-zoom-badge">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7.5"/>
<path d="m20 20-3.5-3.5"/>
<line x1="11" y1="8" x2="11" y2="14"/>
<line x1="8" y1="11" x2="14" y2="11"/>
</svg>
</span>
</button>
<dialog id={dialogId} class="lightbox-dialog" aria-modal="true" aria-labelledby={`${dialogId}-title`}>
<h2 id={`${dialogId}-title`} class="sr-only">{alt}</h2>
<form method="dialog" class="lightbox-close-form">
<button type="submit" class="lightbox-close" aria-label="Close">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"/>
<path d="m6 6 12 12"/>
</svg>
</button>
</form>
<img src={webpDialogSrc} alt={alt} class="lightbox-image" loading="lazy" decoding="async" />
{caption && <p class="lightbox-caption">{caption}</p>}
</dialog>
<style>
/* Trigger: reset native button chrome, keep block layout matching the old <img>. */
.lightbox-trigger {
all: unset;
display: block;
position: relative;
cursor: zoom-in;
width: 100%;
}
.lightbox-trigger:focus-visible {
outline: 2px solid #f0b429;
outline-offset: 4px;
border-radius: 0.5rem;
}
/* Zoom pill (fades in on hover / keyboard focus) */
.lightbox-zoom-badge {
position: absolute;
top: 0.85rem;
right: 0.85rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 9999px;
background: rgba(240, 180, 41, 0.92);
color: #060a13;
opacity: 0;
transform: translateY(-3px);
transition: opacity 180ms ease-out, transform 180ms ease-out;
pointer-events: none;
box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.5);
}
.lightbox-trigger:hover .lightbox-zoom-badge,
.lightbox-trigger:focus-visible .lightbox-zoom-badge {
opacity: 1;
transform: translateY(0);
}
/* Dialog */
.lightbox-dialog {
padding: 0;
margin: auto;
width: min(98vw, 1800px);
height: min(96vh, 1200px);
background: #060a13;
color: #e8eaed;
border: 1px solid rgba(240, 180, 41, 0.25);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 40px 100px -30px rgba(0, 0, 0, 0.8);
}
.lightbox-dialog::backdrop {
background: rgba(6, 10, 19, 0.82);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.lightbox-close-form {
position: absolute;
top: 0.75rem;
right: 0.75rem;
margin: 0;
z-index: 2;
}
.lightbox-close {
all: unset;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
color: #f0b429;
background: rgba(12, 17, 26, 0.92);
border: 1px solid rgba(240, 180, 41, 0.35);
transition: background 160ms ease-out, transform 160ms ease-out, border-color 160ms ease-out;
}
.lightbox-close:hover {
background: rgba(240, 180, 41, 0.18);
border-color: rgba(240, 180, 41, 0.7);
transform: scale(1.06);
}
.lightbox-close:focus-visible {
outline: 2px solid #f0b429;
outline-offset: 2px;
}
.lightbox-image {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
background: #060a13;
cursor: zoom-out;
}
.lightbox-caption {
position: absolute;
left: 0;
right: 0;
bottom: 0.5rem;
margin: 0;
text-align: center;
color: #9aa3b2;
font-size: 0.875rem;
padding: 0 1rem;
}
/* Open animation */
@keyframes lightbox-in {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
@keyframes lightbox-backdrop-in {
from { opacity: 0; }
to { opacity: 1; }
}
.lightbox-dialog[open] {
animation: lightbox-in 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.lightbox-dialog[open]::backdrop {
animation: lightbox-backdrop-in 220ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.lightbox-zoom-badge {
transition: none;
}
.lightbox-dialog[open],
.lightbox-dialog[open]::backdrop {
animation: none;
}
.lightbox-close {
transition: none;
}
}
</style>
<script>
// Open lightbox on trigger click. Close on click of the image itself or
// the backdrop. Bundled by Astro — CSP script-src 'self'.
const triggers = document.querySelectorAll<HTMLButtonElement>('[data-lightbox-open]');
triggers.forEach((trigger) => {
trigger.addEventListener('click', () => {
const id = trigger.getAttribute('data-lightbox-open');
if (!id) return;
const dialog = document.getElementById(id);
if (dialog instanceof HTMLDialogElement) {
dialog.showModal();
}
});
});
const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog.lightbox-dialog');
dialogs.forEach((dialog) => {
dialog.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
// Click on the dialog element itself (the backdrop area) or the image
// closes. Clicks on the close button are already handled by the native
// form[method=dialog].
if (target === dialog || target.classList.contains('lightbox-image')) {
dialog.close();
}
});
});
</script>

View File

@@ -4,14 +4,14 @@ const year = new Date().getFullYear();
<footer class="border-t border-border mt-24"> <footer class="border-t border-border mt-24">
<div class="max-w-content mx-auto px-6 py-12 flex flex-col md:flex-row md:items-center md:justify-between gap-8"> <div class="max-w-content mx-auto px-6 py-12 flex flex-col md:flex-row md:items-center md:justify-between gap-8">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<svg width="24" height="24" viewBox="0 0 32 32" aria-hidden="true"> <img
<rect width="32" height="32" rx="6" fill="#0c111a"/> src="/icons/cameleer-32.png"
<g fill="none" stroke="#f0b429" stroke-width="1.6" stroke-linecap="round"> width="24"
<path d="M4 10 Q10 6 16 12 T28 10"/> height="24"
<path d="M4 16 Q10 12 16 18 T28 16"/> alt=""
<path d="M4 22 Q10 18 16 24 T28 22"/> decoding="async"
</g> class="shrink-0 opacity-80"
</svg> />
<span class="text-text-muted text-sm">© {year} Cameleer</span> <span class="text-text-muted text-sm">© {year} Cameleer</span>
</div> </div>
<nav class="flex items-center gap-8 text-sm text-text-muted" aria-label="Footer"> <nav class="flex items-center gap-8 text-sm text-text-muted" aria-label="Footer">

View File

@@ -4,14 +4,14 @@ import CTAButtons from './CTAButtons.astro';
<header class="sticky top-0 z-40 backdrop-blur-md bg-bg/80 border-b border-border"> <header class="sticky top-0 z-40 backdrop-blur-md bg-bg/80 border-b border-border">
<div class="max-w-content mx-auto px-6 h-16 flex items-center justify-between gap-6"> <div class="max-w-content mx-auto px-6 h-16 flex items-center justify-between gap-6">
<a href="/" class="flex items-center gap-2 group" aria-label="Cameleer home"> <a href="/" class="flex items-center gap-2 group" aria-label="Cameleer home">
<svg width="28" height="28" viewBox="0 0 32 32" aria-hidden="true"> <img
<rect width="32" height="32" rx="6" fill="#0c111a"/> src="/icons/cameleer-48.png"
<g fill="none" stroke="#f0b429" stroke-width="1.6" stroke-linecap="round"> width="32"
<path d="M4 10 Q10 6 16 12 T28 10"/> height="32"
<path d="M4 16 Q10 12 16 18 T28 16"/> alt=""
<path d="M4 22 Q10 18 16 24 T28 22"/> decoding="async"
</g> class="shrink-0"
</svg> />
<span class="font-sans font-bold text-lg tracking-tight text-text group-hover:text-accent transition-colors">Cameleer</span> <span class="font-sans font-bold text-lg tracking-tight text-text group-hover:text-accent transition-colors">Cameleer</span>
</a> </a>
<nav class="flex items-center gap-8 text-sm" aria-label="Primary"> <nav class="flex items-center gap-8 text-sm" aria-label="Primary">

View File

@@ -3,24 +3,66 @@ interface Props {
opacity?: number; opacity?: number;
lines?: number; lines?: number;
} }
const { opacity = 0.12, lines = 8 } = Astro.props; const { opacity = 0.35, lines = 9 } = Astro.props;
const paths: string[] = []; interface Line {
d: string;
width: number; // stroke width in CSS px (non-scaling)
lineOpacity: number; // per-line opacity (0..1) — varies depth
tone: 'amber' | 'cyan';
}
const out: Line[] = [];
const stepY = 100 / (lines + 1); const stepY = 100 / (lines + 1);
for (let i = 1; i <= lines; i++) { for (let i = 1; i <= lines; i++) {
const y = i * stepY; const y = i * stepY;
const amp = 4 + (i % 3) * 2; // Mix two frequencies so adjacent lines don't read parallel.
paths.push(`M0,${y} Q25,${y - amp} 50,${y + amp * 0.6} T100,${y}`); const amp = 3 + (i % 3) * 2 + Math.sin(i * 1.7) * 1.2;
const phase = (i * 13) % 25; // shift crests horizontally
const d = `M0,${y} Q${25 + phase / 3},${y - amp} ${50 + phase / 5},${y + amp * 0.6} T100,${y + (i % 2 ? 1 : -1)}`;
// Vary stroke weight with a triangle wave — gives the feel of cartographic contour intervals.
const triangle = Math.abs(((i + 2) % 4) - 2) / 2;
const width = 0.6 + triangle * 0.9;
// Depth: middle lines darker, edges lighter.
const depth = 1 - Math.abs((i - (lines + 1) / 2)) / ((lines + 1) / 2);
const lineOpacity = 0.35 + depth * 0.65;
// One cyan line roughly every 4th — echo of the cross-route correlation color.
const tone: 'amber' | 'cyan' = i % 4 === 2 ? 'cyan' : 'amber';
out.push({ d, width, lineOpacity, tone });
} }
--- ---
<svg <div
class="absolute inset-0 w-full h-full pointer-events-none" class="topo-wrap absolute inset-0 pointer-events-none"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true" aria-hidden="true"
style={`opacity:${opacity}`} style={`--topo-opacity:${opacity}`}
> >
<g fill="none" stroke="#f0b429" stroke-width="0.15" vector-effect="non-scaling-stroke"> <svg
{paths.map((d) => <path d={d} />)} class="topo-svg absolute inset-0 w-full h-full"
</g> viewBox="0 0 100 100"
</svg> preserveAspectRatio="none"
>
<g fill="none" vector-effect="non-scaling-stroke" stroke-linecap="round">
{out.map((l) => (
<path
d={l.d}
stroke={l.tone === 'cyan' ? '#5cc8ff' : '#f0b429'}
stroke-width={l.width}
stroke-opacity={l.lineOpacity}
/>
))}
</g>
</svg>
</div>
<style>
.topo-wrap {
opacity: var(--topo-opacity, 0.35);
/* Soft edge fade — lines should feel like they dissolve at the section
boundaries rather than hit them hard. */
-webkit-mask-image: radial-gradient(ellipse at 50% 45%, black 55%, transparent 95%);
mask-image: radial-gradient(ellipse at 50% 45%, black 55%, transparent 95%);
}
.topo-svg {
filter: blur(0.15px);
}
</style>

View File

@@ -1,40 +0,0 @@
---
interface Tile {
outcome: string;
capability: string;
}
// tile.capability is a compile-time constant defined below — never feed
// user-supplied or CMS content into set:html further down (XSS risk).
const tiles: Tile[] = [
{
outcome: 'Cut debugging time in half.',
capability:
'Every processor on every route, timed to the nanosecond. Choice branches, splits, multicasts, and error handlers preserved as a proper execution tree — not a pile of log lines.',
},
{
outcome: 'Ship integrations with confidence.',
capability:
'Capture real payloads and replay them on demand. Deep-trace a specific correlation ID across services. Push trace settings to a running agent without a redeploy.',
},
{
outcome: 'Keep what you have built.',
capability:
'One `-javaagent` flag. No code changes, no SDK, no framework lock-in. Native understanding of 45+ EIP node types across Apache Camel 4.x.',
},
];
---
<section class="border-b border-border">
<div class="max-w-content mx-auto px-6 py-20 md:py-24">
<div class="grid md:grid-cols-3 gap-6 md:gap-8">
{tiles.map((tile) => (
<div class="rounded-lg border border-border bg-bg-elevated p-7 md:p-8 hover:border-border-strong transition-colors">
<h2 class="text-xl md:text-2xl font-bold text-text mb-3 leading-snug">
{tile.outcome}
</h2>
<p class="text-text-muted leading-relaxed" set:html={tile.capability.replace(/`([^`]+)`/g, '<code class="font-mono text-accent bg-bg border border-border rounded px-1 py-0.5 text-sm">$1</code>')}></p>
</div>
))}
</div>
</div>
</section>

View File

@@ -6,13 +6,13 @@ import TopographicBg from '../TopographicBg.astro';
<TopographicBg opacity={0.18} lines={6} /> <TopographicBg opacity={0.18} lines={6} />
<div class="relative max-w-content mx-auto px-6 py-24 md:py-32 text-center"> <div class="relative max-w-content mx-auto px-6 py-24 md:py-32 text-center">
<h2 class="text-display font-bold text-text mb-6"> <h2 class="text-display font-bold text-text mb-6">
Start seeing your routes. Ship integrations. Sleep through the night.
</h2> </h2>
<p class="text-lg md:text-xl text-text-muted max-w-prose mx-auto mb-10"> <p class="text-lg md:text-xl text-text-muted max-w-prose mx-auto mb-10">
14-day free trial. Your first app, instrumented and live in under 10 minutes. 14-day free trial. Your first Camel app, hosted, traced, and running in under ten minutes. No code changes.
</p> </p>
<div class="flex justify-center"> <div class="flex justify-center">
<CTAButtons size="lg" /> <CTAButtons size="lg" showSecondary={false} />
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,27 +1,128 @@
--- ---
import CTAButtons from '../CTAButtons.astro'; import CTAButtons from '../CTAButtons.astro';
import RouteDiagram from '../RouteDiagram.astro';
import TopographicBg from '../TopographicBg.astro'; import TopographicBg from '../TopographicBg.astro';
import Lightbox from '../Lightbox.astro';
interface Pin {
label: string;
body: string;
top: string;
left: string;
}
const pins: Pin[] = [
{ label: '01', body: 'Correlation ID — click to follow one exchange across services.', top: '14%', left: '12%' },
{ label: '02', body: 'Failure in context — circuit breaker tripped, fallback ran, tried backend:80.', top: '46%', left: '52%' },
{ label: '03', body: 'Full error pinned — exception, stack trace, headers, payload.', top: '78%', left: '78%' },
];
--- ---
<section class="relative overflow-hidden border-b border-border"> <section class="relative overflow-hidden border-b border-border">
<TopographicBg opacity={0.14} lines={10} /> <TopographicBg opacity={0.22} lines={11} />
<div class="relative max-w-content mx-auto px-6 pt-20 pb-24 md:pt-28 md:pb-32"> <div class="relative max-w-content mx-auto px-6 pt-16 pb-20 md:pt-24 md:pb-24 lg:pt-28">
<div class="max-w-3xl"> <div class="grid lg:grid-cols-12 gap-10 lg:gap-14 items-center">
<p class="text-accent font-mono text-xs tracking-[0.25em] uppercase mb-6"> <div class="lg:col-span-5">
Observability · Apache Camel <img
</p> src="/icons/cameleer-192.png"
<h1 class="text-display font-bold text-text mb-6"> width="64"
See every route.<br /> height="64"
Reach into every flow. alt=""
</h1> decoding="async"
<p class="text-lg md:text-xl text-text-muted max-w-prose leading-relaxed mb-10"> class="shrink-0 mb-5 hero-mark"
Zero-code tracing, processor-level detail, and live control for Apache Camel — />
from a single <code class="font-mono text-accent bg-bg-elevated border border-border rounded px-1.5 py-0.5 text-base">-javaagent</code> flag. <p
</p> class="inline-flex items-center gap-2 mb-7 rounded-full border border-accent/30 bg-accent/[0.08] text-accent px-3.5 py-1 text-sm italic font-medium"
<CTAButtons size="lg" /> >
</div> <span aria-hidden="true" class="text-base">✦</span>
<div class="mt-16 md:mt-20"> Your camels called. They want a GPS.
<RouteDiagram /> </p>
<h1 class="font-bold text-text mb-6 hero-h1">
Ship Camel integrations. Sleep through the night.
</h1>
<p class="text-lg md:text-xl text-text-muted max-w-prose leading-relaxed mb-8">
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.
</p>
<CTAButtons
size="lg"
secondaryLabel="See it in action ↓"
secondaryHref="#walkthrough"
/>
<p class="mt-4 font-mono text-xs text-text-faint">
14-day trial · from €20/mo · no credit card
</p>
</div>
<div class="lg:col-span-7 relative">
<div class="hero-shot relative rounded-lg border border-border-strong bg-bg-elevated overflow-hidden">
<Lightbox
src="/product/exchange-detail.png"
alt="Cameleer Mission Control — route execution detail with processor-level trace"
width={1920}
height={945}
loading="eager"
fetchpriority="high"
/>
<div class="absolute inset-0 ring-1 ring-inset ring-accent/10 pointer-events-none rounded-lg"></div>
{pins.map((pin) => (
<span
aria-hidden="true"
class="hero-pin absolute inline-flex items-center justify-center w-7 h-7 rounded-full bg-accent text-bg font-mono text-xs font-bold pointer-events-none"
style={`top:${pin.top};left:${pin.left}`}
>
{pin.label}
</span>
))}
</div>
<ul class="hero-pin-legend mt-5 grid sm:grid-cols-3 gap-3 text-text-muted">
{pins.map((pin) => (
<li class="flex items-start gap-2 text-sm leading-snug">
<span class="font-mono text-accent text-xs mt-0.5">{pin.label}</span>
<span>{pin.body}</span>
</li>
))}
</ul>
<div aria-hidden="true" class="hero-shot-glow"></div>
</div>
</div> </div>
</div> </div>
</section> </section>
<style>
.hero-h1 {
font-size: clamp(2.25rem, 4.5vw, 4rem);
line-height: 1.05;
letter-spacing: -0.02em;
}
.hero-shot {
box-shadow:
0 1px 0 rgba(240, 180, 41, 0.08) inset,
0 30px 60px -20px rgba(0, 0, 0, 0.6),
0 10px 25px -10px rgba(0, 0, 0, 0.5);
}
.hero-pin {
box-shadow: 0 0 0 4px rgba(240, 180, 41, 0.22), 0 4px 10px -2px rgba(0, 0, 0, 0.5);
transform: translate(-50%, -50%);
}
.hero-shot-glow {
position: absolute;
inset: 10% -5% 10% -5%;
background: radial-gradient(
60% 60% at 50% 50%,
rgba(240, 180, 41, 0.18),
transparent 70%
);
filter: blur(40px);
z-index: -1;
}
@keyframes hero-mark-sway {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-2px) rotate(-1.5deg); }
}
.hero-mark {
animation: hero-mark-sway 7s ease-in-out infinite;
transform-origin: 50% 90%;
}
@media (prefers-reduced-motion: reduce) {
.hero-mark { animation: none; }
}
</style>

View File

@@ -9,19 +9,18 @@ interface Step {
const steps: Step[] = [ const steps: Step[] = [
{ {
n: '01', n: '01',
title: 'Add the agent', title: 'Point us at your Camel app',
body: 'Drop the Cameleer agent JAR alongside your Camel app and start it with a single flag. That is the entire installation.', body: 'Drop it in, or connect one you already run. No code changes.',
code: 'java \\\n -javaagent:cameleer-agent.jar \\\n -jar your-camel-app.jar',
}, },
{ {
n: '02', n: '02',
title: 'Launch your app', title: 'We take it from there',
body: 'Every route, processor, exchange, and route graph is discovered and reported automatically. Configurable redaction keeps sensitive fields out of the trace.', body: 'Every route, every processor, every exchange discovered and traced automatically. Sensitive fields are masked by default.',
}, },
{ {
n: '03', n: '03',
title: 'See it in Mission Control', title: 'Watch it run',
body: 'Browse executions, tap live traffic, replay failed exchanges, and follow flows across services. Nothing to instrument, nothing to maintain.', body: 'Browse executions, tap live traffic, replay failed exchanges, follow flows across services.',
}, },
]; ];
--- ---
@@ -30,11 +29,11 @@ const steps: Step[] = [
<div class="max-w-2xl mb-16"> <div class="max-w-2xl mb-16">
<p class="text-cyan font-mono text-xs tracking-[0.25em] uppercase mb-4">For engineers</p> <p class="text-cyan font-mono text-xs tracking-[0.25em] uppercase mb-4">For engineers</p>
<h2 class="text-hero font-bold text-text mb-4">How it works</h2> <h2 class="text-hero font-bold text-text mb-4">How it works</h2>
<p class="text-text-muted text-lg">Three steps. No code changes. Works across Camel 4.x.</p> <p class="text-text-muted text-lg">Three steps. Nothing to maintain.</p>
</div> </div>
<ol class="grid md:grid-cols-3 gap-6 md:gap-8"> <ol class="grid md:grid-cols-3 gap-6 md:gap-8">
{steps.map((step) => ( {steps.map((step) => (
<li class="relative rounded-lg border border-border bg-bg-elevated p-7"> <li class="relative rounded-lg border border-border bg-bg-elevated p-7 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]">
<div class="font-mono text-accent text-sm tracking-wider mb-3">{step.n}</div> <div class="font-mono text-accent text-sm tracking-wider mb-3">{step.n}</div>
<h3 class="text-lg font-bold text-text mb-3">{step.title}</h3> <h3 class="text-lg font-bold text-text mb-3">{step.title}</h3>
<p class="text-text-muted leading-relaxed mb-4">{step.body}</p> <p class="text-text-muted leading-relaxed mb-4">{step.body}</p>

View File

@@ -21,27 +21,13 @@ const tiers: Tier[] = [
cta: 'Start free trial', cta: 'Start free trial',
}, },
{ {
name: 'MID', name: 'Starter',
price: '20 € /mo', price: '20 € /mo',
sub: '2 environments · 10 apps · 7-day retention', sub: '2 environments · 10 apps · 7-day retention',
href: auth.signUpUrl, href: auth.signUpUrl,
cta: 'Start on MID', cta: 'Start on Starter',
highlight: true, highlight: true,
}, },
{
name: 'HIGH',
price: 'Contact',
sub: 'Unlimited envs · 50 apps · 90-day retention · Debugger, Replay',
href: auth.salesMailto,
cta: 'Talk to sales',
},
{
name: 'BUSINESS',
price: 'Contact',
sub: 'Unlimited everything · 365-day retention · all features',
href: auth.salesMailto,
cta: 'Talk to sales',
},
]; ];
--- ---
<section class="border-b border-border"> <section class="border-b border-border">
@@ -50,15 +36,25 @@ const tiers: Tier[] = [
<p class="text-accent font-mono text-xs tracking-[0.25em] uppercase mb-4">Pricing</p> <p class="text-accent font-mono text-xs tracking-[0.25em] uppercase mb-4">Pricing</p>
<h2 class="text-hero font-bold text-text mb-4">Start free. Grow when you need to.</h2> <h2 class="text-hero font-bold text-text mb-4">Start free. Grow when you need to.</h2>
<p class="text-text-muted text-lg"> <p class="text-text-muted text-lg">
No credit card for the trial. No credit card. No sales call. Just a working trial in ten minutes.
<a href="/pricing" class="text-accent hover:underline">See full comparison →</a>
</p> </p>
</div> </div>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-5"> <div class="grid md:grid-cols-2 gap-5 lg:items-stretch max-w-3xl">
{tiers.map((tier) => ( {tiers.map((tier) => (
<div class={`rounded-lg border bg-bg-elevated p-6 flex flex-col ${tier.highlight ? 'border-accent' : 'border-border'}`}> <div
class={`relative rounded-lg bg-bg-elevated p-6 flex flex-col transition-all duration-200 ease-out hover:-translate-y-0.5
${tier.highlight
? 'ring-2 ring-accent shadow-[0_20px_50px_-20px_rgba(240,180,41,0.35)] md:-translate-y-2 md:pt-8 md:pb-7'
: 'border border-border hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]'}`}
>
{tier.highlight && (
<span class="absolute -top-3 left-6 inline-flex items-center gap-1.5 rounded-full bg-accent text-bg px-3 py-0.5 text-[11px] font-bold tracking-wide font-mono">
<span aria-hidden="true">★</span>
MOST POPULAR
</span>
)}
<div class="mb-4"> <div class="mb-4">
<div class="font-mono text-xs tracking-wider text-text-muted mb-2">{tier.name.toUpperCase()}</div> <div class={`font-mono text-xs tracking-wider mb-2 ${tier.highlight ? 'text-accent' : 'text-text-muted'}`}>{tier.name.toUpperCase()}</div>
<div class="text-2xl font-bold text-text">{tier.price}</div> <div class="text-2xl font-bold text-text">{tier.price}</div>
</div> </div>
<p class="text-text-muted text-sm leading-relaxed flex-grow mb-5">{tier.sub}</p> <p class="text-text-muted text-sm leading-relaxed flex-grow mb-5">{tier.sub}</p>
@@ -74,5 +70,10 @@ const tiers: Tier[] = [
</div> </div>
))} ))}
</div> </div>
<p class="mt-8 font-mono text-sm text-text-muted">
<a href="/pricing" class="text-cyan hover:text-accent transition-colors">
See all plans (Scale, Enterprise) →
</a>
</p>
</div> </div>
</section> </section>

View File

@@ -0,0 +1,33 @@
---
import { getAuthConfig } from '../../config/auth';
const auth = getAuthConfig();
// PENDING — [Founder Name] placeholder must be filled in before publish.
const founderName = '[Founder Name]';
const designPartnerSubject = 'Design partner enquiry — Cameleer';
const designPartnerHref = `mailto:${auth.salesEmail}?subject=${encodeURIComponent(designPartnerSubject)}`;
---
<section class="border-b border-border">
<div class="max-w-content mx-auto px-6 py-16 md:py-20">
<div class="max-w-3xl mx-auto">
<p class="text-accent font-mono text-xs tracking-[0.25em] uppercase mb-4">
// Built by people who've done this before
</p>
<blockquote class="border-l-[3px] border-accent pl-5 max-w-[62ch]">
<p class="text-lg md:text-xl text-text italic leading-relaxed mb-3">
“We spent 15 years building integration monitoring for banks that couldnt afford downtime. Cameleer is what wed build today — purpose-built for Apache Camel, no retrofit.”
</p>
<footer class="text-sm font-mono text-text-muted">
— <span class="text-text">{founderName}</span>, co-founder
</footer>
</blockquote>
<a
href={designPartnerHref}
class="inline-flex items-center gap-2 mt-7 font-mono text-sm text-cyan hover:text-accent transition-colors"
>
Apply to the design-partner program <span aria-hidden="true">→</span>
</a>
</div>
</div>
</section>

View File

@@ -0,0 +1,104 @@
---
import Lightbox from '../Lightbox.astro';
interface Callout {
title: string;
body: string;
}
const callouts: Callout[] = [
{
title: 'Cross-service correlation.',
body: 'Every exchange carries its correlation ID forward. One click jumps to what the downstream route did with the same message.',
},
{
title: 'Runtime detail, not guesswork.',
body: 'Circuit breaker tripped. Fallback path ran. Request tried backend:80. The pieces a 3 AM page actually needs — already captured.',
},
{
title: 'The whole story of a failure.',
body: 'Exception class, message, stack trace, headers, payload — all pinned to the exchange. No log-grepping tour.',
},
];
---
<section id="walkthrough" class="border-b border-border bg-bg">
<div class="max-w-content mx-auto px-6 py-24 md:py-32">
<div class="max-w-3xl mb-14 md:mb-20">
<p class="text-cyan font-mono text-xs tracking-[0.25em] uppercase mb-4">// When something breaks</p>
<h2 class="text-hero font-bold text-text mb-5">
The 3 AM page. With and without Cameleer.
</h2>
<p class="text-lg text-text-muted leading-relaxed">
Same Camel app. Same failed exchange. Different night.
</p>
</div>
<div class="grid md:grid-cols-2 gap-6 md:gap-8 items-stretch">
<div class="without-card relative rounded-lg border border-dashed border-border-strong bg-bg overflow-hidden">
<div class="px-5 pt-5 pb-3 font-mono text-[11px] tracking-[0.2em] uppercase text-text-faint border-b border-border">
Without Cameleer · 03:12 AM
</div>
<pre class="font-mono text-[13px] leading-[1.65] text-text-muted px-5 py-5 overflow-x-auto whitespace-pre"><code><span class="text-text">$</span> kubectl logs camel-router-7d4f8c
<span class="text-rose">ERROR</span> org.apache.camel.CamelExecutionException
at org.apache.camel.processor.SendProcessor.process
at org.apache.camel.processor.Pipeline.process
...
<span class="text-text">$</span> grep "order-842" *.log
router-3.log: <span class="text-accent">WARN</span> exchange order-842 stuck in saga-fulfillment
router-3.log: <span class="text-rose">ERROR</span> processor backend:80 → connect timeout
<span class="text-text">$</span> ssh prod-integration-3
prod-integration-3 $ kubectl logs ...
&gt; <span class="text-cyan">slack #integration-team</span>
"anyone know why order-842 is stuck??"
<span class="text-text-faint">[3 of 4 reactions, no answer]</span>
<span class="text-accent">~47 min later: someone wakes up an SRE.</span></code></pre>
</div>
<figure class="with-card relative rounded-lg border border-border-strong bg-bg-elevated overflow-hidden">
<div class="px-5 pt-5 pb-3 font-mono text-[11px] tracking-[0.2em] uppercase text-accent border-b border-border">
With Cameleer · 30 sec
</div>
<Lightbox
src="/product/error-detail.png"
alt="Cameleer Mission Control — failed exchange order-842 with full execution context"
width={1920}
height={945}
loading="lazy"
imgClass="block w-full h-auto"
/>
<div class="px-5 py-4 font-mono text-[13px] leading-[1.6] text-text-muted border-t border-border">
<span class="text-accent">▸</span> Open exchange <span class="text-text">order-842</span> → see the failure pinned → click <span class="text-text">Replay</span> after fix.
</div>
<div class="absolute inset-0 ring-1 ring-inset ring-accent/10 pointer-events-none rounded-lg"></div>
</figure>
</div>
<ul class="grid md:grid-cols-3 gap-6 md:gap-8 mt-14">
{callouts.map((c, i) => (
<li class="relative pl-10">
<span
class="absolute left-0 top-0 inline-flex items-center justify-center w-7 h-7 rounded-full border border-accent/40 bg-accent/10 text-accent font-mono text-xs"
aria-hidden="true"
>
{String(i + 1).padStart(2, '0')}
</span>
<h3 class="text-text font-semibold mb-1.5">{c.title}</h3>
<p class="text-text-muted leading-relaxed">{c.body}</p>
</li>
))}
</ul>
</div>
</section>
<style>
.with-card {
box-shadow:
0 1px 0 rgba(240, 180, 41, 0.08) inset,
0 30px 60px -25px rgba(0, 0, 0, 0.7),
0 12px 30px -12px rgba(0, 0, 0, 0.5);
}
</style>

View File

@@ -1,6 +1,4 @@
--- ---
// Final nJAMS-legacy wording is subject to Hendrik's trademark review before go-live
// (see docs/superpowers/specs/2026-04-24-cameleer-website-design.md §10).
--- ---
<section class="border-b border-border"> <section class="border-b border-border">
<div class="max-w-content mx-auto px-6 py-20 md:py-24"> <div class="max-w-content mx-auto px-6 py-20 md:py-24">
@@ -11,22 +9,22 @@
</h2> </h2>
</div> </div>
<div class="grid md:grid-cols-2 gap-8 md:gap-12"> <div class="grid md:grid-cols-2 gap-8 md:gap-12">
<div class="rounded-lg border border-border bg-bg-elevated p-8"> <div class="rounded-lg border border-border bg-bg-elevated p-8 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]">
<h3 class="text-xl font-bold text-text mb-4">Generic APMs do not understand Camel. Cameleer does.</h3> <h3 class="text-xl font-bold text-text mb-4">Generic APMs do not understand Camel. Cameleer does.</h3>
<p class="text-text-muted leading-relaxed mb-4"> <p class="text-text-muted leading-relaxed mb-4">
Our Java agent speaks 45+ Apache Camel EIP node types natively — choices, splits, multicasts, doTry, error handlers, dynamic endpoints, thread boundaries in async routes. It extracts your route topology as a first-class graph, not a pile of metrics. Most monitoring tools see your app as a Java process and a pile of HTTP calls. Cameleer understands that you are running a Camel app — choices, splits, multicasts, error handlers, and every other EIP pattern as first-class citizens.
</p> </p>
<p class="text-text-muted leading-relaxed"> <p class="text-text-muted leading-relaxed">
A bidirectional protocol lets the server push deep-trace requests, per-route recording toggles, and signed config changes back to running agents — turning passive observability into active control. Not something you build in a weekend. So when you ask "why did this exchange fail?", you get an answer, not a log tail. And you can reach back into a running app to replay a message, deep-trace a correlation ID, or toggle recording — observability that does things, not just shows them.
</p> </p>
</div> </div>
<div class="rounded-lg border border-border bg-bg-elevated p-8"> <div class="rounded-lg border border-border bg-bg-elevated p-8 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]">
<h3 class="text-xl font-bold text-text mb-4">Built by people who have shipped this class of product before.</h3> <h3 class="text-xl font-bold text-text mb-4">Built by people who've operated integration in production for 15 years.</h3>
<p class="text-text-muted leading-relaxed mb-4"> <p class="text-text-muted leading-relaxed mb-4">
The Cameleer team spent years building and supporting integration monitoring for banks, insurers, and logistics operators. We know what integration teams actually need at 3 AM — and what they never use. We spent over a decade building integration monitoring for banks, insurers, and logistics operators — the kind of shops where a stuck exchange is a regulatory event, not just an inconvenience.
</p> </p>
<p class="text-text-muted leading-relaxed"> <p class="text-text-muted leading-relaxed">
Cameleer is what we would build today, purpose-built for Apache Camel — no legacy, no retrofit, no assumptions about a generic middleware platform. Cameleer is what we'd build today, purpose-built for Apache Camel. No legacy, no retrofit, no assumptions about a generic middleware platform.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -4,57 +4,57 @@ import { resolveAuthConfig } from './auth';
describe('resolveAuthConfig', () => { describe('resolveAuthConfig', () => {
it('returns both URLs and sales email from env', () => { it('returns both URLs and sales email from env', () => {
const cfg = resolveAuthConfig({ const cfg = resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
}); });
expect(cfg.signInUrl).toBe('https://auth.cameleer.io/sign-in'); expect(cfg.signInUrl).toBe('https://app.cameleer.io/sign-in');
expect(cfg.signUpUrl).toBe('https://auth.cameleer.io/sign-in?first_screen=register'); expect(cfg.signUpUrl).toBe('https://app.cameleer.io/sign-in?first_screen=register');
expect(cfg.salesEmail).toBe('sales@cameleer.io'); expect(cfg.salesEmail).toBe('sales@cameleer.io');
}); });
it('throws if PUBLIC_AUTH_SIGNIN_URL is missing', () => { it('throws if PUBLIC_AUTH_SIGNIN_URL is missing', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/PUBLIC_AUTH_SIGNIN_URL/); })).toThrow(/PUBLIC_AUTH_SIGNIN_URL/);
}); });
it('throws if a URL is not https', () => { it('throws if a URL is not https', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'http://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'http://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/must be https/); })).toThrow(/must be https/);
}); });
it('throws if sales email is not a valid mailto target', () => { it('throws if sales email is not a valid mailto target', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'not-an-email', PUBLIC_SALES_EMAIL: 'not-an-email',
})).toThrow(/PUBLIC_SALES_EMAIL/); })).toThrow(/PUBLIC_SALES_EMAIL/);
}); });
it('throws if PUBLIC_AUTH_SIGNUP_URL is missing', () => { it('throws if PUBLIC_AUTH_SIGNUP_URL is missing', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/PUBLIC_AUTH_SIGNUP_URL/); })).toThrow(/PUBLIC_AUTH_SIGNUP_URL/);
}); });
it('throws if PUBLIC_AUTH_SIGNUP_URL is not https', () => { it('throws if PUBLIC_AUTH_SIGNUP_URL is not https', () => {
expect(() => resolveAuthConfig({ expect(() => resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'http://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'http://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
})).toThrow(/must be https/); })).toThrow(/must be https/);
}); });
it('exposes signUpUrl distinct from signInUrl', () => { it('exposes signUpUrl distinct from signInUrl', () => {
const cfg = resolveAuthConfig({ const cfg = resolveAuthConfig({
PUBLIC_AUTH_SIGNIN_URL: 'https://auth.cameleer.io/sign-in', PUBLIC_AUTH_SIGNIN_URL: 'https://app.cameleer.io/sign-in',
PUBLIC_AUTH_SIGNUP_URL: 'https://auth.cameleer.io/sign-in?first_screen=register', PUBLIC_AUTH_SIGNUP_URL: 'https://app.cameleer.io/sign-in?first_screen=register',
PUBLIC_SALES_EMAIL: 'sales@cameleer.io', PUBLIC_SALES_EMAIL: 'sales@cameleer.io',
}); });
expect(cfg.signUpUrl).not.toBe(cfg.signInUrl); expect(cfg.signUpUrl).not.toBe(cfg.signInUrl);

View File

@@ -31,7 +31,8 @@ const ogUrl = new URL(ogImage, Astro.site ?? 'https://www.cameleer.io').toString
<meta name="description" content={description} /> <meta name="description" content={description} />
<link rel="canonical" href={canonical} /> <link rel="canonical" href={canonical} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/png" sizes="32x32" href="/icons/cameleer-32.png" />
<link rel="apple-touch-icon" href="/icons/cameleer-180.png" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="Cameleer" /> <meta property="og:site_name" content="Cameleer" />
@@ -46,6 +47,8 @@ const ogUrl = new URL(ogImage, Astro.site ?? 'https://www.cameleer.io').toString
<meta name="twitter:image" content={ogUrl} /> <meta name="twitter:image" content={ogUrl} />
<meta name="robots" content="index,follow" /> <meta name="robots" content="index,follow" />
<slot name="head" />
</head> </head>
<body class="min-h-screen bg-bg text-text font-sans antialiased"> <body class="min-h-screen bg-bg text-text font-sans antialiased">
<slot /> <slot />

View File

@@ -20,7 +20,7 @@ export function buildSecurityHeaders(): Record<string, string> {
"connect-src 'self'", "connect-src 'self'",
"frame-ancestors 'none'", "frame-ancestors 'none'",
"base-uri 'self'", "base-uri 'self'",
// No forms on this marketing site today (all auth redirects go to auth.cameleer.io // No forms on this marketing site today (all auth redirects go to app.cameleer.io
// as plain <a> navigations). If a future form is added, relax to 'self' or an allow-list. // as plain <a> navigations). If a future form is added, relax to 'self' or an allow-list.
"form-action 'none'", "form-action 'none'",
"object-src 'none'", "object-src 'none'",

View File

@@ -3,20 +3,33 @@ import BaseLayout from '../layouts/BaseLayout.astro';
import SiteHeader from '../components/SiteHeader.astro'; import SiteHeader from '../components/SiteHeader.astro';
import SiteFooter from '../components/SiteFooter.astro'; import SiteFooter from '../components/SiteFooter.astro';
import Hero from '../components/sections/Hero.astro'; import Hero from '../components/sections/Hero.astro';
import DualValueProps from '../components/sections/DualValueProps.astro'; import SocialProofStrip from '../components/sections/SocialProofStrip.astro';
import ThreeAmWalkthrough from '../components/sections/ThreeAmWalkthrough.astro';
import HowItWorks from '../components/sections/HowItWorks.astro'; import HowItWorks from '../components/sections/HowItWorks.astro';
import WhyUs from '../components/sections/WhyUs.astro'; import WhyUs from '../components/sections/WhyUs.astro';
import PricingTeaser from '../components/sections/PricingTeaser.astro'; import PricingTeaser from '../components/sections/PricingTeaser.astro';
import FinalCTA from '../components/sections/FinalCTA.astro'; import FinalCTA from '../components/sections/FinalCTA.astro';
--- ---
<BaseLayout <BaseLayout
title="Cameleer — Zero-code observability for Apache Camel" title="Cameleer — Ship Camel integrations. Sleep through the night."
description="See every route. Reach into every flow. Zero-code tracing, processor-level detail, and live control for Apache Camel — from a single -javaagent flag." description="The hosted runtime and observability platform for Apache Camel. Auto-traced, replay-ready, cross-service correlated — so the 3 AM page becomes a 30-second answer."
> >
<Fragment slot="head">
<link
rel="preload"
as="image"
type="image/webp"
href="/product/exchange-detail-1280.webp"
imagesrcset="/product/exchange-detail-1280.webp 1280w, /product/exchange-detail-1920.webp 1920w"
imagesizes="(min-width: 1024px) 56vw, 100vw"
fetchpriority="high"
/>
</Fragment>
<SiteHeader /> <SiteHeader />
<main> <main>
<Hero /> <Hero />
<DualValueProps /> <SocialProofStrip />
<ThreeAmWalkthrough />
<HowItWorks /> <HowItWorks />
<WhyUs /> <WhyUs />
<PricingTeaser /> <PricingTeaser />

View File

@@ -33,7 +33,7 @@ const tiers: FullTier[] = [
cta: 'Start free trial', cta: 'Start free trial',
}, },
{ {
name: 'MID', name: 'Starter',
price: '20 €', price: '20 €',
priceNote: 'per month', priceNote: 'per month',
envs: '2 environments', envs: '2 environments',
@@ -41,28 +41,28 @@ const tiers: FullTier[] = [
retention: '7-day retention', retention: '7-day retention',
features: ['Everything in Trial', 'Data flow lineage', 'Cross-service correlation'], features: ['Everything in Trial', 'Data flow lineage', 'Cross-service correlation'],
href: auth.signUpUrl, href: auth.signUpUrl,
cta: 'Start on MID', cta: 'Start on Starter',
highlight: true, highlight: true,
}, },
{ {
name: 'HIGH', name: 'Scale',
price: 'Contact', price: 'Contact',
priceNote: 'sales', priceNote: 'sales',
envs: 'Unlimited environments', envs: 'Unlimited environments',
apps: '50 apps', apps: '50 apps',
retention: '90-day retention', retention: '90-day retention',
features: ['Everything in MID', 'Live debugger', 'Exchange replay', 'Live tap'], features: ['Everything in Starter', 'Live debugger', 'Exchange replay', 'Live tap'],
href: auth.salesMailto, href: auth.salesMailto,
cta: 'Talk to sales', cta: 'Talk to sales',
}, },
{ {
name: 'BUSINESS', name: 'Enterprise',
price: 'Contact', price: 'Contact',
priceNote: 'sales', priceNote: 'sales',
envs: 'Unlimited environments', envs: 'Unlimited environments',
apps: 'Unlimited apps', apps: 'Unlimited apps',
retention: '365-day retention', retention: '365-day retention',
features: ['Everything in HIGH', 'Priority support', 'SLA', 'Dedicated success contact'], features: ['Everything in Scale', 'Priority support', 'SLA', 'Dedicated success contact'],
href: auth.salesMailto, href: auth.salesMailto,
cta: 'Talk to sales', cta: 'Talk to sales',
}, },
@@ -117,7 +117,7 @@ const tiers: FullTier[] = [
))} ))}
</div> </div>
<p class="text-center text-text-faint text-sm mt-10"> <p class="text-center text-text-faint text-sm mt-10">
Prices in EUR, excluding VAT. Billed monthly. Annual billing available for HIGH and BUSINESS — <a href={auth.salesMailto} class="text-accent hover:underline">talk to sales</a>. Prices in EUR, excluding VAT. Billed monthly. Annual billing available for Scale and Enterprise — <a href={auth.salesMailto} class="text-accent hover:underline">talk to sales</a>.
</p> </p>
</div> </div>
</section> </section>

View File

@@ -67,7 +67,7 @@ const lastUpdated = '2026-04-24';
<section class="mb-10"> <section class="mb-10">
<h2 class="text-lg font-bold text-text mb-3">6. External links</h2> <h2 class="text-lg font-bold text-text mb-3">6. External links</h2>
<p class="text-text-muted leading-relaxed"> <p class="text-text-muted leading-relaxed">
Sign-in and sign-up links on this site navigate you to <span class="font-mono text-accent">auth.cameleer.io</span> (Logto identity service) and subsequently <span class="font-mono text-accent">platform.cameleer.io</span>. Those services have their own privacy policies, which apply from the moment you arrive there. Sign-in and sign-up links on this site navigate you to <span class="font-mono text-accent">app.cameleer.io</span> (the Cameleer app, where authentication is handled by Logto). That service has its own privacy policy, which applies from the moment you arrive there.
</p> </p>
</section> </section>

72
src/placeholder.test.ts Normal file
View File

@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const placeholderDir = join(process.cwd(), 'placeholder');
const indexPath = join(placeholderDir, 'index.html');
describe('placeholder/index.html', () => {
const html = readFileSync(indexPath, 'utf8');
it('starts with the HTML5 doctype', () => {
expect(html.toLowerCase().trimStart()).toMatch(/^<!doctype html>/);
});
it('has the back-shortly title', () => {
expect(html).toContain('<title>Cameleer — Back shortly</title>');
});
it('is not indexable by search engines', () => {
expect(html).toContain('<meta name="robots" content="noindex">');
});
it('declares the dark color-scheme matching the live site', () => {
expect(html).toContain('<meta name="color-scheme" content="dark">');
expect(html).toContain('<meta name="theme-color" content="#060a13">');
});
it('contains the sentinel string the deploy workflow greps for', () => {
// The workflow's post-deploy smoke test fails if this string is missing.
expect(html).toContain('Routes are remapping');
});
it('uses the live hero subhead verbatim', () => {
expect(html).toContain(
'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.'
);
});
it('contains __SALES_EMAIL__ tokens at both the mailto href and the link text', () => {
const matches = html.match(/__SALES_EMAIL__/g) ?? [];
expect(matches.length).toBeGreaterThanOrEqual(2);
});
it('contains no other __TOKEN__ style placeholders', () => {
// Guard against a forgotten token that would survive the sed substitution.
const allTokens = html.match(/__[A-Z][A-Z0-9_]+__/g) ?? [];
const nonSales = allTokens.filter((t) => t !== '__SALES_EMAIL__');
expect(nonSales).toEqual([]);
});
it('references the sibling cameleer-logo.png by relative path', () => {
expect(html).toContain('src="./cameleer-logo.png"');
});
it('references the sibling favicon.png by relative path', () => {
expect(html).toContain('href="./favicon.png"');
});
it('has no <script> tags (placeholder must work without JS)', () => {
expect(html).not.toMatch(/<script[\s>]/i);
});
});
describe('placeholder/ asset siblings', () => {
it('cameleer-logo.png exists on disk', () => {
expect(existsSync(join(placeholderDir, 'cameleer-logo.png'))).toBe(true);
});
it('favicon.png exists on disk', () => {
expect(existsSync(join(placeholderDir, 'favicon.png'))).toBe(true);
});
});

View File

@@ -18,6 +18,13 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
} }
body { body {
@apply min-h-screen; @apply min-h-screen;