Skip to content

Commit 5194bcd

Browse files
fix: preserve search params in canonical URL on stateful routes (#864)
Adds an opt-in `staticData.includeSearchInCanonical` flag that threads the current search string into the canonical link, og:url, and twitter:url tags, so iOS Share preserves the user's configured view on routes whose state lives in URL search params. Applied to: - /stats/npm/, /stats/npm/$packages, /$libraryId/$version/docs/npm-stats - /builder - /maintainers - /partners - /intent/registry - /shop, /shop/search, /shop/collections/$handle
1 parent 1e63ccf commit 5194bcd

13 files changed

Lines changed: 49 additions & 4 deletions

File tree

src/router.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ declare module '@tanstack/react-router' {
7878
baseParent?: boolean
7979
Title?: () => any
8080
showNavbar?: boolean
81+
includeSearchInCanonical?: boolean
8182
}
8283
}
8384

src/routes/$libraryId/$version.docs.npm-stats.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ export const Route = createFileRoute('/$libraryId/$version/docs/npm-stats')({
8383
height: v.fallback(v.optional(v.number(), 400), 400),
8484
}),
8585
component: RouteComponent,
86+
staticData: {
87+
includeSearchInCanonical: true,
88+
},
8689
})
8790

8891
type NpmStatsSearch = {

src/routes/__root.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,21 @@ function ShellComponent({ children }: { children: React.ReactNode }) {
184184
select: (s) => s.location?.pathname || '/',
185185
})
186186

187+
const canonicalSearchStr = useRouterState({
188+
select: (s) => s.location?.searchStr || '',
189+
})
190+
191+
const includeSearchInCanonical = useMatches({
192+
select: (s) =>
193+
s.some((d) => d.staticData?.includeSearchInCanonical === true),
194+
})
195+
187196
const preferredCanonicalPath = getCanonicalPath(canonicalPath)
188-
const pageUrl = canonicalUrl(preferredCanonicalPath ?? canonicalPath)
197+
const canonicalSearch = includeSearchInCanonical ? canonicalSearchStr : ''
198+
const pageUrl = canonicalUrl(
199+
preferredCanonicalPath ?? canonicalPath,
200+
canonicalSearch,
201+
)
189202

190203
const showDevtools = import.meta.env.DEV && canShowDevtools
191204

@@ -199,7 +212,10 @@ function ShellComponent({ children }: { children: React.ReactNode }) {
199212
<html lang="en" className={htmlClass} suppressHydrationWarning>
200213
<head>
201214
{preferredCanonicalPath ? (
202-
<link rel="canonical" href={canonicalUrl(preferredCanonicalPath)} />
215+
<link
216+
rel="canonical"
217+
href={canonicalUrl(preferredCanonicalPath, canonicalSearch)}
218+
/>
203219
) : null}
204220
<meta property="og:url" content={pageUrl} />
205221
<meta name="twitter:url" content={pageUrl} />

src/routes/builder.index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const Route = createFileRoute('/builder/')({
3131
validateSearch: builderSearchSchema,
3232
component: RouteComponent,
3333
staticData: {
34+
includeSearchInCanonical: true,
3435
Title: () => (
3536
<Link
3637
to="/builder"

src/routes/intent/registry/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export const Route = createFileRoute('/intent/registry/')({
5858
}),
5959
}),
6060
component: IntentRegistryPage,
61+
staticData: {
62+
includeSearchInCanonical: true,
63+
},
6164
})
6265

6366
function IntentRegistryPage() {

src/routes/maintainers.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const searchSchema = v.object({
3535
export const Route = createFileRoute('/maintainers')({
3636
component: RouteComponent,
3737
validateSearch: searchSchema,
38+
staticData: {
39+
includeSearchInCanonical: true,
40+
},
3841
head: () => ({
3942
meta: seo({
4043
title: 'Maintainers | TanStack',

src/routes/partners.index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ function getPartnerFilterAnalytics(search: PartnersSearch) {
7878
export const Route = createFileRoute('/partners/')({
7979
component: PartnersIndexPage,
8080
validateSearch: searchSchema,
81+
staticData: {
82+
includeSearchInCanonical: true,
83+
},
8184
head: () => ({
8285
meta: seo({
8386
title: 'Partners',

src/routes/shop.collections.$handle.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export const Route = createFileRoute('/shop/collections/$handle')({
6363
}
6464
},
6565
component: CollectionPage,
66+
staticData: {
67+
includeSearchInCanonical: true,
68+
},
6669
})
6770

6871
function CollectionPage() {

src/routes/shop.index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export const Route = createFileRoute('/shop/')({
4747
return { page, sortId: sortOptionId(sortOption) }
4848
},
4949
component: ShopIndex,
50+
staticData: {
51+
includeSearchInCanonical: true,
52+
},
5053
})
5154

5255
function ShopIndex() {

src/routes/shop.search.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export const Route = createFileRoute('/shop/search')({
3030
return { query: q, totalCount: page.totalCount, page }
3131
},
3232
component: SearchPage,
33+
staticData: {
34+
includeSearchInCanonical: true,
35+
},
3336
})
3437

3538
function SearchPage() {

0 commit comments

Comments
 (0)