feat(home): browse-by-category cards + /stack/$category pages + stacked social dropdown#935
Conversation
… 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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds library categories and category pages with homepage cards and typed route registration for /stack/$category; centralizes docs cache header computation and wires it into docs routes; tightens GitHub/docs cache refresh behavior; refactors navbar social links into a dropdown; replaces 404 with a route-recovery NotFound UI. ChangesLibrary categorization and category browsing
Navbar social links dropdown
Docs caching and headers
GitHub/docs content cache behavior
NotFound & blog integration
Scripts
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
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 Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/stack/CategoryArticle.tsx`:
- Around line 25-27: The code assumes libraries has at least one item so topPick
= libraries.find(...) ?? libraries[0] can be undefined; before dereferencing
topPick.id in the render (where topPick is used) add a guard/fallback UI when
libraries.length === 0 (e.g., return null, a placeholder, or an empty-state
component) or ensure topPick is a defined object (fallback to a safe default) so
rendering never accesses topPick.id on undefined; update the logic around
topPick, libraries and any use of meta.topPickId in the CategoryArticle
component to handle empty arrays safely.
- Line 89: The duplicated id={library.id} causes invalid HTML because
TopPickBlock and LibraryEntry both render the same id; fix by only rendering the
id on one of those components or generating a unique id for the TopPick variant
(e.g., prepend/append a stable prefix/suffix) so anchors remain deterministic;
locate the id usage in the section element and in the TopPickBlock/LibraryEntry
renderings and change one side to use a unique key like `top-pick-${library.id}`
or remove the id from the non-anchor instance.
- Around line 136-139: The span currently always outputs the literal "tanstack/"
plus library.repo?.split('/').pop(), causing "tanstack/undefined" when
library.repo is missing; update the rendering in CategoryArticle (the span that
includes <GitHub /> and the repo text) to only render the "tanstack/" prefix and
repo fragment when library.repo is truthy (e.g., conditionally render the entire
suffix or replace with a fallback/omit the prefix when library.repo is absent)
so the UI never shows "tanstack/undefined".
- Around line 270-272: In CategoryArticle.tsx where items.map(({ post, lib }) =>
...) renders each <li> with key={post.slug}, replace the unstable key with a
composite unique key that includes the library identifier (for example use
post.slug combined with lib.id or lib.slug) to avoid collisions when the same
post appears under multiple libraries; update the map key expression (in the
items.map callback) to something like `${post.slug}-${lib.id}` and ensure lib.id
(or chosen lib identifier) is present before using it.
🪄 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: 03188ce8-e70f-4c08-97c6-ec661b44dc13
📒 Files selected for processing (7)
src/components/Navbar.tsxsrc/components/stack/CategoryArticle.tsxsrc/components/stack/stack-categories.tssrc/libraries/libraries.tssrc/routeTree.gen.tssrc/routes/index.tsxsrc/routes/stack.$category.tsx
| const topPick = | ||
| libraries.find((lib) => lib.id === meta.topPickId) ?? libraries[0] | ||
| const relatedPosts = libraries |
There was a problem hiding this comment.
Guard against empty category library lists before dereferencing topPick.id.
If getCategoryLibraries(slug) returns an empty array, topPick is undefined and Line 77 will throw at render time. Add an early guard/fallback UI before using topPick.id.
Suggested fix
export function CategoryArticle({ slug }: { slug: CategorySlug }) {
const meta = categoryMeta[slug]
const libraries = getCategoryLibraries(slug)
+ if (libraries.length === 0) {
+ return null
+ }
const topPick =
libraries.find((lib) => lib.id === meta.topPickId) ?? libraries[0]Also applies to: 77-77
🤖 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 25 - 27, The code
assumes libraries has at least one item so topPick = libraries.find(...) ??
libraries[0] can be undefined; before dereferencing topPick.id in the render
(where topPick is used) add a guard/fallback UI when libraries.length === 0
(e.g., return null, a placeholder, or an empty-state component) or ensure
topPick is a defined object (fallback to a safe default) so rendering never
accesses topPick.id on undefined; update the logic around topPick, libraries and
any use of meta.topPickId in the CategoryArticle component to handle empty
arrays safely.
|
|
||
| function TopPickBlock({ library }: { library: LibrarySlim }) { | ||
| return ( | ||
| <section id={library.id} className="scroll-mt-6"> |
There was a problem hiding this comment.
id attributes are duplicated for the top-pick library.
The top-pick appears once in TopPickBlock and again in LibraryEntry, both with id={library.id}. Duplicate IDs are invalid HTML and can make anchor targeting inconsistent.
Also applies to: 185-185
🤖 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 89, The duplicated
id={library.id} causes invalid HTML because TopPickBlock and LibraryEntry both
render the same id; fix by only rendering the id on one of those components or
generating a unique id for the TopPick variant (e.g., prepend/append a stable
prefix/suffix) so anchors remain deterministic; locate the id usage in the
section element and in the TopPickBlock/LibraryEntry renderings and change one
side to use a unique key like `top-pick-${library.id}` or remove the id from the
non-anchor instance.
| <span className="ml-auto inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 dark:text-zinc-400"> | ||
| <GitHub className="h-3.5 w-3.5" /> tanstack/ | ||
| {library.repo?.split('/').pop()} | ||
| </span> |
There was a problem hiding this comment.
Avoid rendering tanstack/undefined when library.repo is missing.
The current string always renders the tanstack/ prefix, even when library.repo is absent.
Suggested fix
- <span className="ml-auto inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 dark:text-zinc-400">
- <GitHub className="h-3.5 w-3.5" /> tanstack/
- {library.repo?.split('/').pop()}
- </span>
+ {library.repo && (
+ <span className="ml-auto inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 dark:text-zinc-400">
+ <GitHub className="h-3.5 w-3.5" /> tanstack/{library.repo.split('/').pop()}
+ </span>
+ )}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span className="ml-auto inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 dark:text-zinc-400"> | |
| <GitHub className="h-3.5 w-3.5" /> tanstack/ | |
| {library.repo?.split('/').pop()} | |
| </span> | |
| {library.repo && ( | |
| <span className="ml-auto inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 dark:text-zinc-400"> | |
| <GitHub className="h-3.5 w-3.5" /> tanstack/{library.repo.split('/').pop()} | |
| </span> | |
| )} |
🤖 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 136 - 139, The span
currently always outputs the literal "tanstack/" plus
library.repo?.split('/').pop(), causing "tanstack/undefined" when library.repo
is missing; update the rendering in CategoryArticle (the span that includes
<GitHub /> and the repo text) to only render the "tanstack/" prefix and repo
fragment when library.repo is truthy (e.g., conditionally render the entire
suffix or replace with a fallback/omit the prefix when library.repo is absent)
so the UI never shows "tanstack/undefined".
| {items.map(({ post, lib }) => ( | ||
| <li key={post.slug}> | ||
| <Link |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd src/components/stack && wc -l CategoryArticle.tsxRepository: TanStack/tanstack.com
Length of output: 89
🏁 Script executed:
cd src/components/stack && sed -n '260,285p' CategoryArticle.tsxRepository: TanStack/tanstack.com
Length of output: 1126
🏁 Script executed:
cd src/components/stack && head -50 CategoryArticle.tsx | grep -E "import|export|interface|type|const|function"Repository: TanStack/tanstack.com
Length of output: 570
🏁 Script executed:
cd src/components/stack && sed -n '250,320p' CategoryArticle.tsxRepository: TanStack/tanstack.com
Length of output: 2514
🏁 Script executed:
cd src/components/stack && sed -n '1,250p' CategoryArticle.tsxRepository: TanStack/tanstack.com
Length of output: 8737
🏁 Script executed:
cd src/utils && find . -name "*blog*" -type fRepository: TanStack/tanstack.com
Length of output: 95
🏁 Script executed:
cd src/utils && grep -n "getPostsForLibrary" blog.ts blog.functions.tsRepository: TanStack/tanstack.com
Length of output: 143
🏁 Script executed:
cd src/utils && cat blog.tsRepository: TanStack/tanstack.com
Length of output: 2338
🏁 Script executed:
cd src && find . -name "*categories*" -o -name "*stack*" | head -20Repository: TanStack/tanstack.com
Length of output: 938
🏁 Script executed:
cd src/components/stack && ls -laRepository: TanStack/tanstack.com
Length of output: 323
🏁 Script executed:
find src -name "stack-categories*" -type fRepository: TanStack/tanstack.com
Length of output: 106
🏁 Script executed:
cd src/components/stack && cat stack-categories.tsRepository: TanStack/tanstack.com
Length of output: 3659
🏁 Script executed:
cd content && find . -name "*.md" -type f | head -5 | xargs head -20Repository: TanStack/tanstack.com
Length of output: 123
🏁 Script executed:
cd content && grep -r "library:" . | head -10Repository: TanStack/tanstack.com
Length of output: 123
🏁 Script executed:
find . -type d -name "blog" | head -5Repository: TanStack/tanstack.com
Length of output: 76
🏁 Script executed:
ls -la src/blog | head -20Repository: TanStack/tanstack.com
Length of output: 1552
🏁 Script executed:
cd src/blog && head -30 announcing-tanstack-query-v5.mdRepository: TanStack/tanstack.com
Length of output: 3028
🏁 Script executed:
cd src/blog && head -30 announcing-tanstack-form-v1.mdRepository: TanStack/tanstack.com
Length of output: 895
🏁 Script executed:
cd src/blog && grep -E "^library:" *.md | head -20Repository: TanStack/tanstack.com
Length of output: 918
🏁 Script executed:
cd src/blog && grep -E "^library:" *.md | grep ","Repository: TanStack/tanstack.com
Length of output: 178
🏁 Script executed:
cd src/blog && grep -E "^(library|title):" tanstack-start-solid-v2.md why-tanstack-start-and-router.mdRepository: TanStack/tanstack.com
Length of output: 352
🏁 Script executed:
cd src && cat -n components/stack/stack-categories.ts | grep -A 3 "slugToGroup"Repository: TanStack/tanstack.com
Length of output: 440
🏁 Script executed:
cd src && find . -name "libraries*" -o -name "index.ts" | grep -i librar | head -10Repository: TanStack/tanstack.com
Length of output: 191
🏁 Script executed:
cd src && rg "librariesByGroup" -A 50 | head -100Repository: TanStack/tanstack.com
Length of output: 5708
🏁 Script executed:
cd src && cat libraries/libraries.ts | head -100Repository: TanStack/tanstack.com
Length of output: 4936
Use a stable unique key per related-post row.
key={post.slug} can collide when the same post appears for multiple libraries in this flattened list. Since posts can be tagged with multiple libraries (e.g., library: router, start) and both may belong to the same category, use a composite key (e.g., key={post.slug}-${lib.id}).
🤖 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 270 - 272, In
CategoryArticle.tsx where items.map(({ post, lib }) => ...) renders each <li>
with key={post.slug}, replace the unstable key with a composite unique key that
includes the library identifier (for example use post.slug combined with lib.id
or lib.slug) to avoid collisions when the same post appears under multiple
libraries; update the map key expression (in the items.map callback) to
something like `${post.slug}-${lib.id}` and ensure lib.id (or chosen lib
identifier) is present before using it.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/utils/github-content-cache.server.ts`:
- Line 9: POSITIVE_STALE_MS is too short (5 minutes) and combined with the
“refresh synchronously now” behavior causes request-path traffic to block on
origin/build; either increase POSITIVE_STALE_MS to a much longer TTL (e.g., 1h)
or split the policy into two constants (e.g., POSITIVE_STALE_MS for serving
stale and POSITIVE_SYNC_REFRESH_MS for when a caller explicitly requests sync
refresh), and change the cache logic so that when an entry is only stale you
return the cached value and schedule an async background refresh instead of
awaiting a synchronous rebuild; update the code paths that call this helper (the
callers at docs.functions.ts:238 and documents.server.ts:413) to rely on the
non-blocking stale-while-revalidate behavior or to explicitly opt into sync
refresh when necessary.
🪄 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: ed3b6925-6d90-4e5c-8e9d-034219d426c9
📒 Files selected for processing (11)
scripts/sync-docs-webhooks.tssrc/routes/$libraryId/$version.docs.$.tsxsrc/routes/$libraryId/$version.docs.community-resources.tsxsrc/routes/$libraryId/$version.docs.framework.$framework.$.tsxsrc/routes/$libraryId/$version.docs.framework.$framework.examples.$.tsxsrc/routes/$libraryId/$version.docs.framework.$framework.{$}[.]md.tsxsrc/routes/$libraryId/$version.docs.tsxsrc/routes/$libraryId/$version.docs.{$}[.]md.tsxsrc/utils/docs-cache-headers.tssrc/utils/docs.functions.tssrc/utils/github-content-cache.server.ts
| } from '~/db/schema' | ||
|
|
||
| const POSITIVE_STALE_MS = 24 * 60 * 60 * 1000 | ||
| const POSITIVE_STALE_MS = 5 * 60 * 1000 |
There was a problem hiding this comment.
Avoid forcing request-path refreshes every 5 minutes.
With POSITIVE_STALE_MS at 5 minutes and the new “refresh synchronously now” behavior, expired positive entries will block on origin/build instead of serving cached data. Since src/utils/docs.functions.ts:238 and src/utils/documents.server.ts:413 await these helpers directly, this makes ordinary page traffic depend on GitHub/build latency every few minutes per key. Please either keep a longer positive TTL here or split the policy so request-path artifacts can still serve stale while refresh runs.
Also applies to: 133-136
🤖 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/github-content-cache.server.ts` at line 9, POSITIVE_STALE_MS is too
short (5 minutes) and combined with the “refresh synchronously now” behavior
causes request-path traffic to block on origin/build; either increase
POSITIVE_STALE_MS to a much longer TTL (e.g., 1h) or split the policy into two
constants (e.g., POSITIVE_STALE_MS for serving stale and
POSITIVE_SYNC_REFRESH_MS for when a caller explicitly requests sync refresh),
and change the cache logic so that when an entry is only stale you return the
cached value and schedule an async background refresh instead of awaiting a
synchronous rebuild; update the code paths that call this helper (the callers at
docs.functions.ts:238 and documents.server.ts:413) to rely on the non-blocking
stale-while-revalidate behavior or to explicitly opt into sync refresh when
necessary.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/NotFound.tsx`:
- Around line 159-197: getLibraryDestinations currently selects top libraries by
score then maps to destinations, which can yield empty results for libraries
without a routable `to` or `latestVersion`; change the flow so you compute each
library's destinations first (using the same logic that creates
NotFoundDestination entries based on `library.to` and `library.latestVersion`),
discard libraries that produce zero destinations, then sort by score and take
the top 2 before flattening to return destinations; update the mapping/filtering
around getLibraryDestinations to ensure only libraries that produce at least one
destination are considered when slicing the top matches.
🪄 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: 2557d4ec-c5dd-4064-b932-8bb78c67c5a9
📒 Files selected for processing (2)
src/components/NotFound.tsxsrc/routes/blog.tsx
| function getLibraryDestinations(pathname: string) { | ||
| return libraries | ||
| .filter((library) => library.visible !== false) | ||
| .map((library) => ({ | ||
| library, | ||
| score: scoreLibraryMatch(library, pathname), | ||
| })) | ||
| .filter((match) => match.score > 0) | ||
| .sort((a, b) => b.score - a.score) | ||
| .slice(0, 2) | ||
| .flatMap(({ library, score }) => { | ||
| const destinations: NotFoundDestination[] = [] | ||
|
|
||
| if (library.to) { | ||
| destinations.push({ | ||
| key: `library-${library.id}`, | ||
| label: library.name, | ||
| description: library.tagline, | ||
| href: library.to, | ||
| icon: BookOpen, | ||
| accent: 'emerald', | ||
| score: score + 2, | ||
| }) | ||
| } | ||
|
|
||
| if (library.latestVersion) { | ||
| destinations.push({ | ||
| key: `library-docs-${library.id}`, | ||
| label: `${library.name.replace(/^TanStack\s+/i, '')} docs`, | ||
| description: 'Open the current docs for this project.', | ||
| href: `/${library.id}/latest/docs/${library.defaultDocs ?? 'overview'}`, | ||
| icon: BookOpen, | ||
| accent: 'cyan', | ||
| score: score + 1, | ||
| }) | ||
| } | ||
|
|
||
| return destinations | ||
| }) |
There was a problem hiding this comment.
Filter non-routable libraries before taking top matches
Line 168 slices to top matches before checking whether each library can actually produce a destination. If a top-scored library has neither to nor latestVersion, it yields no cards and can suppress valid suggestions. This is reachable with entries like react-charts and create-tsrouter-app in src/libraries/libraries.ts (line range 782-834).
Proposed fix
function getLibraryDestinations(pathname: string) {
return libraries
- .filter((library) => library.visible !== false)
+ .filter(
+ (library) =>
+ library.visible !== false && Boolean(library.to || library.latestVersion),
+ )
.map((library) => ({
library,
score: scoreLibraryMatch(library, pathname),
}))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function getLibraryDestinations(pathname: string) { | |
| return libraries | |
| .filter((library) => library.visible !== false) | |
| .map((library) => ({ | |
| library, | |
| score: scoreLibraryMatch(library, pathname), | |
| })) | |
| .filter((match) => match.score > 0) | |
| .sort((a, b) => b.score - a.score) | |
| .slice(0, 2) | |
| .flatMap(({ library, score }) => { | |
| const destinations: NotFoundDestination[] = [] | |
| if (library.to) { | |
| destinations.push({ | |
| key: `library-${library.id}`, | |
| label: library.name, | |
| description: library.tagline, | |
| href: library.to, | |
| icon: BookOpen, | |
| accent: 'emerald', | |
| score: score + 2, | |
| }) | |
| } | |
| if (library.latestVersion) { | |
| destinations.push({ | |
| key: `library-docs-${library.id}`, | |
| label: `${library.name.replace(/^TanStack\s+/i, '')} docs`, | |
| description: 'Open the current docs for this project.', | |
| href: `/${library.id}/latest/docs/${library.defaultDocs ?? 'overview'}`, | |
| icon: BookOpen, | |
| accent: 'cyan', | |
| score: score + 1, | |
| }) | |
| } | |
| return destinations | |
| }) | |
| function getLibraryDestinations(pathname: string) { | |
| return libraries | |
| .filter( | |
| (library) => | |
| library.visible !== false && Boolean(library.to || library.latestVersion), | |
| ) | |
| .map((library) => ({ | |
| library, | |
| score: scoreLibraryMatch(library, pathname), | |
| })) | |
| .filter((match) => match.score > 0) | |
| .sort((a, b) => b.score - a.score) | |
| .slice(0, 2) | |
| .flatMap(({ library, score }) => { | |
| const destinations: NotFoundDestination[] = [] | |
| if (library.to) { | |
| destinations.push({ | |
| key: `library-${library.id}`, | |
| label: library.name, | |
| description: library.tagline, | |
| href: library.to, | |
| icon: BookOpen, | |
| accent: 'emerald', | |
| score: score + 2, | |
| }) | |
| } | |
| if (library.latestVersion) { | |
| destinations.push({ | |
| key: `library-docs-${library.id}`, | |
| label: `${library.name.replace(/^TanStack\s+/i, '')} docs`, | |
| description: 'Open the current docs for this project.', | |
| href: `/${library.id}/latest/docs/${library.defaultDocs ?? 'overview'}`, | |
| icon: BookOpen, | |
| accent: 'cyan', | |
| score: score + 1, | |
| }) | |
| } | |
| return destinations | |
| }) |
🤖 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/NotFound.tsx` around lines 159 - 197, getLibraryDestinations
currently selects top libraries by score then maps to destinations, which can
yield empty results for libraries without a routable `to` or `latestVersion`;
change the flow so you compute each library's destinations first (using the same
logic that creates NotFoundDestination entries based on `library.to` and
`library.latestVersion`), discard libraries that produce zero destinations, then
sort by score and take the top 2 before flattening to return destinations;
update the mapping/filtering around getLibraryDestinations to ensure only
libraries that produce at least one destination are considered when slicing the
top matches.
# Conflicts: # src/routes/$libraryId/$version.docs.framework.$framework.examples.$.tsx
Cherry-picks just the genuinely useful pieces from #931 on top of
main. Leaves the editorial top nav, new partners layout, and most of the home rewrite alone.Summary
frameworklibrary group — Start and Router move out of Data & State into their own framework-tier category./stack/$categorylanding pages — header, "Where to start" top pick, the full list of libraries in the category, and category-tagged blog posts. Intentionally doesn't port jhislop-design's verdict block, criteria/"how we think about it" section, in-this-guide TOC rail, or compare-across-the-stack rail.Dropdown.What is NOT in this PR (from #931)
Test plan
pnpm test(tsc + lint) — 0 errors, only pre-existing warnings in shop/users componentspnpm test:smoke— 10/10 (home, blog, ethos, query/router/table docs, OG images)/,/stack/framework,/stack/state,/stack/ui,/stack/performance,/stack/toolingall return 200; unknown slug returns 404Summary by CodeRabbit
New Features
Refactor
Chores
Bug Fixes / Performance