Last active
January 30, 2025 12:03
-
-
Save andrelandgraf/0112631dcdf6640e4bd44360d3e7a08e to your computer and use it in GitHub Desktop.
sitemap.xml generator for remix.run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import childProcess from 'child_process'; | |
import fs from 'fs'; | |
import dotenv from 'dotenv'; | |
import prettier from 'prettier'; | |
const rootDir = process.cwd(); | |
dotenv.config({ | |
path: `${rootDir}/.env.production`, | |
}); | |
interface Route { | |
id: string; | |
path?: string; | |
file: string; | |
children?: Route[]; | |
} | |
const today = new Date().toISOString(); | |
const domain = process.env.HOST; | |
console.log(`Updating sitemap on ${today} for domain ${domain}...`); | |
const consideredRoutes: string[] = []; | |
function addPathParts(path1 = '', path2 = ''): string { | |
return path1.endsWith('/') || path2.startsWith('/') ? `${path1}${path2}` : `${path1}/${path2}`; | |
} | |
function pathToEntry(path: string): string { | |
return ` | |
<url> | |
<loc>${addPathParts(domain, path)}</loc> | |
<lastmod>${today}</lastmod> | |
<changefreq>daily</changefreq> | |
<priority>0.7</priority> | |
</url> | |
`; | |
} | |
async function depthFirstHelper(route: Route, currentPath = ''): Promise<string> { | |
let sitemapContent = ''; | |
const isLayoutRoute = !route.path; | |
const pathIncludesParam = (route.path && route.path.includes(':')) || currentPath.includes(':'); | |
if (!isLayoutRoute && !pathIncludesParam) { | |
const filePath = `${rootDir}/app/${route.file}`; | |
const routeContent = fs.readFileSync(filePath, 'utf8'); | |
// no default export means API route | |
if (routeContent.includes('export default')) { | |
const nextPath = addPathParts(currentPath, route.path); | |
const isConsidered = consideredRoutes.includes(nextPath); | |
if (!isConsidered) { | |
sitemapContent += pathToEntry(nextPath); | |
consideredRoutes.push(nextPath); | |
} | |
} | |
} | |
if (route.children) { | |
for (const childRoute of route.children) { | |
const nextPath = addPathParts(currentPath, route.path); | |
sitemapContent += await depthFirstHelper(childRoute, nextPath); | |
} | |
} | |
return sitemapContent; | |
} | |
async function routesToSitemap(routes: Route[]): Promise<string> { | |
let sitemapContent = ''; | |
for (const route of routes) { | |
sitemapContent += await depthFirstHelper(route, ''); | |
} | |
return ` | |
<?xml version="1.0" encoding="UTF-8"?> | |
<urlset | |
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" | |
> | |
${sitemapContent} | |
</urlset> | |
`; | |
} | |
const formatSitemap = (sitemap: string) => prettier.format(sitemap, { parser: 'html' }); | |
async function main() { | |
const output = childProcess.execSync('npx remix routes --json'); | |
const routes: Route[] = JSON.parse(output.toString()); | |
const root = routes.find((r) => r.id === 'root'); | |
if (!root) { | |
throw new Error('Root not found'); | |
} | |
const childRoutes = root.children; | |
if (!childRoutes) { | |
throw new Error('Root has no children routes'); | |
} | |
console.log(`Found ${childRoutes.length} root children routes!`); | |
const sitemap = await routesToSitemap(childRoutes); | |
const formattedSitemap = formatSitemap(sitemap); | |
fs.writeFileSync('./public/sitemap.xml', formattedSitemap, 'utf8'); | |
console.log('sitemap.xml updated 🎉'); | |
return formattedSitemap; | |
} | |
main(); |
Alternative could be using a resource route like I explain in RSS in Remix
@CanRau yes, you will need to do that when you have dynamic content as well. Kent is doing this on his page: https://github.com/kentcdodds/kentcdodds.com/blob/main/app/other-routes.server.ts
Uh yea, also stumbled upon his approach though not (yet) sure what the reason is, on first look seems more complicated 😳
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey, glad this is helpful to you! Right now, the script only considers "static" routes (without route params) that don't change at runtime. That's why I use it during build time.
I like that your idea gets rid of the node code and makes it platform agnostic! I think your solution depends on the cli package though, right? You would need to install it as a dependency instead of a devDependency to use it during runtime. It would be awesome if Remix could provide an official API for that. Kent opened a ticket to access the route information at runtime through an official API: remix-run/remix#741 Maybe leave your suggestions there as well!