Building Portable SvelteKit Apps: Hash Routing Guide

SvelteKit has introduced powerful new features for building truly portable web applications that can run anywhere - from Electron apps to simple file shares. With hash-based routing and inline bundle strategies, you can now create self-contained SvelteKit applications that work without any server configuration.
This guide walks you through building a portable SvelteKit app step-by-step, showing you how to leverage these new capabilities to create applications that can be shared as single HTML files or deployed to any hosting environment without server-side setup.
Link to section: Understanding Portable Web ApplicationsUnderstanding Portable Web Applications
Portable web applications solve a fundamental deployment challenge: running sophisticated single-page applications without requiring server configuration or complex hosting setups. Traditional SvelteKit apps rely on server-side routing, which means your hosting environment needs to redirect all routes to your main HTML file.
Hash-based routing changes this completely. Instead of routes like /about
or /settings
, your app uses routes like /#/about
and /#/settings
. The crucial difference is that everything after the hash symbol stays on the client side - servers never see these fragments, making your app work in any hosting scenario.
Combined with SvelteKit's new inline bundle strategy, you can package your entire application - JavaScript, CSS, images, and all - into a single HTML file. This creates unprecedented portability for web applications.
Link to section: Setting Up Your ProjectSetting Up Your Project
Start by creating a new SvelteKit project using the latest Svelte CLI. The sv
command provides a streamlined interface for project creation and management:
npx sv create my-portable-app
Select the minimal template when prompted, and choose TypeScript for better development experience:
◇ Which template would you like?
│ SvelteKit minimal
◇ Add type checking with Typescript?
│ Yes, using Typescript syntax
Navigate to your project directory:
cd my-portable-app
npm install
Verify your setup by running the development server:
npm run dev
You should see your SvelteKit app running at http://localhost:5173
. The default setup uses pathname-based routing, which we'll change to hash-based routing next.
Link to section: Configuring Hash-Based RoutingConfiguring Hash-Based Routing
Hash-based routing is configured in your svelte.config.js
file. Open the file and modify the kit configuration to include the router option:
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: 'index.html'
}),
router: {
type: 'hash'
}
}
};
export default config;
This configuration enables hash-based routing and sets up the static adapter with a fallback to index.html
. The fallback is important because it ensures your app works even when accessed directly via file system.

Important considerations when using hash routing:
- Server-side rendering (SSR) is automatically disabled
- You cannot use
+page.server.js
or+layout.server.js
files - All data loading must happen on the client side
- Progressive enhancement patterns need adjustment
Update your navigation links to use hash-based routes. If you have a navigation component, modify it like this:
<!-- src/lib/components/Navigation.svelte -->
<nav>
<a href="/#/">Home</a>
<a href="/#/about">About</a>
<a href="/#/contact">Contact</a>
</nav>
Create corresponding route files in your src/routes
directory. The file structure remains the same, but the URLs will now use hash fragments:
src/routes/
├── +layout.svelte
├── +page.svelte
├── about/
│ └── +page.svelte
└── contact/
└── +page.svelte
Test your hash routing by running npm run dev
and navigating to http://localhost:5173/#/about
. You should see the hash fragment in your browser's address bar, and navigation should work smoothly.
Link to section: Implementing Inline Bundle StrategyImplementing Inline Bundle Strategy
The inline bundle strategy packages all your JavaScript and CSS directly into your HTML file, creating a truly self-contained application. Configure this by adding the bundleStrategy
option to your svelte.config.js
:
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: 'index.html'
}),
router: {
type: 'hash'
},
output: {
bundleStrategy: 'inline'
}
}
};
export default config;
Build your application to see the inline bundling in action:
npm run build
Examine the generated build/index.html
file. Instead of separate JavaScript and CSS files in a _app
directory, everything is now inlined directly into the HTML:
<!DOCTYPE html>
<html>
<head>
<style>
/* All your CSS is inlined here */
.svelte-1234567 { color: blue; }
</style>
</head>
<body>
<div id="svelte"><!-- Your app markup --></div>
<script>
// All your JavaScript is inlined here
(function() {
// SvelteKit runtime and your components
})();
</script>
</body>
</html>
This single file contains your entire application and can be opened directly in a browser or deployed anywhere without additional configuration.
Link to section: Building a Complete Example ApplicationBuilding a Complete Example Application
Let's build a practical example to demonstrate these concepts. Create a simple note-taking app that showcases hash routing and client-side data persistence:
First, create a notes store for client-side state management:
// src/lib/stores/notes.js
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
function createNotesStore() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
load: () => {
if (browser) {
const stored = localStorage.getItem('portable-notes');
if (stored) {
set(JSON.parse(stored));
}
}
},
add: (note) => {
update(notes => {
const newNotes = [...notes, {
id: Date.now(),
text: note,
created: new Date().toISOString()
}];
if (browser) {
localStorage.setItem('portable-notes', JSON.stringify(newNotes));
}
return newNotes;
});
},
remove: (id) => {
update(notes => {
const filtered = notes.filter(note => note.id !== id);
if (browser) {
localStorage.setItem('portable-notes', JSON.stringify(filtered));
}
return filtered;
});
}
};
}
export const notes = createNotesStore();
Create the main notes page with add and display functionality:
<!-- src/routes/+page.svelte -->
<script>
import { onMount } from 'svelte';
import { notes } from '$lib/stores/notes.js';
let newNote = '';
onMount(() => {
notes.load();
});
function addNote() {
if (newNote.trim()) {
notes.add(newNote.trim());
newNote = '';
}
}
function removeNote(id) {
notes.remove(id);
}
</script>
<svelte:head>
<title>Portable Notes App</title>
</svelte:head>
<main>
<h1>Portable Notes</h1>
<section class="add-note">
<input
bind:value={newNote}
placeholder="Enter a new note..."
on:keydown={(e) => e.key === 'Enter' && addNote()}
/>
<button on:click={addNote}>Add Note</button>
</section>
<section class="notes-list">
{#each $notes as note (note.id)}
<div class="note">
<p>{note.text}</p>
<small>{new Date(note.created).toLocaleDateString()}</small>
<button on:click={() => removeNote(note.id)}>Delete</button>
</div>
{/each}
</section>
<nav>
<a href="/#/about">About This App</a>
</nav>
</main>
<style>
main {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.add-note {
margin-bottom: 2rem;
display: flex;
gap: 0.5rem;
}
.add-note input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.note {
border: 1px solid #eee;
padding: 1rem;
margin-bottom: 0.5rem;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.note p {
margin: 0;
flex: 1;
}
.note small {
color: #666;
margin-right: 0.5rem;
}
</style>
Add an about page to demonstrate routing:
<!-- src/routes/about/+page.svelte -->
<svelte:head>
<title>About - Portable Notes</title>
</svelte:head>
<main>
<h1>About Portable Notes</h1>
<p>
This is a demonstration of a portable SvelteKit application using:
</p>
<ul>
<li>Hash-based routing for client-side navigation</li>
<li>Inline bundling for single-file deployment</li>
<li>LocalStorage for client-side data persistence</li>
</ul>
<p>
This entire application is contained in a single HTML file that can be:
</p>
<ul>
<li>Opened directly in any web browser</li>
<li>Shared via email or file transfer</li>
<li>Deployed to any web hosting service</li>
<li>Embedded in Electron or mobile apps</li>
</ul>
<nav>
<a href="/#/">Back to Notes</a>
</nav>
</main>
<style>
main {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
</style>
Link to section: Advanced Configuration and Asset HandlingAdvanced Configuration and Asset Handling
For truly self-contained applications, you need to inline all assets including images, fonts, and other media files. Configure Vite to inline assets by adding to your vite.config.js
:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
build: {
assetsInlineLimit: 100000 // Inline assets smaller than 100KB
}
});
This configuration tells Vite to inline any asset smaller than 100KB directly into your bundle. For larger assets, consider using data URLs or external CDN links.
Handle navigation programmatically using SvelteKit's goto
function with hash routes:
import { goto } from '$app/navigation';
// Navigate to hash-based routes
function navigateToAbout() {
goto('/#/about');
}
Remember that with hash routing, you lose some SvelteKit features:
- No server-side rendering or prerendering
- No
+page.server.js
or+layout.server.js
files - No form actions or server-side API routes
- Progressive enhancement works differently
Link to section: Deployment and DistributionDeployment and Distribution
Build your portable application:
npm run build
The build
directory now contains a fully self-contained application. The index.html
file is your complete app. Test it locally by opening the file directly in your browser:
# On macOS
open build/index.html
# On Windows
start build/index.html
# On Linux
xdg-open build/index.html
Deploy options for portable SvelteKit apps:
Static Hosting Services: Upload to Netlify, Vercel, or GitHub Pages without any configuration. The hash routing works immediately.
File Sharing: Send the index.html
file via email or file transfer. Recipients can double-click to run the app locally.
Electron Integration: Package the HTML file into an Electron app for desktop distribution.
Content Management Systems: Upload as a static file to WordPress, Drupal, or other CMS platforms.
The portability means your app works in environments where traditional SPA deployment would require server configuration for route handling.
Link to section: Performance Considerations and LimitationsPerformance Considerations and Limitations
Inline bundling creates larger initial payloads since all code loads upfront instead of being split across multiple requests. Monitor your bundle size:
# Check build output size
ls -lh build/index.html
For applications >1MB
, consider whether inline bundling is appropriate. The single-request model can actually be faster for small to medium applications due to reduced network overhead.
Hash routing limitations to consider:
- SEO is challenging since search engines handle hash fragments differently
- Browser history behaves differently than pathname routing
- Deep linking requires special handling
- Analytics tracking needs adjustment for hash-based navigation
Plan your application architecture accordingly. Hash routing works best for internal tools, offline applications, or situations where traditional deployment isn't feasible.
Link to section: Troubleshooting Common IssuesTroubleshooting Common Issues
Navigation not working: Ensure all internal links use the /#/
prefix. Links like /about
won't work with hash routing.
Assets not loading: Check your assetsInlineLimit
configuration and verify assets are being inlined properly during build.
LocalStorage not persisting: Remember that localStorage
is domain-specific. Apps opened via file://
protocol have different storage behavior than http://
served apps.
Build size too large: Consider splitting your application or using external CDN resources for large assets instead of inlining everything.
Link to section: Migrating Existing ApplicationsMigrating Existing Applications
Modern Svelte 5 applications can be easily converted to portable apps. Use the Svelte CLI migration tools to update your project structure:
npx sv migrate svelte-5
Then apply the hash routing and inline bundling configurations shown in this guide. Most client-side functionality transfers directly, but server-side logic needs refactoring to client-side equivalents.
This approach opens new deployment possibilities for SvelteKit applications, making them truly portable across any hosting environment or distribution method. Whether you're building internal tools, demos, or applications for environments with hosting restrictions, these techniques provide unprecedented flexibility for SvelteKit deployment.