Landing shows the GitHub star count and a customer quote

The navbar gets a "Star" pill with the repository's star count from sm up;
phones keep the bare GitHub mark. A new section before the closing CTA
shows one customer quote with a star ask underneath. The footer's
"Star us on GitHub" line is gone, so the page asks twice, not three times.

The count is read once per web deploy (deploy-web.yml, own step, job token
only there) and inlined by next build as NEXT_PUBLIC_GITHUB_STARS, so
visitors' browsers never contact GitHub and the CSP stays as it is.
Without a value the pill renders without a counter (D-303).

The desktop links, bell and sign-in now start at lg instead of md: the bar
already overflowed between 768 and ~1000 px and the pill widened it further.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-10-01 13:13:33 +02:00
parent 66208b3767
commit 5b0dfaad99
10 changed files with 237 additions and 38 deletions
+19
View File
@@ -78,6 +78,23 @@ jobs:
working-directory: ${{ env.WEB_DIR }}
run: npm ci
# Star counter for the GitHub star link (utils/githubStars.ts). Baked into the static HTML
# so visitors' browsers never call GitHub; stale until the next web deploy. The token stays
# in this step, never in the build. Fail-soft: without a number the link has no counter.
- name: Read GitHub star count (fail-soft)
id: stars
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
count=$(gh api "repos/$REPO" --jq .stargazers_count 2>/dev/null || true)
if [[ "$count" =~ ^[0-9]+$ ]]; then
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "GitHub stars: $count"
else
echo "::warning::GitHub star count unavailable - the star link renders without a counter"
fi
# next.config output:'export' -- `next build` writes the static site to out/
# (prebuild: sync-error-codes, generate-lastmod, generate-version, whats-new).
- name: Build
@@ -98,6 +115,8 @@ jobs:
GITHUB_SHA: ${{ github.sha }}
# Docs checkout for public/whats-new.json; the prebuild fails in CI without it.
WHATS_NEW_DOCS_DIR: ${{ github.workspace }}/whats-new-docs
# Star counter from the step above; empty when the lookup failed.
NEXT_PUBLIC_GITHUB_STARS: ${{ steps.stars.outputs.count }}
run: |
npm run build
test -f out/version.json || { echo "out/version.json missing - export did not run"; exit 1; }
@@ -49,6 +49,8 @@ html {
--lp-warn: #d97706;
--lp-warn-soft: rgba(217, 119, 6, 0.1);
--lp-danger: #dc2626;
/* GitHub star glyph only (GitHubStarLink) — never text or a fill */
--lp-star: #eab308;
/* Terminal is dark in both themes */
--lp-term-bg: #0d1424;
--lp-term-line: #22304a;
@@ -73,6 +75,7 @@ html {
--lp-warn: #f5a623;
--lp-warn-soft: rgba(245, 166, 35, 0.12);
--lp-danger: #f87171;
--lp-star: #facc15;
}
body {
@@ -762,6 +765,7 @@ html.auth-pending .landing-v2::after {
--lp-warn: #d97706;
--lp-warn-soft: rgba(217, 119, 6, 0.1);
--lp-danger: #dc2626;
--lp-star: #eab308;
}
/* Neutralize the global .dark utility overrides inside the light-only
@@ -7,6 +7,7 @@ import { Story } from "../components/landing/Story";
import { CapabilitiesStrip } from "../components/landing/CapabilitiesStrip";
import { Comparison } from "../components/landing/Comparison";
import { HowItWorks } from "../components/landing/HowItWorks";
import { Testimonials } from "../components/landing/Testimonials";
import { FinalCta } from "../components/landing/FinalCta";
import { SiteFooter } from "../components/SiteFooter";
@@ -29,6 +30,7 @@ export default function LandingPage() {
<CapabilitiesStrip />
<Comparison />
<HowItWorks />
<Testimonials />
<FinalCta />
<SiteFooter />
</div>
@@ -0,0 +1,8 @@
/** The GitHub mark (Octicon), inherits the text color. */
export function GitHubIcon({ className = "w-4 h-4" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
);
}
@@ -0,0 +1,36 @@
import { GITHUB_REPO_URL, GITHUB_STARS, formatStarCount } from "@/utils/githubStars";
import { GitHubIcon } from "./GitHubIcon";
/**
* "Star us" pill for the public surface: [GitHub mark] label │ ★ count.
* The counter segment only renders when the deploy baked a star count in (utils/githubStars.ts).
* No hooks, so server components (Testimonials) and client components (LandingNavbar) share it.
*/
export function GitHubStarLink({ label }: { label: string }) {
return (
<a
href={GITHUB_REPO_URL}
target="_blank"
rel="noopener noreferrer"
aria-label={
GITHUB_STARS !== null
? `Star Autopilot Monitor on GitHub (${GITHUB_STARS} stars)`
: "Star Autopilot Monitor on GitHub"
}
className="inline-flex items-stretch h-8 rounded-lg border border-[var(--lp-line)] bg-[var(--lp-surface)] text-[13px] font-medium text-[var(--lp-ink-soft)] hover:text-[var(--lp-ink)] hover:border-[var(--lp-ink-faint)] transition-colors overflow-hidden whitespace-nowrap"
>
<span className="inline-flex items-center gap-1.5 px-2.5">
<GitHubIcon className="w-[15px] h-[15px]" />
{label}
</span>
{GITHUB_STARS !== null && (
<span className="inline-flex items-center gap-1 px-2.5 border-l border-[var(--lp-line)] bg-[var(--lp-surface-2)] font-semibold text-[var(--lp-ink)]">
<svg className="w-3 h-3 text-[var(--lp-star)]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2.5l2.94 5.96 6.56.95-4.75 4.63 1.12 6.54L12 17.5l-5.87 3.08 1.12-6.54L2.5 9.41l6.56-.95z" />
</svg>
{formatStarCount(GITHUB_STARS)}
</span>
)}
</a>
);
}
@@ -1,5 +1,7 @@
import { DOCS_URL } from "@/utils/config";
import { GITHUB_REPO_URL } from "@/utils/githubStars";
import { BrandMark } from "./BrandMark";
import { GitHubIcon } from "./GitHubIcon";
const LINK_COLUMNS = [
{
@@ -17,7 +19,7 @@ const LINK_COLUMNS = [
{ label: "Documentation", href: DOCS_URL, external: true },
{ label: "Help & Support", href: "/help" },
{ label: "Feedback", href: "https://github.com/okieselbach/AutopilotMonitor/issues", external: true },
{ label: "GitHub", href: "https://github.com/okieselbach/AutopilotMonitor", external: true },
{ label: "GitHub", href: GITHUB_REPO_URL, external: true },
],
},
{
@@ -71,15 +73,13 @@ export function SiteFooter() {
</svg>
</a>
<a
href="https://github.com/okieselbach/AutopilotMonitor"
href={GITHUB_REPO_URL}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] transition-colors"
title="GitHub"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
<GitHubIcon className="w-4 h-4" />
</a>
</div>
</div>
@@ -110,22 +110,11 @@ export function SiteFooter() {
</div>
{/* Bottom bar */}
<div className="mt-10 pt-5 border-t border-[var(--lp-line-soft)] flex flex-col sm:flex-row items-center justify-between gap-2">
<div className="mt-10 pt-5 border-t border-[var(--lp-line-soft)] text-center sm:text-left">
<p className="text-[11px] text-[var(--lp-ink-faint)]">
&copy; 2026 Autopilot Monitor. Built with ❤️ by Oliver Kieselbach.{" "}
<span className="inline-block">Hosted on Azure by glueckkanja AG.</span>
</p>
<p className="text-[11px] text-[var(--lp-ink-faint)]">
Open source. Star us on{" "}
<a
href="https://github.com/okieselbach/AutopilotMonitor"
target="_blank"
rel="noopener noreferrer"
className="text-[var(--lp-ink-soft)] hover:text-[var(--lp-accent-ink)] transition-colors"
>
GitHub
</a>
</p>
</div>
</div>
</footer>
@@ -6,6 +6,9 @@ import { useAuth } from "../../contexts/AuthContext";
import { getPortalLoginUrl, shouldCrossOriginToPortal } from "../../lib/hostRouting";
import { DOCS_URL } from "@/utils/config";
import { BrandMark } from "../BrandMark";
import { GitHubIcon } from "../GitHubIcon";
import { GitHubStarLink } from "../GitHubStarLink";
import { GITHUB_REPO_URL } from "@/utils/githubStars";
import { useWhatsNew } from "@/hooks/useWhatsNew";
// Root-anchored (/#…) so the links also work from subpages
@@ -18,14 +21,6 @@ const NAV_LINKS = [
{ href: DOCS_URL, label: "Docs", external: true },
];
function GitHubIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
);
}
function BellIcon({ className }: { className?: string }) {
// Ringing bell: the public site announces "there is news" without claiming a count —
// anonymous visitors have no seen mark, so a counter would read 9+ for everyone.
@@ -78,16 +73,18 @@ export function LandingNavbar() {
// see components/NavLink.tsx.)
return (
<nav className="sticky top-0 z-40 bg-[var(--lp-nav)] backdrop-blur-xl border-b border-[var(--lp-line-soft)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center gap-8">
{/* Measured widths: the full desktop bar (links, star link, bell, sign-in, CTA) needs
~1000px, so it starts at lg; below that the burger menu carries links and sign-in. */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center gap-4 lg:gap-8">
<Link href="/" prefetch={false} className="flex items-center gap-2.5 shrink-0">
<BrandMark className="w-6 h-6" />
{/* Wordmark needs ~360px alongside CTA + burger; mark alone below that */}
<span className="hidden min-[360px]:block text-[15px] font-bold tracking-tight text-[var(--lp-ink)] whitespace-nowrap">
{/* Wordmark needs ~390px alongside GitHub mark + CTA + burger; mark alone below that */}
<span className="hidden min-[390px]:block text-[15px] font-bold tracking-tight text-[var(--lp-ink)] whitespace-nowrap">
Autopilot Monitor
</span>
</Link>
<div className="hidden md:flex items-center gap-1">
<div className="hidden lg:flex items-center gap-1">
{NAV_LINKS.map(link =>
link.external ? (
<a
@@ -111,20 +108,25 @@ export function LandingNavbar() {
)}
</div>
<div className="ml-auto flex items-center gap-1.5 sm:gap-2 shrink-0">
<div className="ml-auto flex items-center gap-1 sm:gap-2 shrink-0">
{/* Phones get the bare GitHub mark; the star pill with its counter starts at sm */}
<a
href="https://github.com/okieselbach/AutopilotMonitor"
href={GITHUB_REPO_URL}
target="_blank"
rel="noopener noreferrer"
className="hidden md:block p-2 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
className="sm:hidden p-1.5 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
title="GitHub"
aria-label="GitHub"
>
<GitHubIcon className="w-4 h-4" />
</a>
<div className="hidden sm:flex">
<GitHubStarLink label="Star" />
</div>
<button
type="button"
onClick={() => whatsNew.open("landing")}
className="hidden md:block p-2 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
className="hidden lg:block p-2 rounded-lg text-[var(--lp-ink-faint)] hover:text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
title="What's new"
aria-label="What's new"
>
@@ -132,7 +134,7 @@ export function LandingNavbar() {
</button>
<button
onClick={handleSignIn}
className="hidden md:block px-3 py-2 text-sm font-semibold text-[var(--lp-ink)] hover:text-[var(--lp-accent-ink)] transition-colors"
className="hidden lg:block px-3 py-2 text-sm font-semibold text-[var(--lp-ink)] hover:text-[var(--lp-accent-ink)] transition-colors"
>
Sign in
</button>
@@ -145,7 +147,7 @@ export function LandingNavbar() {
</Link>
<button
onClick={() => setMenuOpen(open => !open)}
className="md:hidden p-2 -mr-2 rounded-lg text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
className="lg:hidden p-2 -mr-2 rounded-lg text-[var(--lp-ink)] hover:bg-[var(--lp-surface-2)] transition-colors"
aria-expanded={menuOpen}
aria-controls="landing-mobile-menu"
aria-label={menuOpen ? "Close menu" : "Open menu"}
@@ -168,13 +170,13 @@ export function LandingNavbar() {
{/* nav's backdrop-blur makes it the containing block, so this stays
absolute (not fixed) and stretches a viewport height below the bar */}
<div
className="md:hidden absolute top-full left-0 right-0 h-screen bg-black/20"
className="lg:hidden absolute top-full left-0 right-0 h-screen bg-black/20"
onClick={() => setMenuOpen(false)}
aria-hidden="true"
/>
<div
id="landing-mobile-menu"
className="md:hidden absolute top-full left-0 right-0 bg-[var(--lp-surface)] border-b border-[var(--lp-line-soft)] shadow-lg"
className="lg:hidden absolute top-full left-0 right-0 bg-[var(--lp-surface)] border-b border-[var(--lp-line-soft)] shadow-lg"
>
<div className="px-6 py-4 flex flex-col gap-1">
{NAV_LINKS.map(link => (
@@ -189,7 +191,7 @@ export function LandingNavbar() {
</a>
))}
<a
href="https://github.com/okieselbach/AutopilotMonitor"
href={GITHUB_REPO_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => setMenuOpen(false)}
@@ -0,0 +1,56 @@
import { GitHubStarLink } from "../GitHubStarLink";
import { Reveal } from "./Reveal";
/**
* Customer voice before the closing CTA: one quote, shared with permission and attributed by
* role only. More voices need a different layout (a grid of bubbles), not copies of this one.
*/
export function Testimonials() {
return (
<section className="py-20 sm:py-24 px-6 border-t border-[var(--lp-line-soft)] bg-[var(--lp-surface)]">
<div className="max-w-7xl mx-auto grid lg:grid-cols-12 gap-10 lg:gap-6">
<Reveal className="lg:col-span-4">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-[var(--lp-accent-ink)]">From the field</p>
<h2 className="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-[var(--lp-ink)] text-balance">
What endpoint teams see on day one.
</h2>
</Reveal>
<Reveal className="lg:col-span-8" delayMs={100}>
<figure>
<div className="relative rounded-2xl border border-[var(--lp-line)] bg-[var(--lp-bg)] px-6 pt-7 pb-8 sm:px-10 sm:pt-9 sm:pb-10">
<span aria-hidden="true" className="block h-6 sm:h-8 font-serif text-6xl sm:text-7xl leading-none text-[var(--lp-accent)]">
&ldquo;
</span>
<blockquote className="mt-3 text-2xl sm:text-3xl font-semibold tracking-tight leading-snug text-[var(--lp-ink)] text-balance">
Wow — for the first time I can actually see all of our enrollments running out there.
</blockquote>
{/* Speech-bubble tail: the upper half of the rotated square covers the bubble border */}
<span
aria-hidden="true"
className="absolute left-10 -bottom-[9px] w-4 h-4 rotate-45 bg-[var(--lp-bg)] border-r border-b border-[var(--lp-line)]"
/>
</div>
<figcaption className="mt-5 flex items-center gap-3.5 pl-6 sm:pl-8">
<span
aria-hidden="true"
className="inline-flex w-11 h-11 shrink-0 items-center justify-center rounded-full bg-[var(--lp-accent-soft)] text-[var(--lp-accent-ink)]"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</span>
<span className="text-[15px] font-semibold text-[var(--lp-ink)]">Head of Endpoint Management</span>
</figcaption>
</figure>
<div className="mt-8 flex flex-wrap items-center gap-x-4 gap-y-3">
<p className="text-[15px] text-[var(--lp-ink-soft)]">Seeing the same in your tenant? Leave a star.</p>
<GitHubStarLink label="Star on GitHub" />
</div>
</Reveal>
</div>
</section>
);
}
@@ -0,0 +1,52 @@
import { describe, it, expect } from "vitest";
import { formatStarCount, parseStarCount } from "../githubStars";
describe("parseStarCount", () => {
it("parses a plain integer", () => {
expect(parseStarCount("45")).toBe(45);
});
it("accepts zero", () => {
expect(parseStarCount("0")).toBe(0);
});
it("trims surrounding whitespace", () => {
expect(parseStarCount(" 276\n")).toBe(276);
});
it("returns null when the env value is missing or empty", () => {
expect(parseStarCount(undefined)).toBeNull();
expect(parseStarCount("")).toBeNull();
});
it("rejects anything that is not a plain non-negative integer", () => {
expect(parseStarCount("null")).toBeNull();
expect(parseStarCount("-1")).toBeNull();
expect(parseStarCount("4.5")).toBeNull();
expect(parseStarCount("1e3")).toBeNull();
expect(parseStarCount("45 stars")).toBeNull();
});
it("rejects values beyond the safe integer range", () => {
expect(parseStarCount("99999999999999999999")).toBeNull();
});
});
describe("formatStarCount", () => {
it("shows counts below 1000 verbatim", () => {
expect(formatStarCount(0)).toBe("0");
expect(formatStarCount(45)).toBe("45");
expect(formatStarCount(999)).toBe("999");
});
it("abbreviates thousands with one decimal", () => {
expect(formatStarCount(1000)).toBe("1k");
expect(formatStarCount(1234)).toBe("1.2k");
expect(formatStarCount(12000)).toBe("12k");
expect(formatStarCount(12345)).toBe("12.3k");
});
it("truncates rather than rounding up", () => {
expect(formatStarCount(1999)).toBe("1.9k");
});
});
@@ -0,0 +1,31 @@
/**
* GitHub star count for the public surface (navbar, testimonials, footer).
*
* The count is baked in at deploy time: deploy-web.yml reads it with the job token and passes
* it to `next build` as NEXT_PUBLIC_GITHUB_STARS, which Next inlines into the static HTML.
* Visitors' browsers never call GitHub — the CSP would block it, and it would be a third-party
* connection the privacy page does not list. CI, local builds and a failed lookup have no
* value; the star link then renders without the counter.
*/
export const GITHUB_REPO_URL = "https://github.com/okieselbach/AutopilotMonitor";
/** Parses the build-time env value. Anything but a plain non-negative integer yields null. */
export function parseStarCount(raw: string | undefined): number | null {
const value = raw?.trim();
if (!value || !/^\d+$/.test(value)) return null;
const count = Number(value);
return Number.isSafeInteger(count) ? count : null;
}
/**
* Compact display: 45 → "45", 1234 → "1.2k", 12000 → "12k".
* Truncates instead of rounding so the label never overstates the count.
*/
export function formatStarCount(count: number): string {
if (count < 1000) return String(count);
const thousands = Math.floor(count / 100) / 10;
return `${thousands}k`;
}
export const GITHUB_STARS = parseStarCount(process.env.NEXT_PUBLIC_GITHUB_STARS);