diff --git a/apps/marketing/public/logos/better-auth.svg b/apps/marketing/public/logos/better-auth.svg new file mode 100644 index 0000000..f0dee60 --- /dev/null +++ b/apps/marketing/public/logos/better-auth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/public/logos/cloudflare.svg b/apps/marketing/public/logos/cloudflare.svg new file mode 100644 index 0000000..0d601bf --- /dev/null +++ b/apps/marketing/public/logos/cloudflare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/public/logos/drizzle.svg b/apps/marketing/public/logos/drizzle.svg new file mode 100644 index 0000000..d9b8730 --- /dev/null +++ b/apps/marketing/public/logos/drizzle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/public/logos/nextjs.svg b/apps/marketing/public/logos/nextjs.svg new file mode 100644 index 0000000..ff82d0b --- /dev/null +++ b/apps/marketing/public/logos/nextjs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/public/logos/polar.svg b/apps/marketing/public/logos/polar.svg new file mode 100644 index 0000000..b3b160a --- /dev/null +++ b/apps/marketing/public/logos/polar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/public/logos/react.svg b/apps/marketing/public/logos/react.svg new file mode 100644 index 0000000..a99b5da --- /dev/null +++ b/apps/marketing/public/logos/react.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/marketing/public/logos/resend.svg b/apps/marketing/public/logos/resend.svg new file mode 100644 index 0000000..4371f60 --- /dev/null +++ b/apps/marketing/public/logos/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/marketing/public/logos/tailwind.svg b/apps/marketing/public/logos/tailwind.svg new file mode 100644 index 0000000..4e8cbb0 --- /dev/null +++ b/apps/marketing/public/logos/tailwind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/public/logos/trpc.svg b/apps/marketing/public/logos/trpc.svg new file mode 100644 index 0000000..9a03cb5 --- /dev/null +++ b/apps/marketing/public/logos/trpc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/public/logos/turborepo.svg b/apps/marketing/public/logos/turborepo.svg new file mode 100644 index 0000000..071a4e9 --- /dev/null +++ b/apps/marketing/public/logos/turborepo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/marketing/src/app/[locale]/page.tsx b/apps/marketing/src/app/[locale]/page.tsx index a607445..197aaef 100644 --- a/apps/marketing/src/app/[locale]/page.tsx +++ b/apps/marketing/src/app/[locale]/page.tsx @@ -7,6 +7,7 @@ import { SoftwareApplicationJsonLd, WebsiteJsonLd, } from "@/components/seo/json-ld"; +import { getRepoStars } from "@/lib/github"; import { ogMetadata, siteBase } from "@/lib/og"; const HOME_TITLE = "vibestack — The SaaS starter where Claude writes the rest."; @@ -67,8 +68,9 @@ const StickyCTA = dynamic(() => })), ); -export default function HomePage() { +export default async function HomePage() { const siteUrl = siteBase(); + const stars = await getRepoStars(); return (
@@ -79,7 +81,7 @@ export default function HomePage() { description="Opinionated, AI-first SaaS starter. Stack pre-wired, Claude Code skills vendored in the repo. For devs and vibe-coders." /> - + diff --git a/apps/marketing/src/components/marketing/footer.tsx b/apps/marketing/src/components/marketing/footer.tsx index 7372987..7cbbed6 100644 --- a/apps/marketing/src/components/marketing/footer.tsx +++ b/apps/marketing/src/components/marketing/footer.tsx @@ -22,7 +22,7 @@ const columns: { title: string; links: FooterLink[] }[] = [ { label: "Journal", href: "/blog" }, { label: "GitHub", - href: "https://github.com/f-amine/vibestack", + href: "https://github.com/f-amine/vibe-stack", external: true, }, ], diff --git a/apps/marketing/src/components/marketing/github-mark.tsx b/apps/marketing/src/components/marketing/github-mark.tsx new file mode 100644 index 0000000..be1e2bf --- /dev/null +++ b/apps/marketing/src/components/marketing/github-mark.tsx @@ -0,0 +1,15 @@ +// GitHub mark, currentColor so it inherits text color. Decorative by default. +export function GithubMark({ className }: { className?: string }) { + return ( + + ); +} diff --git a/apps/marketing/src/components/marketing/header.tsx b/apps/marketing/src/components/marketing/header.tsx index 0522d9f..e94dc5a 100644 --- a/apps/marketing/src/components/marketing/header.tsx +++ b/apps/marketing/src/components/marketing/header.tsx @@ -4,6 +4,8 @@ import { cn } from "@vibestack/ui/lib/utils"; import Link from "next/link"; import { useEffect, useState } from "react"; import { WEB_URLS } from "@/lib/app-urls"; +import { GITHUB_URL } from "@/lib/github"; +import { GithubMark } from "./github-mark"; const links = [ { href: "#features", label: "Features" }, @@ -59,6 +61,15 @@ export function MarketingHeader() {
+ + + (null); useGSAP( @@ -72,29 +74,31 @@ export function Hero() {

- — The AI-first SaaS starter + — Built for vibe-coders in a hurry

- The SaaS starter where + Ship your SaaS this weekend, + let{" "} Claude {" "} - writes the rest. + wire the rest.

- Auth, billing, email, storage, admin, deploy: all pre-wired. Every - Claude Code skill vendored in the repo. Bring the business logic, - skip the plumbing. + Clone it, describe your idea, watch it ship. Auth, billing, email, + storage, and deploy come pre-wired, so you skip the plumbing and + build the thing you actually care about. Made for vibe-coders who + would rather move fast than wire boilerplate.

@@ -102,7 +106,7 @@ export function Hero() { href={WEB_URLS.signUp} className="hero-cta group inline-flex items-center justify-center gap-2 rounded-full bg-[color:var(--marketing-accent)] px-7 py-3.5 font-medium text-[color:var(--marketing-bg)] text-base transition-transform hover:scale-[1.02]" > - Start your SaaS + Start building - See the workflow + + Star on GitHub + {typeof stars === "number" && ( + + ★ + {formatStars(stars)} + + )}
diff --git a/apps/marketing/src/components/marketing/sections/stack-strip.tsx b/apps/marketing/src/components/marketing/sections/stack-strip.tsx index fb897b9..cf17702 100644 --- a/apps/marketing/src/components/marketing/sections/stack-strip.tsx +++ b/apps/marketing/src/components/marketing/sections/stack-strip.tsx @@ -1,46 +1,52 @@ -// Honest social proof: the stack itself. No invented customer logos — -// for a free starter, the credible flex is what it's built on. - +// The stack itself is the proof for a free starter — real logos, no invented +// customer wall. Calm/monochrome at rest, brand color on hover. const stack = [ - { name: "Next.js", meta: "16" }, - { name: "React", meta: "19" }, - { name: "Better Auth", meta: "1.6" }, - { name: "Drizzle", meta: "pg" }, - { name: "tRPC", meta: "v11" }, - { name: "Tailwind", meta: "v4" }, - { name: "Polar", meta: "billing" }, - { name: "Resend", meta: "email" }, - { name: "R2", meta: "storage" }, - { name: "Turborepo", meta: "pnpm" }, + { name: "Next.js", meta: "16", logo: "/logos/nextjs.svg" }, + { name: "React", meta: "19", logo: "/logos/react.svg" }, + { name: "Better Auth", meta: "1.6", logo: "/logos/better-auth.svg" }, + { name: "Drizzle", meta: "pg", logo: "/logos/drizzle.svg" }, + { name: "tRPC", meta: "v11", logo: "/logos/trpc.svg" }, + { name: "Tailwind", meta: "v4", logo: "/logos/tailwind.svg" }, + { name: "Polar", meta: "billing", logo: "/logos/polar.svg" }, + { name: "Resend", meta: "email", logo: "/logos/resend.svg" }, + { name: "R2", meta: "storage", logo: "/logos/cloudflare.svg" }, + { name: "Turborepo", meta: "pnpm", logo: "/logos/turborepo.svg" }, ]; export function StackStrip() { return (
-
-

- No fake logos. The stack is the proof. -

-
    - {stack.map((s) => ( -
  • - +

    + The whole stack, already wired. You just build. +

    +
      + {stack.map((s) => ( +
    • + {s.name} + + {s.name} - + {s.meta} -
    • - ))} -
    -
+ + + ))} +
); diff --git a/apps/marketing/src/lib/github.ts b/apps/marketing/src/lib/github.ts new file mode 100644 index 0000000..30059a4 --- /dev/null +++ b/apps/marketing/src/lib/github.ts @@ -0,0 +1,26 @@ +// Repo identity + a cached server-side star count. getRepoStars() runs in +// RSC only; the result is revalidated hourly so we hit GitHub once an hour +// for the whole site, never per visitor. +export const GITHUB_REPO = "f-amine/vibe-stack"; +export const GITHUB_URL = `https://github.com/${GITHUB_REPO}`; + +export async function getRepoStars(): Promise { + try { + const res = await fetch(`https://api.github.com/repos/${GITHUB_REPO}`, { + headers: { Accept: "application/vnd.github+json" }, + next: { revalidate: 3600 }, + }); + if (!res.ok) return null; + const data = (await res.json()) as { stargazers_count?: number }; + return typeof data.stargazers_count === "number" + ? data.stargazers_count + : null; + } catch { + return null; + } +} + +/** 1234 -> "1.2k", 2 -> "2". */ +export function formatStars(n: number): string { + return n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k` : `${n}`; +}