Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
sitemap.xml generator for remix.run
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();
@andrelandgraf
Copy link
Author

andrelandgraf commented Dec 16, 2021

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!

@CanRau
Copy link

CanRau commented Dec 25, 2021

Alternative could be using a resource route like I explain in RSS in Remix

@andrelandgraf
Copy link
Author

andrelandgraf commented Dec 28, 2021

@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

@CanRau
Copy link

CanRau commented Dec 28, 2021

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