Post

Do I need a sitemap for my SvelteKit app, and how do I create it?

Thilo Maier •
Last modified:

Last week I refactored parts of this website and accidentally broke the endpoint that creates this sitemap. I decided to read up on doing sitemaps the right way. Here is what I learned.

Google’s take on sitemaps

Web developers often assume that Google will only index their site regularly if they have 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.

The website you’re reading this on is small and all pages are discoverable by crawlers, so I wouldn’t strictly need a sitemap. Still, I submitted one in May 2021 as an initial SEO boost. You can see when Google last read a sitemap in the Search Console. For my site it was almost two years ago (at the time of writing):

Screenshot of submitted sitemaps in the Google Search Console. Sitemap submission date: May 21, 2021. Sitemap last read: June 28, 2021.
For small sites, a sitemap is only initially relevant.

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. 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>
sitemap.xml

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 blog posts that mention two additional tags, <priority> and <changefreq>. There is no need to worry about choosing values for these 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.ts. The GET handler assembles an XML string to which you add the relevant routes. You don’t need to include every route, only those you want indexed by Google (for example, your posts). The catch is figuring out how to retrieve the entries for your sitemap. There’s no copy-paste blueprint because it depends on how you manage your content. Below, I’ll walk through the high-level steps.

Step 1: Figure out how to retrieve the pages for your sitemap

I write my posts in Markdown and use Content Collections to access post metadata. Retrieving all posts for a sitemap requires one server-side import:

import { all as posts } from '$lib/server/collections/posts';

The import comes from this file:

import { allPosts } from 'content-collections';

export const all = allPosts.toSorted((a, b) => {
	return b.publishedDate.localeCompare(a.publishedDate);
});
src/lib/server/collections/posts.ts

and all the heavy lifting is done by Content Collections. Less fancy solutions are also totally fine. For example, you could add an endpoint that returns the posts from a manually maintained JSON file. If you manage your posts in a CMS, that endpoint would retrieve them via an API call.

Step 2: Create a sitemap endpoint

Create an endpoint at src/routes/sitemap.xml/+server.ts and add a GET handler. My handler uses the post collection:

import { ORIGIN } from '$env/static/private';
import { all as posts } from '$lib/server/collections/posts';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async () => {
	// Create sitemap entries for posts.
	const postEntries = posts.map(
		(post) => `\t<url>
		<loc>${ORIGIN}${post.path}</loc>
		<lastmod>${post.lastmodDate ? post.lastmodDate : post.publishedDate}</lastmod>
	</url>`
	);

	// Add additional collections to this array.
	const pages = [...postEntries];

	const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.join('\n')}
</urlset>`;

	return new Response(sitemap, {
		headers: {
			'Cache-Control': 'max-age=0, s-maxage=3600',
			'Content-Type': 'application/xml'
		}
	});
};
src/routes/sitemap.xml/+server.ts

This endpoint reads all posts from the posts collection and wraps them into <url> tags in the format discussed in the first part of this post. The content type of the response is application/xml. Since the paths in my post collection are relative, I need to prepend the origin from environment variable ORIGIN because sitemaps require absolute URLs.

The above code is a simplified version of my actual endpoint, which you can explore on GitHub. If you want to see an example of how to define content collections with transformations, look at the file content-collections.ts.

Alternative sitemap creation

If your SvelteKit site uses adapter-static, you can use the 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.