feat(home): editorial homepage, sticky top nav, and category drill-down#931
feat(home): editorial homepage, sticky top nav, and category drill-down#931jhislop-design wants to merge 1 commit into
Conversation
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>
📝 WalkthroughWalkthroughThis PR introduces an editorial layout system with a new top navigation component, replaces the home page with an editorial design, adds buyer's guide pages for stack categories, and applies the editorial pattern across multiple routes by suppressing the left sidebar navigation where appropriate. ChangesEditorial Navigation and Stack Categories
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (21)
src/components/Navbar.tsxsrc/components/editorial/EditorialTopNav.tsxsrc/components/home/HomeEditorial.tsxsrc/components/stack/CategoryArticle.tsxsrc/components/stack/stack-categories.tssrc/routeTree.gen.tssrc/routes/__root.tsxsrc/routes/blog.tsxsrc/routes/index.tsxsrc/routes/libraries.tsxsrc/routes/libraries_.$framework.tsxsrc/routes/merch.tsxsrc/routes/partners.tsxsrc/routes/showcase/$id.tsxsrc/routes/showcase/edit.$id.tsxsrc/routes/showcase/index.tsxsrc/routes/showcase/submit.tsxsrc/routes/stack.$category.tsxsrc/routes/stats/npm/index.tsxsrc/utils/documents.server.tssrc/utils/partners.tsx
| <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> |
There was a problem hiding this comment.
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.
| const relatedPosts = libraries | ||
| .flatMap((lib) => getPostsForLibrary(lib.id).map((p) => ({ post: p, lib }))) | ||
| .slice(0, 4) |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
| <PageViewTracker /> | ||
| {hideNavbar ? children : <Navbar>{children}</Navbar>} | ||
| <EditorialTopNav /> | ||
| {hideNavbar ? children : <Navbar hideHeader>{children}</Navbar>} |
There was a problem hiding this comment.
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.
| // 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), | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
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 (
/)HomeStatsSectiondeferred-loaded with editorial framingSticky editorial top nav (sitewide)
Sponsoredtooltip on hover (`rel="sponsored"` for SEO honesty)Category drill-down (`/stack/{state,ui,performance,tooling}`)
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
Test plan
Open questions for @tannerlinsley
🤖 Generated with Claude Code
Summary by CodeRabbit