Thilo Maier

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 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, so I would not need a sitemap. Yet, 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, it was almost two years ago:

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. 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>
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 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 because a sitemap depends on how you manage the content of your site. 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, which it obtains from a file that contains an array with metadata for all posts. 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 an 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);
}
Structure of src/routes/sitemap.xml/+server.js.

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 create_entry(path, lastmod) {
	return `<url>
    <loc>${new URL(path, PUBLIC_CANONICAL_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. Google expects absolute URLs in a sitemap. Since I prerender the sitemap, I cannot obtain the origin of the URL from which the app is served in production. Therefore, I created an environment variable PUBLIC_CANONICAL_ORIGIN, which contains my site's canonical origin https://maier.tech. This variable could also be private and named CANONICAL_ORIGIN. But I also need access to the canonical origin in my SEO components client-side, which means that the variable has to be public.

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) => create_entry(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);
}
src/routes/sitemap.xml/+server.js with error handling.

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.