From afc277bc1c6a2eca44a878918d60a6650f6b872e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 16:05:53 +0000 Subject: [PATCH 1/2] fix: preserve search params in canonical URL on npm stats pages The npm stats pages encode their entire UI state (selected packages, range, transform, etc.) in search params, but __root.tsx built the canonical link, og:url, and twitter:url from pathname only. iOS Share read those tags and shared a bare /stats/npm URL, dropping the comparison the user had configured. Routes can now opt in via staticData.includeSearchInCanonical; when set, the current searchStr is appended to the canonical URL. Applied to /stats/npm/, /stats/npm/$packages, and the per-library /$libraryId/$version/docs/npm-stats route. --- src/router.tsx | 1 + .../$libraryId/$version.docs.npm-stats.tsx | 3 +++ src/routes/__root.tsx | 20 +++++++++++++++++-- src/routes/stats/npm/$packages.tsx | 3 +++ src/routes/stats/npm/index.tsx | 1 + src/utils/seo.ts | 6 ++++-- 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/router.tsx b/src/router.tsx index 43d1eecde..38caea13e 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -78,6 +78,7 @@ declare module '@tanstack/react-router' { baseParent?: boolean Title?: () => any showNavbar?: boolean + includeSearchInCanonical?: boolean } } diff --git a/src/routes/$libraryId/$version.docs.npm-stats.tsx b/src/routes/$libraryId/$version.docs.npm-stats.tsx index e8a78268f..2f86df413 100644 --- a/src/routes/$libraryId/$version.docs.npm-stats.tsx +++ b/src/routes/$libraryId/$version.docs.npm-stats.tsx @@ -83,6 +83,9 @@ export const Route = createFileRoute('/$libraryId/$version/docs/npm-stats')({ height: v.fallback(v.optional(v.number(), 400), 400), }), component: RouteComponent, + staticData: { + includeSearchInCanonical: true, + }, }) type NpmStatsSearch = { diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 33cf76f80..864aaf702 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -184,8 +184,21 @@ function ShellComponent({ children }: { children: React.ReactNode }) { select: (s) => s.location?.pathname || '/', }) + const canonicalSearchStr = useRouterState({ + select: (s) => s.location?.searchStr || '', + }) + + const includeSearchInCanonical = useMatches({ + select: (s) => + s.some((d) => d.staticData?.includeSearchInCanonical === true), + }) + const preferredCanonicalPath = getCanonicalPath(canonicalPath) - const pageUrl = canonicalUrl(preferredCanonicalPath ?? canonicalPath) + const canonicalSearch = includeSearchInCanonical ? canonicalSearchStr : '' + const pageUrl = canonicalUrl( + preferredCanonicalPath ?? canonicalPath, + canonicalSearch, + ) const showDevtools = import.meta.env.DEV && canShowDevtools @@ -199,7 +212,10 @@ function ShellComponent({ children }: { children: React.ReactNode }) { {preferredCanonicalPath ? ( - + ) : null} diff --git a/src/routes/stats/npm/$packages.tsx b/src/routes/stats/npm/$packages.tsx index 145fa0bad..e34ab70b1 100644 --- a/src/routes/stats/npm/$packages.tsx +++ b/src/routes/stats/npm/$packages.tsx @@ -76,6 +76,9 @@ export const Route = createFileRoute('/stats/npm/$packages')({ } }, component: RouteComponent, + staticData: { + includeSearchInCanonical: true, + }, }) function RouteComponent() { diff --git a/src/routes/stats/npm/index.tsx b/src/routes/stats/npm/index.tsx index bb69f9259..f1e098199 100644 --- a/src/routes/stats/npm/index.tsx +++ b/src/routes/stats/npm/index.tsx @@ -192,6 +192,7 @@ export const Route = createFileRoute('/stats/npm/')({ }, component: RouteComponent, staticData: { + includeSearchInCanonical: true, Title: () => { return ( Date: Sun, 26 Apr 2026 23:27:10 +0000 Subject: [PATCH 2/2] fix: preserve search params in canonical URL on more stateful routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the includeSearchInCanonical opt-in to additional routes whose UI state lives in search params, so iOS Share and other clients that read the canonical/og:url/twitter:url tags share the user's configured view rather than a default one. - /builder (entire builder config: framework, features, tab, file…) - /maintainers (libraries, viewMode, groupBy, sortBy) - /partners (libraries, status) - /intent/registry (q, tab, framework, sort, view) - /shop, /shop/search, /shop/collections/$handle --- src/routes/builder.index.tsx | 1 + src/routes/intent/registry/index.tsx | 3 +++ src/routes/maintainers.tsx | 3 +++ src/routes/partners.index.tsx | 3 +++ src/routes/shop.collections.$handle.tsx | 3 +++ src/routes/shop.index.tsx | 3 +++ src/routes/shop.search.tsx | 3 +++ 7 files changed, 19 insertions(+) diff --git a/src/routes/builder.index.tsx b/src/routes/builder.index.tsx index cca955c7e..19b941e6a 100644 --- a/src/routes/builder.index.tsx +++ b/src/routes/builder.index.tsx @@ -31,6 +31,7 @@ export const Route = createFileRoute('/builder/')({ validateSearch: builderSearchSchema, component: RouteComponent, staticData: { + includeSearchInCanonical: true, Title: () => ( ({ meta: seo({ title: 'Maintainers | TanStack', diff --git a/src/routes/partners.index.tsx b/src/routes/partners.index.tsx index bff2bff4c..511a983ab 100644 --- a/src/routes/partners.index.tsx +++ b/src/routes/partners.index.tsx @@ -78,6 +78,9 @@ function getPartnerFilterAnalytics(search: PartnersSearch) { export const Route = createFileRoute('/partners/')({ component: PartnersIndexPage, validateSearch: searchSchema, + staticData: { + includeSearchInCanonical: true, + }, head: () => ({ meta: seo({ title: 'Partners', diff --git a/src/routes/shop.collections.$handle.tsx b/src/routes/shop.collections.$handle.tsx index 0d58b25fe..b70fb1947 100644 --- a/src/routes/shop.collections.$handle.tsx +++ b/src/routes/shop.collections.$handle.tsx @@ -63,6 +63,9 @@ export const Route = createFileRoute('/shop/collections/$handle')({ } }, component: CollectionPage, + staticData: { + includeSearchInCanonical: true, + }, }) function CollectionPage() { diff --git a/src/routes/shop.index.tsx b/src/routes/shop.index.tsx index 165888f69..1ddc6d894 100644 --- a/src/routes/shop.index.tsx +++ b/src/routes/shop.index.tsx @@ -47,6 +47,9 @@ export const Route = createFileRoute('/shop/')({ return { page, sortId: sortOptionId(sortOption) } }, component: ShopIndex, + staticData: { + includeSearchInCanonical: true, + }, }) function ShopIndex() { diff --git a/src/routes/shop.search.tsx b/src/routes/shop.search.tsx index 831b23f6b..4aa44a9e6 100644 --- a/src/routes/shop.search.tsx +++ b/src/routes/shop.search.tsx @@ -30,6 +30,9 @@ export const Route = createFileRoute('/shop/search')({ return { query: q, totalCount: page.totalCount, page } }, component: SearchPage, + staticData: { + includeSearchInCanonical: true, + }, }) function SearchPage() {