Head & SEO
Furin's head() API lets you control <head> content per page and route. It runs on the server during SSR/SSG/ISR and is re-evaluated during SPA navigation so the document title and meta tags stay correct.
Basic usage
Define head() on a page or route:
export default route.page({
head: ({ post }) => ({
meta: [
{ title: post.title },
{ name: "description", content: post.excerpt },
],
}),
component: ({ post }) => <article>{post.title}</article>,
});
The head() function receives the same props as the page component (loader data, params, query).
MetaDescriptor
The meta array accepts the following descriptor shapes:
Title
{ title: "My Page" }
Renders <title>My Page</title>.
Charset
{ charSet: "utf-8" }
Renders <meta charset="utf-8" />.
Standard meta tag
{ name: "description", content: "A great page" }
Renders <meta name="description" content="A great page" />.
Open Graph / Twitter Cards
{ property: "og:title", content: "My Page" }
Renders <meta property="og:title" content="My Page" />.
HTTP equiv
{ httpEquiv: "refresh", content: "30" }
Renders <meta http-equiv="refresh" content="30" />.
JSON-LD
{ "script:ld+json": {
"@context": "https://schema.org",
"@type": "Article",
headline: "My Page",
}}
Renders <script type="application/ld+json">{...}</script> with safe JSON encoding.
Custom tags
{ tagName: "meta", "data-custom": "value" }
Renders <meta data-custom="value" />.
Links
Add <link> tags for stylesheets, canonical URLs, favicons, or preload hints:
head: () => ({
links: [
{ rel: "canonical", href: "https://example.com/blog/my-post" },
{ rel: "stylesheet", href: "/custom.css" },
{ rel: "icon", href: "/favicon.ico" },
],
});
Inline scripts and styles
You can inject inline scripts and styles. Security warning: children is injected as raw HTML — never pass user-controlled or loader-derived data without sanitisation.
head: () => ({
scripts: [
{ type: "module", children: `console.log("page loaded");` },
],
styles: [
{ children: `body { background: #fafafa; }` },
],
});
SPA navigation
During client-side navigation, Furin extracts the new head() result, updates document.title, and re-renders the <head> content. This ensures:
- Title changes reflect in the browser tab
- Meta tags update for social sharing previews
- Analytics scripts fire correctly
export const route = createRoute({
layout: ({ children }) => (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/* Page-specific head is injected here automatically */}
</head>
<body>{children}</body>
</html>
),
});
SEO checklist
A complete SEO setup for a blog post:
export default route.page({
head: ({ post }) => ({
meta: [
{ title: post.title },
{ name: "description", content: post.excerpt },
{ property: "og:title", content: post.title },
{ property: "og:description", content: post.excerpt },
{ property: "og:type", content: "article" },
{ property: "og:url", content: `https://example.com/blog/${post.slug}` },
{ name: "twitter:card", content: "summary_large_image" },
{ "script:ld+json": {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
datePublished: post.publishedAt,
}},
],
links: [
{ rel: "canonical", href: `https://example.com/blog/${post.slug}` },
],
}),
// ...
});