Stanislav Khromov

I’ve been building static single page apps using adapter-static for a while now, and I love how simple the deployment story is – just upload a folder of files and you’re done! If you are unfamiliar with this adapter, it compiles down your app to a folder of HTML, CSS and JavaScript files.

A couple of months ago, the SvelteKit maintainers added two features that really caught my attention. They were actually both part of Advent of Svelte in 2024. With these new features, we can make our SvelteKit projects way more flexible. For example, you can make a project run without any web server at all, meaning you can send your whole SvelteKit app to someone as a single HTML file, have them double click it and have it all work out of the box.

Let me show you how I applied these features to an existing project of mine.

Prefer to watch a video? See this:

The new “single” bundle strategy

Before we had the bundle strategy feature, there was really no way to tell SvelteKit how we wanted it to split the JavaScript bundles for our project. SvelteKit would just do its thing and create a bunch of different JavaScript files that would load as you navigated around.

Now we have three different options to choose from in our svelte.config.ts file. The default “split” option is what we’ve always had – it splits the app up into multiple JavaScript and CSS files so they’re loaded lazily as the user navigates. This is usually what you want because you don’t want users loading every component in your app just because they hit one page.

Then there’s “single“, which creates just one JavaScript bundle and one CSS file, which is something people have been asking for for a long.

But there’s also the new “inline” option. This one takes all your JavaScript and CSS and inlines it directly into the HTML file. I didn’t see many people asking for this specifically, but it made me curious about whether it would be possible to build a completely self-contained SvelteKit app!

The new “hash” router

The second addition was the hash router configuration. Before this, there was basically only one way to route things in SvelteKit – using normal paths like /about or /settings. But now we can use hash routing, which means paths like /#/about and /#/settings work just the same.

This might seem like a small change, but it’s actually quite impactful. The thing about hash parameters is that they’re client-only – the server doesn’t even see them. That means you can run your app without any server-side routing configuration at all. However, this also means that server-side rendering and prerendering are disabled. In fact, you can’t even have any +server.ts files in your project anymore if you use the hash router, so beware!

You configure the hash router by setting the router option to hash in svelte.config.ts

Testing it on a real project

The project I decided to test these features on is called DeriVault. It’s a deterministic password manager I built that doesn’t require any connection to the internet. I figured it would be perfect for this experiment since it’s already designed to work completely offline.

Let me walk you through what happened when I added these features. First, I added the bundle strategy to my svelte.config.ts:

const config = {
	preprocess: vitePreprocess(),
	kit: {
		output: {
			bundleStrategy: 'inline'
		},
		adapter: adapter({
			fallback: 'index.html'
		})
	}
};

When I built the site with this configuration, instead of having all these different JavaScript files in my _app/immutable/chunks folder, everything was instead inlined into the index.html file!

Making the hash router work

Adding the hash router is also simple, we add this key to config.kit in svelte.config.ts:

router: {
	type: 'hash'
}

One challenge was refactoring my navigation. I had to go through and change every call to goto from something like `goto(‘/vault’)` to `goto(‘#/vault’)`.

The trickiest part was handling query parameters. When you have a URL like #/add?edit=1, edit is technically not a query parameter anymore because everything after the hash is just a fragment. So I wrote this little utility function to handle this:

export function getHashParams(url) {
	const urlObj = typeof url === 'string' ? new URL(url) : url;
	let hashParams = new URLSearchParams();
	
	if (urlObj.hash && urlObj.hash.includes('?')) {
		const queryString = urlObj.hash.substring(urlObj.hash.indexOf('?') + 1);
		if (queryString) {
			hashParams = new URLSearchParams(queryString);
		}
	}
	
	return hashParams;
}

Then in my load functions, instead of using url.searchParams.get('edit'), I had to use getHashParams(url).get('edit'). The big benefit was that I didn’t have to change my route structure – I could keep using my filesystem routes and route parameters just like before.

Putting it all together

After making all these changes and building the project, I wanted to see if it really worked. So I deleted everything from the build folder except the `index.html` file. Then I double-clicked it.

And it worked! The whole app loaded up, I could navigate between routes, add passwords, everything worked just like it did when served from a web server. I was honestly amazed that an entire SvelteKit app could run from a single HTML file opened directly in the browser.

When would you actually use this?

Now you might be wondering when you’d actually want to use these features. For the inline bundle strategy, I can think of a few scenarios. You could build tools that need to work completely offline and can be shared as a single file. You could send someone an interactive tool as an email attachment (though watch the file size!). You could save apps on USB drives or deploy them to devices with limited file systems.

The single bundle strategy (without inlining) has its own use cases. If you’re building Capacitor apps, they traditionally haven’t been able to use HTTP/2 multiplexing effectively, so having fewer files to load can improve the load performance.

The hash router is particularly useful when you can’t control the web server configuration. GitHub Pages is a great example – it’s difficult to set up proper routing for a SPA there, but with hash routing you don’t need to.

Some things to keep in mind

These features are really cool, but they’re not for every project. The inline strategy means everyone downloads everything upfront, which goes against the whole idea of code splitting. You also lose server-side rendering with the hash router, which affects SEO.

But for the right use cases, these features make the DX so much better.

What do you think? Have you tried these features yet? I’d love to hear what kind of projects you’d build with a single-file SvelteKit app!

Social image by Walls.io on Unsplash

🇾đŸ‡Ș Full-stack impostor syndrome sufferer & Software Engineer at Schibsted Media Group

View Comments

There are currently no comments.

Next Post