Skip to content

Commit 0b98cbf

Browse files
committed
feat(sitemap): add routes for robots.txt and sitemap.xml
1 parent 5922104 commit 0b98cbf

File tree

4 files changed

+212
-0
lines changed

4 files changed

+212
-0
lines changed

src/routeTree.gen.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import { Route as TermsRouteImport } from './routes/terms'
1414
import { Route as TenetsRouteImport } from './routes/tenets'
1515
import { Route as SupportRouteImport } from './routes/support'
1616
import { Route as SponsorsEmbedRouteImport } from './routes/sponsors-embed'
17+
import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml'
1718
import { Route as RssDotxmlRouteImport } from './routes/rss[.]xml'
19+
import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt'
1820
import { Route as PrivacyRouteImport } from './routes/privacy'
1921
import { Route as PartnersEmbedRouteImport } from './routes/partners-embed'
2022
import { Route as PartnersRouteImport } from './routes/partners'
@@ -144,11 +146,21 @@ const SponsorsEmbedRoute = SponsorsEmbedRouteImport.update({
144146
path: '/sponsors-embed',
145147
getParentRoute: () => rootRouteImport,
146148
} as any)
149+
const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({
150+
id: '/sitemap.xml',
151+
path: '/sitemap.xml',
152+
getParentRoute: () => rootRouteImport,
153+
} as any)
147154
const RssDotxmlRoute = RssDotxmlRouteImport.update({
148155
id: '/rss.xml',
149156
path: '/rss.xml',
150157
getParentRoute: () => rootRouteImport,
151158
} as any)
159+
const RobotsDottxtRoute = RobotsDottxtRouteImport.update({
160+
id: '/robots.txt',
161+
path: '/robots.txt',
162+
getParentRoute: () => rootRouteImport,
163+
} as any)
152164
const PrivacyRoute = PrivacyRouteImport.update({
153165
id: '/privacy',
154166
path: '/privacy',
@@ -710,7 +722,9 @@ export interface FileRoutesByFullPath {
710722
'/partners': typeof PartnersRoute
711723
'/partners-embed': typeof PartnersEmbedRoute
712724
'/privacy': typeof PrivacyRoute
725+
'/robots.txt': typeof RobotsDottxtRoute
713726
'/rss.xml': typeof RssDotxmlRoute
727+
'/sitemap.xml': typeof SitemapDotxmlRoute
714728
'/sponsors-embed': typeof SponsorsEmbedRoute
715729
'/support': typeof SupportRoute
716730
'/tenets': typeof TenetsRoute
@@ -816,7 +830,9 @@ export interface FileRoutesByTo {
816830
'/partners': typeof PartnersRoute
817831
'/partners-embed': typeof PartnersEmbedRoute
818832
'/privacy': typeof PrivacyRoute
833+
'/robots.txt': typeof RobotsDottxtRoute
819834
'/rss.xml': typeof RssDotxmlRoute
835+
'/sitemap.xml': typeof SitemapDotxmlRoute
820836
'/sponsors-embed': typeof SponsorsEmbedRoute
821837
'/support': typeof SupportRoute
822838
'/tenets': typeof TenetsRoute
@@ -925,7 +941,9 @@ export interface FileRoutesById {
925941
'/partners': typeof PartnersRoute
926942
'/partners-embed': typeof PartnersEmbedRoute
927943
'/privacy': typeof PrivacyRoute
944+
'/robots.txt': typeof RobotsDottxtRoute
928945
'/rss.xml': typeof RssDotxmlRoute
946+
'/sitemap.xml': typeof SitemapDotxmlRoute
929947
'/sponsors-embed': typeof SponsorsEmbedRoute
930948
'/support': typeof SupportRoute
931949
'/tenets': typeof TenetsRoute
@@ -1038,7 +1056,9 @@ export interface FileRouteTypes {
10381056
| '/partners'
10391057
| '/partners-embed'
10401058
| '/privacy'
1059+
| '/robots.txt'
10411060
| '/rss.xml'
1061+
| '/sitemap.xml'
10421062
| '/sponsors-embed'
10431063
| '/support'
10441064
| '/tenets'
@@ -1144,7 +1164,9 @@ export interface FileRouteTypes {
11441164
| '/partners'
11451165
| '/partners-embed'
11461166
| '/privacy'
1167+
| '/robots.txt'
11471168
| '/rss.xml'
1169+
| '/sitemap.xml'
11481170
| '/sponsors-embed'
11491171
| '/support'
11501172
| '/tenets'
@@ -1252,7 +1274,9 @@ export interface FileRouteTypes {
12521274
| '/partners'
12531275
| '/partners-embed'
12541276
| '/privacy'
1277+
| '/robots.txt'
12551278
| '/rss.xml'
1279+
| '/sitemap.xml'
12561280
| '/sponsors-embed'
12571281
| '/support'
12581282
| '/tenets'
@@ -1364,7 +1388,9 @@ export interface RootRouteChildren {
13641388
PartnersRoute: typeof PartnersRoute
13651389
PartnersEmbedRoute: typeof PartnersEmbedRoute
13661390
PrivacyRoute: typeof PrivacyRoute
1391+
RobotsDottxtRoute: typeof RobotsDottxtRoute
13671392
RssDotxmlRoute: typeof RssDotxmlRoute
1393+
SitemapDotxmlRoute: typeof SitemapDotxmlRoute
13681394
SponsorsEmbedRoute: typeof SponsorsEmbedRoute
13691395
SupportRoute: typeof SupportRoute
13701396
TenetsRoute: typeof TenetsRoute
@@ -1448,13 +1474,27 @@ declare module '@tanstack/react-router' {
14481474
preLoaderRoute: typeof SponsorsEmbedRouteImport
14491475
parentRoute: typeof rootRouteImport
14501476
}
1477+
'/sitemap.xml': {
1478+
id: '/sitemap.xml'
1479+
path: '/sitemap.xml'
1480+
fullPath: '/sitemap.xml'
1481+
preLoaderRoute: typeof SitemapDotxmlRouteImport
1482+
parentRoute: typeof rootRouteImport
1483+
}
14511484
'/rss.xml': {
14521485
id: '/rss.xml'
14531486
path: '/rss.xml'
14541487
fullPath: '/rss.xml'
14551488
preLoaderRoute: typeof RssDotxmlRouteImport
14561489
parentRoute: typeof rootRouteImport
14571490
}
1491+
'/robots.txt': {
1492+
id: '/robots.txt'
1493+
path: '/robots.txt'
1494+
fullPath: '/robots.txt'
1495+
preLoaderRoute: typeof RobotsDottxtRouteImport
1496+
parentRoute: typeof rootRouteImport
1497+
}
14581498
'/privacy': {
14591499
id: '/privacy'
14601500
path: '/privacy'
@@ -2379,7 +2419,9 @@ const rootRouteChildren: RootRouteChildren = {
23792419
PartnersRoute: PartnersRoute,
23802420
PartnersEmbedRoute: PartnersEmbedRoute,
23812421
PrivacyRoute: PrivacyRoute,
2422+
RobotsDottxtRoute: RobotsDottxtRoute,
23822423
RssDotxmlRoute: RssDotxmlRoute,
2424+
SitemapDotxmlRoute: SitemapDotxmlRoute,
23832425
SponsorsEmbedRoute: SponsorsEmbedRoute,
23842426
SupportRoute: SupportRoute,
23852427
TenetsRoute: TenetsRoute,

src/routes/robots[.]txt.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { setResponseHeader } from '@tanstack/react-start/server'
3+
import { generateRobotsTxt, getSiteOrigin } from '~/utils/sitemap'
4+
5+
export const Route = createFileRoute('/robots.txt')({
6+
server: {
7+
handlers: {
8+
GET: async ({ request }: { request: Request }) => {
9+
const content = generateRobotsTxt(getSiteOrigin(request))
10+
11+
setResponseHeader('Content-Type', 'text/plain; charset=utf-8')
12+
setResponseHeader(
13+
'Cache-Control',
14+
'public, max-age=300, must-revalidate',
15+
)
16+
setResponseHeader(
17+
'CDN-Cache-Control',
18+
'max-age=3600, stale-while-revalidate=3600',
19+
)
20+
21+
return new Response(content)
22+
},
23+
},
24+
},
25+
})

src/routes/sitemap[.]xml.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { setResponseHeader } from '@tanstack/react-start/server'
3+
import { generateSitemapXml, getSiteOrigin } from '~/utils/sitemap'
4+
5+
export const Route = createFileRoute('/sitemap.xml')({
6+
server: {
7+
handlers: {
8+
GET: async ({ request }: { request: Request }) => {
9+
const content = generateSitemapXml(getSiteOrigin(request))
10+
11+
setResponseHeader('Content-Type', 'application/xml; charset=utf-8')
12+
setResponseHeader(
13+
'Cache-Control',
14+
'public, max-age=300, must-revalidate',
15+
)
16+
setResponseHeader(
17+
'CDN-Cache-Control',
18+
'max-age=3600, stale-while-revalidate=3600',
19+
)
20+
21+
return new Response(content)
22+
},
23+
},
24+
},
25+
})

src/utils/sitemap.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { libraries } from '~/libraries'
2+
import { getPublishedPosts } from '~/utils/blog'
3+
import { env } from '~/utils/env'
4+
5+
export type SitemapEntry = {
6+
path: string
7+
lastModified?: string
8+
}
9+
10+
const HIGH_VALUE_STATIC_SITEMAP_PATHS = [
11+
'/',
12+
'/blog',
13+
'/libraries',
14+
'/learn',
15+
'/showcase',
16+
'/support',
17+
'/partners',
18+
'/workshops',
19+
'/maintainers',
20+
'/builder',
21+
'/explore',
22+
'/ethos',
23+
'/tenets',
24+
] as const satisfies ReadonlyArray<string>
25+
26+
function trimTrailingSlash(url: string) {
27+
return url.replace(/\/$/, '')
28+
}
29+
30+
function escapeXml(value: string) {
31+
return value
32+
.replace(/&/g, '&amp;')
33+
.replace(/</g, '&lt;')
34+
.replace(/>/g, '&gt;')
35+
.replace(/"/g, '&quot;')
36+
.replace(/'/g, '&apos;')
37+
}
38+
39+
function asLastModified(value: string) {
40+
return new Date(`${value}T12:00:00.000Z`).toISOString()
41+
}
42+
43+
function getLibraryEntries(): Array<SitemapEntry> {
44+
return libraries.flatMap((library) => {
45+
if (library.visible === false || !library.latestVersion) {
46+
return []
47+
}
48+
49+
const basePath = `/${library.id}/latest`
50+
const entries: Array<SitemapEntry> = [{ path: basePath }]
51+
52+
if (library.defaultDocs) {
53+
entries.push({
54+
path: `${basePath}/docs/${library.defaultDocs}`,
55+
})
56+
}
57+
58+
return entries
59+
})
60+
}
61+
62+
function getBlogEntries(): Array<SitemapEntry> {
63+
return getPublishedPosts().map((post) => ({
64+
path: `/blog/${post.slug}`,
65+
lastModified: asLastModified(post.published),
66+
}))
67+
}
68+
69+
export function getSiteOrigin(request: Request) {
70+
return trimTrailingSlash(env.SITE_URL || new URL(request.url).origin)
71+
}
72+
73+
export function getSitemapEntries(): Array<SitemapEntry> {
74+
const entries = [
75+
...HIGH_VALUE_STATIC_SITEMAP_PATHS.map((path) => ({ path })),
76+
...getLibraryEntries(),
77+
...getBlogEntries(),
78+
]
79+
80+
return Array.from(
81+
new Map(entries.map((entry) => [entry.path, entry])).values(),
82+
)
83+
}
84+
85+
export function generateSitemapXml(origin: string) {
86+
const urls = getSitemapEntries()
87+
.map((entry) => {
88+
const loc = `${origin}${entry.path}`
89+
90+
return [
91+
' <url>',
92+
` <loc>${escapeXml(loc)}</loc>`,
93+
entry.lastModified
94+
? ` <lastmod>${entry.lastModified}</lastmod>`
95+
: '',
96+
' </url>',
97+
]
98+
.filter(Boolean)
99+
.join('\n')
100+
})
101+
.join('\n')
102+
103+
return `<?xml version="1.0" encoding="UTF-8"?>
104+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
105+
${urls}
106+
</urlset>`
107+
}
108+
109+
export function generateRobotsTxt(origin: string) {
110+
return [
111+
'User-agent: *',
112+
'Allow: /',
113+
'Disallow: /admin',
114+
'Disallow: /account',
115+
'Disallow: /api',
116+
'Disallow: /oauth',
117+
'',
118+
`Sitemap: ${origin}/sitemap.xml`,
119+
].join('\n')
120+
}

0 commit comments

Comments
 (0)