SvelteKit – SPA vs. SSG

This one has tripped me a few times so I thought I’d make a note about it. When reading or learning SvelteKit you might have come across two terms – SPA and SSG. They seem related but are not. SPA and SSG are disjoint concepts and are not mutually exclusive. It is possible to have all 4 combinations below:

SPASSGWhat it means
00
01
10
11

What SPA means – SPA is equivalent to putting:

export ssr = false

in src/routes/+layout.js. A Single Page Application as its name suggests exists as a single HTML file with references to JS chunks. The sub-pages (paths if you will) like /foo, /bar etc. are virtual in the sense that the JS application intercepts them and routing happens client-side – there is no call to the server when you request /foo, /bar etc. The SPA is bad for SEO but for internal applications in a company accessed by company-only employees, SEO does not matter. For completeness and accuracy we should add that API (XHR) calls still go to server (backend). SPA does not mean you cannot make any backend calls. The key point is that when you click on a link within the application, it handles it completely client-side. If you type the link in the browser address bar that’s different – the browser will download the application again (from its cache) and the app will start (boot) all over again.

Now let’s talk about SSG. SSG is equivalent to using adapter-static in svelte.config.js. The adapters influence what happens when you run npm run build. Adapters have no effect when you run npm run dev or npm serve. A static adapter can be used when there is no server-side logic in the SvelteKit app i.e., its a pure browser application. There is either no backend or (more likely) the backend is being developed and run as a separate app (Express.js for example). The static adapter will generate HTML+JS when you run npm run build. This HTML+JS has to be served somehow and that is outside the scope of static adapter. We assume you will host the HTML+JS on a NGINX server for example or any other way you wish.

Now as exercise challenge yourself and fill in the table above.

Now-a-days AI is really good and you can get all the answers using AI eliminating the need for blog posts. See below for what happens if I have server-side logic in a sveltekit app and try to use adapter-static. Does it still work?

No, it generally won’t work as expected if you have real server-side logic (things that need to run dynamically on the server at request time).

What adapter-static does

adapter-static turns your SvelteKit app into a pure static site (or SPA with a fallback). During npm run build, it:

  • Prerenders all pages into static HTML files.
  • Outputs only HTML, CSS, JS, and any prerendered JSON/endpoint files.
  • Deploys to any static host (Netlify, Vercel static, GitHub Pages, Cloudflare Pages, Surge, etc.).

There is no server runtime at all after the build.

What happens to server-side logic

Type of server-side code What happens with adapter-static Does it work?
+server.js / +server.ts endpoints Stripped out by default. Cannot run dynamically. No (unless export const prerender = true;)
+page.server.js / +layout.server.js Server load functions are stripped. Data won’t load at runtime. No (unless prerendered)
Universal load in +page.js Runs at build time during prerendering (if possible). Client-side version may run on navigation. Partially (static data only)
Dynamic server features (DB queries, auth, secrets, POST/PUT, etc.) Impossible — no server to execute them. No
Forms / actions Won’t work unless everything is prerendered or you move logic to an external backend. Usually no

By default, the build will fail with an error like:

@sveltejs/adapter-static: all routes must be fully prerenderable (unless using the 'fallback' option)

This is a safety check to prevent you from deploying a broken app where some routes are missing or non-functional.

When it can work (limited cases)

You can make some server-side code compatible if you prerender it at build time:

  • Add export const prerender = true; to the route (or to the root +layout.js to apply broadly).
  • For +server.js endpoints, this generates static files (e.g. /api/data.json becomes a static JSON file).
  • For server load functions, data is baked into the HTML at build time.

This works well for static sites (blogs, docs, marketing pages) or pages with data that rarely changes.

If the data must be dynamic (user-specific, real-time, depends on request time), it cannot work with adapter-static.

Recommended alternatives

  • Keep server-side logic → Use a different adapter:
    • @sveltejs/adapter-node (self-hosted Node server)
    • Platform-specific ones (Vercel, Netlify, Cloudflare, etc.) for serverless/edge functions.
  • Move the backend elsewhere → Keep adapter-static for the frontend and call an external API (REST, GraphQL, Supabase, your own Node/Express/FastAPI backend, etc.).
  • For SPA mode (client-side only routing) → You can use adapter-static with fallback: 'index.html' and export const ssr = false; in your root layout, but you still cannot have server-only logic.

Quick test / debugging tip

npm run preview often hides these issues because it still runs a dev-like server. Always test a real static build:

npm run build
npx serve ./build   # or http-server, python -m http.server, etc.

In short: adapter-static is great for fully static content, but it’s incompatible with dynamic server-side logic unless you prerender everything at build time or move that logic to a separate backend. If your app needs real server capabilities, switch to a non-static adapter.

This entry was posted in Software, Computers, programming and tagged , . Bookmark the permalink.

Leave a comment