From ed3fbd42a9ad7ed80cb3e3170a42bb64933d7efa Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:57:55 +0100 Subject: [PATCH] fix docs indexing with sitemap, canonical urls, and root redirects --- app/lib/shared.ts | 2 ++ app/routes.ts | 2 +- app/routes/docs.tsx | 9 ++++++--- app/routes/intro-redirect.tsx | 9 --------- app/routes/sitemap.ts | 14 ++++++++++++++ public/robots.txt | 1 + react-router.config.ts | 3 ++- workers/app.ts | 15 +++++++++++++++ wrangler.jsonc | 4 ++++ 9 files changed, 45 insertions(+), 14 deletions(-) delete mode 100644 app/routes/intro-redirect.tsx create mode 100644 app/routes/sitemap.ts diff --git a/app/lib/shared.ts b/app/lib/shared.ts index 73de898..0ce8c6a 100644 --- a/app/lib/shared.ts +++ b/app/lib/shared.ts @@ -1,4 +1,6 @@ export const appName = 'Talo' export const docsRoute = '/' +export const siteOrigin = 'https://docs.trytalo.com' + export const baseApiUrl = import.meta.env.DEV ? 'http://localhost:3000' : 'https://api.trytalo.com' diff --git a/app/routes.ts b/app/routes.ts index 4da4c46..5db92d3 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -2,7 +2,7 @@ import { index, route, type RouteConfig } from '@react-router/dev/routes' export default [ index('routes/home.tsx'), - route('intro', 'routes/intro-redirect.tsx'), route('api/search', 'routes/search.ts'), + route('sitemap.xml', 'routes/sitemap.ts'), route(':version/*', 'routes/docs.tsx'), ] satisfies RouteConfig diff --git a/app/routes/docs.tsx b/app/routes/docs.tsx index 1a76e90..01e7568 100644 --- a/app/routes/docs.tsx +++ b/app/routes/docs.tsx @@ -8,6 +8,7 @@ import { Feedback } from '@/components/feedback/client' import { useMDXComponents } from '@/components/mdx' import { onPageFeedback } from '@/lib/feedback' import { baseOptions } from '@/lib/layout.shared' +import { siteOrigin } from '@/lib/shared' import { docs, source } from '@/lib/source' import type { Route } from './+types/docs' @@ -23,11 +24,12 @@ export async function loader({ params }: Route.LoaderArgs) { return { path: page.path, + url: page.url, pageTree: await source.serializePageTree(source.getPageTree()), } } -function Content({ path }: { path: string }) { +function Content({ path, url }: { path: string; url: string }) { const page = docs.getPage(path) if (!page) { throw new Error(`unknown page: ${path}`) @@ -48,6 +50,7 @@ function Content({ path }: { path: string }) { {path === 'index.mdx' ? 'Talo - open source, self-hostable game backend' : page.title} + {page.title} {page.description} @@ -62,11 +65,11 @@ function Content({ path }: { path: string }) { } export default function Page({ loaderData }: Route.ComponentProps) { - const { path, pageTree } = useFumadocsLoader(loaderData) + const { path, url, pageTree } = useFumadocsLoader(loaderData) return ( - + ) } diff --git a/app/routes/intro-redirect.tsx b/app/routes/intro-redirect.tsx deleted file mode 100644 index c6d9b5c..0000000 --- a/app/routes/intro-redirect.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { redirect } from 'react-router' - -export async function loader() { - throw redirect('/') -} - -export default function IntroRedirect() { - return null -} diff --git a/app/routes/sitemap.ts b/app/routes/sitemap.ts new file mode 100644 index 0000000..22234f5 --- /dev/null +++ b/app/routes/sitemap.ts @@ -0,0 +1,14 @@ +import { siteOrigin } from '@/lib/shared' +import { source } from '@/lib/source' + +export function loader() { + const urls = source.getPages().map((page) => `${siteOrigin}${page.url}`) + const body = ` + +${urls.join('\n')} +` + + return new Response(body, { + headers: { 'content-type': 'application/xml; charset=utf-8' }, + }) +} diff --git a/public/robots.txt b/public/robots.txt index 1a5a2dd..d09952c 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,3 +1,4 @@ User-agent: * Disallow: # Algolia-Crawler-Verif: 50BA364995958603 +Sitemap: https://docs.trytalo.com/sitemap.xml diff --git a/react-router.config.ts b/react-router.config.ts index 4ad4a11..b85bdaf 100644 --- a/react-router.config.ts +++ b/react-router.config.ts @@ -10,7 +10,6 @@ export default { const paths: string[] = [] for (const path of getStaticPaths()) { - // `/` navigates to the default version via the client component if (path !== '/') { paths.push(path) } @@ -21,6 +20,8 @@ export default { paths.push(getUrl(slugs)) } + paths.push('/sitemap.xml') + return paths }, } satisfies Config diff --git a/workers/app.ts b/workers/app.ts index 79ab11a..ab4e63c 100644 --- a/workers/app.ts +++ b/workers/app.ts @@ -6,6 +6,10 @@ type Env = {} // Pre-versioning paths (e.g. /docs/godot/install) redirect to the default version. const DEFAULT_VERSION = '1.x' const LEGACY_SECTIONS = ['godot', 'unity', 'http', 'sockets', 'selfhosting', 'integrations'] +const INTRO_SEGMENT = 'intro' +// React Router sets this while prerendering the SPA fallback during `pnpm build`. +// That request must render the shell, so it must skip the `/` redirect below. +const SPA_FALLBACK_HEADER = 'X-React-Router-SPA-Mode' const requestHandler = createRequestHandler( () => import('virtual:react-router/server-build'), @@ -15,6 +19,14 @@ const requestHandler = createRequestHandler( export default { async fetch(request) { const { pathname, origin } = new URL(request.url) + // `/` carries no version, so send it to the default one. + if (pathname === '/' && !request.headers.has(SPA_FALLBACK_HEADER)) { + return Response.redirect(`${origin}/${DEFAULT_VERSION}`, 301) + } + // `/intro` pre-dates the versioned paths and now lives at `/1.x`. + if (pathname === '/intro' || pathname === '/intro/') { + return Response.redirect(`${origin}/${DEFAULT_VERSION}`, 301) + } // Old `/docs/*` URLs redirect to the prefix-less paths. if (pathname === '/docs' || pathname === '/docs/') { return Response.redirect(`${origin}/${DEFAULT_VERSION}`, 301) @@ -23,6 +35,9 @@ export default { if (match) { const rest = match[1] const [first] = rest.split('/') + if (first === INTRO_SEGMENT) { + return Response.redirect(`${origin}/${DEFAULT_VERSION}`, 301) + } const target = LEGACY_SECTIONS.includes(first) ? `/${DEFAULT_VERSION}/${rest}` : `/${rest}` return Response.redirect(`${origin}${target}`, 301) } diff --git a/wrangler.jsonc b/wrangler.jsonc index 29b6182..5b0fca9 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -5,5 +5,9 @@ "observability": { "enabled": true, }, + "assets": { + "html_handling": "drop-trailing-slash", + "run_worker_first": ["/"], + }, "main": "./workers/app.ts", }