-
-
Save andrelandgraf/0112631dcdf6640e4bd44360d3e7a08e to your computer and use it in GitHub Desktop.
| 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(); |
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!
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 😳
Instead of opening a child process, could you instead
require('@remix-run/dev/config/format').formatRoutesAsJson(require('./server/build/routes'))? I don't think it's really part of the public API since you have to get it from config/format, but you would get the same result without having to shell out.This would be great to have available as a module at runtime, so in my /app/routes/sitemap[.xml].tsx I can do
Which could return a response with XML content type headers.
Thanks for making this. Great example!