Skip to content

Commit a9a20b1

Browse files
committed
marketing: refresh homepage nav
- Drop the Pricing link. - Replace the plain GitHub link with a star-on-github pill that shows the live repo star count (fetched server-side with a short TTL cache and graceful fallback), at the left of the right-hand cluster. - Add a top-level Sign in link pointing at the cloud auth flow. - Give the nav an opaque band with a bottom hairline so the hero grid no longer bleeds through it. - Group the browse and auth links for a cleaner rhythm, and size the nav Get started button to match the pill height.
1 parent 529b908 commit a9a20b1

3 files changed

Lines changed: 156 additions & 23 deletions

File tree

apps/marketing/src/lib/github.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// GitHub repo metadata for the nav's "Star on GitHub" pill.
2+
//
3+
// The marketing site is SSR (astro.config `output: "server"`) on Cloudflare
4+
// Workers, so this runs per request rather than at build time. To keep that
5+
// cheap and resilient we:
6+
// - memoize the count in module scope with a TTL, so a warm isolate hits the
7+
// GitHub API at most once per window and dedupes concurrent requests;
8+
// - hint Cloudflare to edge-cache the upstream response (ignored by Node in
9+
// `astro dev`);
10+
// - time out fast so a slow upstream never holds up the homepage; and
11+
// - resolve to null on any failure, in which case the nav simply drops the
12+
// count and keeps the link.
13+
14+
const REPO = "RhysSullivan/executor";
15+
16+
export const GITHUB_REPO_URL = `https://github.com/${REPO}`;
17+
18+
/** Compact star count, e.g. 1234 -> "1.2k". */
19+
export function formatStars(n: number): string {
20+
return n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k` : String(n);
21+
}
22+
23+
const TTL_OK_MS = 30 * 60 * 1000; // cache a real count for ~30 min per isolate
24+
const TTL_FAIL_MS = 60 * 1000; // but retry soon after a miss, never pin a null
25+
const TIMEOUT_MS = 4000;
26+
27+
let cached: { value: number | null; at: number } | null = null;
28+
let inflight: Promise<number | null> | null = null;
29+
30+
async function fetchStars(): Promise<number | null> {
31+
const controller = new AbortController();
32+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
33+
try {
34+
const init: RequestInit & { cf?: { cacheTtl: number; cacheEverything: boolean } } = {
35+
headers: { "User-Agent": "executor.sh", Accept: "application/vnd.github+json" },
36+
signal: controller.signal,
37+
cf: { cacheTtl: 1800, cacheEverything: true },
38+
};
39+
const res = await fetch(`https://api.github.com/repos/${REPO}`, init);
40+
if (!res.ok) return null;
41+
const data = (await res.json()) as { stargazers_count?: number };
42+
return typeof data.stargazers_count === "number" ? data.stargazers_count : null;
43+
} catch {
44+
return null;
45+
} finally {
46+
clearTimeout(timer);
47+
}
48+
}
49+
50+
/** Live star count, memoized per isolate with a TTL. null when unavailable. */
51+
export function getStars(): Promise<number | null> {
52+
const now = Date.now();
53+
if (cached) {
54+
const ttl = cached.value != null ? TTL_OK_MS : TTL_FAIL_MS;
55+
if (now - cached.at < ttl) return Promise.resolve(cached.value);
56+
}
57+
if (inflight) return inflight;
58+
inflight = fetchStars().then((value) => {
59+
cached = { value, at: Date.now() };
60+
inflight = null;
61+
return value;
62+
});
63+
return inflight;
64+
}

apps/marketing/src/pages/index.astro

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import { SelfHostContactModal } from "../components/self-host-contact-modal";
66
// Imported so Vite emits it as a hashed build asset (served from /_astro/...);
77
// the deploy pipeline does not pick up newly added /public files.
88
import ycBackedBy from "../assets/yc-backed-by.svg?url";
9+
import { getStars, formatStars } from "../lib/github";
910
1011
const GH = "https://github.com/RhysSullivan/executor";
12+
// Cloud sign-in. Same origin in prod: the cloud Worker owns executor.sh and
13+
// /api/* is app-owned (apps/cloud/src/app-paths.ts), so this hits the WorkOS
14+
// login flow directly. Returning users land here; new users use Get started.
15+
const SIGN_IN = "/api/auth/login";
16+
const stars = await getStars();
1117
1218
// Copied to the clipboard by the "Set up with your agent" hero CTA. A visitor
1319
// pastes it into their coding agent (Claude, Cursor, ...) and the agent picks
@@ -63,7 +69,7 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
6369

6470
<div class="relative z-10">
6571
{/* ─── NAV ─── */}
66-
<nav>
72+
<nav class="border-b border-rule bg-surface">
6773
<div
6874
class="max-w-[1200px] mx-auto px-6 sm:px-10 h-14 flex items-center justify-between"
6975
>
@@ -73,28 +79,49 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
7379
>executor</span
7480
>
7581
</a>
76-
<div class="flex items-center gap-5">
77-
<a
78-
href="#pricing"
79-
class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline"
80-
>Pricing</a
81-
>
82-
<a
83-
href="https://executor.sh/docs"
84-
class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline"
85-
>Docs</a
86-
>
87-
<a
88-
href="/blog"
89-
class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline"
90-
>Blog</a
91-
>
92-
<a
93-
href={GH}
94-
class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors"
95-
>GitHub</a
96-
>
97-
<a href="#install" class="btn-primary">Get started →</a>
82+
<div class="flex items-center gap-6 sm:gap-7">
83+
<div class="flex items-center gap-5">
84+
<a
85+
href={GH}
86+
target="_blank"
87+
rel="noopener"
88+
class="gh-star"
89+
aria-label="Star Executor on GitHub"
90+
>
91+
<svg
92+
class="gh-star__ico"
93+
width="13"
94+
height="13"
95+
viewBox="0 0 16 16"
96+
aria-hidden="true"
97+
>
98+
<path
99+
fill="#e3b341"
100+
d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z"
101+
></path>
102+
</svg>
103+
<span class="hidden sm:inline">star on github</span>
104+
{stars != null && <span class="gh-star__n">{formatStars(stars)}</span>}
105+
</a>
106+
<a
107+
href="https://executor.sh/docs"
108+
class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline"
109+
>Docs</a
110+
>
111+
<a
112+
href="/blog"
113+
class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline"
114+
>Blog</a
115+
>
116+
</div>
117+
<div class="flex items-center gap-4">
118+
<a
119+
href={SIGN_IN}
120+
class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors hidden sm:inline"
121+
>Sign in</a
122+
>
123+
<a href="#install" class="btn-primary btn-primary--nav">Get started →</a>
124+
</div>
98125
</div>
99126
</div>
100127
</nav>

apps/marketing/src/styles/global.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ body {
138138
background: #1f1f24;
139139
}
140140

141+
/* Compact nav variant: height matched to the gh-star pill (and its 7px radius)
142+
so the right cluster reads as one even row. */
143+
.btn-primary--nav {
144+
display: inline-flex;
145+
align-items: center;
146+
height: 31px;
147+
padding: 0 0.95rem;
148+
border-radius: 7px;
149+
}
150+
141151
/* Light, bordered counterpart to .btn-primary, a real secondary CTA. Sized to
142152
match .btn-primary--hero so it balances under the hero's primary button. */
143153
.btn-secondary {
@@ -182,6 +192,38 @@ body {
182192
color: var(--color-ink);
183193
}
184194

195+
/* GitHub "star on github" pill (nav), matching integrations.sh: a gold star,
196+
a lowercase mono label, and the count behind a hairline divider. All mono,
197+
the metadata voice. The star is the one bit of color, kept to the icon. */
198+
.gh-star {
199+
display: inline-flex;
200+
align-items: center;
201+
gap: 7px;
202+
padding: 5px 11px;
203+
border: 1px solid var(--color-rule);
204+
border-radius: 7px;
205+
color: var(--color-ink-2);
206+
font-family: var(--font-mono);
207+
font-size: 12.5px;
208+
white-space: nowrap;
209+
transition:
210+
color 0.15s ease,
211+
border-color 0.15s ease;
212+
}
213+
.gh-star:hover {
214+
color: var(--color-ink);
215+
border-color: var(--color-rule-strong);
216+
}
217+
.gh-star__ico {
218+
flex-shrink: 0;
219+
}
220+
.gh-star__n {
221+
font-variant-numeric: tabular-nums;
222+
color: var(--color-ink-3);
223+
padding-left: 8px;
224+
border-left: 1px solid var(--color-rule);
225+
}
226+
185227
.copy-icons {
186228
position: relative;
187229
display: inline-flex;

0 commit comments

Comments
 (0)