A powerful, lightweight solution for managing meta tags in SvelteKit applications with intelligent data cascading.
You define your metadata in +page.ts or +layout.ts load functions, using our helper functions for simpler code - like so:
// src/routes/+layout.ts
import { metaLoad } from "@opensky/seo";
export const load = metaLoad.layout({
sitename: "My Awesome Site",
title: "Home",
titleTemplate: "My Awesome Site - {page}",
description: "A fantastic SvelteKit application",
icon: "./favicon.png",
});Congrats! You now have seo meta tags for your entire site. Customizing meta tags from here on is easy
- Features
- Installation
- Quick Start
- Core Concepts
- Advanced Usage
- Supported Metadata Properties
- License
- Data Cascade: Define meta tags at the root level and override selectively at deeper levels
- Simple API: Straightforward helper functions to reduce boiler plate code
- Type Safe: Fully typed to make adding tags easier, with conflicting properties caught at compile time
- Zero Dependencies: No runtime dependencies; dev-only validation warnings are stripped from production bundles
- Flexible Integration: Works with SvelteKit's layout and page structure
- Performance Optimized: Works with SvelteKit parallel execution of load functions
- SEO-friendly: Fully supports server-side rendering
bun add @opensky/seo
# or
npm install @opensky/seo// src/routes/+layout.ts
import { metaLoad } from "@opensky/seo";
export const load = metaLoad.layout({
sitename: "My SvelteKit App",
title: "Welcome",
titleTemplate: "My SvelteKit App - {page}",
description: "A fantastic SvelteKit application",
icon: "./favicon.png",
});<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { SeoTags } from '@opensky/seo';
let { children } = $props();
</script>
<SeoTags />
{@render children()}// src/routes/blog/+layout.ts
import { metaLoad } from "@opensky/seo";
export const load = metaLoad.layout({
title: "Blog",
titleTemplate: "Svelte Blog - {page}",
description: "Read our latest articles and updates",
});// src/routes/blog/[slug]/+page.ts
import { metaLoadWithData } from "@opensky/seo";
export const load = metaLoadWithData.page(({ data }) => ({
title: data.post.title,
description: "This is a detailed description of my blog post",
}));And that's it! Your meta tags will be automatically generated and updated across your site.
SvelteKit Meta uses a data cascade pattern to efficiently manage metadata:
- Layout Metadata: Each layout can extend or override parent metadata. Unlike other tools, you can have many layers of layout metadata set.
- Page Metadata: Pages can further customize metadata for specific routes, overwriting and extending data inherited from layouts above the page in route tree.
Metadata flows down through your application's routing hierarchy inheriting any data above, with lower levels being able to selectively override higher-level values.
The provided helpers for writing the load functions take care of passing parent data and data from any +page.server.ts load functions down to the page. Other packages will lose the data from page server load functions, but using these helpers ensures that the page receives everything from the data cascade.
Titles are handled specially to allow you to set a title template and for that to work in a correct fashion.
SvelteKit Meta provides several helper functions to simplify integration. You still have to create +page.ts and +layout.ts files but these make it simpler to define your meta tags with minimal boilerplate code
Use these functions when you have metadata that doesn't depend on route parameters or server data.
metaLoad.page(metaTags)- Creates a page load function with static metadatametaLoad.layout(metaTags)- Creates a layout load function with static metadatametaLoad.resetLayout(metaTags)- Creates a reset layout load function with static metadata
Use these functions when you need to access route parameters, server data, or other context to generate metadata.
metaLoadWithData.page(callback)- Creates a page load function with dynamic metadatametaLoadWithData.layout(callback)- Creates a layout load function with dynamic metadatametaLoadWithData.resetLayout(callback)- Creates a reset layout load function with dynamic metadata
Use these functions when you want full control over your load function but still want to add metadata.
addMetaTags.page(metaTags)- Adds page metadata to load function return valueaddMetaTags.layout(metaTags)- Adds layout metadata to load function return valueaddMetaTags.resetLayout(metaTags)- Adds reset layout metadata to load function return value
Title templates let you define a pattern for constructing page titles. Set one in a layout, and pages below it have their titles run through the template:
// src/routes/+layout.ts - applies to every page
export const load = metaLoad.layout({
titleTemplate: "My Site - {page}",
});
// src/routes/blog/+layout.ts - applies to pages below /blog
export const load = metaLoad.layout({
title: "Blog",
titleTemplate: "Blog - {page}",
});
// src/routes/blog/[slug]/+page.ts
export const load = metaLoad.page({
title: "My Amazing Post",
});
// Title at /blog/my-amazing-post: "Blog - My Amazing Post"When you pass a plain string, the template is automatically scoped to the route of the layout that declares it. You can scope it to a different route explicitly with the object form:
titleTemplate: { route: "/blog", template: "Blog - {page}" }- A template applies to routes below its route - not the route itself. In the example above, visiting
/blogproduces "My Site - Blog" (the parent template), while/blog/my-amazing-postproduces "Blog - My Amazing Post". This lets a section layout set its own title and a template for its children without the template applying to the section's landing page. - The root template (
/) is the final fallback and applies everywhere, including the home page. - When several templates match, the most specific (deepest route) wins.
- Matching is based on SvelteKit route ids, so layouts with dynamic segments (
/blog/[slug]) work, and route groups (/(marketing)) are ignored since they don't appear in URLs. resetLayoutclears inherited templates along with the rest of the metadata cascade.
The manual addMetaTags helpers can't see your route automatically. Either use the object form with an explicit route, or pass the route id from your load function:
// +layout.ts
export async function load({ data, route }) {
return {
...data,
...addMetaTags.layout({ titleTemplate: "Docs - {page}" }, route.id),
};
}For dynamic metadata, you can use the metaLoadWithData helper to incorporate route parameters:
// src/routes/blog/[slug]/+page.ts
import { metaLoadWithData } from "@opensky/seo";
export const load = metaLoadWithData.page(async ({ params }) => {
const { slug } = params;
// Fetch article data based on slug
const article = await getArticleBySlug(slug);
return {
title: article.title,
description: article.excerpt,
image: article.featuredImage,
};
});For tags this package doesn't manage (verification meta tags, extra link elements, scripts, etc.), use Svelte's native <svelte:head> in any page or layout. It composes with the tags rendered by SeoTags:
<svelte:head>
<meta name="google-site-verification" content="..." />
</svelte:head>If you need full control over your load function, you can use the addMetaTags helpers:
// +page.ts
export async function load({ data, route, params, url }) {
const post = await fetchPost(params.slug);
return {
...data,
post,
...addMetaTags.page({ title: post.title }),
};
}
// +layout.ts
export async function load({ data, route, params }) {
const section = await fetchSection();
return {
...data,
section,
...addMetaTags.layout({ title: section.title }, route.id),
};
}The metaLoadWithData and metaLoad helpers automatically merge parent data, server data, and your meta data. If you are using addMetaTags, it is important that you handle this otherwise server data will not reach the page.
You must take in data as a destructured argument in your load function and spread it on the return value
// +page.ts
export async function load({ data }) {
/* ^ include data */
return {
...data,
/* ^ include data */
...addMetaTags.page({ title: post.title }),
};
}SvelteKit Meta supports a comprehensive range of metadata properties:
| Property | Description |
|---|---|
canonical |
Canonical URL for the page (og:url, link:rel:canonical) |
icon |
Favicon path as string or IconOpinionated object |
maskIcon |
Object with url (svg file) and optional color (hex) for Safari Pinned Tabs |
theme |
Theme color for browser UI elements (string or { light, dark } object) |
colorScheme |
Color scheme preference for the page (meta:color-scheme) |
sitename |
Name of the website (og:sitename) |
title |
Page title (warns in dev if > 70 characters) (og:title, meta:title) |
titleTemplate |
Template for child page titles, e.g. "Reviews - {page}". String (scoped to the declaring layout) or { route, template } |
description |
Page description (warns in dev if > 200 characters) (og:description, twitter:description) |
author |
Content author(s) as string or array (meta:author, og:author) |
twitterSite |
The X Account for the publishing site (twitter:site) |
twitterCreator |
The X Account for the author/creator of this page (twitter:creator) |
date |
Publication date in ISO 8601 format (og meta) |
modified |
Last modified date (og:modified-time, meta:modified, meta:last-modified) |
type |
Content type configuration (basic, article, largeImage, player or ContentTypeAdvanced) |
image |
Primary image for social sharing (string or Media object) |
images |
Multiple images for social sharing (array of Media objects) |
video |
Primary video for social sharing (string or Media object) |
videos |
Multiple videos for social sharing (array of Media objects) |
Media objects for images and videos can include:
type Media = {
url: string; // Primary URL for the media resource
secureUrl?: string; // HTTPS URL for the media resource
type?: string; // MIME type of the media
width?: number; // Width in pixels
height?: number; // Height in pixels
alt?: string; // Alt text description
};Icons can be configured with more advanced options:
type IconOpinionated = {
svg?: string;
small?: {
url: string;
size: number;
type?: string;
};
large?: {
url: string;
size: number;
type?: string;
};
};Content type can be configured with advanced options for different platforms:
type ContentTypeAdvanced = {
twitter: "summary" | "largeImage" | "player";
og: "website" | "article";
};Note: Properties like
image/imagesandvideo/videosare mutually exclusive. Providing both is a TypeScript error; if both slip through anyway (plain JS), the plural version wins.
For share images generated on the fly (a card with the post's title on it), og() points the image property at an image endpoint. The contract is plain: query params in, image out — so it works with any generator (satori, takumi, a prerendered folder of PNGs).
// src/routes/blog/+layout.ts — define the card once for the section
import { metaLoad, og } from "@opensky/seo";
export const load = metaLoad.layout({
image: og("/og", { heading: "Blog", theme: "dark" }),
});
// src/routes/blog/[slug]/+page.ts — hydrate it with page data
import { metaLoadWithData, ogParams } from "@opensky/seo";
export const load = metaLoadWithData.page(({ data }) => ({
title: data.post.title,
image: ogParams(
{ heading: data.post.title, author: data.post.author },
{ alt: `Card for ${data.post.title}` }
),
}));
// → og:image = /og?author=…&heading=…&theme=dark (route and theme inherited)og(route, params?, options?) defines the template: the endpoint route, base params, and output details (width/height default to 1200×630, type to image/png, plus alt). ogParams(params, options?) contributes params to the inherited template without restating the route. The emitted tags include og:image:width/height/type/alt automatically.
Rules, all instances of the cascade's "deepest wins per key":
og()sets the template — a deeperog()replaces the route and output options.ogParams()sets individual params — they merge per param across levels.- Params survive an
og()route change; set a param tonullto remove an inherited value. - A plain
imagevalue (string or Media object) anywhere overrides the generated template entirely. altcan be set by either helper; deepest wins.
For type safety, pass your endpoint's param shape as a generic — your card component's props type works directly:
import type { ComponentProps } from "svelte";
import type Card from "$lib/og/Card.svelte";
image: og<ComponentProps<typeof Card>>("/og", { heading: post.title });The endpoint itself is out of this package's scope — it's any request handler that turns query params into an image (see examples/sveltekit/src/routes/og/+server.ts for a minimal one; for production PNGs, render a component with an engine like takumi or satori).
Scrapers require absolute URLs for og:image, og:url, and video tags, so relative paths (/og.png) are automatically resolved against the current origin when the tags render. Absolute URLs pass through untouched.
One caveat for prerendered pages: SvelteKit uses a placeholder origin (http://sveltekit-prerender) during prerendering, so resolution is skipped there unless you configure your real origin:
// svelte.config.js
const config = {
kit: {
prerender: { origin: "https://mysite.com" },
},
};MIT License