Server-Side Rendering
Server-Side Rendering
Section titled “Server-Side Rendering”The single biggest SEO architecture decision: how is HTML delivered to the crawler?
Four strategies
Section titled “Four strategies”| Strategy | When to use | SEO impact |
|---|---|---|
| SSR (Server-Side Rendering) | Dynamic, user-specific content | Excellent — crawler gets ready HTML |
| SSG (Static Site Generation) | Rarely-changing content (blog, marketing) | Excellent + fastest LCP |
| ISR (Incremental Static Regeneration) | Many pages, occasionally updated | SSG with auto-refresh |
| CSR (Client-Side Rendering) | App-like SPAs | SEO-hostile — only when necessary |
| Hybrid | Mixed (marketing SSG, app CSR) | OK if done right |
How crawlers actually see content
Section titled “How crawlers actually see content”Modern Googlebot uses Chromium and CAN render JavaScript — but with delay and cost:
- First-pass crawl: HTML only, ranked initially based on what’s in the initial HTML response
- Second-pass crawl (rendering queue): JavaScript is executed, additional content discovered, possibly weeks later
This means:
- CSR-only sites have delayed indexing of dynamic content
- Server-rendered content ranks faster than client-rendered content
- Bing and other crawlers are less sophisticated than Google; CSR is more hostile to them
For SEO-critical pages (landing pages, blog posts, product pages), SSR or SSG is strongly preferred over CSR.
SvelteKit adapter choice
Section titled “SvelteKit adapter choice”@sveltejs/adapter-node
Section titled “@sveltejs/adapter-node”Node.js server runtime, supports SSR + ISR. Default for Nitrogen sites.
import adapter from '@sveltejs/adapter-node';
export default { kit: { adapter: adapter({ out: 'build', precompress: true // gzip + brotli at build }) }};Pairs with Docker deployment, reverse proxy (Traefik/Nginx), HTTPS via Let’s Encrypt.
@sveltejs/adapter-static
Section titled “@sveltejs/adapter-static”Pure SSG, no Node runtime needed. For small marketing sites with no dynamic content.
import adapter from '@sveltejs/adapter-static';
export default { kit: { adapter: adapter({ pages: 'build', assets: 'build', fallback: undefined, precompress: true }) }};Generates dist/ directory with static HTML files, deployable to any
static host (Cloudflare Pages, GitHub Pages, Netlify, S3).
@sveltejs/adapter-auto
Section titled “@sveltejs/adapter-auto”Auto-detects deploy platform (Vercel, Netlify, Cloudflare). NOT recommended
for self-hosted Nitrogen projects — use explicit adapter-node.
Per-page rendering directives
Section titled “Per-page rendering directives”SvelteKit lets you control rendering per page:
// +page.js or +page.server.jsexport const prerender = true; // generate HTML at build timeexport const ssr = true; // runtime SSR enabled (default)export const csr = true; // client-side hydration enabled (default)Common combinations:
SSG (prerender)
Section titled “SSG (prerender)”export const prerender = true;Page is rendered at build time, written as a static HTML file. Fastest possible delivery. Use for: landing pages, blog posts, marketing pages.
SSR (default)
Section titled “SSR (default)”// no directives — default is ssr: true, csr: truePage is rendered on each request. Required for dynamic data (user-specific content, real-time data).
Static + interactive
Section titled “Static + interactive”export const prerender = true; // HTML at build time// csr: true is default — page hydrates client-sidePage is prerendered but JavaScript hydrates for interactivity. Best of both worlds for marketing pages with interactive elements (carousels, forms).
No-JS pages
Section titled “No-JS pages”export const prerender = true;export const csr = false;Page is prerendered AND has no client-side JavaScript. Pure HTML/CSS. Use for documentation pages, blog posts where interactivity isn’t needed.
Benefits:
- Zero JavaScript download (smallest payload)
- Perfect LCP (no JS parse/execute time)
- Perfect INP (no event handlers to optimize)
- Works without JavaScript enabled (accessibility benefit)
Dynamic SSR
Section titled “Dynamic SSR”export const ssr = true;export const csr = true;// no prerender — rendered on each requestUsed when content varies per request (user-specific dashboards, search results, real-time data).
Testing what crawlers see
Section titled “Testing what crawlers see”View HTML without JavaScript
Section titled “View HTML without JavaScript”The simplest test:
# Chrome DevTools → Cmd+Shift+P → "Disable JavaScript" → reloadThen view source. If the content you care about is there → SSR/SSG is working. If not, you’re shipping CSR-dependent content.
Search Console URL Inspection
Section titled “Search Console URL Inspection”Search Console → URL Inspection → enter URL → "Test live URL" → View"Crawled page" rendered HTMLShows exactly what Googlebot sees after JavaScript execution (the second-pass crawl result).
Quick curl test
Section titled “Quick curl test”curl -A "Googlebot" -L https://example.com/page > rendered.htmlThe -A flag sends Googlebot user-agent (useful if your server delivers
different content per user-agent, which is now an anti-pattern but still
exists).
Content visibility timing
Section titled “Content visibility timing”Different SSR patterns produce different “time-to-content” for crawlers:
| Pattern | First-pass HTML | Time to indexable |
|---|---|---|
| SSG (prerender) | Full content | Immediate on crawl |
| SSR | Full content | Immediate on crawl |
| SSR + hydration | Full content | Immediate on crawl |
| CSR (SPA) | Empty shell + JS | Wait for render queue (days/weeks) |
The “render queue” delay matters because content that gets indexed faster also accumulates link equity faster, ranks better, etc. SSR’s advantage is real.
SSR-related SEO patterns
Section titled “SSR-related SEO patterns”Lazy-loaded content
Section titled “Lazy-loaded content”Content loaded after page interaction (infinite scroll, “show more” buttons) is NOT in the initial HTML. Crawlers may or may not discover it.
Best practice: lazy-load content from a server-rendered “shell” — the first batch of items is in initial HTML, more loads on interaction. Crawlers see the first batch (which has the SEO value); users see the lazy-loaded extras.
Conditional rendering
Section titled “Conditional rendering”{#if mounted} <!-- This won't be in SSR output! --> <div>Client-only content</div>{/if}If mounted is set in onMount, the SSR HTML doesn’t include this content.
Crawlers see only the SSR’d portion. Use this knowledge to deliberately keep
content out of the SSR HTML (e.g., personalization widgets).
Dynamic imports
Section titled “Dynamic imports”const Component = await import('./Heavy.svelte');Dynamic imports happen at runtime. The imported component’s content is NOT in the initial HTML. Use only for genuinely large, deferrable components.
Common pitfalls
Section titled “Common pitfalls””It looks like client rendering, but it’s SSR”
Section titled “”It looks like client rendering, but it’s SSR””If your +page.svelte uses onMount to fetch data:
<script> let data = $state(null);
onMount(async () => { data = await fetch('/api/data').then(r => r.json()); });</script>
{#if data} <h1>{data.title}</h1>{/if}This runs client-side only. The SSR HTML contains no <h1> content.
Crawlers see nothing.
Fix: use +page.server.js or +page.js load function:
export async function load() { const data = await fetchFromDB(); return { data };}<script> let { data } = $props();</script>
<h1>{data.title}</h1> <!-- now SSR'd -->Personalization breaks caching
Section titled “Personalization breaks caching”<p>Hello, {user.name}!</p>If the page is SSR’d per request with user-specific content, you can’t cache it on CDN. This kills performance.
Solutions:
- Move personalization to client-side after initial render (gives up some UX)
- Use edge personalization with cache key including user segment
- Render generic page server-side, personalize parts client-side
Hydration mismatch warnings
Section titled “Hydration mismatch warnings”If SSR output differs from client render, Svelte warns about hydration mismatch. Common cause: server-side rendering with current time, client-side rendering with different time.
Fix: ensure SSR and CSR use the same data. For time-dependent content, either show server time consistently or wait until after hydration to show client time.
Cross-references
Section titled “Cross-references”- Performance & CWV — SSR’s role in LCP
- Crawl Budget — server-side rendering reduces crawl cost
- Caching Strategies — caching SSR’d output