SvelteKit SEO in Production: Prerendering, Dynamic Meta Tags, and Technical SEO Lessons from Four Live Sites
What actually breaks search visibility in SvelteKit apps, and the rendering, head, and crawlability patterns we use to fix it.
2026-08-03 · By Filip Lauc
Does SvelteKit have good SEO out of the box?
SvelteKit is SEO-capable by default but not SEO-complete. It ships server-side rendering, so crawlers receive real HTML on first request. What it does not give you automatically is per-route rendering strategy, canonical URLs, dynamic Open Graph tags, sitemaps, structured data, or trailing-slash consistency. Those are all decisions you make.
The failure mode we see most often is not missing SSR. It is a SvelteKit site where SSR technically works, but the content that matters arrives after hydration. A page load returns a shell, then a client-side Firestore query fills in the product list, the article body, or the team bios. Crawlers can execute JavaScript, but they do it on their own schedule and with their own budget. If your primary content depends on a client-side fetch, you are gambling on that budget.
The fix is architectural rather than cosmetic. Content that search engines should index belongs in a load function so it is part of the server response, and the route needs an explicit rendering decision. Everything else, the meta tags, the sitemap, the JSON-LD, is downstream of getting rendering right. In the four sites we open sourced (Agrimatco, Genos, Bioinspekt, Human Glycome), rendering strategy was the first thing we settled per route group, before any head management work.
Prerender vs SSR: how to decide per route in SvelteKit
Prerender any route whose HTML is identical for every visitor and changes only when content changes. Use SSR for routes that depend on request context, user session, search parameters, or data that must be fresh on every hit. Use client-side rendering only for genuinely private, non-indexable surfaces like an admin panel.
In practice this splits cleanly. Marketing pages, product category pages, research and publication pages, contact pages, and blog posts are prerender candidates. Their content lives in Firestore, but it is edited through a CMS and republished, so a build-time snapshot is correct until the next publish. Cart, checkout, account, and search-results routes are SSR or client-rendered because their output is per-user or per-query. A CMS back office is `export const ssr = false` territory, plus a `noindex` header, because there is nothing to crawl and nothing you want crawled.
The operational catch with prerendering a CMS-backed site is invalidation. If content is edited in Firestore and pages are prerendered at build time, an edit does not reach production until a rebuild runs. We solve that with a deploy trigger from the CMS, so publishing content kicks off a build rather than relying on someone remembering. If your editorial cadence is high enough that build-per-publish is painful, SSR with a CDN cache and explicit cache headers is the better trade, and you keep prerendering for the routes that almost never change.
Managing dynamic meta tags, canonicals, and Open Graph in SvelteKit
Return SEO data from each route's load function and render it in a single reusable head component. That keeps title, description, canonical URL, Open Graph, and Twitter Card tags in one place, guarantees they are present in the server-rendered HTML, and means a new route cannot silently ship without them.
The anti-pattern is scattering `<svelte:head>` blocks across every page and hoping each one is complete. Titles drift, descriptions get copy-pasted, canonicals get forgotten, and Open Graph images end up pointing at relative paths that social scrapers cannot resolve. A single `<Seo>` component with typed props fixes this structurally. Layouts supply defaults such as site name, default share image, and locale, and individual pages override only what differs. Because the data comes from `load`, it is in the initial HTML rather than being patched in after hydration.
A few details are worth being pedantic about, because they cause duplicate-content problems that are hard to notice from inside the app.
Sitemaps, robots.txt, and crawlability with dynamic routes
Generate your sitemap from the same data source that generates your routes, served from a `+server.ts` endpoint at `/sitemap.xml` with an XML content type. Hand-maintained sitemap files go stale within a sprint. A generated one stays accurate because adding content to the CMS automatically adds it to the sitemap.
For a Firestore-backed site, the endpoint queries the published collections, maps documents to absolute URLs with `lastmod` from the document's updated timestamp, and returns the XML string. If the site is fully prerendered, prerender the sitemap endpoint too so it becomes a static file at build time. If it is SSR, add a cache header so you are not running the same set of Firestore reads for every crawler request, which is a real cost line item, not a theoretical one.
Robots.txt deserves the same treatment as any other route: keep it in the repo, not in a hosting console where nobody can review changes. Point it at the sitemap, disallow the admin and API surfaces, and be careful about staging. A staging deploy that shares a domain pattern with production and lacks a blanket disallow is one of the fastest ways to get duplicate content indexed.
- • Generate `/sitemap.xml` from CMS data, include `lastmod`, and exclude drafts and unlisted routes
- • Prerender the sitemap endpoint for static sites, cache it for SSR sites
- • Keep `robots.txt` in source control and reference the absolute sitemap URL
- • Block admin, API, and preview routes explicitly, and send `X-Robots-Tag: noindex` on staging environments
- • Pick one trailing-slash convention in `svelte.config.js` and make canonicals match it
Structured data and Core Web Vitals pitfalls specific to SvelteKit
Add JSON-LD as a server-rendered script tag built from the same load-function data you use for meta tags, choosing schema types that match the page's actual purpose: Organization and WebSite on the homepage, Product and Offer on commerce pages, Article for posts, and ScholarlyArticle or Dataset for research content. Never inject JSON-LD client-side.
The Core Web Vitals traps in SvelteKit are less about the framework and more about how easy it is to undo its advantages. Svelte ships small bundles, so the biggest LCP contributor is usually an unoptimized hero image or a self-hosted font loaded without `font-display: swap` and without a preload. CLS almost always comes from images rendered without explicit width and height, or from a layout that shifts once a client-side fetch resolves. The second case is worth calling out because it is a rendering-strategy bug wearing a performance costume: if content pops in after hydration, you have both a layout shift and an indexing risk from the same root cause.
On Firebase-hosted SvelteKit sites, watch the cold-start boundary. Routes that fall through to a Cloud Function for SSR pay a startup cost that shows up as slow TTFB in field data, which then drags LCP. Prerendering the routes that do not need per-request logic removes those routes from the function path entirely, which is the cheapest performance win available and one more reason to make the prerender decision deliberately rather than by default.
A practical SvelteKit SEO checklist we run before launch
Before any SvelteKit site goes live, we verify five things: every indexable route returns complete content in view-source, every route has a unique title, description, and self-referencing canonical, the sitemap and robots.txt resolve and agree with each other, structured data validates, and field-realistic Core Web Vitals are measured on a throttled connection rather than on a developer laptop.
The most useful single check is disabling JavaScript and loading the site. Whatever renders is roughly what a crawler can rely on without spending extra budget. If your product descriptions, article bodies, or navigation vanish, that is your work queue. The second most useful check is fetching your own URLs with curl and reading the raw HTML, which surfaces canonical mistakes, absolute-versus-relative Open Graph image paths, and trailing-slash redirects that browser devtools tend to hide.
None of this is exotic, and that is the point. SvelteKit gives you the rendering primitives; the SEO outcome depends on making per-route decisions explicitly and keeping head data, sitemaps, and structured data derived from a single source rather than duplicated by hand. We have applied the same set of patterns across public sites and CMS-backed platforms, and the source for several of them is open if you want to see the wiring rather than read about it.
- • Load the site with JavaScript disabled and confirm primary content and navigation are present
- • curl each key template and check title, description, canonical, and absolute OG image URLs in the raw HTML
- • Confirm `/sitemap.xml` and `/robots.txt` return correct content types and consistent URLs
- • Validate JSON-LD with a schema testing tool for each distinct page type
- • Measure LCP, CLS, and INP on mobile throttling, then check TTFB on SSR routes for cold-start cost
- • Verify staging is blocked from indexing and production redirects resolve in one hop
Key Takeaways
- • SvelteKit gives you SSR, not SEO. Rendering strategy, canonicals, sitemaps, and structured data are all explicit decisions per route.
- • Prerender routes whose HTML is the same for everyone, SSR routes that depend on request or session, and disable SSR only for non-indexable admin surfaces.
- • Return SEO data from load functions into one reusable head component so meta tags are server-rendered and impossible to forget on new routes.
- • Generate sitemap.xml from the same CMS data that generates routes, and keep robots.txt in source control with staging explicitly blocked.
- • Most SvelteKit CWV problems trace back to unoptimized images, unpreloaded fonts, or content arriving after hydration, which is also an indexing bug.
If you want to see these rendering and head-management patterns in a complete production codebase, the Agrimatco source on GitHub is a full SvelteKit and Firebase site we open sourced, alongside our Genos, Bioinspekt, and Human Glycome repositories.
Frequently Asked Questions
Is SvelteKit good for SEO compared to Next.js?
Both are equally capable for SEO because both server-render HTML and support static prerendering. SvelteKit tends to ship smaller JavaScript bundles, which helps interaction metrics, while Next.js has a larger ecosystem of prebuilt SEO and image-optimization tooling. The deciding factor is almost never the framework, it is whether your team makes per-route rendering decisions and keeps meta tags server-rendered.
Can Google index a client-side rendered SvelteKit app?
Google can render JavaScript and often will index a client-rendered SvelteKit app, but it is a slower, less reliable path that consumes crawl budget, and other crawlers including many social and AI scrapers do far less JavaScript execution. If a page matters for search or link previews, its content should be in the server response. Reserve client-only rendering for authenticated or admin routes.
How do I make prerendered SvelteKit pages update when CMS content changes?
Trigger a rebuild and redeploy from your CMS publish action, using a webhook to your CI pipeline or hosting provider. That keeps prerendered HTML in sync without manual deploys. If content changes many times a day, switch those routes to SSR behind a CDN cache with short revalidation and keep prerendering for rarely edited pages.
Do I need a separate SEO library for SvelteKit meta tags?
No. A small typed component wrapping `<svelte:head>`, fed by data from your load functions, covers titles, descriptions, canonicals, Open Graph, Twitter Cards, and JSON-LD in under a hundred lines. Libraries are convenient but add a dependency for logic you will want to control anyway, especially around canonical URL construction and trailing-slash consistency.
Why is my SvelteKit site slow on Firebase Hosting even though the bundle is small?
Usually because SSR routes fall through to a Cloud Function that pays a cold-start penalty, which shows up as high TTFB and drags LCP down. Prerendering routes that do not need per-request logic removes them from the function path entirely. Also check for unoptimized hero images and self-hosted fonts loaded without preload, which dominate LCP on otherwise lean Svelte builds.
Sources
- 1. svelte.dev
- 2. web.dev
- 3. teta.so
- 4. okupter.com
- 5. rodneylab.com
Written by
Filip Lauc
CEO, Jaspero
Filip Lauc is the CEO of Jaspero, a software development agency based in Osijek, Croatia. A full-stack JavaScript developer with over a decade of experience across Angular, Svelte, and Node.js, he leads Jaspero's work as a long-term embedded engineering partner for clients like GlycanAge, where his team has served as the dedicated engineering team for six years.
Let's Build Together
Your vision,
our expertise.
From AI integration to full-stack development, we turn ambitious ideas into products that perform.