Server Metadata
Set page titles, meta tags, Open Graph, and lang attributes with the metadata() function.
The metadata() function lets you define SEO and head tags for each page, running on the server before load().
Basic Usage
Export a metadata function from +page.server.ts:
import type { MetadataEvent } from "bosia";
export function metadata({ params }: MetadataEvent) {
return {
title: "About — My App",
description: "Learn more about our app.",
};
}This renders a <title> tag and a <meta name="description"> tag in the page <head>.
Open Graph & Social Tags
Use the meta array to add Open Graph, Twitter Card, or any custom meta tags:
export function metadata({ params }: MetadataEvent) {
return {
title: "Blog Post",
description: "A great blog post.",
meta: [
{ property: "og:title", content: "Blog Post" },
{ property: "og:description", content: "A great blog post." },
{ property: "og:type", content: "article" },
{ name: "twitter:card", content: "summary_large_image" },
],
};
}Tags with property render as <meta property="...">, tags with name render as <meta name="...">.
Language & Link Tags
Set the <html lang> attribute and add <link> tags for canonical URLs, hreflang alternates, and more:
export function metadata() {
return {
title: "Mon Blog",
lang: "fr",
link: [
{ rel: "canonical", href: "https://example.com/blog" },
{ rel: "alternate", href: "https://example.com/en/blog", hreflang: "en" },
{ rel: "alternate", href: "https://example.com/fr/blog", hreflang: "fr" },
],
};
}Passing Data to load()
The data property lets you share fetched data with load(), avoiding duplicate queries:
import type { MetadataEvent, LoadEvent } from "bosia";
export function metadata({ params }: MetadataEvent) {
const post = await db.getPost(params.slug);
return {
title: `${post.title} — Blog`,
description: post.excerpt,
meta: [{ property: "og:title", content: post.title }],
// Pass to load() — avoids a second DB query
data: { post },
};
}
export async function load({ params, metadata }: LoadEvent) {
// Reuse data from metadata(), fall back to fresh query
const post = metadata?.post ?? (await db.getPost(params.slug));
return { post };
}The data object from metadata() becomes event.metadata in load(). If no metadata() function exists, event.metadata is null.
MetadataEvent Properties
| Property | Type | Description |
|---|---|---|
params |
Record<string, string> |
Dynamic route parameters |
url |
URL |
The request URL |
locals |
Record<string, any> |
Data set by middleware hooks |
cookies |
Cookies |
Read/write cookies |
fetch |
Function |
Fetch helper (cookies forwarded same-origin only — see Server Loaders → Cookie Forwarding) |
Metadata Return Type
| Property | Type | Description |
|---|---|---|
title |
string |
Page <title> tag |
description |
string |
<meta name="description"> tag |
meta |
Array<{ name?: string; property?: string; content: string }> |
Custom meta tags |
lang |
string |
<html lang> attribute |
link |
Array<{ rel: string; href: string; hreflang?: string }> |
<link> tags (canonical, hreflang, etc.) |
data |
Record<string, any> |
Data passed to load() as event.metadata |
All properties are optional.
Form Actions
metadata() also runs when a <form method="POST"> submit re-renders the page, so titles, meta tags, lang and the data handed to load() are the same as on a plain GET.
Redirects & Errors
You can throw redirect() or error() from inside metadata() — they behave exactly as they do in load():
import { redirect } from "bosia";
import type { MetadataEvent } from "bosia";
export function metadata({ locals }: MetadataEvent) {
if (!locals.user) redirect(303, "/login");
return { title: "Dashboard" };
}Any other error inside metadata() is logged and the page renders without metadata, rather than failing the request.
Title vs ``
Never set <title> in both metadata() and a <svelte:head> on the same route — they do not merge, and the winner differs by audience.
metadata()'s title is what ships in the SSR HTML, so scrapers, curl and non-JS crawlers read it. Svelte compiles <svelte:head><title> to a document.title write that runs on mount and on every reactive update, so the browser tab ends up showing that one instead. A route declaring both advertises one title and displays another.
Pick metadata() for anything share-critical and delete the competing <svelte:head><title>.
Client-Side Navigation
During client-side navigation, Bosia sends title, description, meta, link and lang from metadata() in the data response. The client router rebuilds the <head> from them without a full page reload, so og:*, twitter:*, canonical and robots match what a hard load of the same URL would render.
metadata.data is deliberately not sent — it feeds load() on the server and may hold values the browser should never see.
The router only replaces the tags metadata() produced (marked data-bosia-meta in the HTML). Tags you add through <svelte:head> or a plugin's head fragment are left alone.
A page whose metadata() returns no title keeps the previous page's title rather than flashing the Bosia App fallback. Give every route a title if that matters to you.
Timeouts
The metadata() function has a configurable timeout via the METADATA_TIMEOUT environment variable (in milliseconds). If metadata() takes too long, it times out gracefully and the page renders without metadata.
Caching Interaction
If metadata() calls cookies.get() or cookies.getAll(), the data response is automatically marked with Cache-Control: private, no-cache. See Server Loaders for details.