feat(home): editorial homepage, sticky top nav, and category drill-down by jhislop-design · Pull Request #931 · TanStack/tanstack.com · GitHub
Skip to content

feat(home): editorial homepage, sticky top nav, and category drill-down#931

Open
jhislop-design wants to merge 1 commit into
mainfrom
jonny/distracted-hermann-e2cbe7
Open

feat(home): editorial homepage, sticky top nav, and category drill-down#931
jhislop-design wants to merge 1 commit into
mainfrom
jonny/distracted-hermann-e2cbe7

Conversation

@jhislop-design

@jhislop-design jhislop-design commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Reframes the TanStack home and library-browse surfaces as an editorial review-site (Tom's Hardware / Top 10 Reviews) where every block is a teaser into a deeper page rather than one tall landing page. The contextual library left rail on docs pages is preserved.

What's new

Editorial homepage (/)

  • Featured Stack hero — TanStack Start branded card with the existing application-starter form embedded inline
  • Top Libraries leaderboard side rail (Query, Router, Table, Form)
  • Trust pillars (4 icon + label cards)
  • Trusted in production marquee (unchanged)
  • By the numbersHomeStatsSection deferred-loaded with editorial framing
  • Top picks by category — 4 cards, each a teaser into a category article
  • Latest writing — 4 most recent blog posts
  • Trusted partners — Gold / Silver / Bronze tiered with logo + tagline cards
  • Community pair — Discord + YouTube as compact 2-col CTAs

Sticky editorial top nav (sitewide)

  • Eyebrow strip + brand + 11 primary links + search + 2×3 social cluster (GitHub/Discord/YouTube/X/Bluesky/Instagram) + theme toggle
  • Pinned Gold Partners strip with logos + Sponsored tooltip on hover (`rel="sponsored"` for SEO honesty)
  • Sticky positioning publishes its rendered height into `--navbar-height` so the existing docs sidebar lines up underneath
  • Mobile drawer for the same nav at `< lg`

Category drill-down (`/stack/{state,ui,performance,tooling}`)

  • Buyer's-guide layout: breadcrumb → hero → Top Pick → quick-verdict table → ranked deep-dives → "How we think about it" criteria → related writing
  • Sticky right rail: in-guide TOC, "Compare across the stack" sibling links, back-to-libraries CTA

Pages that drop the left rail and use the editorial top nav

`/`, `/stack/$category`, `/libraries`, `/libraries/$framework`, `/partners`, `/blog`, `/showcase` (+ children), `/stats/npm`, `/merch`

Heads-up changes for maintainers

  • Railway promoted from bronze → gold partner tier. Please confirm — this affects the Gold strip and the home Partners section.
  • Removed several deferred home sections from the homepage render (`HomeSocialProofSection`, `HomeCommunitySection`, `HomeBytesSection`, standalone `HomeApplicationStarter`). Components themselves are untouched — only the homepage layout no longer mounts them. Easy to add back wherever.
  • Dev-only: `src/utils/documents.server.ts` gains a `DATABASE_URL`-missing fallback so docs pages render in dev without a local Postgres. Prod path is unchanged.

Test plan

  • `pnpm format` — only touched files in this PR
  • `pnpm test` (tsc + lint) — 0 errors
  • `pnpm test:smoke` — 10/10 (home, blog, ethos, query/router/table docs, OG images)
  • `pnpm build` — clean
  • Manual: `/`, `/stack/state`, `/stack/ui`, `/stack/performance`, `/stack/tooling`, `/libraries`, `/partners`, `/blog`, `/showcase`, `/stats`, `/merch`, `/router/latest` all render correctly at `1440x900`. Sticky nav stays pinned. Active state (cyan pill) tracks current page.

Open questions for @tannerlinsley

  1. Railway → gold: ship it, or keep at bronze and pin via a separate "Featured partner" mechanism?
  2. The 4 removed deferred home sections — keep them off home, or want them somewhere on the editorial home (and where)?
  3. Comfortable having the editorial top nav globally, or scope it back to just the homepage + `/stack/*`?

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added stack buyer's guide with comprehensive category articles for state management, UI frameworks, performance tools, and tooling solutions
    • Redesigned homepage with editorial layout featuring library leaderboards, featured content, partner tiers, and community calls-to-action
    • Introduced editorial top navigation with primary navigation links, search functionality, social links, and theme toggle
    • Enhanced document fetching with fallback caching support
    • Updated Railway partner status to gold tier

Review Change Stack

Reframes the home and library-browse surfaces as an editorial review
site (Tom's Hardware / Top 10 Reviews vibe) where every block is a
teaser into a deeper page rather than a single tall landing.

New surfaces
- src/components/home/HomeEditorial.tsx: featured Start hero with
  embedded application starter, top-libraries leaderboard side rail,
  trust pillars, Trusted-By marquee, live OSS Stats deferred section,
  four "Top picks by category" cards, latest writing, tiered partners
  (Gold/Silver/Bronze), Discord + YouTube community pair.
- src/components/editorial/EditorialTopNav.tsx: sticky global top nav
  (eyebrow strip + brand + 11-item primary nav + search + compact 2x3
  social cluster + theme + mobile drawer) and a pinned Gold Partners
  strip with "Sponsored" hover tooltips.
- src/components/stack/* + src/routes/stack.\$category.tsx: four
  buyer's-guide-style category articles
  (/stack/{state,ui,performance,tooling}) with hero, Top Pick, quick
  verdict table, ranked list, criteria block, related writing and a
  sticky right-rail TOC + cross-category compare.

Global shell
- src/routes/__root.tsx: render EditorialTopNav on every page; non-
  editorial pages still wrap children with <Navbar hideHeader> so the
  contextual library left rail is preserved on docs pages.
- src/components/Navbar.tsx: add hideHeader prop. When true the global
  Navbar skips its own fixed top header and lets EditorialTopNav own
  --navbar-height (published by EditorialTopNav via ResizeObserver).

Page opt-ins
- /, /stack/\$category, /libraries, /libraries/\$framework, /partners,
  /blog, /showcase + children, /stats/npm, /merch all set
  staticData.showNavbar: false to drop the library left rail and use
  the editorial top-nav-only layout.

Data and dev DX
- src/utils/partners.tsx: promote Railway from bronze -> gold.
- src/utils/documents.server.ts: when DATABASE_URL is unset (dev
  without Postgres), fall back from getCachedGitHubTextFile to the
  in-memory fetchCached path so library docs pages render locally
  without a DB.

Notes for reviewers
- Removed deferred home sections (HomeSocialProofSection,
  HomeCommunitySection, HomeBytesSection, standalone
  HomeApplicationStarter usage) are kept as components -- only their
  homepage render is gone.
- routeTree.gen.ts is regenerated by the build; included for
  completeness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/editorial/EditorialTopNav.tsx`:
- Around line 124-131: The Search button in EditorialTopNav is rendered as an
active control but lacks any click handler; either wire it to the search action
(e.g., call the existing openSearch / onOpenSearch / toggleSearchModal prop or
dispatch a search-open action from EditorialTopNav) or mark it non-interactive
by adding the disabled attribute and adjusting aria-disabled/title to reflect
unavailability (or hide it). Locate the button element in EditorialTopNav (the
one with aria-label="Search (⌘K)" and <Search /> icon) and either attach the
proper onClick handler that opens the search UI or change its attributes/classes
to disabled/hidden and update accessible labels accordingly.

In `@src/components/stack/CategoryArticle.tsx`:
- Line 129: The section element in CategoryArticle.tsx uses id={library.id} in
two places, causing duplicate IDs; update both occurrences to produce unique IDs
(for example append a stable suffix such as the library index, type, or role) so
each section id is distinct — e.g. replace id={library.id} with something like
id={`${library.id}-${index}`} or id={`${library.id}-editors`} in the
editor's-pick render path and any other place that reused library.id, and ensure
any internal anchor links/refs that relied on library.id are updated to match
the new unique id format.
- Around line 32-34: relatedPosts can contain the same post multiple times (from
different libraries) causing duplicate entries and key collisions with
post.slug; dedupe the array returned from libraries.flatMap by post identifier
(e.g., post.id or post.slug) before calling slice(0, 4) so you only pick unique
posts, then render those; also update the list key usage (where post.slug is
used) to a truly unique key if you intentionally render duplicates (e.g.,
`${post.slug}-${lib.id}`) or just use post.id after deduping to avoid
collisions.

In `@src/routes/__root.tsx`:
- Line 235: The Navbar is always rendered with hideHeader enabled which hides
the header/menu trigger; change the prop so it reflects the hideNavbar flag
instead of always being true — replace the current JSX that renders <Navbar
hideHeader>{children}</Navbar> with <Navbar
hideHeader={hideNavbar}>{children}</Navbar> (or simply
<Navbar>{children}</Navbar> if you want the header shown when hideNavbar is
false) so the Navbar header/menu trigger remains available when appropriate;
update the expression around hideNavbar/children accordingly.

In `@src/utils/documents.server.ts`:
- Around line 412-421: The dev-only in-memory fallback is being triggered
whenever DATABASE_URL is unset, bypassing the InvalidCacheKeyError guard; change
the condition so the fallback runs only in development (e.g.,
process.env.NODE_ENV === 'development') and/or a dedicated dev flag, and keep
the InvalidCacheKeyError guard in place: update the block that calls
fetchCached({ key, ttl, fn: () => fetchRepoFileFromOrigin(repoPair, ref,
filepath) }) to run only when in dev, and ensure any InvalidCacheKeyError thrown
during cache/key validation (the existing InvalidCacheKeyError path) still
bubbles or is handled before falling back to fetchRepoFileFromOrigin.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ccb3d127-13be-4ac8-bc61-86347e5eb31a

📥 Commits

Reviewing files that changed from the base of the PR and between 231bc91 and dbe2ca7.

📒 Files selected for processing (21)
  • src/components/Navbar.tsx
  • src/components/editorial/EditorialTopNav.tsx
  • src/components/home/HomeEditorial.tsx
  • src/components/stack/CategoryArticle.tsx
  • src/components/stack/stack-categories.ts
  • src/routeTree.gen.ts
  • src/routes/__root.tsx
  • src/routes/blog.tsx
  • src/routes/index.tsx
  • src/routes/libraries.tsx
  • src/routes/libraries_.$framework.tsx
  • src/routes/merch.tsx
  • src/routes/partners.tsx
  • src/routes/showcase/$id.tsx
  • src/routes/showcase/edit.$id.tsx
  • src/routes/showcase/index.tsx
  • src/routes/showcase/submit.tsx
  • src/routes/stack.$category.tsx
  • src/routes/stats/npm/index.tsx
  • src/utils/documents.server.ts
  • src/utils/partners.tsx

Comment on lines +124 to +131
<button
type="button"
aria-label="Search (⌘K)"
title="Search · ⌘K"
className="hidden h-9 w-9 items-center justify-center rounded-md text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 sm:flex dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-white"
>
<Search size={16} />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the search control functional or explicitly disabled.

Line 124 renders an active button with a search affordance/title, but it has no click behavior. This creates a broken primary-nav action for users.

Suggested direction
-          <button
+          <button
             type="button"
             aria-label="Search (⌘K)"
             title="Search · ⌘K"
+            onClick={openSearch} // wire existing command palette/search entry point
             className="hidden h-9 w-9 items-center justify-center rounded-md text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 sm:flex dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-white"
           >
             <Search size={16} />
           </button>

If search is not ready yet, render it disabled/hidden instead of interactive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editorial/EditorialTopNav.tsx` around lines 124 - 131, The
Search button in EditorialTopNav is rendered as an active control but lacks any
click handler; either wire it to the search action (e.g., call the existing
openSearch / onOpenSearch / toggleSearchModal prop or dispatch a search-open
action from EditorialTopNav) or mark it non-interactive by adding the disabled
attribute and adjusting aria-disabled/title to reflect unavailability (or hide
it). Locate the button element in EditorialTopNav (the one with
aria-label="Search (⌘K)" and <Search /> icon) and either attach the proper
onClick handler that opens the search UI or change its attributes/classes to
disabled/hidden and update accessible labels accordingly.

Comment on lines +32 to +34
const relatedPosts = libraries
.flatMap((lib) => getPostsForLibrary(lib.id).map((p) => ({ post: p, lib })))
.slice(0, 4)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

De-duplicate related posts before slicing/rendering.

Lines 32-34 can include the same blog post multiple times (across multiple libraries), and Line 419 then reuses post.slug keys, which can collide.

Minimal fix
-  const relatedPosts = libraries
-    .flatMap((lib) => getPostsForLibrary(lib.id).map((p) => ({ post: p, lib })))
-    .slice(0, 4)
+  const relatedPosts = Array.from(
+    new Map(
+      libraries
+        .flatMap((lib) =>
+          getPostsForLibrary(lib.id).map((p) => ({ post: p, lib })),
+        )
+        .map((item) => [item.post.slug, item] as const),
+    ).values(),
+  ).slice(0, 4)

Also applies to: 417-419

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/stack/CategoryArticle.tsx` around lines 32 - 34, relatedPosts
can contain the same post multiple times (from different libraries) causing
duplicate entries and key collisions with post.slug; dedupe the array returned
from libraries.flatMap by post identifier (e.g., post.id or post.slug) before
calling slice(0, 4) so you only pick unique posts, then render those; also
update the list key usage (where post.slug is used) to a truly unique key if you
intentionally render duplicates (e.g., `${post.slug}-${lib.id}`) or just use
post.id after deduping to avoid collisions.

accent: { from: string; to: string }
}) {
return (
<section id={library.id} className="scroll-mt-6">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid duplicate element IDs for the editor’s pick library.

Line 129 and Line 297 can produce the same id, which makes in-page anchors ambiguous and violates unique-id constraints.

Minimal fix
-    <section id={library.id} className="scroll-mt-6">
+    <section id={`top-pick-${library.id}`} className="scroll-mt-6">

Also applies to: 297-297

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/stack/CategoryArticle.tsx` at line 129, The section element in
CategoryArticle.tsx uses id={library.id} in two places, causing duplicate IDs;
update both occurrences to produce unique IDs (for example append a stable
suffix such as the library index, type, or role) so each section id is distinct
— e.g. replace id={library.id} with something like id={`${library.id}-${index}`}
or id={`${library.id}-editors`} in the editor's-pick render path and any other
place that reused library.id, and ensure any internal anchor links/refs that
relied on library.id are updated to match the new unique id format.

Comment thread src/routes/__root.tsx
<PageViewTracker />
{hideNavbar ? children : <Navbar>{children}</Navbar>}
<EditorialTopNav />
{hideNavbar ? children : <Navbar hideHeader>{children}</Navbar>}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

hideHeader is always enabled here, which can leave left-rail navigation inaccessible.

On Line 235, hideHeader is passed as a boolean shorthand, so it is always true whenever Navbar renders. That removes the Navbar header/menu trigger, but the sidebar still depends on that trigger on non-inline layouts.

Suggested fix
-            {hideNavbar ? children : <Navbar hideHeader>{children}</Navbar>}
+            {hideNavbar ? children : <Navbar hideHeader={hideNavbar}>{children}</Navbar>}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/__root.tsx` at line 235, The Navbar is always rendered with
hideHeader enabled which hides the header/menu trigger; change the prop so it
reflects the hideNavbar flag instead of always being true — replace the current
JSX that renders <Navbar hideHeader>{children}</Navbar> with <Navbar
hideHeader={hideNavbar}>{children}</Navbar> (or simply
<Navbar>{children}</Navbar> if you want the header shown when hideNavbar is
false) so the Navbar header/menu trigger remains available when appropriate;
update the expression around hideNavbar/children accordingly.

Comment on lines +412 to +421

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dev fallback is currently active in all environments and skips invalid-path protection.

Line 415 enables this path whenever DATABASE_URL is missing, not just in development, and it bypasses the InvalidCacheKeyError guard path. That can mask prod misconfig and allow malformed paths to hit origin fetches.

Suggested patch
-  if (!process.env.DATABASE_URL) {
+  if (process.env.NODE_ENV === 'development' && !process.env.DATABASE_URL) {
+    if (!isValidFilepath(filepath)) {
+      return null
+    }
     return fetchCached({
       key,
       ttl: 60_000,
       fn: () => fetchRepoFileFromOrigin(repoPair, ref, filepath),
     })
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/documents.server.ts` around lines 412 - 421, The dev-only in-memory
fallback is being triggered whenever DATABASE_URL is unset, bypassing the
InvalidCacheKeyError guard; change the condition so the fallback runs only in
development (e.g., process.env.NODE_ENV === 'development') and/or a dedicated
dev flag, and keep the InvalidCacheKeyError guard in place: update the block
that calls fetchCached({ key, ttl, fn: () => fetchRepoFileFromOrigin(repoPair,
ref, filepath) }) to run only when in dev, and ensure any InvalidCacheKeyError
thrown during cache/key validation (the existing InvalidCacheKeyError path)
still bubbles or is handled before falling back to fetchRepoFileFromOrigin.

tannerlinsley added a commit that referenced this pull request May 25, 2026
…ed social dropdown (#935)

* feat(home): browse-by-category cards, /stack/$category pages, stacked social dropdown

Cherry-picks the genuinely useful pieces from #931 onto main:

- Add a "framework" library group containing Start and Router, separate from
  data-and-state, so the framework-tier libraries get their own category.
- Replace the bulky Open Source Libraries grid on the home page with a
  condensed five-card "Browse the stack" section (one card per category,
  linking to a dedicated category page).
- Add /stack/$category landing pages: header, "Where to start" top pick,
  the full list of libraries in the category, and category-tagged blog
  posts. Drops the editorial-style verdict block, criteria section,
  in-this-guide rail, and compare-across-the-stack rail.
- Replace the inline 2x3 social icon strip in Navbar with a stacked
  three-icon trigger that opens a labeled dropdown of all six channels.

* style: oxfmt tidy on Navbar and index

* Fix docs webhook invalidation

* Add friendly URL-aware 404 page
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant