SvelteKit dynamic page components

Time for the usual preamble about what Svelte is, what is SvelteKit, what is a component, how is data sent over the internet, what is HTTP, what is caching, what’s a file, where and what is my kitchen sink.

Moving swiftly on.

Svelte’s default behaviour

Its +page.svelte’s all the way down!

SvelteKit out of the box will look at your src/routes directory and traverse each subdirectory in search of specially named JS/TS or Svelte files. The main special files are:

  • +layout This is the a wrapper component which will render with subpaths or the page inside the <slot> element.
  • +page This comes in 3 flavours:
    • +page.server.js (yes yes or .ts) Only rendered on the server
    • +page.js Rendered on both the server and the client side
    • +page.svelte The actual Svelte component which will be rendered to the HTML of your page

How other frameworks handle this

The big contender in this field is NextJS. Traditionally Next would look at the src/pages directory and just render any JS/TS file which default export‘ed a React component.

The newer app router stuff is slightly different though.

Let’s say we want to mimic that same behaviour. ie. we go to http://localhost:5173/some-component and that renders the component which lives in src/routes/[url]/some-component.svelte

The solution

The file which is called in Svelte kit when a request comes in for a given route is the +page.server file. Below is an example server file which runs a dynamic import looking for .svelte files in this directory.

// +page.server.js
import { error } from '@sveltejs/kit';
import { componentUrls } from '../data.js';

 * @type {import('./$types.js').PageLoad}
export function load({ params }) {
	const url = componentUrls.find(url => url === params.url);

	if (!url) throw error(404);
	return url;

But what is that componentUrls array, you ask?

Why, its a glob import old boy!

// data.js
export const componentUrls = Object.keys(
	import.meta.glob('./[url]/*.svelte', { eager: true })
).map(path => path.split('[url]/')[1].replace(/\.svelte$/, ''));

By using a glob in the import, we get back a list of files which we can then process into a list of url options for our url to hit.

The next file the request passes through is the +page file.

This file runs once on the server and once on the client. Here we dynamically import the specific Svelte component and pass the default export as forward in the response object under the key component (this could be any name by the way).

// +page.js
 * @type {import('./$types.js').PageLoad}
export async function load({ params, data }) {
	/* @vite-ignore */
	const component = await import(`./${params.url}.svelte`);

	return {
		component: component.default

Finally we hit the +page.svelte component. We ingest the data passed from +page and pull out the component key.

<!-- +page.svelte -->
	 * @type {import('./$types').PageData}
	export let data;

<svelte:component this={data.component} />

Thankfully Svelte provides us with a handy svelte:component tag which we can use in our templates to dynamically pass components to be rendered.

And there we have it, we can now go to http://localhost:5173/my-component and we’ll see the src/routes/[url]/my-component.svelte file rendered in the browser.