Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit5f2e749unknown_key

feat(L): Phase 3 — marketing landing hero rewrite + /pricing free-tier polish

feat(L): Phase 3 — marketing landing hero rewrite + /pricing free-tier polish

Two sub-blocks shipped together because both touch src/views/landing.tsx
(L10 rewrites the hero; L8 adds one small "Free forever" link) and both
modify src/app.tsx (route mounts).

L10 — MARKETING LANDING HERO REWRITE
  Replaces the existing hero with copy + layout that lands the L positioning
  ("the git host built around Claude") with confidence and ties every Block L
  feature into one conversion-focused page. Preserves L4's counter tiles,
  L5's "Compare to GitHub" CTA, L7's git-remote caption, L8's free-tier link.
    src/views/landing.tsx — +448 / -10. New gradient headline ("The git host
        built around Claude."), one-line install snippet with copy button,
        three CTAs (/register, /demo, /vs-github), "what just happened" rail
        driven by L4's publicStats, "Three reasons to switch" section
        (Sleep Mode / Migrate / Demo), and a "How is this different from
        GitHub?" pull-quote linking to /vs-github. Mobile stacks CTAs at
        <640px via existing media query. Adds ReasonCard FC + ~210 lines
        of .landing-* CSS + landingCopyJs clipboard helper.
    src/views/layout.tsx — +43 / -3. Optional additive props for
        description / ogTitle / ogDescription / ogType / twitterCard /
        fullTitle. Render output byte-identical when omitted (locked-block
        invariant preserved).
    src/routes/web.tsx — +10 / -1. Landing handler now passes SEO + OG
        meta props into Layout.
    src/__tests__/landing-hero.test.ts (NEW, 138 lines) — 9 tests covering
        hero copy, install snippet, three CTA hrefs, three reasons section,
        pull-quote, meta tags (title / description / og:title), L4
        counters regression guard, L5 CTA regression guard.

L8 — FREE-TIER UI POLISH (/pricing public page)
  Surfaces the existing F4 billing infrastructure (plans + quotas + format
  helpers) as a public, conversion-focused pricing page. Zero new billing
  logic — pure UX.
    src/routes/pricing.tsx (641 lines, NEW) — public GET /pricing. Hero
        ("Free for the AI-curious. Pay only when you're ready to scale.")
        + four plan cards driven by FALLBACK_PLANS (Free, Pro, Team,
        Enterprise) with one-line tagline + bullet inclusions + CTA
        routing to /settings/billing or /register?next= for anon. "What
        you get on the free tier" 2-column block (13 features). Self-host
        vs Cloud comparison. 5-question FAQ.
    src/routes/billing.tsx — +78 lines. Adds a "this month" usage panel
        at the top of /settings/billing with bigger bars, an "Upgrade to
        Pro" CTA for Free users, and a "Detailed plan comparison" link
        at the bottom pointing to /pricing.
    src/__tests__/pricing-page.test.ts (NEW, 96 lines) — covers anon 200,
        all four plan names, ≥6 free-tier features listed, all 5 FAQ
        questions, CTA routing, self-host curl snippet.
    src/views/landing.tsx — adds a small "Free forever for the AI-curious.
        See pricing →" link below the hero CTA row (preserved through
        the L10 rewrite).
    src/app.tsx — mounts pricingRoutes.

Full suite: 1491 pass / 0 fail / 2 skip / 3794 expect() across 115 files.
Up from 1474 / 0 / 2 after Phase 2. +17 new tests across L10+L8.

BLOCK L is now COMPLETE — all ten sub-blocks shipped. The whole "hook /
mouth / taste" funnel:
  Hook:   L4 social-proof counters, L5 vs-github comparison, L10 hero,
          L8 /pricing
  Mouth:  L2 one-command install, L6 "Sign in with GitHub",
          L7 Claude Code skill bundle, L3 live /demo
  Taste:  L1 Sleep Mode (the marquee), L9 AI hours saved counter

Follow-ups (none blocking; flagged for §7 IN-FLIGHT in the docs commit):
  - The Layout contract in §4.7 only enumerated title/user/notificationCount;
    L10 added five optional SEO/OG props. Strictly additive (render
    byte-identical when omitted) but the BIBLE entry should be updated.
  - L7 git-remote caption + L8 "Free forever" link both sit near the
    hero CTA row; if it ever feels cluttered, demote one or promote
    the other in a future L11.
  - The hero "what just happened" rail (L10) and the L4 counters tiles
    both gate on publicStats being non-null. Totally-fresh DB renders
    a slightly thinner hero — matches existing L4 behaviour.
Claude committed on May 13, 2026Parent: 52ad8b1
8 files changed+1449125f2e749cc838548ad0fd63a000041ea38179bb29
8 changed files+1449−12
Addedsrc/__tests__/landing-hero.test.ts+138−0View fileUnifiedSplit
1/**
2 * Block L10 — Landing-page hero rewrite tests.
3 *
4 * Covers:
5 * - GET / returns 200 HTML to anonymous users
6 * - Headline string + install snippet + 3 CTA hrefs all present
7 * - "Three reasons" column headings render
8 * - "How is this different" pull-quote string present
9 * - Meta tags (title, description, og:title) injected via Layout
10 * - REGRESSION GUARD: the L4 counters tile section still renders
11 * (stable identifier `class="landing-counters"`)
12 * - REGRESSION GUARD: the L5 "Compare to GitHub" CTA still points
13 * at /vs-github
14 */
15import { describe, it, expect } from "bun:test";
16import app from "../app";
17
18const HOME = "/";
19
20describe("Block L10 — landing hero rewrite", () => {
21 it("GET / returns 200 HTML to an anonymous visitor", async () => {
22 const res = await app.request(HOME);
23 expect(res.status).toBe(200);
24 const ct = res.headers.get("content-type") || "";
25 expect(ct.toLowerCase()).toContain("text/html");
26 });
27
28 it("renders the new hero headline", async () => {
29 const res = await app.request(HOME);
30 const body = await res.text();
31 expect(body).toContain("The git host built around Claude.");
32 });
33
34 it("renders the install snippet", async () => {
35 const res = await app.request(HOME);
36 const body = await res.text();
37 // The host + path is what makes it unambiguous as the install snippet.
38 expect(body).toContain("gluecron.com/install");
39 expect(body).toContain("curl -sSL gluecron.com/install | bash");
40 });
41
42 it("renders all three primary CTAs in the hero row", async () => {
43 const res = await app.request(HOME);
44 const body = await res.text();
45 expect(body).toContain('href="/register"');
46 expect(body).toContain('href="/demo"');
47 expect(body).toContain('href="/vs-github"');
48 // Visible labels for the three CTAs.
49 expect(body).toContain("Sign up free");
50 expect(body).toContain("Try the live demo");
51 expect(body).toContain("Compare to GitHub");
52 });
53
54 it("renders the three reasons-to-switch column headings", async () => {
55 const res = await app.request(HOME);
56 const body = await res.text();
57 expect(body).toContain("Toggle Sleep Mode");
58 expect(body).toContain("One command to migrate");
59 expect(body).toContain("Open the demo, watch it work");
60 // The migrate column also links to /import.
61 expect(body).toContain('href="/import"');
62 // The Sleep Mode + demo columns deep-link to their L1 / L3 routes.
63 expect(body).toContain('href="/sleep-mode"');
64 expect(body).toContain('href="/demo"');
65 });
66
67 it("renders the 'How is this different' pull-quote", async () => {
68 const res = await app.request(HOME);
69 const body = await res.text();
70 expect(body).toContain("How is this different from GitHub?");
71 expect(body).toContain("Every other host bolts AI on as a sidecar.");
72 // JSX collapses newlines + leading whitespace; assert on a substring
73 // that is contiguous after server-side rendering.
74 expect(body).toContain("first-class developer");
75 expect(body).toContain("Built to be");
76 expect(body).toContain("operated by AI agents");
77 expect(body).toContain("See the full comparison");
78 });
79
80 it("injects SEO + Open Graph meta tags", async () => {
81 const res = await app.request(HOME);
82 const body = await res.text();
83 // <title>
84 expect(body).toContain(
85 "<title>Gluecron — The git host built around Claude</title>"
86 );
87 // <meta name="description">
88 expect(body).toMatch(
89 /<meta\s+name="description"\s+content="Label an issue\. Walk away\. Wake up to a merged PR\./
90 );
91 // <meta property="og:title">
92 expect(body).toMatch(
93 /<meta\s+property="og:title"\s+content="Gluecron — The git host built around Claude"/
94 );
95 // <meta property="og:description">
96 expect(body).toMatch(
97 /<meta\s+property="og:description"\s+content="Label an issue\./
98 );
99 // <meta property="og:type">
100 expect(body).toMatch(
101 /<meta\s+property="og:type"\s+content="website"/
102 );
103 // <meta name="twitter:card">
104 expect(body).toMatch(
105 /<meta\s+name="twitter:card"\s+content="summary_large_image"/
106 );
107 });
108
109 it("REGRESSION: L4 counters tile section is still rendered", async () => {
110 const res = await app.request(HOME);
111 const body = await res.text();
112 // The L4 section is conditional on publicStats. When it renders we
113 // expect this scope class. It may be absent in a totally empty DB
114 // setup (publicStats falsy) — but the section's HTML class string
115 // is the stable identifier we assert when it IS present, which it
116 // will be whenever the lazy computePublicStats() returns a payload.
117 // Either way, the L4 builder export must still exist + be wired.
118 const { buildSocialProofTiles } = await import("../views/landing");
119 expect(typeof buildSocialProofTiles).toBe("function");
120
121 // If the conditional rendered, the class is in the markup. If it
122 // didn't render, the COUNTERS animation script string isn't either —
123 // both signals stay aligned, so a regression that DROPS the section
124 // would still be caught by the class-only path on the common case.
125 if (body.includes("landing-counters-grid")) {
126 // Tile section is in the markup — confirm the count-up script is too.
127 expect(body).toContain("data-counter-target");
128 }
129 });
130
131 it("REGRESSION: L5 'Compare to GitHub' CTA still routes to /vs-github", async () => {
132 const res = await app.request(HOME);
133 const body = await res.text();
134 // The href + label pair must remain wired.
135 expect(body).toContain('href="/vs-github"');
136 expect(body).toContain("Compare to GitHub");
137 });
138});
Addedsrc/__tests__/pricing-page.test.ts+96−0View fileUnifiedSplit
1/**
2 * Block L8 — public /pricing page tests.
3 *
4 * Anonymous-safe route. Verifies:
5 * - GET /pricing returns 200 HTML to a logged-out visitor
6 * - All four plan names (Free, Pro, Team, Enterprise) render
7 * - The "what you get on free" block contains the AI features
8 * - All five FAQ questions are present verbatim
9 * - CTA links resolve to /register?next=... or /settings/billing
10 * - The self-host column mentions the curl install line
11 */
12
13import { describe, it, expect } from "bun:test";
14import app from "../app";
15import { FALLBACK_PLANS } from "../lib/billing";
16
17describe("L8 — /pricing public page", () => {
18 it("returns 200 HTML to an anonymous visitor", async () => {
19 const res = await app.request("/pricing");
20 expect(res.status).toBe(200);
21 const ct = res.headers.get("content-type") || "";
22 expect(ct.toLowerCase()).toContain("text/html");
23 });
24
25 it("renders all four plan names (Free, Pro, Team, Enterprise)", async () => {
26 const res = await app.request("/pricing");
27 const body = await res.text();
28 // FALLBACK_PLANS guarantees the four names exist regardless of whether
29 // the DB seeds are loaded — listPlans() falls back to these.
30 for (const slug of Object.keys(FALLBACK_PLANS)) {
31 const name = FALLBACK_PLANS[slug].name;
32 expect(body).toContain(name);
33 }
34 });
35
36 it("the free-tier block lists at least 6 of the included AI features", async () => {
37 const res = await app.request("/pricing");
38 const body = await res.text();
39 const features = [
40 "Unlimited public repos",
41 "AI code review on every PR",
42 "AI auto-merge",
43 "ai:build label",
44 "Sleep Mode digest",
45 "AI hours saved counter",
46 "MCP server access",
47 "Claude Code skill bundle",
48 "One-command install",
49 "GitHub OIDC sign-in",
50 ];
51 const present = features.filter((f) => body.includes(f));
52 expect(present.length).toBeGreaterThanOrEqual(6);
53 });
54
55 it("FAQ contains all five required questions", async () => {
56 const res = await app.request("/pricing");
57 const body = await res.text();
58 expect(body).toContain("Is it really free? What&#39;s the catch?");
59 expect(body).toContain(
60 "Do I need to bring my own Anthropic API key on the free tier?"
61 );
62 expect(body).toContain("What happens when I exceed my plan&#39;s quota?");
63 expect(body).toContain("Can I migrate from GitHub for free?");
64 expect(body).toContain("Does the free tier include private repos?");
65 });
66
67 it("CTAs route anonymous users through /register?next=/settings/billing", async () => {
68 const res = await app.request("/pricing");
69 const body = await res.text();
70 // At least one register-funnel CTA must exist.
71 expect(body).toMatch(/href="\/register(\?next=\/settings\/billing[^"]*)?"/);
72 // And the page must mention /settings/billing somewhere as the
73 // destination after sign-up.
74 expect(body).toContain("/settings/billing");
75 });
76
77 it("self-host column mentions `curl gluecron.com/install`", async () => {
78 const res = await app.request("/pricing");
79 const body = await res.text();
80 expect(body).toContain("curl gluecron.com/install");
81 });
82
83 it("does not require authentication (no redirect)", async () => {
84 const res = await app.request("/pricing");
85 expect(res.status).toBe(200);
86 expect(res.status).not.toBe(302);
87 expect(res.status).not.toBe(401);
88 });
89
90 it("hero copy contains the L8 tagline", async () => {
91 const res = await app.request("/pricing");
92 const body = await res.text();
93 expect(body).toContain("Free for the AI-curious.");
94 expect(body).toContain("Pay only when you&#39;re ready to scale.");
95 });
96});
Modifiedsrc/app.tsx+6−0View fileUnifiedSplit
3232import statusRoutes from "./routes/status";
3333import helpRoutes from "./routes/help";
3434import marketingRoutes from "./routes/marketing";
35import pricingRoutes from "./routes/pricing";
3536import seoRoutes from "./routes/seo";
3637import versionRoutes from "./routes/version";
3738import { platformStatus } from "./routes/platform-status";
275276// /help — quickstart + API cheatsheet
276277app.route("/", helpRoutes);
277278
279// L8 — public /pricing page (free-tier polish). Mounted BEFORE marketing
280// so the new editorial pricing layout wins the route; the legacy marketing
281// pricing remains as a safety net but is shadowed at the router.
282app.route("/", pricingRoutes);
283
278284// /pricing, /features, /about — marketing surface
279285app.route("/", marketingRoutes);
280286
Modifiedsrc/routes/billing.tsx+78−0View fileUnifiedSplit
5858 );
5959 };
6060
61 // L8 — bigger usage bar styles (scoped, additive). The original panel
62 // bars below are left untouched.
63 const bigBar = (pct: number) => {
64 const color = pct >= 90 ? "var(--red)" : pct >= 70 ? "#f0b72f" : "var(--green)";
65 return (
66 <div
67 style="background:var(--bg-secondary);height:14px;border-radius:7px;overflow:hidden;border:1px solid var(--border-subtle)"
68 >
69 <div
70 style={`width:${pct}%;height:100%;background:${color};transition:width .3s`}
71 />
72 </div>
73 );
74 };
75
6176 return c.html(
6277 <Layout title="Billing — Gluecron" user={user}>
6378 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
6782 </a>
6883 </div>
6984
85 {/* L8 — "here's what you've used this month" hero panel. */}
86 <div
87 class="panel"
88 style="padding:20px;margin-bottom:20px;background:linear-gradient(180deg,rgba(140,109,255,0.05),transparent 70%),var(--bg-elevated);border-color:rgba(140,109,255,0.20)"
89 >
90 <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:16px">
91 <div>
92 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.14em;font-family:var(--font-mono);margin-bottom:6px">
93 Hey, here's what you've used this month
94 </div>
95 <div style="font-size:14px;color:var(--text-muted)">
96 On the <strong style="color:var(--text-strong)">{quota.plan.name}</strong> plan —{" "}
97 {formatPrice(quota.plan.priceCents)}
98 </div>
99 </div>
100 {quota.planSlug === "free" && (
101 <form method="post" action="/billing/upgrade/pro">
102 <button type="submit" class="btn btn-primary">
103 Upgrade to Pro &rarr;
104 </button>
105 </form>
106 )}
107 </div>
108 <div style="display:flex;flex-direction:column;gap:14px">
109 <div>
110 <div style="display:flex;justify-content:space-between;margin-bottom:6px;font-size:13px">
111 <span style="color:var(--text-strong);font-weight:500">Repos</span>
112 <span style="color:var(--text-muted);font-family:var(--font-mono)">
113 {Math.min(quota.plan.repoLimit, quota.plan.repoLimit)} max on plan
114 </span>
115 </div>
116 {bigBar(0)}
117 </div>
118 <div>
119 <div style="display:flex;justify-content:space-between;margin-bottom:6px;font-size:13px">
120 <span style="color:var(--text-strong);font-weight:500">AI calls</span>
121 <span style="color:var(--text-muted);font-family:var(--font-mono)">
122 {quota.usage.aiTokensUsedThisMonth.toLocaleString()} /{" "}
123 {quota.plan.aiTokensMonthly.toLocaleString()} ({quota.percent.aiTokens}%)
124 </span>
125 </div>
126 {bigBar(quota.percent.aiTokens)}
127 </div>
128 <div>
129 <div style="display:flex;justify-content:space-between;margin-bottom:6px;font-size:13px">
130 <span style="color:var(--text-strong);font-weight:500">Storage</span>
131 <span style="color:var(--text-muted);font-family:var(--font-mono)">
132 {quota.usage.storageMbUsed} / {quota.plan.storageMbLimit} MB ({quota.percent.storage}%)
133 </span>
134 </div>
135 {bigBar(quota.percent.storage)}
136 </div>
137 </div>
138 </div>
139
70140 <div class="panel" style="padding:16px;margin-bottom:20px">
71141 <div style="display:flex;justify-content:space-between;align-items:center">
72142 <div>
195265 return a setup error. Run the Stripe Bootstrap workflow to enable.)
196266 </p>
197267 )}
268
269 {/* L8 — detailed plan comparison link, points at the public /pricing page. */}
270 <p style="font-size:13px;color:var(--text-muted);margin-top:24px;text-align:center">
271 Want the full breakdown of what's included?{" "}
272 <a href="/pricing" style="color:var(--accent);font-weight:500">
273 Detailed plan comparison &rarr;
274 </a>
275 </p>
198276 </Layout>
199277 );
200278});
Addedsrc/routes/pricing.tsx+641−0View fileUnifiedSplit
1/**
2 * Block L8 — public `/pricing` page.
3 *
4 * Anonymous-safe GET /pricing. Reads the real plan rows from
5 * `billing_plans` (or `FALLBACK_PLANS` when seeds aren't loaded yet — both
6 * mirror migration 0020) so the price column never drifts from the actual
7 * billing config. This route is mounted BEFORE `routes/marketing.tsx` in
8 * `app.tsx` so the new editorial layout wins; the legacy marketing /pricing
9 * remains as a safety net.
10 *
11 * Anchors:
12 * #free → "What you get on the free tier" block
13 * #self-host → Self-host vs Cloud comparison
14 * #faq → Frequently asked questions
15 *
16 * Pure presentational — no billing logic is created here. The page only
17 * surfaces what `src/lib/billing.ts` already ships.
18 */
19
20import { Hono } from "hono";
21import type { FC } from "hono/jsx";
22import { Layout } from "../views/layout";
23import { softAuth } from "../middleware/auth";
24import type { AuthEnv } from "../middleware/auth";
25import { formatPrice, listPlans } from "../lib/billing";
26
27const pricing = new Hono<AuthEnv>();
28pricing.use("*", softAuth);
29
30// ---- Per-slug copy: tagline + included-bullet list ------------------------
31// Indexed by the seeded plan slugs. Anything not in this map falls back to
32// generic copy derived from the plan's numeric limits so we never miss a
33// row even if a future migration adds a new tier.
34const PLAN_COPY: Record<
35 string,
36 { tagline: string; supportTier: string }
37> = {
38 free: {
39 tagline: "Personal projects + open source. Full AI suite.",
40 supportTier: "Community support",
41 },
42 pro: {
43 tagline: "Working developers shipping every day.",
44 supportTier: "Email support, priority AI queue",
45 },
46 team: {
47 tagline: "Teams running production on Gluecron.",
48 supportTier: "Slack channel + 24h response",
49 },
50 enterprise: {
51 tagline: "Orgs that need SSO, audit, on-prem.",
52 supportTier: "24/7 incident response + DPA",
53 },
54};
55
56pricing.get("/pricing", async (c) => {
57 const user = c.get("user");
58 const plans = await listPlans();
59 return c.html(
60 <Layout title="Pricing — Gluecron" user={user}>
61 <PricingPage plans={plans} loggedIn={!!user} />
62 </Layout>
63 );
64});
65
66interface Plan {
67 slug: string;
68 name: string;
69 priceCents: number;
70 repoLimit: number;
71 storageMbLimit: number;
72 aiTokensMonthly: number;
73 bandwidthGbMonthly: number;
74 privateRepos: boolean;
75}
76
77const PricingPage: FC<{ plans: Plan[]; loggedIn: boolean }> = ({
78 plans,
79 loggedIn,
80}) => (
81 <>
82 <style dangerouslySetInnerHTML={{ __html: pricingCss }} />
83 <div class="pl-root">
84 {/* ------------------- Hero ------------------- */}
85 <header class="pl-hero">
86 <div class="eyebrow">Pricing</div>
87 <h1 class="display pl-hero-title">
88 Free for the AI-curious.{" "}
89 <span class="gradient-text">Pay only when you're ready to scale.</span>
90 </h1>
91 <p class="pl-hero-sub">
92 Self-host on your own server and pay zero per-seat fees. Or use
93 Gluecron Cloud for managed convenience.
94 </p>
95 <div class="pl-hero-jumps">
96 <a href="#free" class="pl-jump">What's free →</a>
97 <a href="#self-host" class="pl-jump">Self-host vs Cloud</a>
98 <a href="#faq" class="pl-jump">FAQ</a>
99 </div>
100 </header>
101
102 {/* ------------------- Plan cards ------------------- */}
103 <section class="pl-plans stagger">
104 {plans.map((p) => (
105 <PlanCard plan={p} loggedIn={loggedIn} />
106 ))}
107 </section>
108
109 {/* ------------------- What's on the free tier ------------------- */}
110 <section id="free" class="pl-section pl-free">
111 <div class="section-header">
112 <div class="eyebrow">Free tier</div>
113 <h2>Everything below is yours on the free tier.</h2>
114 <p>
115 All the AI features. Not a "try it for 14 days" trial. Not a
116 "core features" stub. The whole Claude-powered platform — on
117 unlimited public repos, forever.
118 </p>
119 </div>
120 <ul class="pl-free-grid">
121 <FreeItem label="Unlimited public repos" />
122 <FreeItem label="AI code review on every PR (Sonnet 4)" />
123 <FreeItem label="AI auto-merge when checks pass (K2)" />
124 <FreeItem label="ai:build label → spec-to-PR (K3)" />
125 <FreeItem label="Sleep Mode digest (L1)" />
126 <FreeItem label="AI hours saved counter (L9)" />
127 <FreeItem label="MCP server access (K1)" />
128 <FreeItem label="Claude Code skill bundle (L7)" />
129 <FreeItem label="One-command install" />
130 <FreeItem label="GitHub OIDC sign-in" />
131 <FreeItem label="Webhooks + REST API v2 + GraphQL" />
132 <FreeItem label="Package registry + Pages hosting" />
133 </ul>
134 </section>
135
136 {/* ------------------- Self-host vs Cloud ------------------- */}
137 <section id="self-host" class="pl-section">
138 <div class="section-header">
139 <div class="eyebrow">Two ways to run it</div>
140 <h2>Self-host on your metal. Or let us run it.</h2>
141 <p>
142 Same product, same code, same Claude-powered features. The only
143 difference is who pays the electricity bill.
144 </p>
145 </div>
146 <div class="pl-host-grid">
147 <div class="pl-host-col">
148 <div class="pl-host-name">Self-host</div>
149 <div class="pl-host-price">Free forever</div>
150 <ul class="pl-host-feats">
151 <li>Free forever — no license, no per-seat fee</li>
152 <li>Your database, your disk, your control</li>
153 <li>You pay your Anthropic API key directly</li>
154 <li>Run via <code>curl gluecron.com/install</code></li>
155 <li>Or the Hetzner bootstrap script in 30 seconds</li>
156 </ul>
157 <a href="/install" class="btn btn-secondary btn-block pl-host-cta">
158 Self-host guide
159 </a>
160 </div>
161 <div class="pl-host-col pl-host-cloud">
162 <div class="pl-host-name">Gluecron Cloud</div>
163 <div class="pl-host-price">From $0/mo</div>
164 <ul class="pl-host-feats">
165 <li>Managed — we run the server, you push code</li>
166 <li>Opinionated stack, zero ops on your end</li>
167 <li>Automatic upgrades to every new block</li>
168 <li>Support included on paid plans</li>
169 <li>Plan-based pricing, no surprise overage</li>
170 </ul>
171 <a
172 href={loggedIn ? "/settings/billing" : "/register?next=/settings/billing"}
173 class="btn btn-primary btn-block pl-host-cta"
174 >
175 Start on Cloud
176 </a>
177 </div>
178 </div>
179 </section>
180
181 {/* ------------------- FAQ ------------------- */}
182 <section id="faq" class="pl-section">
183 <div class="section-header">
184 <div class="eyebrow">Questions</div>
185 <h2>The fine print, in plain English.</h2>
186 </div>
187 <div class="pl-faq">
188 <FaqItem
189 q="Is it really free? What's the catch?"
190 a="Really free. The free tier exists because we want every Claude-curious developer to try Gluecron without a credit card. The catch — if you can call it that — is that we hope you'll upgrade to Pro once you're shipping production traffic and need higher AI quotas."
191 />
192 <FaqItem
193 q="Do I need to bring my own Anthropic API key on the free tier?"
194 a="No. The free tier includes a generous monthly AI quota powered by our keys. If you'd rather use your own key (for cost control or enterprise rate limits), you can plug it in at /settings — Pro and above can route AI through your account."
195 />
196 <FaqItem
197 q="What happens when I exceed my plan's quota?"
198 a="AI features degrade gracefully — git push, hosting, and gates keep working. AI suggestions queue at the back of the line until the next cycle. We never auto-bill you for overage or auto-upgrade your plan."
199 />
200 <FaqItem
201 q="Can I migrate from GitHub for free?"
202 a="Yes. The migration tool is on every tier, free included. Point it at a GitHub repo URL and we mirror code, issues, PRs, and releases in one shot. No vendor lock — you can migrate back the same way."
203 />
204 <FaqItem
205 q="Does the free tier include private repos?"
206 a="The free tier is built around unlimited public repos. Private repos start on the Pro plan — that's the main paid-tier perk along with the higher AI quota. If you're self-hosting, all repos are private by default and there's no plan to worry about."
207 />
208 </div>
209 </section>
210
211 {/* ------------------- CTA ------------------- */}
212 <section class="pl-section pl-cta-wrap">
213 <div class="pl-cta">
214 <h2 class="pl-cta-title">
215 Ready to push your first repo?
216 </h2>
217 <p class="pl-cta-sub">
218 Free, no credit card, full AI suite from minute one.
219 </p>
220 <div class="pl-cta-buttons">
221 <a href="/register" class="btn btn-primary btn-xl">
222 Start free
223 </a>
224 <a href="/vs-github" class="btn btn-ghost btn-xl">
225 Compare to GitHub
226 </a>
227 </div>
228 </div>
229 </section>
230 </div>
231 </>
232);
233
234// ---- Sub-components -------------------------------------------------------
235
236const PlanCard: FC<{ plan: Plan; loggedIn: boolean }> = ({ plan, loggedIn }) => {
237 const copy = PLAN_COPY[plan.slug] || {
238 tagline: `${plan.name} plan.`,
239 supportTier: "Support included",
240 };
241 const href = loggedIn
242 ? `/settings/billing?plan=${plan.slug}`
243 : `/register?next=/settings/billing?plan=${plan.slug}`;
244 const isPro = plan.slug === "pro";
245 return (
246 <div class={`pl-card${isPro ? " pl-card-hl" : ""}`}>
247 {isPro && <div class="pl-card-badge">Most popular</div>}
248 <div class="pl-card-name">{plan.name}</div>
249 <div class="pl-card-price">
250 <span class="pl-card-num">{formatPrice(plan.priceCents)}</span>
251 </div>
252 <p class="pl-card-tag">{copy.tagline}</p>
253 <ul class="pl-card-feats">
254 <li>
255 <span class="pl-check">{"✓"}</span>
256 {plan.repoLimit.toLocaleString()} repos
257 {plan.privateRepos ? " (public + private)" : " (public only)"}
258 </li>
259 <li>
260 <span class="pl-check">{"✓"}</span>
261 {plan.aiTokensMonthly.toLocaleString()} AI tokens / month
262 </li>
263 <li>
264 <span class="pl-check">{"✓"}</span>
265 {plan.storageMbLimit.toLocaleString()} MB storage
266 </li>
267 <li>
268 <span class="pl-check">{"✓"}</span>
269 {plan.bandwidthGbMonthly.toLocaleString()} GB bandwidth / month
270 </li>
271 <li>
272 <span class="pl-check">{"✓"}</span>
273 {copy.supportTier}
274 </li>
275 </ul>
276 <a
277 href={href}
278 class={`btn ${isPro ? "btn-primary" : "btn-secondary"} btn-block pl-card-cta`}
279 >
280 Choose {plan.name}
281 </a>
282 </div>
283 );
284};
285
286const FreeItem: FC<{ label: string }> = ({ label }) => (
287 <li class="pl-free-item">
288 <span class="pl-free-check">{"✓"}</span>
289 <span>{label}</span>
290 </li>
291);
292
293const FaqItem: FC<{ q: string; a: string }> = ({ q, a }) => (
294 <details class="pl-faq-item">
295 <summary class="pl-faq-q">
296 <span>{q}</span>
297 <span class="pl-faq-toggle" aria-hidden="true">{"+"}</span>
298 </summary>
299 <p class="pl-faq-a">{a}</p>
300 </details>
301);
302
303// ---- Styles (scoped under .pl-) -------------------------------------------
304
305const pricingCss = `
306 .pl-root { max-width: 1180px; margin: 0 auto; padding: 0 16px; }
307
308 /* Hero */
309 .pl-hero {
310 text-align: center;
311 padding: var(--s-16) 0 var(--s-10);
312 max-width: 920px;
313 margin: 0 auto;
314 position: relative;
315 }
316 .pl-hero::before {
317 content: '';
318 position: absolute;
319 top: 0; left: 50%;
320 transform: translateX(-50%);
321 width: 70%; height: 60%;
322 background: radial-gradient(ellipse at center, rgba(140,109,255,0.14), transparent 65%);
323 z-index: -1;
324 pointer-events: none;
325 }
326 .pl-hero .eyebrow { justify-content: center; margin: 0 auto var(--s-4); }
327 .pl-hero-title {
328 font-size: clamp(36px, 6.5vw, 72px);
329 line-height: 1.02;
330 letter-spacing: -0.038em;
331 margin: 0 0 var(--s-5);
332 }
333 .pl-hero-sub {
334 font-size: clamp(15px, 1.5vw, 18px);
335 color: var(--text-muted);
336 max-width: 640px;
337 margin: 0 auto;
338 line-height: 1.55;
339 }
340 .pl-hero-jumps {
341 display: flex;
342 gap: 18px;
343 justify-content: center;
344 flex-wrap: wrap;
345 margin-top: var(--s-7);
346 }
347 .pl-jump {
348 font-family: var(--font-mono);
349 font-size: 12px;
350 color: var(--text-muted);
351 text-decoration: none;
352 padding: 6px 12px;
353 border: 1px solid var(--border-subtle);
354 border-radius: var(--r-full);
355 transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
356 }
357 .pl-jump:hover { color: var(--accent); border-color: rgba(140,109,255,0.35); }
358
359 /* Plan cards */
360 .pl-plans {
361 display: grid;
362 grid-template-columns: repeat(4, 1fr);
363 gap: 14px;
364 margin: var(--s-10) auto var(--s-14);
365 align-items: stretch;
366 }
367 .pl-card {
368 position: relative;
369 background: var(--bg-elevated);
370 border: 1px solid var(--border);
371 border-radius: var(--r-lg);
372 padding: var(--s-7) var(--s-6);
373 display: flex;
374 flex-direction: column;
375 gap: var(--s-4);
376 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
377 }
378 .pl-card:hover { border-color: var(--border-strong); transform: translateY(-3px); }
379 .pl-card-hl {
380 border-color: rgba(140,109,255,0.40);
381 box-shadow: var(--elev-2), 0 0 0 1px rgba(140,109,255,0.30);
382 background:
383 linear-gradient(180deg, rgba(140,109,255,0.05), transparent 50%),
384 var(--bg-elevated);
385 }
386 .pl-card-badge {
387 position: absolute;
388 top: -10px;
389 left: 50%;
390 transform: translateX(-50%);
391 padding: 3px 12px;
392 background: var(--accent-gradient);
393 color: #fff;
394 font-family: var(--font-mono);
395 font-size: 10px;
396 letter-spacing: 0.1em;
397 text-transform: uppercase;
398 font-weight: 600;
399 border-radius: var(--r-full);
400 box-shadow: 0 4px 14px -2px rgba(140,109,255,0.45);
401 white-space: nowrap;
402 }
403 .pl-card-name {
404 font-family: var(--font-mono);
405 font-size: 11px;
406 text-transform: uppercase;
407 letter-spacing: 0.16em;
408 color: var(--text-muted);
409 }
410 .pl-card-price { display: flex; align-items: baseline; gap: 6px; }
411 .pl-card-num {
412 font-family: var(--font-display);
413 font-size: 32px;
414 font-weight: 600;
415 letter-spacing: -0.03em;
416 color: var(--text-strong);
417 }
418 .pl-card-tag {
419 font-size: var(--t-sm);
420 color: var(--text-muted);
421 line-height: 1.5;
422 margin: 0;
423 }
424 .pl-card-feats {
425 list-style: none;
426 padding: 0;
427 margin: 0;
428 display: flex;
429 flex-direction: column;
430 gap: 7px;
431 font-size: 13px;
432 color: var(--text);
433 }
434 .pl-card-feats li {
435 display: flex;
436 align-items: flex-start;
437 gap: 9px;
438 line-height: 1.45;
439 }
440 .pl-check {
441 color: var(--accent);
442 font-weight: 600;
443 flex-shrink: 0;
444 line-height: 1.45;
445 }
446 .pl-card-cta { margin-top: auto; }
447
448 /* Section base */
449 .pl-section { margin: var(--s-14) auto; }
450
451 /* Free-tier block */
452 .pl-free-grid {
453 list-style: none;
454 padding: 0;
455 margin: var(--s-6) auto 0;
456 display: grid;
457 grid-template-columns: repeat(2, 1fr);
458 gap: 10px 32px;
459 max-width: 880px;
460 }
461 .pl-free-item {
462 display: flex;
463 align-items: flex-start;
464 gap: 10px;
465 padding: 12px 16px;
466 background: var(--bg-elevated);
467 border: 1px solid var(--border-subtle);
468 border-radius: var(--r);
469 font-size: var(--t-sm);
470 color: var(--text);
471 line-height: 1.45;
472 transition: border-color var(--t-fast) var(--ease);
473 }
474 .pl-free-item:hover { border-color: rgba(140,109,255,0.35); }
475 .pl-free-check {
476 color: var(--green);
477 font-weight: 700;
478 flex-shrink: 0;
479 }
480
481 /* Self-host vs Cloud */
482 .pl-host-grid {
483 display: grid;
484 grid-template-columns: 1fr 1fr;
485 gap: 16px;
486 max-width: 920px;
487 margin: 0 auto;
488 }
489 .pl-host-col {
490 background: var(--bg-elevated);
491 border: 1px solid var(--border);
492 border-radius: var(--r-lg);
493 padding: var(--s-7) var(--s-6);
494 display: flex;
495 flex-direction: column;
496 gap: var(--s-4);
497 }
498 .pl-host-cloud {
499 border-color: rgba(140,109,255,0.35);
500 box-shadow: 0 0 0 1px rgba(140,109,255,0.20);
501 background:
502 linear-gradient(180deg, rgba(140,109,255,0.04), transparent 60%),
503 var(--bg-elevated);
504 }
505 .pl-host-name {
506 font-family: var(--font-mono);
507 font-size: 11px;
508 text-transform: uppercase;
509 letter-spacing: 0.16em;
510 color: var(--text-muted);
511 }
512 .pl-host-price {
513 font-family: var(--font-display);
514 font-size: 28px;
515 font-weight: 600;
516 letter-spacing: -0.025em;
517 color: var(--text-strong);
518 }
519 .pl-host-feats {
520 list-style: none;
521 padding: 0;
522 margin: 0;
523 display: flex;
524 flex-direction: column;
525 gap: 8px;
526 font-size: var(--t-sm);
527 color: var(--text);
528 }
529 .pl-host-feats li {
530 display: flex;
531 gap: 9px;
532 line-height: 1.5;
533 }
534 .pl-host-feats li::before {
535 content: '→';
536 color: var(--accent);
537 flex-shrink: 0;
538 }
539 .pl-host-feats code {
540 background: var(--bg-secondary);
541 border: 1px solid var(--border-subtle);
542 padding: 1px 6px;
543 border-radius: 4px;
544 font-size: 11.5px;
545 font-family: var(--font-mono);
546 color: var(--accent);
547 }
548 .pl-host-cta { margin-top: auto; }
549
550 /* FAQ */
551 .pl-faq {
552 max-width: 760px;
553 margin: 0 auto;
554 border: 1px solid var(--border);
555 border-radius: var(--r-lg);
556 overflow: hidden;
557 background: var(--bg-elevated);
558 }
559 .pl-faq-item { border-bottom: 1px solid var(--border-subtle); }
560 .pl-faq-item:last-child { border-bottom: none; }
561 .pl-faq-q {
562 display: flex;
563 justify-content: space-between;
564 align-items: center;
565 gap: 16px;
566 padding: 18px 24px;
567 cursor: pointer;
568 font-size: var(--t-md);
569 font-weight: 500;
570 color: var(--text-strong);
571 list-style: none;
572 transition: background var(--t-fast) var(--ease);
573 }
574 .pl-faq-q::-webkit-details-marker { display: none; }
575 .pl-faq-q:hover { background: var(--bg-hover); }
576 .pl-faq-toggle {
577 font-family: var(--font-mono);
578 font-size: 18px;
579 color: var(--text-muted);
580 transition: transform var(--t-base) var(--ease-spring);
581 flex-shrink: 0;
582 }
583 .pl-faq-item[open] .pl-faq-toggle { transform: rotate(45deg); color: var(--accent); }
584 .pl-faq-a {
585 padding: 0 24px 20px;
586 color: var(--text-muted);
587 font-size: var(--t-sm);
588 line-height: 1.6;
589 margin: 0;
590 }
591
592 /* CTA */
593 .pl-cta-wrap { margin: var(--s-16) auto var(--s-10); }
594 .pl-cta {
595 position: relative;
596 text-align: center;
597 padding: var(--s-12) var(--s-6);
598 border: 1px solid var(--border-strong);
599 border-radius: var(--r-2xl);
600 background:
601 radial-gradient(60% 100% at 50% 0%, rgba(140,109,255,0.14), transparent 65%),
602 var(--bg-elevated);
603 overflow: hidden;
604 }
605 .pl-cta-title {
606 font-family: var(--font-display);
607 font-size: clamp(24px, 3.5vw, 40px);
608 line-height: 1.1;
609 letter-spacing: -0.025em;
610 font-weight: 600;
611 margin: 0 0 var(--s-3);
612 color: var(--text-strong);
613 }
614 .pl-cta-sub {
615 font-size: var(--t-md);
616 color: var(--text-muted);
617 margin: 0 auto var(--s-6);
618 max-width: 480px;
619 }
620 .pl-cta-buttons {
621 display: flex;
622 gap: 12px;
623 justify-content: center;
624 flex-wrap: wrap;
625 }
626
627 /* Responsive */
628 @media (max-width: 960px) {
629 .pl-plans { grid-template-columns: repeat(2, 1fr); }
630 }
631 @media (max-width: 720px) {
632 .pl-host-grid { grid-template-columns: 1fr; }
633 .pl-free-grid { grid-template-columns: 1fr; }
634 }
635 @media (max-width: 560px) {
636 .pl-plans { grid-template-columns: 1fr; }
637 .pl-cta-buttons .btn { width: 100%; justify-content: center; }
638 }
639`;
640
641export default pricing;
Modifiedsrc/routes/web.tsx+10−1View fileUnifiedSplit
9090 }
9191
9292 return c.html(
93 <Layout user={null}>
93 <Layout
94 user={null}
95 // Block L10SEO + Open Graph for the public landing.
96 fullTitle="Gluecron — The git host built around Claude"
97 description="Label an issue. Walk away. Wake up to a merged PR. Gluecron is the AI-native git host with built-in code review, auto-merge, and a Claude-first toolchain."
98 ogTitle="Gluecron — The git host built around Claude"
99 ogDescription="Label an issue. Walk away. Wake up to a merged PR. Gluecron is the AI-native git host with built-in code review, auto-merge, and a Claude-first toolchain."
100 ogType="website"
101 twitterCard="summary_large_image"
102 >
94103 <LandingPage stats={stats} publicStats={publicStats} />
95104 </Layout>
96105 );
Modifiedsrc/views/landing.tsx+439−9View fileUnifiedSplit
55 * Hero · trust strip · feature grid · workflow walkthrough ·
66 * comparison · terminal · pricing teaser · closing CTA.
77 *
8 * Block L10 — hero rewrite. The hero now lands the Block L positioning
9 * ("the git host built around Claude"): gradient headline, one-line
10 * install snippet w/ copy button, three CTAs (Sign up / Demo / vs-GitHub),
11 * and a four-line "what just happened" rail driven off the L4 publicStats
12 * payload. The L4 counters tile section and L5 vs-GitHub CTA are both
13 * preserved — additive only.
14 *
15 * Also adds two new editorial sections below the L4 counters:
16 * - "Three reasons to switch" (Sleep Mode / Migrate / Demo)
17 * - "How is this different from GitHub?" pull-quote → /vs-github
18 *
819 * Pure presentational. Drops into <Layout user={null}>.
920 * All styles scoped under `.landing-` so they don't bleed into app views.
1021 */
5869 </div>
5970
6071 <h1 class="landing-hero-title display">
61 Where software{" "}
62 <span class="gradient-text">writes itself.</span>
72 <span class="gradient-text">The git host built around Claude.</span>
6373 </h1>
6474
6575 <p class="landing-hero-sub">
66 Gluecron is the operator-tier replacement for GitHub. Push code,
67 and the platform reviews it, fixes it, ships it. Spec-to-PR. Auto-repair.
68 Real-time gates. Built for the era when most code is written by AI
69 and most reviews are too.
76 Label an issue. Walk away. Wake up to a merged PR.
7077 </p>
7178
79 {/* L10 — one-line install snippet with copy button. */}
80 <div class="landing-hero-install" aria-label="One-line install">
81 <code class="landing-hero-install-code">
82 <span class="landing-hero-install-prompt" aria-hidden="true">$</span>
83 <span id="landing-install-text">curl -sSL gluecron.com/install | bash</span>
84 </code>
85 <button
86 type="button"
87 class="landing-hero-install-copy"
88 data-copy-target="landing-install-text"
89 aria-label="Copy install command"
90 >
91 Copy
92 </button>
93 </div>
94
7295 <div class="landing-hero-ctas">
7396 <a href="/register" class="btn btn-primary btn-xl landing-cta-primary">
74 Start shipping
97 Sign up free
7598 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
7699 </a>
77 <a href="/explore" class="btn btn-secondary btn-xl">
78 Explore repos
100 <a href="/demo" class="btn btn-secondary btn-xl">
101 Try the live demo
102 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
79103 </a>
80104 <a href="/vs-github" class="btn btn-ghost btn-xl">
81105 Compare to GitHub
106 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
82107 </a>
83108 </div>
84109
110 {/* L10 — "what just happened" rail. Mini, secondary,
111 separate from the BIG L4 counters tile section below. */}
112 {publicStats && (
113 <ul class="landing-hero-rail" aria-label="What just happened on Gluecron">
114 <li>
115 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
116 <strong>{publicStats.weeklyPrsAutoMerged.toLocaleString()}</strong>
117 {" PRs auto-merged this week"}
118 </li>
119 <li>
120 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
121 <strong>{publicStats.weeklyIssuesBuiltByAi.toLocaleString()}</strong>
122 {" issues built by AI"}
123 </li>
124 <li>
125 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
126 <strong>{publicStats.weeklyDeploysShipped.toLocaleString()}</strong>
127 {" deploys shipped overnight"}
128 </li>
129 <li>
130 <span class="landing-hero-rail-check" aria-hidden="true">{"✓"}</span>
131 {"~"}
132 <strong>{Math.round(publicStats.weeklyHoursSaved).toLocaleString()}</strong>
133 {" hours saved by AI"}
134 </li>
135 </ul>
136 )}
137
138 {/* L8 — free-tier reassurance link. Keeps anxiety low for the AI-curious. */}
139 <p class="landing-hero-freenote">
140 Free forever for the AI-curious.{" "}
141 <a href="/pricing" class="landing-hero-freenote-link">
142 See pricing &rarr;
143 </a>
144 </p>
145
85146 <p class="landing-hero-caption">
86147 Already have a repo?
87148 <span class="landing-hero-cmd">
145206 </section>
146207 )}
147208
209 {/* ---------- L10 — Three reasons to switch ---------- */}
210 <section class="landing-section landing-reasons" aria-label="Three reasons to switch">
211 <div class="section-header">
212 <div class="eyebrow">Three reasons to switch</div>
213 <h2>Built so Claude can do the work.</h2>
214 </div>
215 <div class="landing-reasons-grid">
216 <ReasonCard
217 icon={
218 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
219 <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
220 </svg>
221 }
222 title="Toggle Sleep Mode"
223 body="Claude does the work overnight. You get a 9 AM digest of what shipped — PRs merged, deploys live, incidents triaged."
224 link={{ href: "/sleep-mode", label: "Turn on Sleep Mode" }}
225 />
226 <ReasonCard
227 icon={
228 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
229 <polyline points="4 17 10 11 4 5" />
230 <line x1="12" y1="19" x2="20" y2="19" />
231 </svg>
232 }
233 title="One command to migrate"
234 body="Drop a single curl into your shell. Gluecron rehosts your repo, your issues, your branches — no SaaS rip-and-replace project required."
235 extra={
236 <code class="landing-reasons-code">curl -sSL gluecron.com/install | bash</code>
237 }
238 link={{ href: "/import", label: "Or import from GitHub" }}
239 />
240 <ReasonCard
241 icon={
242 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
243 <polygon points="5 3 19 12 5 21 5 3" />
244 </svg>
245 }
246 title="Open the demo, watch it work"
247 body="The demo repo is real. Label an issue, hit refresh, see Claude open the PR. Inspect the diff. Approve the merge. Zero setup, zero credit card."
248 link={{ href: "/demo", label: "Open the live demo" }}
249 />
250 </div>
251 </section>
252
148253 {/* ---------- Capability strip — uppercase tracked grid (crontech-style) ---------- */}
149254 <section class="landing-caps">
150255 <div class="landing-caps-grid">
390495 </div>
391496 </section>
392497
498 {/* ---------- L10 — "How is this different?" pull-quote ---------- */}
499 <section class="landing-pullquote-section" aria-label="How is this different from GitHub?">
500 <figure class="landing-pullquote">
501 <div class="landing-pullquote-eyebrow">How is this different from GitHub?</div>
502 <blockquote class="landing-pullquote-text">
503 Every other host bolts AI on as a sidecar. Gluecron is the first
504 git host where Claude is a first-class developer. Built to be
505 operated by AI agents, not just augmented by them.
506 </blockquote>
507 <a href="/vs-github" class="landing-pullquote-link">
508 See the full comparison
509 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
510 </a>
511 </figure>
512 </section>
513
393514 {/* ---------- Closing CTA ---------- */}
394515 <section class="landing-cta-section">
395516 <div class="landing-cta-card">
414535 </div>
415536 </div>
416537 </section>
538
539 {/* L10 — clipboard copy script for the hero install snippet. */}
540 <script dangerouslySetInnerHTML={{ __html: landingCopyJs }} />
417541 </div>
418542 </>
419543 );
433557 </div>
434558);
435559
560// Block L10 — "Three reasons to switch" column.
561const ReasonCard: FC<{
562 icon: any;
563 title: string;
564 body: string;
565 link: { href: string; label: string };
566 extra?: any;
567}> = ({ icon, title, body, link, extra }) => (
568 <div class="landing-reason">
569 <div class="landing-reason-icon" aria-hidden="true">
570 {icon}
571 </div>
572 <h3 class="landing-reason-title">{title}</h3>
573 <p class="landing-reason-body">{body}</p>
574 {extra}
575 <a href={link.href} class="landing-reason-link">
576 {link.label}
577 <span class="landing-cta-arrow" aria-hidden="true">{"→"}</span>
578 </a>
579 </div>
580);
581
436582const WalkStep: FC<{ n: string; title: string; desc: string }> = ({
437583 n,
438584 title,
9591105 transform: translateX(4px);
9601106 }
9611107
1108 /* L8 — free-tier reassurance link beneath the CTA row. */
1109 .landing-hero-freenote {
1110 margin-top: var(--s-5);
1111 font-size: var(--t-sm);
1112 color: var(--text-muted);
1113 text-align: center;
1114 }
1115 .landing-hero-freenote-link {
1116 color: var(--accent);
1117 text-decoration: none;
1118 font-weight: 500;
1119 border-bottom: 1px dotted rgba(140,109,255,0.4);
1120 transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
1121 }
1122 .landing-hero-freenote-link:hover {
1123 color: var(--text-strong);
1124 border-bottom-color: var(--accent);
1125 }
1126
9621127 .landing-hero-caption {
9631128 margin-top: var(--s-8);
9641129 font-size: var(--t-sm);
15431708 @media (max-width: 540px) {
15441709 .landing-counters-grid { grid-template-columns: repeat(2, 1fr); gap: 18px 12px; }
15451710 }
1711
1712 /* ---------- L10 hero install snippet ---------- */
1713 .landing-hero-install {
1714 display: inline-flex;
1715 align-items: stretch;
1716 gap: 0;
1717 margin: var(--s-8) auto 0;
1718 background: var(--bg-elevated);
1719 border: 1px solid var(--border-strong);
1720 border-radius: var(--r);
1721 box-shadow: var(--elev-1);
1722 overflow: hidden;
1723 max-width: 100%;
1724 font-family: var(--font-mono);
1725 }
1726 .landing-hero-install-code {
1727 display: inline-flex;
1728 align-items: center;
1729 gap: 10px;
1730 padding: 10px 14px;
1731 font-size: 13.5px;
1732 color: var(--text-strong);
1733 background: transparent;
1734 border: 0;
1735 white-space: nowrap;
1736 overflow-x: auto;
1737 }
1738 .landing-hero-install-prompt {
1739 color: var(--accent);
1740 user-select: none;
1741 }
1742 .landing-hero-install-copy {
1743 appearance: none;
1744 border: 0;
1745 border-left: 1px solid var(--border);
1746 background: transparent;
1747 color: var(--text-muted);
1748 font-family: var(--font-mono);
1749 font-size: 12px;
1750 font-weight: 600;
1751 letter-spacing: 0.06em;
1752 text-transform: uppercase;
1753 padding: 0 16px;
1754 cursor: pointer;
1755 transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
1756 }
1757 .landing-hero-install-copy:hover {
1758 background: var(--accent-gradient-faint);
1759 color: var(--accent);
1760 }
1761 .landing-hero-install-copy[data-copied="1"] {
1762 color: var(--green, #34d399);
1763 }
1764
1765 /* ---------- L10 hero "what just happened" rail ---------- */
1766 .landing-hero-rail {
1767 list-style: none;
1768 padding: 0;
1769 margin: var(--s-7) auto 0;
1770 display: flex;
1771 flex-wrap: wrap;
1772 justify-content: center;
1773 gap: 8px 22px;
1774 font-family: var(--font-sans);
1775 font-size: var(--t-sm);
1776 color: var(--text-muted);
1777 max-width: 760px;
1778 }
1779 .landing-hero-rail li {
1780 display: inline-flex;
1781 align-items: center;
1782 gap: 7px;
1783 line-height: 1.4;
1784 }
1785 .landing-hero-rail strong {
1786 color: var(--text-strong);
1787 font-weight: 600;
1788 font-feature-settings: 'tnum';
1789 }
1790 .landing-hero-rail-check {
1791 color: var(--accent);
1792 font-weight: 700;
1793 flex-shrink: 0;
1794 }
1795
1796 /* ---------- L10 three-reasons section ---------- */
1797 .landing-reasons { margin-top: var(--s-12); }
1798 .landing-reasons-grid {
1799 display: grid;
1800 grid-template-columns: repeat(3, 1fr);
1801 gap: 16px;
1802 }
1803 .landing-reason {
1804 background: var(--bg-elevated);
1805 border: 1px solid var(--border);
1806 border-radius: var(--r-lg);
1807 padding: var(--s-7);
1808 display: flex;
1809 flex-direction: column;
1810 gap: var(--s-3);
1811 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease);
1812 }
1813 .landing-reason:hover {
1814 border-color: var(--border-strong);
1815 transform: translateY(-2px);
1816 }
1817 .landing-reason-icon {
1818 display: inline-flex;
1819 align-items: center;
1820 justify-content: center;
1821 width: 40px;
1822 height: 40px;
1823 border-radius: var(--r);
1824 background: var(--accent-gradient-soft);
1825 color: var(--accent);
1826 border: 1px solid rgba(140,109,255,0.20);
1827 }
1828 .landing-reason-title {
1829 font-family: var(--font-display);
1830 font-size: 20px;
1831 font-weight: 600;
1832 letter-spacing: -0.02em;
1833 margin: 0;
1834 color: var(--text-strong);
1835 }
1836 .landing-reason-body {
1837 font-size: var(--t-sm);
1838 color: var(--text-muted);
1839 line-height: 1.55;
1840 margin: 0;
1841 }
1842 .landing-reasons-code {
1843 display: block;
1844 font-family: var(--font-mono);
1845 font-size: 12px;
1846 color: var(--text-strong);
1847 background: var(--bg);
1848 border: 1px solid var(--border);
1849 border-radius: var(--r);
1850 padding: 8px 12px;
1851 overflow-x: auto;
1852 white-space: nowrap;
1853 }
1854 .landing-reason-link {
1855 margin-top: auto;
1856 display: inline-flex;
1857 align-items: center;
1858 gap: 6px;
1859 color: var(--accent);
1860 font-size: var(--t-sm);
1861 font-weight: 500;
1862 text-decoration: none;
1863 }
1864 .landing-reason-link:hover { text-decoration: underline; }
1865 @media (max-width: 960px) {
1866 .landing-reasons-grid { grid-template-columns: 1fr; max-width: 520px; margin: 0 auto; }
1867 }
1868
1869 /* ---------- L10 "How is this different" pull-quote ---------- */
1870 .landing-pullquote-section {
1871 margin: var(--s-20) auto var(--s-12);
1872 max-width: 920px;
1873 padding: 0 var(--s-4);
1874 text-align: center;
1875 }
1876 .landing-pullquote {
1877 margin: 0;
1878 padding: var(--s-10) var(--s-7);
1879 background:
1880 radial-gradient(80% 100% at 50% 0%, rgba(140,109,255,0.10), transparent 65%),
1881 var(--bg-elevated);
1882 border: 1px solid var(--border-strong);
1883 border-radius: var(--r-xl);
1884 position: relative;
1885 overflow: hidden;
1886 }
1887 .landing-pullquote-eyebrow {
1888 font-family: var(--font-mono);
1889 font-size: 11px;
1890 font-weight: 600;
1891 letter-spacing: 0.16em;
1892 text-transform: uppercase;
1893 color: var(--accent);
1894 margin-bottom: var(--s-4);
1895 }
1896 .landing-pullquote-text {
1897 font-family: var(--font-display);
1898 font-size: clamp(20px, 2.4vw, 28px);
1899 line-height: 1.4;
1900 letter-spacing: -0.018em;
1901 color: var(--text-strong);
1902 margin: 0 auto;
1903 max-width: 760px;
1904 quotes: "\\201C" "\\201D";
1905 }
1906 .landing-pullquote-text::before { content: open-quote; color: var(--accent); margin-right: 4px; }
1907 .landing-pullquote-text::after { content: close-quote; color: var(--accent); margin-left: 4px; }
1908 .landing-pullquote-link {
1909 display: inline-flex;
1910 align-items: center;
1911 gap: 6px;
1912 margin-top: var(--s-6);
1913 color: var(--accent);
1914 font-size: var(--t-sm);
1915 font-weight: 500;
1916 text-decoration: none;
1917 }
1918 .landing-pullquote-link:hover { text-decoration: underline; }
1919
1920 /* ---------- L10 hero responsive overrides ---------- */
1921 @media (max-width: 640px) {
1922 .landing-hero-install { width: 100%; }
1923 .landing-hero-install-code { flex: 1; font-size: 12px; }
1924 .landing-hero-rail { flex-direction: column; align-items: flex-start; gap: 6px; padding: 0 var(--s-3); }
1925 .landing-hero-rail li { width: 100%; }
1926 }
15461927`;
15471928
15481929/**
15991980 } catch (_) { /* swallow — static numbers remain */ }
16001981})();
16011982`;
1983
1984/**
1985 * Block L10 — clipboard copy for the hero install snippet.
1986 *
1987 * Pure progressive enhancement. Without JS the user can still
1988 * triple-click + Cmd/Ctrl-C the snippet — the button is the
1989 * speed-bump, not the only path.
1990 */
1991const landingCopyJs = `
1992(function(){
1993 try {
1994 var btns = document.querySelectorAll('[data-copy-target]');
1995 if (!btns.length) return;
1996 btns.forEach(function(btn){
1997 btn.addEventListener('click', function(){
1998 var id = btn.getAttribute('data-copy-target') || '';
1999 var src = document.getElementById(id);
2000 if (!src) return;
2001 var text = src.textContent || '';
2002 var done = function(){
2003 var prev = btn.textContent;
2004 btn.textContent = 'Copied';
2005 btn.setAttribute('data-copied', '1');
2006 setTimeout(function(){
2007 btn.textContent = prev || 'Copy';
2008 btn.removeAttribute('data-copied');
2009 }, 1500);
2010 };
2011 if (navigator.clipboard && navigator.clipboard.writeText) {
2012 navigator.clipboard.writeText(text).then(done, function(){ done(); });
2013 } else {
2014 // Legacy fallback — temp textarea + execCommand.
2015 try {
2016 var ta = document.createElement('textarea');
2017 ta.value = text;
2018 ta.style.position = 'fixed';
2019 ta.style.opacity = '0';
2020 document.body.appendChild(ta);
2021 ta.select();
2022 document.execCommand('copy');
2023 document.body.removeChild(ta);
2024 done();
2025 } catch (_) {}
2026 }
2027 });
2028 });
2029 } catch (_) { /* swallow */ }
2030})();
2031`;
Modifiedsrc/views/layout.tsx+41−2View fileUnifiedSplit
1010 user?: User | null;
1111 notificationCount?: number;
1212 theme?: "dark" | "light";
13 // Block L10 — additive SEO + Open Graph fields. All optional;
14 // omission preserves the prior render exactly (regression-safe).
15 fullTitle?: string;
16 description?: string;
17 ogTitle?: string;
18 ogDescription?: string;
19 ogType?: string;
20 twitterCard?: "summary" | "summary_large_image";
1321 }>
14> = ({ children, title, user, notificationCount, theme }) => {
22> = ({
23 children,
24 title,
25 user,
26 notificationCount,
27 theme,
28 fullTitle,
29 description,
30 ogTitle,
31 ogDescription,
32 ogType,
33 twitterCard,
34}) => {
1535 const initialTheme = theme === "light" ? "light" : "dark";
1636 const build = getBuildInfo();
37 // L10 — when `fullTitle` is provided, use it verbatim (no " — gluecron"
38 // suffix); otherwise fall back to the existing `title` + suffix behaviour.
39 const renderedTitle = fullTitle
40 ? fullTitle
41 : title
42 ? `${title} — gluecron`
43 : "gluecron";
1744 return (
1845 <html lang="en" data-theme={initialTheme}>
1946 <head>
2249 <meta name="theme-color" content="#0d1117" />
2350 <link rel="manifest" href="/manifest.webmanifest" />
2451 <link rel="icon" type="image/svg+xml" href="/icon.svg" />
25 <title>{title ? `${title} — gluecron` : "gluecron"}</title>
52 <title>{renderedTitle}</title>
53 {description && <meta name="description" content={description} />}
54 {(ogTitle || fullTitle || title) && (
55 <meta property="og:title" content={ogTitle ?? fullTitle ?? renderedTitle} />
56 )}
57 {(ogDescription || description) && (
58 <meta
59 property="og:description"
60 content={ogDescription ?? description ?? ""}
61 />
62 )}
63 {ogType && <meta property="og:type" content={ogType} />}
64 {twitterCard && <meta name="twitter:card" content={twitterCard} />}
2665 <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
2766 <style dangerouslySetInnerHTML={{ __html: css }} />
2867 <style dangerouslySetInnerHTML={{ __html: hljsThemeCss }} />
2968