Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/libraries/libraries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const query: LibrarySlim = {
scarfId: '53afb586-3934-4624-a37a-e680c1528e17',
ogImage: 'https://github.com/tanstack/query/raw/main/media/repo-header.png',
defaultDocs: 'framework/react/overview',
sitemap: {
includeLandingPage: true,
includeTopLevelDocsPages: true,
},
installPath: 'framework/$framework/installation',
legacyPackages: ['react-query'],
handleRedirects: (href) => {
Expand Down Expand Up @@ -217,6 +221,10 @@ export const router: LibrarySlim = {
scarfId: '3d14fff2-f326-4929-b5e1-6ecf953d24f4',
ogImage: 'https://github.com/tanstack/router/raw/main/media/header.png',
docsRoot: 'docs/router',
sitemap: {
includeLandingPage: true,
includeTopLevelDocsPages: true,
},
legacyPackages: ['react-location'],
hideCodesandboxUrl: true,
handleRedirects: (href) => {
Expand Down Expand Up @@ -282,6 +290,10 @@ export const start: LibrarySlim = {
scarfId: 'b6e2134f-e805-401d-95c3-2a7765d49a3d',
docsRoot: 'docs/start',
defaultDocs: 'framework/react/overview',
sitemap: {
includeLandingPage: true,
includeTopLevelDocsPages: true,
},
installPath: 'framework/$framework/build-from-scratch',
embedEditor: 'codesandbox',
showNetlifyUrl: true,
Expand Down Expand Up @@ -323,6 +335,10 @@ export const table: LibrarySlim = {
scarfId: 'dc8b39e1-3fe9-4f3a-8e56-d4e2cf420a9e',
ogImage: 'https://github.com/tanstack/table/raw/main/media/repo-header.png',
defaultDocs: 'introduction',
sitemap: {
includeLandingPage: true,
includeTopLevelDocsPages: true,
},
corePackageName: '@tanstack/table-core',
legacyPackages: ['react-table'],
handleRedirects: (href) => {
Expand Down Expand Up @@ -392,6 +408,10 @@ export const form: LibrarySlim = {
availableVersions: ['v1'],
scarfId: '72ec4452-5d77-427c-b44a-57515d2d83aa',
ogImage: 'https://github.com/tanstack/form/raw/main/media/repo-header.png',
sitemap: {
includeLandingPage: true,
includeTopLevelDocsPages: true,
},
}

export const virtual: LibrarySlim = {
Expand Down Expand Up @@ -556,6 +576,9 @@ export const db: LibrarySlim = {
scarfId: '302d0fef-cb3f-43c6-b45c-f055b9745edb',
ogImage: 'https://github.com/tanstack/db/raw/main/media/repo-header.png',
defaultDocs: 'overview',
sitemap: {
includeLandingPage: true,
},
}

export const ai: LibrarySlim = {
Expand Down
4 changes: 4 additions & 0 deletions src/libraries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export type LibrarySlim = {
* Defaults to true.
*/
visible?: boolean
sitemap?: {
includeLandingPage?: boolean
includeTopLevelDocsPages?: boolean
}
}

// Extended library type - adds React node content for landing pages
Expand Down
42 changes: 42 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { Route as TermsRouteImport } from './routes/terms'
import { Route as TenetsRouteImport } from './routes/tenets'
import { Route as SupportRouteImport } from './routes/support'
import { Route as SponsorsEmbedRouteImport } from './routes/sponsors-embed'
import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml'
import { Route as RssDotxmlRouteImport } from './routes/rss[.]xml'
import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt'
import { Route as PrivacyRouteImport } from './routes/privacy'
import { Route as PartnersEmbedRouteImport } from './routes/partners-embed'
import { Route as PartnersRouteImport } from './routes/partners'
Expand Down Expand Up @@ -144,11 +146,21 @@ const SponsorsEmbedRoute = SponsorsEmbedRouteImport.update({
path: '/sponsors-embed',
getParentRoute: () => rootRouteImport,
} as any)
const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({
id: '/sitemap.xml',
path: '/sitemap.xml',
getParentRoute: () => rootRouteImport,
} as any)
const RssDotxmlRoute = RssDotxmlRouteImport.update({
id: '/rss.xml',
path: '/rss.xml',
getParentRoute: () => rootRouteImport,
} as any)
const RobotsDottxtRoute = RobotsDottxtRouteImport.update({
id: '/robots.txt',
path: '/robots.txt',
getParentRoute: () => rootRouteImport,
} as any)
const PrivacyRoute = PrivacyRouteImport.update({
id: '/privacy',
path: '/privacy',
Expand Down Expand Up @@ -710,7 +722,9 @@ export interface FileRoutesByFullPath {
'/partners': typeof PartnersRoute
'/partners-embed': typeof PartnersEmbedRoute
'/privacy': typeof PrivacyRoute
'/robots.txt': typeof RobotsDottxtRoute
'/rss.xml': typeof RssDotxmlRoute
'/sitemap.xml': typeof SitemapDotxmlRoute
'/sponsors-embed': typeof SponsorsEmbedRoute
'/support': typeof SupportRoute
'/tenets': typeof TenetsRoute
Expand Down Expand Up @@ -816,7 +830,9 @@ export interface FileRoutesByTo {
'/partners': typeof PartnersRoute
'/partners-embed': typeof PartnersEmbedRoute
'/privacy': typeof PrivacyRoute
'/robots.txt': typeof RobotsDottxtRoute
'/rss.xml': typeof RssDotxmlRoute
'/sitemap.xml': typeof SitemapDotxmlRoute
'/sponsors-embed': typeof SponsorsEmbedRoute
'/support': typeof SupportRoute
'/tenets': typeof TenetsRoute
Expand Down Expand Up @@ -925,7 +941,9 @@ export interface FileRoutesById {
'/partners': typeof PartnersRoute
'/partners-embed': typeof PartnersEmbedRoute
'/privacy': typeof PrivacyRoute
'/robots.txt': typeof RobotsDottxtRoute
'/rss.xml': typeof RssDotxmlRoute
'/sitemap.xml': typeof SitemapDotxmlRoute
'/sponsors-embed': typeof SponsorsEmbedRoute
'/support': typeof SupportRoute
'/tenets': typeof TenetsRoute
Expand Down Expand Up @@ -1038,7 +1056,9 @@ export interface FileRouteTypes {
| '/partners'
| '/partners-embed'
| '/privacy'
| '/robots.txt'
| '/rss.xml'
| '/sitemap.xml'
| '/sponsors-embed'
| '/support'
| '/tenets'
Expand Down Expand Up @@ -1144,7 +1164,9 @@ export interface FileRouteTypes {
| '/partners'
| '/partners-embed'
| '/privacy'
| '/robots.txt'
| '/rss.xml'
| '/sitemap.xml'
| '/sponsors-embed'
| '/support'
| '/tenets'
Expand Down Expand Up @@ -1252,7 +1274,9 @@ export interface FileRouteTypes {
| '/partners'
| '/partners-embed'
| '/privacy'
| '/robots.txt'
| '/rss.xml'
| '/sitemap.xml'
| '/sponsors-embed'
| '/support'
| '/tenets'
Expand Down Expand Up @@ -1364,7 +1388,9 @@ export interface RootRouteChildren {
PartnersRoute: typeof PartnersRoute
PartnersEmbedRoute: typeof PartnersEmbedRoute
PrivacyRoute: typeof PrivacyRoute
RobotsDottxtRoute: typeof RobotsDottxtRoute
RssDotxmlRoute: typeof RssDotxmlRoute
SitemapDotxmlRoute: typeof SitemapDotxmlRoute
SponsorsEmbedRoute: typeof SponsorsEmbedRoute
SupportRoute: typeof SupportRoute
TenetsRoute: typeof TenetsRoute
Expand Down Expand Up @@ -1448,13 +1474,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SponsorsEmbedRouteImport
parentRoute: typeof rootRouteImport
}
'/sitemap.xml': {
id: '/sitemap.xml'
path: '/sitemap.xml'
fullPath: '/sitemap.xml'
preLoaderRoute: typeof SitemapDotxmlRouteImport
parentRoute: typeof rootRouteImport
}
'/rss.xml': {
id: '/rss.xml'
path: '/rss.xml'
fullPath: '/rss.xml'
preLoaderRoute: typeof RssDotxmlRouteImport
parentRoute: typeof rootRouteImport
}
'/robots.txt': {
id: '/robots.txt'
path: '/robots.txt'
fullPath: '/robots.txt'
preLoaderRoute: typeof RobotsDottxtRouteImport
parentRoute: typeof rootRouteImport
}
'/privacy': {
id: '/privacy'
path: '/privacy'
Expand Down Expand Up @@ -2379,7 +2419,9 @@ const rootRouteChildren: RootRouteChildren = {
PartnersRoute: PartnersRoute,
PartnersEmbedRoute: PartnersEmbedRoute,
PrivacyRoute: PrivacyRoute,
RobotsDottxtRoute: RobotsDottxtRoute,
RssDotxmlRoute: RssDotxmlRoute,
SitemapDotxmlRoute: SitemapDotxmlRoute,
SponsorsEmbedRoute: SponsorsEmbedRoute,
SupportRoute: SupportRoute,
TenetsRoute: TenetsRoute,
Expand Down
19 changes: 18 additions & 1 deletion src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
} from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
import appCss from '~/styles/app.css?url'
import { seo } from '~/utils/seo'
import {
canonicalUrl,
getCanonicalPath,
seo,
shouldIndexPath,
} from '~/utils/seo'
import ogImage from '~/images/og.png'
const LazyRouterDevtools = React.lazy(() =>
import('@tanstack/react-router-devtools').then((m) => ({
Expand Down Expand Up @@ -155,6 +160,12 @@ function ShellComponent({ children }: { children: React.ReactNode }) {
select: (s) => s.resolvedLocation?.pathname.startsWith('/router'),
})

const canonicalPath = useRouterState({
select: (s) => s.resolvedLocation?.pathname || '/',
})

const preferredCanonicalPath = getCanonicalPath(canonicalPath)

const showDevtools = canShowLoading && isRouterPage

const hideNavbar = useMatches({
Expand All @@ -166,6 +177,12 @@ function ShellComponent({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={htmlClass} suppressHydrationWarning>
<head>
{preferredCanonicalPath ? (
<link rel="canonical" href={canonicalUrl(preferredCanonicalPath)} />
) : null}
{!shouldIndexPath(canonicalPath) ? (
<meta name="robots" content="noindex, nofollow" />
) : null}
Comment on lines +180 to +185
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

Potential duplicate robots meta tags for filtered showcase pages.

The root layout injects <meta name="robots" content="noindex, nofollow"> when shouldIndexPath returns false (line 183-185). However, the showcase route also injects the same meta tag via seo({ noindex: loaderData?.hasNonCanonicalSearch }) in its head config. When both conditions are true, duplicate robots tags will render since TanStack Router's <HeadContent> doesn't deduplicate meta tags by name.

While search engines generally handle duplicates gracefully, this could cause HTML validation warnings and indicates overlapping responsibility.

Consider either:

  1. Relying solely on the root-level injection for path-based noindex decisions, or
  2. Removing the root-level injection and letting individual routes handle their own noindex logic
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/__root.tsx` around lines 180 - 185, The root layout is injecting a
robots noindex meta via the shouldIndexPath check which can duplicate the same
tag emitted by route-level seo() (e.g., showcase route's seo({ noindex:
loaderData?.hasNonCanonicalSearch })). Remove the root-level robots injection
(the conditional that renders <meta name="robots" content="noindex, nofollow" />
based on shouldIndexPath) and let individual routes (via their seo()
implementations such as the showcase route using
loaderData?.hasNonCanonicalSearch) control noindex behavior to avoid duplicate
meta tags.

<HeadContent />
{hasBaseParent ? <base target="_parent" /> : null}
</head>
Expand Down
25 changes: 25 additions & 0 deletions src/routes/robots[.]txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createFileRoute } from '@tanstack/react-router'
import { setResponseHeader } from '@tanstack/react-start/server'
import { generateRobotsTxt, getSiteOrigin } from '~/utils/sitemap'

export const Route = createFileRoute('/robots.txt')({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => {
const content = generateRobotsTxt(getSiteOrigin(request))

setResponseHeader('Content-Type', 'text/plain; charset=utf-8')
setResponseHeader(
'Cache-Control',
'public, max-age=300, must-revalidate',
)
setResponseHeader(
'CDN-Cache-Control',
'max-age=3600, stale-while-revalidate=3600',
)

return new Response(content)
},
},
},
})
Comment on lines +5 to +25
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate static files =="
fd -a '^(robots\.txt|sitemap\.xml)$'

echo
echo "== References that may indicate static serving expectations =="
rg -n -C2 'robots\.txt|sitemap\.xml|publicDir|static' -g '!src/routeTree.gen.ts'

Repository: TanStack/tanstack.com

Length of output: 31805


🏁 Script executed:

find . -maxdepth 2 -type f \( -name "vite.config.*" -o -name "start.config.*" -o -name "tsconfig.json" -o -name "package.json" \) -exec head -50 {} +

Repository: TanStack/tanstack.com

Length of output: 4407


🏁 Script executed:

rg -n "publicDir|assetDir|public.*robots" --type ts --type json -A 2 -B 2 | head -100

Repository: TanStack/tanstack.com

Length of output: 47


🏁 Script executed:

git ls-files | grep -E "(config|vite|start)" | head -20

Repository: TanStack/tanstack.com

Length of output: 1323


🏁 Script executed:

find . -maxdepth 2 -name "netlify.toml" -o -name ".netlify.toml" | xargs cat 2>/dev/null

Repository: TanStack/tanstack.com

Length of output: 1118


🏁 Script executed:

rg -n "publicDir|public.*assets|robots" vite.config.ts

Repository: TanStack/tanstack.com

Length of output: 47


🏁 Script executed:

cat -n src/routes/robots[.]txt.ts

Repository: TanStack/tanstack.com

Length of output: 1006


🏁 Script executed:

cat -n public/robots.txt

Repository: TanStack/tanstack.com

Length of output: 159


🏁 Script executed:

cat vite.config.ts | head -150

Repository: TanStack/tanstack.com

Length of output: 3588


🏁 Script executed:

rg -n "publicDir|public" vite.config.ts -A 3 -B 3

Repository: TanStack/tanstack.com

Length of output: 47


🏁 Script executed:

rg -n "generateRobotsTxt\|getSiteOrigin" src/utils/sitemap.ts -A 5 -B 2

Repository: TanStack/tanstack.com

Length of output: 47


🏁 Script executed:

cat -n src/utils/sitemap.ts | grep -A 20 "generateRobotsTxt"

Repository: TanStack/tanstack.com

Length of output: 345


🏁 Script executed:

cat -n src/utils/sitemap.ts

Repository: TanStack/tanstack.com

Length of output: 5713


Remove the static public/robots.txt file. It shadows the dynamic route handler, preventing the request-aware origin logic and cache headers from being applied.

The static file hardcodes https://tanstack.com/sitemap.xml, whereas the dynamic handler derives the origin from env.SITE_URL or the request origin, adapting to different deployment environments. The dynamic handler also sets proper cache headers (Cache-Control, CDN-Cache-Control) that the static file lacks. Since Vite copies public/robots.txt to the build output by default and Netlify serves it before reaching the server handler, the dynamic route becomes dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/robots`[.]txt.ts around lines 5 - 25, Remove the static
public/robots.txt file so the dynamic route handler (Route created by
createFileRoute('/robots.txt')) can run; ensure the code using
generateRobotsTxt(getSiteOrigin(request)) and the setResponseHeader calls
(Content-Type, Cache-Control, CDN-Cache-Control) remain in place so the origin
is derived at request-time and proper caching headers are applied.

18 changes: 17 additions & 1 deletion src/routes/showcase/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ const searchSchema = v.object({

export const PAGE_SIZE_OPTIONS = [24, 48, 96, 192] as const

function hasNonCanonicalSearch(search: v.InferOutput<typeof searchSchema>) {
return Boolean(
search.page > 1 ||
search.pageSize !== PAGE_SIZE_OPTIONS[0] ||
search.libraryIds?.length ||
search.useCases?.length ||
search.hasSourceCode ||
search.q,
)
}

export const Route = createFileRoute('/showcase/')({
validateSearch: searchSchema,
loaderDeps: ({ search }) => ({
Expand All @@ -41,13 +52,18 @@ export const Route = createFileRoute('/showcase/')({
},
}),
)

return {
hasNonCanonicalSearch: hasNonCanonicalSearch(deps),
}
},
component: ShowcaseGallery,
head: () => ({
head: ({ loaderData }) => ({
meta: seo({
title: 'Showcase | TanStack',
description:
'Discover projects built with TanStack libraries. See how developers are using TanStack Query, Router, Table, Form, and more in production.',
noindex: loaderData?.hasNonCanonicalSearch,
}),
}),
})
25 changes: 25 additions & 0 deletions src/routes/sitemap[.]xml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createFileRoute } from '@tanstack/react-router'
import { setResponseHeader } from '@tanstack/react-start/server'
import { generateSitemapXml, getSiteOrigin } from '~/utils/sitemap'

export const Route = createFileRoute('/sitemap.xml')({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => {
const content = await generateSitemapXml(getSiteOrigin(request))

setResponseHeader('Content-Type', 'application/xml; charset=utf-8')
setResponseHeader(
'Cache-Control',
'public, max-age=300, must-revalidate',
)
setResponseHeader(
'CDN-Cache-Control',
'max-age=3600, stale-while-revalidate=3600',
)

return new Response(content)
},
},
},
})
Loading
Loading