Skip to content

Commit a02e416

Browse files
committed
feat(sitemap): enhance sitemap configuration for libraries and update sitemap generation logic
1 parent 0b98cbf commit a02e416

File tree

4 files changed

+199
-30
lines changed

4 files changed

+199
-30
lines changed

src/libraries/libraries.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export const query: LibrarySlim = {
3030
scarfId: '53afb586-3934-4624-a37a-e680c1528e17',
3131
ogImage: 'https://github.com/tanstack/query/raw/main/media/repo-header.png',
3232
defaultDocs: 'framework/react/overview',
33+
sitemap: {
34+
includeLandingPage: true,
35+
includeTopLevelDocsPages: true,
36+
},
3337
installPath: 'framework/$framework/installation',
3438
legacyPackages: ['react-query'],
3539
handleRedirects: (href) => {
@@ -217,6 +221,10 @@ export const router: LibrarySlim = {
217221
scarfId: '3d14fff2-f326-4929-b5e1-6ecf953d24f4',
218222
ogImage: 'https://github.com/tanstack/router/raw/main/media/header.png',
219223
docsRoot: 'docs/router',
224+
sitemap: {
225+
includeLandingPage: true,
226+
includeTopLevelDocsPages: true,
227+
},
220228
legacyPackages: ['react-location'],
221229
hideCodesandboxUrl: true,
222230
handleRedirects: (href) => {
@@ -282,6 +290,10 @@ export const start: LibrarySlim = {
282290
scarfId: 'b6e2134f-e805-401d-95c3-2a7765d49a3d',
283291
docsRoot: 'docs/start',
284292
defaultDocs: 'framework/react/overview',
293+
sitemap: {
294+
includeLandingPage: true,
295+
includeTopLevelDocsPages: true,
296+
},
285297
installPath: 'framework/$framework/build-from-scratch',
286298
embedEditor: 'codesandbox',
287299
showNetlifyUrl: true,
@@ -323,6 +335,10 @@ export const table: LibrarySlim = {
323335
scarfId: 'dc8b39e1-3fe9-4f3a-8e56-d4e2cf420a9e',
324336
ogImage: 'https://github.com/tanstack/table/raw/main/media/repo-header.png',
325337
defaultDocs: 'introduction',
338+
sitemap: {
339+
includeLandingPage: true,
340+
includeTopLevelDocsPages: true,
341+
},
326342
corePackageName: '@tanstack/table-core',
327343
legacyPackages: ['react-table'],
328344
handleRedirects: (href) => {
@@ -392,6 +408,10 @@ export const form: LibrarySlim = {
392408
availableVersions: ['v1'],
393409
scarfId: '72ec4452-5d77-427c-b44a-57515d2d83aa',
394410
ogImage: 'https://github.com/tanstack/form/raw/main/media/repo-header.png',
411+
sitemap: {
412+
includeLandingPage: true,
413+
includeTopLevelDocsPages: true,
414+
},
395415
}
396416

397417
export const virtual: LibrarySlim = {
@@ -556,6 +576,9 @@ export const db: LibrarySlim = {
556576
scarfId: '302d0fef-cb3f-43c6-b45c-f055b9745edb',
557577
ogImage: 'https://github.com/tanstack/db/raw/main/media/repo-header.png',
558578
defaultDocs: 'overview',
579+
sitemap: {
580+
includeLandingPage: true,
581+
},
559582
}
560583

561584
export const ai: LibrarySlim = {

src/libraries/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export type LibrarySlim = {
7979
* Defaults to true.
8080
*/
8181
visible?: boolean
82+
sitemap?: {
83+
includeLandingPage?: boolean
84+
includeTopLevelDocsPages?: boolean
85+
}
8286
}
8387

8488
// Extended library type - adds React node content for landing pages

src/routes/sitemap[.]xml.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Route = createFileRoute('/sitemap.xml')({
66
server: {
77
handlers: {
88
GET: async ({ request }: { request: Request }) => {
9-
const content = generateSitemapXml(getSiteOrigin(request))
9+
const content = await generateSitemapXml(getSiteOrigin(request))
1010

1111
setResponseHeader('Content-Type', 'application/xml; charset=utf-8')
1212
setResponseHeader(

src/utils/sitemap.ts

Lines changed: 171 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,54 @@
1-
import { libraries } from '~/libraries'
1+
import { getBranch, libraries } from '~/libraries'
2+
import type { LibrarySlim } from '~/libraries/types'
23
import { getPublishedPosts } from '~/utils/blog'
4+
import { fetchRepoDirectoryContents } from '~/utils/docs'
5+
import type { GitHubFileNode } from '~/utils/documents.server'
36
import { env } from '~/utils/env'
47

8+
const TOP_LEVEL_ROUTE_MODULES = Object.keys(
9+
import.meta.glob('../routes/*.{ts,tsx}'),
10+
)
11+
12+
const TOP_LEVEL_INDEX_ROUTE_MODULES = Object.keys(
13+
import.meta.glob('../routes/*/index.tsx'),
14+
)
15+
516
export type SitemapEntry = {
617
path: string
718
lastModified?: string
819
}
920

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>
21+
const MAX_DOCS_SITEMAP_DEPTH = 3
22+
23+
const EXCLUDED_TOP_LEVEL_ROUTE_NAMES = new Set([
24+
'__root',
25+
'account',
26+
'ads',
27+
'blog.$',
28+
'brand-guide',
29+
'builder.docs',
30+
'dashboard',
31+
'feed',
32+
'feedback-leaderboard',
33+
'llms.txt',
34+
'login',
35+
'merch',
36+
'partners-embed',
37+
'privacy',
38+
'terms',
39+
'robots.txt',
40+
'rss.xml',
41+
'sitemap.xml',
42+
'sponsors-embed',
43+
])
44+
45+
const EXCLUDED_TOP_LEVEL_ROUTE_DIRECTORIES = new Set([
46+
'$libraryId',
47+
'[.]well-known',
48+
'account',
49+
'admin',
50+
'stats',
51+
])
2552

2653
function trimTrailingSlash(url: string) {
2754
return url.replace(/\/$/, '')
@@ -40,23 +67,133 @@ function asLastModified(value: string) {
4067
return new Date(`${value}T12:00:00.000Z`).toISOString()
4168
}
4269

70+
function normalizeRouteName(routeName: string) {
71+
return routeName.replace(/\[\.\]/g, '.')
72+
}
73+
74+
function getTopLevelRoutePath(routeName: string) {
75+
if (routeName === 'index') {
76+
return '/'
77+
}
78+
79+
if (routeName.endsWith('.index')) {
80+
return `/${routeName.slice(0, -'.index'.length)}`
81+
}
82+
83+
return `/${routeName}`
84+
}
85+
86+
function getTopLevelEntries(): Array<SitemapEntry> {
87+
const fileEntries = TOP_LEVEL_ROUTE_MODULES.flatMap((modulePath) => {
88+
const routeName = normalizeRouteName(
89+
modulePath
90+
.split('/')
91+
.at(-1)
92+
?.replace(/\.(ts|tsx)$/, '') ?? '',
93+
)
94+
95+
if (!routeName || EXCLUDED_TOP_LEVEL_ROUTE_NAMES.has(routeName)) {
96+
return []
97+
}
98+
99+
return [{ path: getTopLevelRoutePath(routeName) }]
100+
})
101+
102+
const directoryEntries = TOP_LEVEL_INDEX_ROUTE_MODULES.flatMap(
103+
(modulePath) => {
104+
const routeDirectory = modulePath.split('/').at(-2)
105+
106+
if (
107+
!routeDirectory ||
108+
EXCLUDED_TOP_LEVEL_ROUTE_DIRECTORIES.has(routeDirectory)
109+
) {
110+
return []
111+
}
112+
113+
return [{ path: `/${normalizeRouteName(routeDirectory)}` }]
114+
},
115+
)
116+
117+
return [...fileEntries, ...directoryEntries]
118+
}
119+
43120
function getLibraryEntries(): Array<SitemapEntry> {
44121
return libraries.flatMap((library) => {
45-
if (library.visible === false || !library.latestVersion) {
122+
if (
123+
library.visible === false ||
124+
!library.latestVersion ||
125+
library.sitemap?.includeLandingPage !== true
126+
) {
46127
return []
47128
}
48129

49130
const basePath = `/${library.id}/latest`
50-
const entries: Array<SitemapEntry> = [{ path: basePath }]
131+
return [{ path: basePath }]
132+
})
133+
}
51134

52-
if (library.defaultDocs) {
53-
entries.push({
54-
path: `${basePath}/docs/${library.defaultDocs}`,
55-
})
56-
}
135+
function flattenDocsTree(nodes: Array<GitHubFileNode>): Array<GitHubFileNode> {
136+
return nodes.flatMap((node) => [
137+
node,
138+
...(node.children ? flattenDocsTree(node.children) : []),
139+
])
140+
}
57141

58-
return entries
59-
})
142+
function toDocsSlug(filePath: string, docsRoot: string) {
143+
const docsPrefix = `${docsRoot}/`
144+
145+
if (!filePath.startsWith(docsPrefix) || !filePath.endsWith('.md')) {
146+
return null
147+
}
148+
149+
const slug = filePath.slice(docsPrefix.length, -'.md'.length)
150+
151+
if (!slug || slug.endsWith('/index')) {
152+
return null
153+
}
154+
155+
return slug
156+
}
157+
158+
function isTopLevelDocsSlug(slug: string) {
159+
const segments = slug.split('/')
160+
161+
return segments.length <= MAX_DOCS_SITEMAP_DEPTH
162+
}
163+
164+
function isDefined<T>(value: T | null): value is T {
165+
return value !== null
166+
}
167+
168+
async function getLibraryDocsEntries(
169+
library: LibrarySlim,
170+
): Promise<Array<SitemapEntry>> {
171+
if (
172+
library.visible === false ||
173+
!library.latestVersion ||
174+
library.sitemap?.includeTopLevelDocsPages !== true
175+
) {
176+
return []
177+
}
178+
179+
const docsRoot = library.docsRoot || 'docs'
180+
const branch = getBranch(library, 'latest')
181+
const docsTree = await fetchRepoDirectoryContents({
182+
data: {
183+
repo: library.repo,
184+
branch,
185+
startingPath: docsRoot,
186+
},
187+
}).catch(() => [])
188+
189+
return flattenDocsTree(docsTree)
190+
.filter((node) => node.type === 'file')
191+
.map((node) => toDocsSlug(node.path, docsRoot))
192+
.filter(isDefined)
193+
.filter(isTopLevelDocsSlug)
194+
.map((slug) => ({
195+
path: `/${library.id}/latest/docs/${slug}`,
196+
}))
60197
}
61198

62199
function getBlogEntries(): Array<SitemapEntry> {
@@ -70,10 +207,15 @@ export function getSiteOrigin(request: Request) {
70207
return trimTrailingSlash(env.SITE_URL || new URL(request.url).origin)
71208
}
72209

73-
export function getSitemapEntries(): Array<SitemapEntry> {
210+
export async function getSitemapEntries(): Promise<Array<SitemapEntry>> {
211+
const docsEntries = await Promise.all(
212+
libraries.map((library) => getLibraryDocsEntries(library)),
213+
)
214+
74215
const entries = [
75-
...HIGH_VALUE_STATIC_SITEMAP_PATHS.map((path) => ({ path })),
216+
...getTopLevelEntries(),
76217
...getLibraryEntries(),
218+
...docsEntries.flat(),
77219
...getBlogEntries(),
78220
]
79221

@@ -82,8 +224,8 @@ export function getSitemapEntries(): Array<SitemapEntry> {
82224
)
83225
}
84226

85-
export function generateSitemapXml(origin: string) {
86-
const urls = getSitemapEntries()
227+
export async function generateSitemapXml(origin: string) {
228+
const urls = (await getSitemapEntries())
87229
.map((entry) => {
88230
const loc = `${origin}${entry.path}`
89231

0 commit comments

Comments
 (0)