diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index de9497473..072208b39 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -131,7 +131,13 @@ const MobileCard = ({ ) -export function Navbar({ children }: { children: React.ReactNode }) { +export function Navbar({ + children, + hideHeader = false, +}: { + children: React.ReactNode + hideHeader?: boolean +}) { const matches = useMatches() const { Title } = React.useMemo(() => { @@ -145,6 +151,10 @@ export function Navbar({ children }: { children: React.ReactNode }) { const containerRef = React.useRef(null) React.useEffect(() => { + // When hideHeader is true, the EditorialTopNav owns --navbar-height — + // skip our own measurement so we don't fight it. + if (hideHeader) return + const updateContainerHeight = () => { if (containerRef.current) { const height = containerRef.current.offsetHeight @@ -161,7 +171,7 @@ export function Navbar({ children }: { children: React.ReactNode }) { return () => { window.removeEventListener('resize', updateContainerHeight) } - }, []) + }, [hideHeader]) const [showMenu, setShowMenu] = React.useState(false) const [canLoadAuthControls, setCanLoadAuthControls] = React.useState(false) @@ -760,12 +770,14 @@ export function Navbar({ children }: { children: React.ReactNode }) { return ( <> - {navbar} + {hideHeader ? null : navbar}
{smallMenu} diff --git a/src/components/editorial/EditorialTopNav.tsx b/src/components/editorial/EditorialTopNav.tsx new file mode 100644 index 000000000..6fd93f5c5 --- /dev/null +++ b/src/components/editorial/EditorialTopNav.tsx @@ -0,0 +1,397 @@ +import * as React from 'react' +import { Link } from '@tanstack/react-router' +import { twMerge } from 'tailwind-merge' +import { ArrowRight, Menu, Search, Sparkles, X } from 'lucide-react' + +import { NetlifyImage } from '~/components/NetlifyImage' +import { ThemeToggle } from '~/components/ThemeToggle' +import { GitHub } from '~/ui' +import { BSkyIcon } from '~/components/icons/BSkyIcon' +import { BrandXIcon } from '~/components/icons/BrandXIcon' +import { DiscordIcon } from '~/components/icons/DiscordIcon' +import { InstagramIcon } from '~/components/icons/InstagramIcon' +import { YouTubeIcon } from '~/components/icons/YouTubeIcon' +import { + categoryMeta, + categorySlugs, +} from '~/components/stack/stack-categories' +import { PartnerImage, partners, partnerTierFlares } from '~/utils/partners' + +type NavItem = + | { kind: 'category'; slug: (typeof categorySlugs)[number]; label: string } + | { kind: 'link'; to: string; label: string; accent?: boolean } + | { kind: 'external'; href: string; label: string } + +const PRIMARY_LINKS: NavItem[] = [ + ...categorySlugs.map((slug) => ({ + kind: 'category' as const, + slug, + label: categoryMeta[slug].shortName, + })), + { kind: 'link', to: '/libraries', label: 'Libraries' }, + { kind: 'link', to: '/partners', label: 'Partners' }, + { kind: 'link', to: '/blog', label: 'Blog' }, + { kind: 'link', to: '/showcase', label: 'Showcase' }, + { kind: 'link', to: '/stats', label: 'Stats' }, + { kind: 'link', to: '/merch', label: 'Merch' }, + { kind: 'link', to: '/explore', label: 'Explore', accent: true }, +] + +export function EditorialTopNav() { + const [open, setOpen] = React.useState(false) + const headerRef = React.useRef(null) + + // Publish height as --navbar-height so the global Navbar's sticky/fixed + // left rail (which uses `top-[var(--navbar-height)]`) lines up underneath. + React.useEffect(() => { + const node = headerRef.current + if (!node) return + + const update = () => { + document.documentElement.style.setProperty( + '--navbar-height', + `${node.offsetHeight}px`, + ) + } + + update() + const observer = new ResizeObserver(update) + observer.observe(node) + window.addEventListener('resize', update) + return () => { + observer.disconnect() + window.removeEventListener('resize', update) + } + }, []) + + return ( +
+ {/* Utility strip */} +
+
+

+ + The open-source application stack — built for developers, reliable + for agents. +

+

+ MIT licensed · Production-grade · No vendor lock-in +

+
+
+ + {/* Main bar */} +
+ + + + + TanStack + + + + {/* Desktop primary nav */} + + + {/* Right cluster */} +
+ + + + + + + +
+
+ + {/* Gold partners strip — pinned sponsor row */} + + + {/* Mobile drawer */} + {open && ( +
+ +
+ )} +
+ ) +} + +const SOCIAL_LINKS = [ + { + label: 'GitHub', + href: 'https://github.com/tanstack', + Icon: GitHub, + }, + { + label: 'Discord', + href: 'https://discord.com/invite/WrRKjPJ', + Icon: DiscordIcon, + }, + { + label: 'YouTube', + href: 'https://youtube.com/@tan_stack', + Icon: YouTubeIcon, + }, + { + label: 'X (Twitter)', + href: 'https://x.com/tan_stack', + Icon: BrandXIcon, + }, + { + label: 'Bluesky', + href: 'https://bsky.app/profile/tanstack.com', + Icon: BSkyIcon, + }, + { + label: 'Instagram', + href: 'https://instagram.com/tan_stack', + Icon: InstagramIcon, + }, +] as const + +function SocialCluster() { + return ( +
+ {SOCIAL_LINKS.map(({ label, href, Icon }, i) => { + const col = i % 3 // 0, 1, 2 + const row = Math.floor(i / 3) // 0 or 1 + return ( + 0 && 'border-l border-zinc-200 dark:border-zinc-800', + row > 0 && 'border-t border-zinc-200 dark:border-zinc-800', + )} + > + + + ) + })} +
+ ) +} + +function GoldPartnersStrip() { + const goldFlare = partnerTierFlares.gold + const goldPartners = React.useMemo( + () => + partners + .filter((p) => p.status !== 'inactive' && p.tier === 'gold') + .sort((a, b) => (b.score ?? 0) - (a.score ?? 0)), + [], + ) + + if (goldPartners.length === 0) return null + + return ( +
+
+ + + {goldFlare.icon} + + Gold partners + + + + + + See all partners + + +
+
+ ) +} + +function NavLinkItem({ + item, + onClick, +}: { + item: NavItem + onClick?: () => void +}) { + const cls = + 'inline-flex items-center whitespace-nowrap rounded-md px-2 py-1.5 text-[13px] text-zinc-700 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white' + const activeCls = twMerge( + cls, + 'bg-cyan-600 text-white shadow-sm hover:bg-cyan-700 hover:text-white dark:bg-cyan-500 dark:text-white dark:hover:bg-cyan-400 dark:hover:text-white', + ) + + if (item.kind === 'category') { + return ( + + {item.label} + + ) + } + + if (item.kind === 'link') { + if (item.accent) { + const accentCls = + 'inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-gradient-to-r from-cyan-500 to-emerald-500 px-2.5 py-1 text-[13px] text-white shadow-sm transition-transform hover:-translate-y-0.5 hover:shadow-md' + const accentActive = twMerge( + accentCls, + 'from-cyan-600 to-emerald-600 ring-2 ring-cyan-300 dark:ring-cyan-700', + ) + return ( + + + 🎮 + + {item.label} + + ) + } + return ( + + {item.label} + + ) + } + + return ( + + {item.label} + + ) +} + +function getKey(item: NavItem): string { + if (item.kind === 'category') return `cat-${item.slug}` + if (item.kind === 'link') return `link-${item.to}` + return `ext-${item.href}` +} diff --git a/src/components/home/HomeEditorial.tsx b/src/components/home/HomeEditorial.tsx new file mode 100644 index 000000000..c7d2277c3 --- /dev/null +++ b/src/components/home/HomeEditorial.tsx @@ -0,0 +1,762 @@ +import * as React from 'react' +import { Link } from '@tanstack/react-router' +import { twMerge } from 'tailwind-merge' +import { + ArrowRight, + ArrowUpRight, + Code2, + Flame, + Layers, + Newspaper, + Play, + Shield, + ShieldCheck, + Sparkles, + Star, + TrendingUp, + Zap, +} from 'lucide-react' + +import { librariesByGroup, librariesGroupNamesMap, start } from '~/libraries' +import type { LibrarySlim } from '~/libraries' +import { groupToSlug } from '~/components/stack/stack-categories' +import { HomeApplicationStarter } from '~/components/home/HomeApplicationStarter' +import { HomeDeferredSection } from '~/components/home/HomeDeferredSection' +import { HomeStatsFallback } from '~/components/home/HomeSectionFallbacks' + +const loadHomeStatsSection = () => import('~/components/home/HomeStatsSection') + +const LazyHomeStatsSection = React.lazy(() => + loadHomeStatsSection().then((m) => ({ + default: m.HomeStatsSection, + })), +) +import { + partners, + PartnerImage, + partnerTierFlares, + partnerTierLabels, + type PartnerTier, +} from '~/utils/partners' +import { formatPublishedDate, getPublishedPosts } from '~/utils/blog' +import { TrustedByMarquee } from '~/components/TrustedByMarquee' +import { YouTubeIcon } from '~/components/icons/YouTubeIcon' +import { Button } from '~/ui' + +import discordImage from '~/images/discord-logo-white.svg' + +type GroupId = keyof typeof librariesByGroup + +const GROUP_ORDER: GroupId[] = ['state', 'headlessUI', 'performance', 'tooling'] + +const LEADERBOARD_IDS = ['query', 'router', 'table', 'form'] as const + +const TRUST_PILLARS = [ + { + icon: Code2, + title: 'Type-safe by design', + detail: 'TypeScript-first APIs catch bugs before runtime.', + }, + { + icon: Layers, + title: 'Framework-agnostic', + detail: 'React, Vue, Solid, Svelte, Angular — your choice.', + }, + { + icon: ShieldCheck, + title: 'Production-grade', + detail: 'Battle-tested in the world’s largest apps.', + }, + { + icon: Shield, + title: 'Open source forever', + detail: 'MIT licensed. No vendor lock-in, ever.', + }, +] as const + +export function HomeEditorial() { + const featured = start + const leaderboard = LEADERBOARD_IDS.map((id) => findLib(id)).filter( + (lib): lib is LibrarySlim => Boolean(lib), + ) + const latestPosts = getPublishedPosts().slice(0, 4) + + const partnersByTier = groupPartnersByTier() + + return ( +
+ {/* Hero: featured + leaderboard side rail */} +
+
+
+ + Featured stack + + +
+ + +
+
+ + {/* Trust pillars */} +
+
+ {TRUST_PILLARS.map(({ icon: Icon, title, detail }) => ( +
+
+ +
+
+

{title}

+

+ {detail} +

+
+
+ ))} +
+
+ + {/* Trusted-by marquee */} +
+
+ + Trusted in production + +
+ +
+
+
+ + {/* Live OSS stats — by the numbers */} +
+
+ + By the numbers + + } + title="Live across the TanStack ecosystem" + description="Real-time NPM downloads, GitHub stars, contributors and dependents — refreshed continuously." + /> + } + preload={loadHomeStatsSection} + rootMargin="10%" + timeoutMs={6000} + > + + +
+
+ + {/* Group leaderboards */} +
+
+ + Browse the stack + + } + title="Top picks by category" + action={ + + Full library index + + } + /> +
+ {GROUP_ORDER.map((groupId) => ( + + ))} +
+
+
+ + {/* Latest writing */} + {latestPosts.length > 0 && ( +
+
+ + From the team + + } + title="Latest writing" + action={ + + Visit the blog + + } + /> +
+ {latestPosts.map((post, i) => ( + + ))} +
+
+
+ )} + + {/* Partners (the centerpiece) */} +
+
+ + From our industry + + } + title="Trusted partners we recommend" + description="The companies powering production TanStack apps — ranked by tier of commitment." + action={ + + All partners + + } + /> +
+ + + +
+
+
+ + {/* Community pair */} +
+
+ + Join the community + + } + title="Get closer to the team" + /> +
+ + } + /> + + Subscribe + + } + illustration={ + + } + /> +
+
+
+
+ ) +} + +/* -------------------------------------------------------------------------- */ +/* Building blocks */ +/* -------------------------------------------------------------------------- */ + +function Eyebrow({ + children, + tone = 'muted', +}: { + children: React.ReactNode + tone?: 'muted' | 'accent' +}) { + return ( +

+ {children} +

+ ) +} + +function SectionHeader({ + eyebrow, + title, + description, + action, +}: { + eyebrow: React.ReactNode + title: string + description?: string + action?: React.ReactNode +}) { + return ( +
+
+ {eyebrow} +

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ {action} +
+ ) +} + +function FeaturedStarterCard({ library }: { library: LibrarySlim }) { + return ( +
+ {/* Brand header — gradient strip carrying Start identity */} +
+
+ + TanStack + +

+ {library.name.replace('TanStack ', '')} +

+ {library.badge && ( + + {library.badge} + + )} + + v{library.latestVersion} + + + Open Start + +
+

+ {library.tagline} +

+
+
+ + {/* Interactive starter — pick a template, generate a prompt, deploy */} +
+ +
+
+ ) +} + +function LeaderboardCard({ + library, + rank, +}: { + library: LibrarySlim + rank: number +}) { + return ( +
  • + +
    + {rank} +
    +
    +
    +

    + TanStack +

    + {library.badge && ( + + {library.badge} + + )} +
    +

    + {library.name.replace('TanStack ', '')} +

    +

    + {library.tagline} +

    +
    + + +
  • + ) +} + +function GroupLeaderboardCard({ + groupId, + libraries, +}: { + groupId: GroupId + libraries: readonly LibrarySlim[] +}) { + const topThree = libraries.slice(0, 3) + const groupName = librariesGroupNamesMap[groupId] + const categorySlug = groupToSlug[groupId] + + return ( + + {groupName} +

    + Top in {groupName.toLowerCase()} +

    +
      + {topThree.map((lib, i) => ( +
    1. + + {i + 1} + +
      +

      + {lib.name.replace('TanStack ', '')} +

      +

      + {lib.tagline} +

      +
      +
    2. + ))} +
    + + Read the full guide + + + + ) +} + +function BlogCard({ + post, + featured = false, +}: { + post: { slug: string; title: string; published: string; excerpt?: string } + featured?: boolean +}) { + return ( + +

    + {formatPublishedDate(post.published)} +

    +

    + {post.title} +

    + {post.excerpt && ( +

    + {post.excerpt} +

    + )} + + Read post + + + ) +} + +/* -------------------------------------------------------------------------- */ +/* Partners */ +/* -------------------------------------------------------------------------- */ + +function TierBlock({ + tier, + partners: tierPartners, +}: { + tier: PartnerTier + partners: Partner[] +}) { + if (tierPartners.length === 0) return null + const flare = partnerTierFlares[tier] + const label = partnerTierLabels[tier] + + const gridClass = + tier === 'gold' + ? 'grid gap-4 sm:grid-cols-2' + : tier === 'silver' + ? 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3' + : 'grid gap-3 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4' + + return ( +
    +
    + + {flare.icon} + +

    + {label} partners +

    + + · {tierPartners.length} + +
    +
    + {tierPartners.map((partner) => ( + + ))} +
    +
    + ) +} + +function PartnerTile({ + partner, + tier, +}: { + partner: Partner + tier: PartnerTier +}) { + const sizing = + tier === 'gold' + ? 'p-6 min-h-[140px]' + : tier === 'silver' + ? 'p-5 min-h-[110px]' + : 'p-4 min-h-[88px]' + + const logoSize = + tier === 'gold' + ? 'max-w-[180px]' + : tier === 'silver' + ? 'max-w-[140px]' + : 'max-w-[110px]' + + return ( + +
    + +
    + {partner.tagline && tier !== 'bronze' && ( +

    + {partner.tagline} +

    + )} + +
    + ) +} + +type Partner = (typeof partners)[number] + +function groupPartnersByTier(): Record { + const result: Record = { + gold: [], + silver: [], + bronze: [], + } + for (const partner of partners) { + if (partner.status === 'inactive') continue + if (!partner.tier) continue + result[partner.tier].push(partner) + } + for (const tier of Object.keys(result) as PartnerTier[]) { + result[tier].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)) + } + return result +} + +/* -------------------------------------------------------------------------- */ +/* Community CTAs */ +/* -------------------------------------------------------------------------- */ + +function CommunityCard({ + href, + accent, + badge, + title, + copy, + cta, + illustration, +}: { + href: string + accent: string + badge: string + title: string + copy: string + cta: React.ReactNode + illustration: React.ReactNode +}) { + return ( + +
    +

    + {badge} +

    +

    {title}

    +

    {copy}

    + + {cta} + +
    +
    + {illustration} +
    +
    + ) +} + +/* -------------------------------------------------------------------------- */ +/* Helpers */ +/* -------------------------------------------------------------------------- */ + +function findLib(id: string): LibrarySlim | undefined { + for (const group of Object.values(librariesByGroup)) { + const hit = group.find((l) => l.id === id) + if (hit) return hit + } + return undefined +} diff --git a/src/components/stack/CategoryArticle.tsx b/src/components/stack/CategoryArticle.tsx new file mode 100644 index 000000000..05fe07974 --- /dev/null +++ b/src/components/stack/CategoryArticle.tsx @@ -0,0 +1,566 @@ +import * as React from 'react' +import { Link } from '@tanstack/react-router' +import { twMerge } from 'tailwind-merge' +import { + ArrowLeft, + ArrowRight, + Award, + CheckCircle2, + ChevronRight, + Layers, + Newspaper, + Sparkles, +} from 'lucide-react' +import { GitHub } from '~/ui' + +import type { LibrarySlim } from '~/libraries' +import { formatPublishedDate, getPostsForLibrary } from '~/utils/blog' +import { + categoryMeta, + getCategoryLibraries, + getOtherCategories, + type CategorySlug, +} from './stack-categories' + +export function CategoryArticle({ slug }: { slug: CategorySlug }) { + const meta = categoryMeta[slug] + const libraries = getCategoryLibraries(slug) + const editorsPick = + libraries.find((lib) => lib.id === meta.editorsPickId) ?? libraries[0] + const runners = libraries.filter((lib) => lib.id !== editorsPick.id) + const others = getOtherCategories(slug) + const relatedPosts = libraries + .flatMap((lib) => getPostsForLibrary(lib.id).map((p) => ({ post: p, lib }))) + .slice(0, 4) + + return ( +
    + {/* Breadcrumb strip */} +
    +
    + + Home + + + + Stack + + + + {meta.name} + +
    +
    + + {/* Hero */} +
    +
    +

    + Stack buyer’s guide · {meta.shortName} +

    +

    + {meta.headline} +

    +

    + {meta.intro} +

    +
    + Updated · May 2026 + + {libraries.length} libraries reviewed + + Independent · MIT licensed +
    +
    +
    + + {/* Article body — 2 col with sticky rail */} +
    +
    + + + + + + + + + {relatedPosts.length > 0 && ( + + )} +
    + + +
    +
    + ) +} + +/* -------------------------------------------------------------------------- */ +/* Hero / Editor’s pick */ +/* -------------------------------------------------------------------------- */ + +function EditorsPickBlock({ + library, + accent, +}: { + library: LibrarySlim + accent: { from: string; to: string } +}) { + return ( +
    + + Top pick + +

    + The one we start with: {library.name} +

    + +
    +
    +
    + {library.badge && ( + + {library.badge} + + )} + + v{library.latestVersion} + +
    +

    + TanStack +

    +

    + {library.name.replace('TanStack ', '')} +

    +

    + {library.tagline} +

    +
    +
    + + Read the full review + +
    +
    +
    +
    + {library.description} +
    + + +
    + {library.frameworks.slice(0, 6).map((fw) => ( + + ))} + {library.frameworks.length > 6 && ( + + + {library.frameworks.length - 6} more frameworks + + )} + + tanstack/ + {library.repo?.split('/').pop()} + +
    +
    + ) +} + +/* -------------------------------------------------------------------------- */ +/* Quick verdicts table */ +/* -------------------------------------------------------------------------- */ + +function QuickVerdictBlock({ + libraries, + accent, +}: { + libraries: LibrarySlim[] + accent: { from: string; to: string } +}) { + return ( +
    + + Quick verdict + +

    + At-a-glance — which one is for you? +

    +
      + {libraries.map((lib) => ( +
    • + + {lib.name.replace('TanStack ', '')} + + + {lib.tagline} + + + Open + +
    • + ))} +
    +
    + ) +} + +/* -------------------------------------------------------------------------- */ +/* Ranked list (deep dive per library) */ +/* -------------------------------------------------------------------------- */ + +function RankedListBlock({ + libraries, + editorsPickId, + accent, +}: { + libraries: LibrarySlim[] + editorsPickId: string + accent: { from: string; to: string } +}) { + return ( +
    + + Ranked: each one in detail + +

    + Every library in this category, reviewed +

    +

    + Numbered in the order we’d try them. Click into any library for the full + landing page, docs, examples and version history. +

    +
      + {libraries.map((lib, i) => ( + + ))} +
    +
    + ) +} + +function RankedEntry({ + library, + rank, + isEditorsPick, +}: { + library: LibrarySlim + rank: number + isEditorsPick: boolean +}) { + return ( +
  • +
    +
    + {rank} +
    +
    +
    +

    + TanStack +

    +

    + {library.name.replace('TanStack ', '')} +

    + {library.badge && ( + + {library.badge} + + )} + {isEditorsPick && ( + + Top pick + + )} +
    +

    + {library.tagline} +

    +

    + {library.description} +

    + +
    + {library.frameworks.slice(0, 5).map((fw) => ( + + ))} + {library.frameworks.length > 5 && ( + + + {library.frameworks.length - 5} more + + )} + + Open {library.name.replace('TanStack ', '')}{' '} + + +
    +
    +
    +
  • + ) +} + +/* -------------------------------------------------------------------------- */ +/* Criteria */ +/* -------------------------------------------------------------------------- */ + +function CriteriaBlock({ + meta, +}: { + meta: { criteria: Array<{ title: string; detail: string }> } +}) { + return ( +
    + + How we think about it + +

    + What a library in this category has to earn +

    +
    + {meta.criteria.map((c) => ( +
    +

    {c.title}

    +

    + {c.detail} +

    +
    + ))} +
    +
    + ) +} + +/* -------------------------------------------------------------------------- */ +/* Related posts */ +/* -------------------------------------------------------------------------- */ + +function RelatedPostsBlock({ + items, +}: { + items: Array<{ + post: { slug: string; title: string; published: string; excerpt?: string } + lib: LibrarySlim + }> +}) { + return ( +
    + + From the team + +

    + Recent writing tagged with this category +

    +
      + {items.map(({ post, lib }) => ( +
    • + + + {lib.name.replace('TanStack ', '')} + +
      +

      + {post.title} +

      +

      + {formatPublishedDate(post.published)} +

      +
      + + +
    • + ))} +
    +
    + ) +} + +/* -------------------------------------------------------------------------- */ +/* Side rail */ +/* -------------------------------------------------------------------------- */ + +function TocBlock({ + libraries, + editorsPickId, +}: { + libraries: LibrarySlim[] + editorsPickId: string +}) { + return ( +
    +

    + In this guide +

    +
      + {libraries.map((lib, i) => ( +
    1. + + + {i + 1} + + + {lib.name.replace('TanStack ', '')} + + {lib.id === editorsPickId && ( + + )} + +
    2. + ))} +
    +
    + ) +} + +function OtherCategoriesBlock({ + others, +}: { + others: Array<{ slug: CategorySlug; shortName: string; name: string }> +}) { + return ( +
    +

    + Compare across the stack +

    +
      + {others.map((c) => ( +
    • + + {c.shortName} + + +
    • + ))} +
    +
    + ) +} + +function FullIndexCta() { + return ( + + + + See every TanStack library + + + + ) +} + +/* -------------------------------------------------------------------------- */ +/* Atoms */ +/* -------------------------------------------------------------------------- */ + +function SectionEyebrow({ children }: { children: React.ReactNode }) { + return ( +

    + {children} +

    + ) +} + +function FrameworkChip({ label }: { label: string }) { + return ( + + {label} + + ) +} diff --git a/src/components/stack/stack-categories.ts b/src/components/stack/stack-categories.ts new file mode 100644 index 000000000..5ed0ca6fe --- /dev/null +++ b/src/components/stack/stack-categories.ts @@ -0,0 +1,174 @@ +import { librariesByGroup, librariesGroupNamesMap } from '~/libraries' +import type { LibrarySlim } from '~/libraries' + +export type GroupId = keyof typeof librariesByGroup + +export type CategorySlug = 'state' | 'ui' | 'performance' | 'tooling' + +export const slugToGroup: Record = { + state: 'state', + ui: 'headlessUI', + performance: 'performance', + tooling: 'tooling', +} + +export const groupToSlug: Record = { + state: 'state', + headlessUI: 'ui', + performance: 'performance', + tooling: 'tooling', +} + +export const categorySlugs = Object.keys(slugToGroup) as CategorySlug[] + +export type CategoryMeta = { + slug: CategorySlug + groupId: GroupId + name: string + shortName: string + headline: string + intro: string + editorsPickId: string + criteria: Array<{ title: string; detail: string }> + /** Accent gradient classes for the hero / numbered chips. */ + accent: { from: string; to: string; text: string } +} + +export const categoryMeta: Record = { + state: { + slug: 'state', + groupId: 'state', + name: librariesGroupNamesMap.state, + shortName: 'Data & State', + headline: 'The best of TanStack — for data & state', + intro: + 'Routing, server state, async data, reactive stores. The libraries you reach for when an app needs to remember things, fetch things, and stay coherent across the screen.', + editorsPickId: 'start', + criteria: [ + { + title: 'Type-safe end to end', + detail: + 'Search params, loaders, mutations and store shapes that the compiler can actually check.', + }, + { + title: 'Framework agnostic core', + detail: + 'A pure JS core with adapters for React, Solid, Vue, Svelte and Angular — so the same patterns work everywhere.', + }, + { + title: 'Sensible defaults', + detail: + 'Caching, retries, dedup, optimistic UI — all opinionated where it matters and overridable where it doesn’t.', + }, + ], + accent: { + from: 'from-cyan-500', + to: 'to-emerald-500', + text: 'text-cyan-600 dark:text-cyan-400', + }, + }, + ui: { + slug: 'ui', + groupId: 'headlessUI', + name: librariesGroupNamesMap.headlessUI, + shortName: 'UI & UX', + headline: 'The best of TanStack — for UI & UX', + intro: + 'Headless primitives for the surfaces users actually touch. Tables, forms, keyboard shortcuts — owned by you, styled by you, validated by the compiler.', + editorsPickId: 'table', + criteria: [ + { + title: 'Headless by default', + detail: + 'No baked-in markup. You bring the design system; TanStack brings the behavior.', + }, + { + title: 'Performance under real loads', + detail: + 'Sortable, filterable, virtualised — at row counts that break most off-the-shelf grids.', + }, + { + title: 'Accessibility kept honest', + detail: + 'Keyboard, focus and ARIA built in — not bolted on after launch.', + }, + ], + accent: { + from: 'from-blue-500', + to: 'to-yellow-500', + text: 'text-blue-600 dark:text-blue-400', + }, + }, + performance: { + slug: 'performance', + groupId: 'performance', + name: librariesGroupNamesMap.performance, + shortName: 'Performance', + headline: 'The best of TanStack — for performance', + intro: + 'Keep large lists buttery, throttle the noisy events, and stop writing the same debounce-and-pray code in every project.', + editorsPickId: 'virtual', + criteria: [ + { + title: 'Built for the long list', + detail: + 'Virtualisation that scales to hundreds of thousands of rows without dropping frames.', + }, + { + title: 'Rate-limiting that survives review', + detail: + 'Debounce, throttle, queue, batch — primitives that compose instead of one-off hooks.', + }, + { + title: 'Drops into existing apps', + detail: + 'No re-architecture required. Add where the bottleneck is, leave the rest alone.', + }, + ], + accent: { + from: 'from-purple-500', + to: 'to-lime-500', + text: 'text-purple-600 dark:text-purple-400', + }, + }, + tooling: { + slug: 'tooling', + groupId: 'tooling', + name: librariesGroupNamesMap.tooling, + shortName: 'Tooling', + headline: 'The best of TanStack — for tooling', + intro: + 'Devtools, scaffolds, and packaging defaults that take the boring decisions off your plate, so the interesting work stays interesting.', + editorsPickId: 'devtools', + criteria: [ + { + title: 'Visibility into the runtime', + detail: + 'Unified devtools panel for the whole TanStack stack — and your own debug surfaces alongside.', + }, + { + title: 'Opinionated where it matters', + detail: + 'Lint, build, version, publish — the same pipeline TanStack itself uses for 20+ packages.', + }, + { + title: 'Agent-aware', + detail: + 'CLI, MCP server, and Agent Skills designed so humans and AI tools share the same surface area.', + }, + ], + accent: { + from: 'from-indigo-500', + to: 'to-orange-500', + text: 'text-indigo-600 dark:text-indigo-400', + }, + }, +} + +export function getCategoryLibraries(slug: CategorySlug): LibrarySlim[] { + return [...librariesByGroup[slugToGroup[slug]]] +} + +export function getOtherCategories(slug: CategorySlug): CategoryMeta[] { + return categorySlugs.filter((s) => s !== slug).map((s) => categoryMeta[s]) +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index ca6a34ad8..659d12464 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -49,6 +49,7 @@ import { Route as BlogIndexRouteImport } from './routes/blog.index' import { Route as AdminIndexRouteImport } from './routes/admin/index' import { Route as AccountIndexRouteImport } from './routes/account/index' import { Route as LibraryIdIndexRouteImport } from './routes/$libraryId/index' +import { Route as StackCategoryRouteImport } from './routes/stack.$category' import { Route as ShowcaseSubmitRouteImport } from './routes/showcase/submit' import { Route as ShowcaseIdRouteImport } from './routes/showcase/$id' import { Route as ShopSearchRouteImport } from './routes/shop.search' @@ -353,6 +354,11 @@ const LibraryIdIndexRoute = LibraryIdIndexRouteImport.update({ path: '/', getParentRoute: () => LibraryIdRouteRoute, } as any) +const StackCategoryRoute = StackCategoryRouteImport.update({ + id: '/stack/$category', + path: '/stack/$category', + getParentRoute: () => rootRouteImport, +} as any) const ShowcaseSubmitRoute = ShowcaseSubmitRouteImport.update({ id: '/showcase/submit', path: '/showcase/submit', @@ -953,6 +959,7 @@ export interface FileRoutesByFullPath { '/shop/search': typeof ShopSearchRoute '/showcase/$id': typeof ShowcaseIdRoute '/showcase/submit': typeof ShowcaseSubmitRoute + '/stack/$category': typeof StackCategoryRoute '/$libraryId/': typeof LibraryIdIndexRoute '/account/': typeof AccountIndexRoute '/admin/': typeof AdminIndexRoute @@ -1090,6 +1097,7 @@ export interface FileRoutesByTo { '/shop/search': typeof ShopSearchRoute '/showcase/$id': typeof ShowcaseIdRoute '/showcase/submit': typeof ShowcaseSubmitRoute + '/stack/$category': typeof StackCategoryRoute '/$libraryId': typeof LibraryIdIndexRoute '/account': typeof AccountIndexRoute '/admin': typeof AdminIndexRoute @@ -1234,6 +1242,7 @@ export interface FileRoutesById { '/shop/search': typeof ShopSearchRoute '/showcase/$id': typeof ShowcaseIdRoute '/showcase/submit': typeof ShowcaseSubmitRoute + '/stack/$category': typeof StackCategoryRoute '/$libraryId/': typeof LibraryIdIndexRoute '/account/': typeof AccountIndexRoute '/admin/': typeof AdminIndexRoute @@ -1381,6 +1390,7 @@ export interface FileRouteTypes { | '/shop/search' | '/showcase/$id' | '/showcase/submit' + | '/stack/$category' | '/$libraryId/' | '/account/' | '/admin/' @@ -1518,6 +1528,7 @@ export interface FileRouteTypes { | '/shop/search' | '/showcase/$id' | '/showcase/submit' + | '/stack/$category' | '/$libraryId' | '/account' | '/admin' @@ -1661,6 +1672,7 @@ export interface FileRouteTypes { | '/shop/search' | '/showcase/$id' | '/showcase/submit' + | '/stack/$category' | '/$libraryId/' | '/account/' | '/admin/' @@ -1790,6 +1802,7 @@ export interface RootRouteChildren { OauthTokenRoute: typeof OauthTokenRoute ShowcaseIdRoute: typeof ShowcaseIdRoute ShowcaseSubmitRoute: typeof ShowcaseSubmitRoute + StackCategoryRoute: typeof StackCategoryRoute ShowcaseIndexRoute: typeof ShowcaseIndexRoute StatsIndexRoute: typeof StatsIndexRoute ApiApplicationStarterResolveRoute: typeof ApiApplicationStarterResolveRoute @@ -2122,6 +2135,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibraryIdIndexRouteImport parentRoute: typeof LibraryIdRouteRoute } + '/stack/$category': { + id: '/stack/$category' + path: '/stack/$category' + fullPath: '/stack/$category' + preLoaderRoute: typeof StackCategoryRouteImport + parentRoute: typeof rootRouteImport + } '/showcase/submit': { id: '/showcase/submit' path: '/showcase/submit' @@ -3109,6 +3129,7 @@ const rootRouteChildren: RootRouteChildren = { OauthTokenRoute: OauthTokenRoute, ShowcaseIdRoute: ShowcaseIdRoute, ShowcaseSubmitRoute: ShowcaseSubmitRoute, + StackCategoryRoute: StackCategoryRoute, ShowcaseIndexRoute: ShowcaseIndexRoute, StatsIndexRoute: StatsIndexRoute, ApiApplicationStarterResolveRoute: ApiApplicationStarterResolveRoute, diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 3fdd1c87d..73fb6eb6e 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -35,6 +35,7 @@ const LazySearchModal = React.lazy(() => import { Spinner } from '~/components/Spinner' import { ThemeProvider, useHtmlClass } from '~/components/ThemeProvider' import { Navbar } from '~/components/Navbar' +import { EditorialTopNav } from '~/components/editorial/EditorialTopNav' import { THEME_COLORS } from '~/utils/utils' import { trackPageView } from '~/utils/analytics' import { twMerge } from 'tailwind-merge' @@ -230,7 +231,8 @@ function ShellComponent({ children }: { children: React.ReactNode }) { - {hideNavbar ? children : {children}} + + {hideNavbar ? children : {children}} {showDevtools && LazyAppDevtools ? ( diff --git a/src/routes/blog.tsx b/src/routes/blog.tsx index 6f4bf25dd..53c2ba0c9 100644 --- a/src/routes/blog.tsx +++ b/src/routes/blog.tsx @@ -3,6 +3,11 @@ import { seo } from '~/utils/seo' import { Button } from '~/ui' export const Route = createFileRoute('/blog')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail across all + // /blog/* routes via this layout. + showNavbar: false, + }, head: () => ({ meta: seo({ title: 'Blog | TanStack', diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 9e16b636c..c951cfa9b 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,94 +1,14 @@ -import * as React from 'react' -import { - ClientOnly, - Link, - createFileRoute, - useRouterState, -} from '@tanstack/react-router' +import { createFileRoute } from '@tanstack/react-router' -import discordImage from '~/images/discord-logo-white.svg' -import { librariesByGroup, librariesGroupNamesMap, Library } from '~/libraries' -import { NetlifyImage } from '~/components/NetlifyImage' - -import { TrustedByMarquee } from '~/components/TrustedByMarquee' -import { ArrowRight, Code2, Layers, Shield, Zap, Play } from 'lucide-react' -import { YouTubeIcon } from '~/components/icons/YouTubeIcon' -import { Card } from '~/components/Card' -import LibraryCard from '~/components/LibraryCard' -import { HomeApplicationStarter } from '~/components/home/HomeApplicationStarter' -import { HomeDeferredSection } from '~/components/home/HomeDeferredSection' -import { - HomeBytesFallback, - HomeCommunityFallback, - HomeSocialProofFallback, - HomeStatsFallback, -} from '~/components/home/HomeSectionFallbacks' -import { Button } from '~/ui' +import { HomeEditorial } from '~/components/home/HomeEditorial' import { seo } from '~/utils/seo' -const LazyBrandContextMenu = React.lazy(() => - import('~/components/BrandContextMenu').then((m) => ({ - default: m.BrandContextMenu, - })), -) - -const loadHomeSocialProofSection = () => - import('~/components/home/HomeSocialProofSection') - -const LazyHomeSocialProofSection = React.lazy(() => - loadHomeSocialProofSection().then((m) => ({ - default: m.HomeSocialProofSection, - })), -) - -const loadHomeCommunitySection = () => - import('~/components/home/HomeCommunitySection') - -const LazyHomeCommunitySection = React.lazy(() => - loadHomeCommunitySection().then((m) => ({ - default: m.HomeCommunitySection, - })), -) - -const loadHomeBytesSection = () => import('~/components/home/HomeBytesSection') - -const LazyHomeBytesSection = React.lazy(() => - loadHomeBytesSection().then((m) => ({ - default: m.HomeBytesSection, - })), -) - -const loadHomeStatsSection = () => import('~/components/home/HomeStatsSection') - -const LazyHomeStatsSection = React.lazy(() => - loadHomeStatsSection().then((m) => ({ - default: m.HomeStatsSection, - })), -) - -function getDeferredSectionStage(hash: string) { - const normalizedHash = hash.replace(/^#/, '') - - if (!normalizedHash) { - return 0 - } - - if (['partners', 'blog'].includes(normalizedHash)) { - return 1 - } - - if (['courses', 'sponsors', 'maintainers'].includes(normalizedHash)) { - return 2 - } - - if (normalizedHash === 'bytes') { - return 3 - } - - return 0 -} - export const Route = createFileRoute('/')({ + // Skip the global left-rail Navbar on the editorial homepage; the + // EditorialTopNav (rendered in __root.tsx) handles navigation. + staticData: { + showNavbar: false, + }, head: () => ({ meta: seo({ title: 'TanStack | The open-source application stack for the web.', @@ -96,435 +16,5 @@ export const Route = createFileRoute('/')({ 'Headless, type-safe, composable tools for building modern web applications that work naturally for developers and reliably for agents.', }), }), - component: Index, + component: HomeEditorial, }) - -function Index() { - const locationHash = useRouterState({ - select: (state) => state.location.hash, - }) - const deferredSectionStage = getDeferredSectionStage(locationHash) - - return ( - <> -
    -
    -
    -
    - -
    - -
    - - - -
    - -
    - - -
    -
    - } - > -
    - - - - -
    - -
    -
    -
    -

    - - TanStack - -

    -
    -

    - The application stack for the web. -

    -

    - Headless, type-safe, composable tools for building modern web - applications that work naturally for developers{' '} - and reliably for agents. -

    -
    -
    -
    - -
    -
    -
    - 0} - fallback={} - preload={loadHomeStatsSection} - rootMargin="10%" - timeoutMs={6000} - > - - -
    -
    -
    - -
    - -
    - -
    -

    - - Open Source Libraries - -

    - - {Object.entries(librariesByGroup).map( - ([groupName, groupLibraries]) => ( -
    -

    - { - librariesGroupNamesMap[ - groupName as keyof typeof librariesGroupNamesMap - ] - } -

    - {/* Library Cards */} -
    - {groupLibraries.map((library, i: number) => { - return ( - - ) - })} -
    -
    - ), - )} -
    - -
    - -
    - - {/* Why TanStack Section */} -
    -
    -

    Why TanStack?

    -

    - Our libraries are built on principles that put developers first -

    -
    -
    - -
    - -
    -

    Framework Agnostic

    -

    - Every library starts with a provider-agnostic core. Use React, - Vue, Solid, Angular, or vanilla JS—your choice. -

    -
    - -
    - -
    -

    Type-Safe by Design

    -

    - First-class TypeScript support that catches bugs at compile time - and makes refactoring fearless. -

    -
    - -
    - -
    -

    Production-Grade

    -

    - Battle-tested in the world's largest apps. Built for real - workloads, not just happy-path demos. -

    -
    - -
    - -
    -

    No Vendor Lock-in

    -

    - Open source and independent. No hidden agendas, no platform - bias—just great tools for developers. -

    -
    -
    -
    - -
    -
    - - = 1} - fallback={} - preload={loadHomeSocialProofSection} - > - - - - = 2} - fallback={} - preload={loadHomeCommunitySection} - > - - - -
    -
    -
    - Discord Logo -
    -
    -

    - - TanStack on Discord - -

    -

    - The official TanStack community to ask questions, network and - make new friends and get lightning fast news about what's coming - next for TanStack! -

    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -

    - - TanStack on YouTube - -

    -

    - The official TanStack YouTube channel. Tutorials, deep dives, - release walkthroughs, and more — free for everyone! -

    -
    -
    - -
    -
    -
    - -
    - = 3} - fallback={} - preload={loadHomeBytesSection} - rootMargin="10%" - > - - -
    - - ) -} - -function OpenSourceUnderline() { - return ( - - open-source - - - ) -} diff --git a/src/routes/libraries.tsx b/src/routes/libraries.tsx index d0cc06df4..02d8a3860 100644 --- a/src/routes/libraries.tsx +++ b/src/routes/libraries.tsx @@ -11,6 +11,10 @@ import { } from './-libraries-utils' export const Route = createFileRoute('/libraries')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, + }, component: LibrariesPage, head: () => ({ meta: [ diff --git a/src/routes/libraries_.$framework.tsx b/src/routes/libraries_.$framework.tsx index a4d621a84..7123733c5 100644 --- a/src/routes/libraries_.$framework.tsx +++ b/src/routes/libraries_.$framework.tsx @@ -10,6 +10,10 @@ import { } from './-libraries-utils' export const Route = createFileRoute('/libraries_/$framework')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, + }, loader: ({ params }) => { const frameworkOption = getFrameworkOption(params.framework) diff --git a/src/routes/merch.tsx b/src/routes/merch.tsx index fbd2e7238..3ff50d6c1 100644 --- a/src/routes/merch.tsx +++ b/src/routes/merch.tsx @@ -7,6 +7,10 @@ import { twMerge } from 'tailwind-merge' import { BaseballCapIcon } from '~/components/icons/BaseballCapIcon' export const Route = createFileRoute('/merch')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, + }, component: RouteComp, head: () => ({ meta: seo({ diff --git a/src/routes/partners.tsx b/src/routes/partners.tsx index 185262525..a2d87e791 100644 --- a/src/routes/partners.tsx +++ b/src/routes/partners.tsx @@ -1,6 +1,11 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/partners')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail across all + // /partners/* routes via this layout. + showNavbar: false, + }, component: PartnersLayout, }) diff --git a/src/routes/showcase/$id.tsx b/src/routes/showcase/$id.tsx index c8546ad73..d5a95a6ad 100644 --- a/src/routes/showcase/$id.tsx +++ b/src/routes/showcase/$id.tsx @@ -8,6 +8,10 @@ import { } from '~/queries/showcases' export const Route = createFileRoute('/showcase/$id')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, + }, params: { parse: (params) => ({ id: v.parse(v.pipe(v.string(), v.uuid()), params.id), diff --git a/src/routes/showcase/edit.$id.tsx b/src/routes/showcase/edit.$id.tsx index 6b2e56ef3..177c7649e 100644 --- a/src/routes/showcase/edit.$id.tsx +++ b/src/routes/showcase/edit.$id.tsx @@ -5,6 +5,10 @@ import { requireAuth } from '~/utils/auth.functions' import { getShowcase } from '~/utils/showcase.functions' export const Route = createFileRoute('/showcase/edit/$id')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, + }, beforeLoad: async ({ params }) => { try { const user = await requireAuth() diff --git a/src/routes/showcase/index.tsx b/src/routes/showcase/index.tsx index 0ba895212..0afaf693f 100644 --- a/src/routes/showcase/index.tsx +++ b/src/routes/showcase/index.tsx @@ -28,6 +28,10 @@ function hasNonCanonicalSearch(search: v.InferOutput) { } export const Route = createFileRoute('/showcase/')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, + }, validateSearch: searchSchema, loaderDeps: ({ search }) => ({ page: search.page, diff --git a/src/routes/showcase/submit.tsx b/src/routes/showcase/submit.tsx index 1c517ead9..5b5a9f8a4 100644 --- a/src/routes/showcase/submit.tsx +++ b/src/routes/showcase/submit.tsx @@ -4,6 +4,10 @@ import { ShowcaseSubmitForm } from '~/components/ShowcaseSubmitForm' import { requireAuth } from '~/utils/auth.functions' export const Route = createFileRoute('/showcase/submit')({ + staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, + }, beforeLoad: async () => { try { const user = await requireAuth() diff --git a/src/routes/stack.$category.tsx b/src/routes/stack.$category.tsx new file mode 100644 index 000000000..079d652ec --- /dev/null +++ b/src/routes/stack.$category.tsx @@ -0,0 +1,41 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +import { CategoryArticle } from '~/components/stack/CategoryArticle' +import { + categoryMeta, + categorySlugs, + type CategorySlug, +} from '~/components/stack/stack-categories' +import { seo } from '~/utils/seo' + +function isCategorySlug(value: string): value is CategorySlug { + return (categorySlugs as readonly string[]).includes(value) +} + +export const Route = createFileRoute('/stack/$category')({ + // Skip the global left-rail Navbar on category articles; EditorialTopNav + // (rendered in __root.tsx) provides navigation. + staticData: { + showNavbar: false, + }, + loader: ({ params }) => { + if (!isCategorySlug(params.category)) { + throw notFound() + } + return { category: params.category, meta: categoryMeta[params.category] } + }, + head: ({ loaderData }) => ({ + meta: seo({ + title: loaderData + ? `${loaderData.meta.shortName} — TanStack stack guide` + : 'TanStack stack guide', + description: loaderData?.meta.intro, + }), + }), + component: StackCategoryPage, +}) + +function StackCategoryPage() { + const { category } = Route.useLoaderData() + return +} diff --git a/src/routes/stats/npm/index.tsx b/src/routes/stats/npm/index.tsx index 97f569427..fcdf20930 100644 --- a/src/routes/stats/npm/index.tsx +++ b/src/routes/stats/npm/index.tsx @@ -197,6 +197,8 @@ export const Route = createFileRoute('/stats/npm/')({ }, component: RouteComponent, staticData: { + // Editorial top-nav surface; suppress the global left rail. + showNavbar: false, includeSearchInCanonical: true, Title: () => { return ( diff --git a/src/utils/documents.server.ts b/src/utils/documents.server.ts index dfe920858..528f94492 100644 --- a/src/utils/documents.server.ts +++ b/src/utils/documents.server.ts @@ -409,6 +409,17 @@ export async function fetchRepoFile( }) } + // Dev fallback: when there is no DATABASE_URL configured, the DB-backed + // GitHub cache cannot run. Use an in-memory cache and a direct raw fetch + // so docs pages still work without a local Postgres. + if (!process.env.DATABASE_URL) { + return fetchCached({ + key, + ttl: 60_000, + fn: () => fetchRepoFileFromOrigin(repoPair, ref, filepath), + }) + } + try { return await getCachedGitHubTextFile({ repo: repoPair, diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 3a01dcd33..82d7daede 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -1061,7 +1061,7 @@ const railway = (() => { libraries: libraries.map((l) => l.id), status: 'active' as const, score: 0.145, - tier: 'bronze' as const, + tier: 'gold' as const, href, brandColor: '#0B0D0E', tagline: 'Instant Deployment',