SvelteKit load Functions for Meta and Canonical
SvelteKitβs load functions are where a routeβs data β and therefore its metadata β should originate. Feed a load result into a svelte:head block and every route emits its own title, description, and canonical in the server-rendered HTML, then keeps them correct as the user navigates client-side. This article deepens the SvelteKit SEO and routing guide by focusing on the load-to-head pipeline and canonical normalization.
Problem Statement
SvelteKit offers two flavors of load, and choosing correctly matters for both correctness and where the code runs. A server load (in +page.server.ts) executes only on the server; it can read request headers, touch a database, and use secrets, and its returned data is serialized into the server-rendered HTML. A universal load (in +page.ts) runs on the server for the first request and then on the client for subsequent navigations. Either can feed metadata, because the first paint is always server-rendered β but the canonical URL is where teams slip, computing it on the client or emitting the raw request URL complete with tracking parameters.
The dependable pattern is to derive the canonical from the request URL inside load, strip the parameters that create duplicates, return it as data, and render it through svelte:head. Because svelte:head is written into the HTML during server rendering and also updates reactively on navigation, the metadata is right in the first response and stays right afterward.
Step-by-step fix
-
Return metadata from
load, canonical included. Build the canonical fromurl, stripping parameters that create duplicates, and return it alongside content.// src/routes/articles/[slug]/+page.ts import type { PageLoad } from './$types'; export const load: PageLoad = async ({ params, url, fetch }) => { const res = await fetch(`/api/articles/${params.slug}`); const article = await res.json(); const canonical = `${url.origin}/articles/${params.slug}`; return { article, canonical }; }; -
Use a server
loadwhen the data is server-only. For database access or request headers, move the function to+page.server.ts; the returned data still lands in the server HTML.// src/routes/articles/[slug]/+page.server.ts import type { PageServerLoad } from './$types'; import { error } from '@sveltejs/kit'; export const load: PageServerLoad = async ({ params, url }) => { const article = await db.article.find(params.slug); if (!article) throw error(404, 'Not found'); return { article, canonical: `${url.origin}/articles/${params.slug}` }; }; -
Render the head from that data in
svelte:head. The block is server-rendered into the HTML and updates reactively on navigation.<!-- src/routes/articles/[slug]/+page.svelte --> <script lang="ts"> export let data; </script> <svelte:head> <title>{data.article.title} | Example</title> <meta name="description" content={data.article.summary} /> <meta property="og:title" content={data.article.title} /> <link rel="canonical" href={data.canonical} /> </svelte:head> <article> <h1>{data.article.title}</h1> </article> -
Normalize tracking and pagination parameters. Strip them before building the canonical so variant URLs consolidate to one.
const clean = new URL(url); ['utm_source', 'utm_medium', 'utm_campaign', 'ref'].forEach((p) => clean.searchParams.delete(p)); const canonical = `${clean.origin}${clean.pathname}`; -
Emit JSON-LD from the same data. Render a structured-data script in the component so it ships in the server HTML.
<svelte:head> {@html `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', headline: data.article.title })}</script>`} </svelte:head>
Validation
Confirm the head is in the server response and that navigation keeps the canonical aligned with the current route.
- Server HTML has the route title.
curl -sL https://example.com/articles/first/ | grep -i "<title>"returns the article-specific title. - Canonical is normalized. Request the route with a
?utm_source=xsuffix and confirm the responseβs canonical omits the parameter. - JavaScript disabled still shows the head. Disable JavaScript and view source; title, description, and canonical remain because
svelte:headwas server-rendered. - Client navigation updates the canonical. After a client-side transition, read the canonical from the DOM; it should match the new URL.
// After client navigation, assert the canonical follows the route
console.assert(
document.querySelector('link[rel="canonical"]').href === location.origin + location.pathname,
'Canonical did not track the client navigation'
);
Common Pitfalls
- Computing the canonical in
onMount. A client-only canonical is missing from the server HTML the crawler indexes first; build it inload. - Emitting the raw request URL. Returning the URL with tracking parameters as canonical lets each variant claim canonical status, splitting signals.
- Choosing the wrong
loadflavor. Putting server-only work in a universalloadleaks it to the client or fails on navigation; use+page.server.tsfor secrets and database access. - Setting the title with
document.title. Imperative assignment in a client effect bypasses SSR; always usesvelte:head. - Forgetting a per-route description. Relying on a layout-level default gives many routes the same description; return one from each routeβs
load.
Frequently Asked Questions
What is the difference between server and universal load in SvelteKit for SEO?
A server load runs only on the server, so it suits secrets, database access, and reading request headers, and its data is embedded in the server-rendered HTML. A universal load runs on the server for the first request and then on the client during navigation. For SEO metadata, both work because the first response is server-rendered; choose server load when the data needs server-only resources and universal load when it can run in either place.
How do I set a canonical URL in SvelteKit load?
Build the canonical from the request URL in a load function, stripping tracking and pagination parameters, and return it as load data. The page component then reads that value and renders a canonical link inside svelte:head. Computing it in load keeps the canonical correct in the initial server HTML and consistent when the client navigates between routes.
Does svelte:head metadata work without JavaScript?
Yes. During server rendering SvelteKit writes the svelte:head contents into the document head of the HTML response, so a crawler that runs no JavaScript still sees the title, description, and canonical. The same svelte:head block also updates reactively during client-side navigation, so metadata stays correct in both the initial load and subsequent transitions.
Related
- SvelteKit SEO and Routing Guide β the routing and rendering context for these load patterns.
- Prerendering SvelteKit Routes for Search Engines β when to prerender the routes these load functions feed.
- Canonical URL Management in SPAs β the canonical normalization strategy applied here.
β Back to SvelteKit SEO and Routing Guide