Deploying a Static SvelteKit Application on a Website's Subdirectory
2026/07/08
The goal
-
I want to build a static SvelteKit application and
plop those built files into my web server so the
application can be served from the endpoint
[site-url]/piano. - I want to keep all these repositories separate to isolate dependencies and minimize the amount of overhead in running the web server.
The problems
- SvelteKit is ideal for rapidly developing full-stack web applications that require interaction with the web server, but my use case isn't that. This application runs entirely locally, so I'm looking for a tool that makes a bunch of static HTML, CSS, and JS files I can just drag and drop into the folder my web server serves up.
-
Since the app will be nested in a subdirectory of
the web server's root folder, we'll need to
point all links to that directory by prepending
every
hrefwith/piano, including links in the HTML head pointing to CSS and JS assets. -
We'll need a way to copy the built SvelteKit
files into the
piano/subdirectory of the web server.
The solution
First, use
@sveltejs/adapter-static
to build the application in its own repository.
-
In the SvelteKit app's folder, run
npm i @sveltejs/adapter-static. -
Modify your
vite.config.js|ts(which is where SvelteKit has moved its configuration in an admirable effort to cull some of our many root-level configuration files):
import adapter from "@sveltejs/adapter-static";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
sveltekit({
compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) =>
filename.split(/[/\\]/).includes("node_modules")
? undefined
: true,
},
adapter: adapter(),
paths: {
base: "/piano", // Change this field to your target subdirectory
relative: false,
},
}),
],
});
The docs say
the base property under the
paths property allows us to specify a value
for SvelteKit to prepend to all of its assets, and the
relative flag being off ensures
%sveltekit.assets% and references to build
artifacts will always be root-relative paths. The docs
also note the next step:
"Note that you need to prepend all your root-relative links with the base value or they will point to the root of your domain, not your base (this is how the browser works)"
Awkwardly, it then suggests using base from
$app/paths, which appears to be deprecated.
Fret not, we can accomplish this with a simple helper
function.
-
Make a file called
make-path.tsor similar (or add it to your junk drawer ofutils.ts).
import { PUBLIC_BASE_URL } from "$env/static/public";
export const makePath = (path: string): string => `${PUBLIC_BASE_URL}${path}`;
-
This is the tedious part. Modify all your
hrefattributes to call this function, like in this sample consumer:
<script lang="ts">
import { makePath } from "$lib/make-path";
</script>
// BEFORE
<a class="back-home" href="/">← Back home</a>
// AFTER
<a class="back-home" href={makePath("/")}>← Back home</a>
Voila! At this point, run your development server and
note how all the URLs have the proper, prepended path.
When you run npm run build, your bundled
app should now be ready to serve!
- Copy the built files to the subdirectory of the web server.
The last thing to do is to modify your build process to
copy over the SvelteKit app's built files into the
intended subdirectory. This step is going to differ
based on how your build process is configured. In my
case, this is a simple
npm script
that I run after building the frontend repository.
Currently, this script looks for an environment variable
called SITE_SOURCE, and if present, copies
the contents of that path into the
dist directory of the web server, which is
the folder that's served up online.
That makes the process for updating and deploying the website on my VPS look like:
# Build the frontend
cd apps/jthome
git pull
npm run build
# Collect the files
cd ../jtwebserver
git pull
npm run collect
# Restart the server with the new files included
pm2 restart jtserver
I like this pattern because no repository paths are
hard-coded; wherever the SITE_SOURCE may be
on the target machine doesn't matter, and if no
environment variable is set, the script doesn't run
and prints an error.
Anticipating future use cases in which I'd like to
deploy other isolated applications under subdirectories,
I created a new file called modules.js,
which will be in charge of looking for external
repositories' content and copying them over to the
web server if available.
import fs from "fs";
const MODULES = [
{
destination: "./dist/piano",
enabled: true,
execute: false, // Import and run the module when true, otherwise just copy files
source: "../piano-curriculum/build",
},
];
const copyModule = ({ destination, enabled, source }) => {
if (!enabled) {
return;
}
if (!fs.existsSync(destination)) {
fs.mkdirSync(destination);
}
fs.cpSync(source, destination, { recursive: true });
console.log(`Copied ${source} to ${destination}`);
};
const copyModules = () => {
if (MODULES.every((module) => module.enabled === false)) {
console.log("No modules enabled");
return;
}
MODULES.forEach((module) => copyModule(module));
};
export default copyModules;
Then, invoke this copyModules() method in
the script the "collect" script:
...
// Pull from SITE_SOURCE
console.log("Copying...");
fs.cpSync(siteSource, distDirectory, { recursive: true });
console.log(`Copied files from ${siteSource} to ${distDirectory}!`);
// Copy modules if enabled
copyModules();
The MODULES array contains any information
about any external repositories whose build artifacts
I'd like to copy to the web server. The
enabled property can be edited before
running the script if I want to fine-tune which modules
will be copied over. The execute property
is currently unused, but was put in place in case I have
server-side logic that needs to run to deploy an
external application. In the case of this SvelteKit
application, I just need to copy files over to the
destination and it should "just work".
In the future, adding another external app to my web
server will simply entail adding another object to
MODULES.
Comments
SvelteKit isn't intended for making static,
self-contained apps with just client-side logic, but it
can certainly be used in this way! I appreciate
Svelte's approach of opting for a compiler instead
of a runtime dependency like React (without the new
React Compiler) or Vue. This allows programming in a
powerful superscript of JavaScript but without the
performance detriments or dependency management
headaches that typically accompany JavaScript
frameworks. I did struggle with state management and
I'm wondering if I'm misusing or overusing the
$derived() rune.