Prerendering Routes for SEO in SvelteKit
Prerender your content routes to static HTML so crawlers receive complete markup on the first byte instead of an empty shell waiting on hydration.
When to Use This Approach
Reach for prerendering when your SvelteKit routes match any of these conditions:
- The content is the same for every visitor — blog posts, docs, marketing pages, and CMS-driven articles that do not depend on request-time data.
- Crawler TTFB matters — static files served from the CDN have no server render latency, which helps ISR vs SSG vs CSR routing decisions land on the fastest option.
- You want build-time guarantees — a failed prerender surfaces broken data at build, not silently as a blank page in production.
Implementation Steps
Step 1: Enable prerendering on the route
Mark the route for static generation. Placing this in a +layout.ts prerenders the entire subtree; placing it in +page.ts scopes it to one page.
// src/routes/blog/[slug]/+page.ts
export const prerender = true;
export const ssr = true;
Validation: Run npm run build and confirm the build log lists the route under “Prerendered” rather than “Server” pages.
Step 2: Enumerate dynamic params with entries()
The builder cannot guess dynamic segments, so provide them explicitly:
// src/routes/blog/[slug]/+page.ts
import type { EntryGenerator } from './$types';
import { getAllSlugs } from '$lib/cms';
export const entries: EntryGenerator = async () => {
const slugs = await getAllSlugs();
return slugs.map((slug) => ({ slug }));
};
Validation: curl -s http://localhost:4173/blog/known-slug | grep '<h1' after npm run preview — the heading must appear in the raw response.
Step 3: Set the trailing-slash policy
Fix one URL form so the builder never emits both /blog/my-post and /blog/my-post/:
// src/routes/+layout.ts
export const trailingSlash = 'never';
Validation: After building, confirm the output contains blog/my-post.html and not a nested blog/my-post/index.html directory form.
Step 4: Configure the adapter output
Point the adapter at your deploy target so the emitted files ship to the CDN:
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({ pages: 'build', assets: 'build', precompress: true }),
},
};
Validation: ls build/blog/ should list one .html file per slug returned by entries().
Step 5: Verify the emitted HTML files
Confirm each prerendered file carries the SEO tags and content before deploying:
# Check a sample of prerendered files for the canonical tag and heading
for f in build/blog/*.html; do
grep -q 'rel="canonical"' "$f" || echo "MISSING canonical: $f"
done
Validation: The loop prints nothing when every file contains a canonical tag; any printed line is a page to fix before promotion.
SEO Impact Summary
| Signal | What improves | What breaks if misconfigured |
|---|---|---|
| First-byte HTML | Crawlers receive complete markup with no hydration wait | ssr = false alongside prerender emits an empty shell |
| Crawl coverage | Every slug in entries() becomes an indexable static file |
Missing slugs in entries() are never generated and return 404 |
| URL consistency | trailingSlash fixes one canonical form |
Mixed slash policy creates duplicate URL variants |
| TTFB | CDN-served static files respond fastest | Falling back to SSR reintroduces server render latency |
Measurable signals to watch:
- Build log: prerendered page count should equal the number of slugs returned by
entries(). - GSC Coverage: prerendered URLs should move to “Indexed” within 1–2 crawl cycles with no “Discovered – currently not indexed” backlog.
- Response headers: prerendered routes should return
age/CDN-cache headers rather than a dynamic server signature.
Edge Cases and Gotchas
Preview and draft routes must not be prerendered
If a preview route depends on request-time draft tokens, prerendering it will bake a stale or empty snapshot into a static file. Exclude preview paths from entries() and leave them as on-demand SSR so they are never emitted to the public build.
Client-side navigation can mask a missing entry
A slug reachable through in-app navigation may appear to work in the browser even when it was never prerendered, because SvelteKit renders it client-side. Always test with a fresh curl to the built file — if the file is absent, the crawler gets a 404 regardless of what the SPA shows.
Incremental content needs a rebuild or a fallback Pure prerendering captures content at build time only. When the CMS publishes new entries, either trigger a rebuild or move volatile sections to an SSR route. For a hybrid model, compare the static-versus-dynamic tradeoffs in controlling static and dynamic rendering in Next.js App Router, which maps closely onto SvelteKit’s page options.
prerender = true with dynamic data fetch can fail the build
If a load function calls an endpoint that is unreachable at build time, the prerender crashes the whole build. This is a feature, not a bug — it surfaces broken data early. Guard fetches with clear errors and make the CMS reachable from the CI build environment.
Frequently Asked Questions
How do I prerender dynamic routes in SvelteKit?
Set export const prerender = true on the route and export an entries generator that returns every param combination, such as one object per slug fetched from your CMS. The builder cannot discover dynamic paths on its own, so entries() is what tells it which pages to emit. Keep the generator’s data source reachable from CI so the build does not fail.
Does prerender emit static HTML?
Yes. Prerendering runs the route during the build and writes a fully rendered .html file to the output directory. That file is served directly from the CDN, so crawlers get complete markup with no server compute and no dependence on client-side hydration.
How do I set trailing slash policy?
Export const trailingSlash with a value of never, always, or ignore from the route or a shared layout. Setting it to never emits /blog/my-post and keeps a single canonical URL form, which prevents the crawler from indexing both slashed and unslashed duplicates.
Part of: SvelteKit SEO Configuration
Related
- Controlling Static and Dynamic Rendering in Next.js App Router — the App Router equivalent of choosing static versus dynamic per route
- ISR vs SSG vs CSR Routing — decide which routes belong in a static prerender pass
- SvelteKit SEO Configuration — the full metadata, adapter, and sitemap setup this page fits into