Do I need a sitemap for my SvelteKit app, and how do I create it?
Last modified:
Last week I refactored parts of this website and broke the endpoint that creates this sitemap. I decided to read up on sitemaps before fixing the route. Here is what I learned.
Google’s take on sitemaps
Web developers often assume that Google might only index their site regularly with a sitemap. But is this true? In the Google Search Console docs, Google answers the question “Do I need a sitemap?” with it depends. Google recommends a sitemap when
- you launch a new site, and no or few external links point to your site or
- your site is large, and not all pages are linked and discoverable by Google’s crawler.
You do not need a sitemap if
- your site is small (up to 500 relevant pages) or
- your site is linked correctly, and Google can find all relevant pages by crawling it.
My website maier.tech is small, and all pages are discoverable by a crawler. As recommended by Google, I submitted a sitemap in May 2021 as an initial SEO boost. You can see when Google last read a sitemap in the Google Search Console. For my site maier.tech it was almost two years ago:
Different types of sitemaps
Google supports different types of sitemaps. If your site already has an RSS feed, you can submit the feed URL as a sitemap and call it a day. But the most common sitemap type is XML. A simple XML sitemap that indexes only the homepage looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://maier.tech/</loc>
<lastmod>2023-02-28</lastmod>
</url>
</urlset>
Every indexed page goes in a <url>
tag. The <loc>
tag contains the URL of the indexed page. The <lastmod>
tag contains the last modified date. You may have encountered posts mentioning two more tags, <priority>
and <changefreq>
. There is no need to worry about choosing values for these two tags. Google ignores both (and so does Bing).
Creating a sitemap with SvelteKit
SvelteKit’s SEO docs show an example of a sitemap implemented as an endpoint in src/routes/sitemap.xml/+server.js
. The GET
handler assembles an XML string to which you add relevant routes. There is no need to add all routes, only those you want to be indexed by Google. The catch is that you need to figure out how to retrieve the entries for your sitemap. There is no copy-paste blueprint for how to create a sitemap with SvelteKit. But I will walk you through the steps.
Create endpoints to retrieve relevant pages
I created an endpoint src/api/posts/+server.js
that returns a list of all posts. I manage my posts in Markdown files; the endpoint reads the frontmatters and returns them. If I managed my posts in a CMS, the endpoint would retrieve them via an API call to the CMS. Add an endpoint for each type of content you want to include in your sitemap.
Create a sitemap endpoint
Create endpoint src/routes/sitemap.xml/+server.js
and add an async GET
handler with the following structure:
export async function GET({ fetch, setHeaders }) {
setHeaders({
'Content-Type': 'application/xml'
});
const response = await fetch('/api/posts');
...
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
Add an entry for each post.
</urlset>
`;
return new Response(sitemap);
}
Since this endpoint returns XML, I set the content type to application/xml
. Then I fetch all posts from my endpoint /api/posts
. At the end of the handler, I create the XML string, wrap it in a response object and return it.
To make creating entries easier, I use this helper function:
function createEntry(path, lastmod) {
return `
<url>
<loc>${new URL(path, ORIGIN).href}</loc>
${lastmod ? `<lastmod>${lastmod}</lastmod>` : ''}
</url>
`;
}
path
is a relative path, and lastmod
is a date string in ISO format. I get both from my /api/posts
endpoint. ORIGIN
is an environment variable containing my site’s origin, https://maier.tech
. Google expects absolute URLs in a sitemap. And since a SvelteKit app cannot determine its public URL, I use the ORIGIN
variable to assemble absolute URLs.
Let’s add error handling to wrap up the handler:
export async function GET({ fetch, setHeaders }) {
setHeaders({
'Content-Type': 'application/xml'
});
const response = await fetch('/api/posts');
if (!response.ok) {
throw error(500, 'Failed to fetch posts.');
}
const raw_posts = await response.json();
const posts = raw_posts.map((post) => createEntry(post.path, post.lastmod));
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts.join('\n')}
</urlset>
`;
return new Response(sitemap);
}
The above code is a simplified version of my actual endpoint, which you can explore on GitHub. My actual endpoint adds caching, validation, and prerendering.
Alternative sitemap creation
If your SvelteKit site uses adapter-static, you can use package svelte-sitemap. With this package, instead of implementing an endpoint for a sitemap, you can configure the postbuild
hook in package.json
to scan all routes in the build
directory and create build/sitemap.xml
. This approach only works with adapter-static since svelte-sitemap cannot determine all possible routes for other adapters.