Metatags
The head tags one page needs for search results and link previews: title, description, canonical, Open Graph, and an X card. Site-level values are props too, so a small wrapper of your own holds them while each page passes only what changes.
Import
import { Metatags } from "@hyzer-labs/ui"Demo
This component renders nothing visible. Its whole output is a <svelte:head> block, and rendering it here would put a second <title> into this site's own
head, so there is no live preview. Every example below is the exact source, and the last tab
shows what it emits. It takes no class prop and forwards no other attributes, because
it renders no element to put them on.
Every prop arrives directly on the page that needs it. With no image, the
card falls back to twitter:card="summary" and Metatags emits no image tags.
<Metatags
siteUrl="https://example.com"
siteName="Example"
url="/pricing"
title="Pricing"
description="Three plans, no seat minimums."
/>Site-level values (siteUrl, siteName, a default image) live once in a small wrapper you own, not in this library. That
keeps the library framework-agnostic: $app/state is a SvelteKit import, so
it belongs in your app. url comes from page.url.pathname, and
every other prop forwards straight through. The rest of this page assumes this pattern.
<!-- src/lib/Seo.svelte: in YOUR app, not this library -->
<script lang="ts">
import { page } from '$app/state';
import { Metatags } from '@hyzer-labs/ui';
import type { ComponentProps } from 'svelte';
let props: Omit<ComponentProps<typeof Metatags>, 'siteUrl' | 'siteName' | 'url'> = $props();
</script>
<Metatags
siteUrl="https://example.com"
siteName="Example"
url={page.url.pathname}
image="/og/default.png"
imageAlt="Example"
{...props}
/><!-- any +page.svelte -->
<Seo title="Pricing" description="Three plans, no seat minimums." />The usual pattern needs nothing special: every page renders <Metatags> (or your Seo wrapper) with that page's own values, like every other example
on this page. When you navigate in the browser, SvelteKit swaps the page and Svelte's
own <svelte:head> reconciliation replaces the previous tag set with the new
one. There is nothing to clean up by hand.
The alternative is rendering it exactly once, in the root layout, fed by each page's own load function through page.data. A page returns whatever
changes:
// src/routes/blog/[slug]/+page.ts
export async function load({ params }) {
const post = await getPost(params.slug);
return { title: post.title, description: post.excerpt };
}and the layout merges those values with the site defaults:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { page } from '$app/state';
import { Metatags } from '@hyzer-labs/ui';
let { children } = $props();
</script>
<Metatags
siteUrl="https://example.com"
siteName="Example"
url={page.url.pathname}
title={page.data.title}
description={page.data.description}
/>
{@render children()}Render it in one place, not both
<Metatags> in the root layout and in an individual page
ships two full tag sets in the server-rendered HTML. Crawlers are the real audience for these
tags, and a crawler reads that raw HTML and typically keeps only the first occurrence of a
given tag. So the "override" you see while clicking around in a browser is a client-side illusion,
not what search engines and link unfurlers get. Pick one location per site.A per-page type and a per-page image, built with an og-image
endpoint's own ?message= parameter. That query string is the endpoint's contract
rather than a Metatags prop, so it stays one line at the call site. Percent-encode the value:
a raw space makes the URL invalid, and Slack then drops the preview image with no error.
<Seo
type="article"
title={post.title}
description={post.excerpt}
image={`/og.png?message=${encodeURIComponent(post.title)}`}
imageAlt={post.title}
/>Anything this component does not emit goes in the default snippet, rendered last inside
the same head block: twitter:site, robots, article:published_time, JSON-LD in a <script type="application/ld+json">. Use it for tags this component
does not manage, not to override the ones it does. A snippet that also sets og:title ships two og:title tags.
<Metatags {...base}>
<meta name="robots" content="noindex" />
<meta name="twitter:site" content="@example" />
</Metatags>Exactly what the "Minimal call" tab renders into <head>, in order:
title, description, canonical, Open Graph, then the X (Twitter) card.
<title>Pricing | Example</title>
<meta name="description" content="Three plans, no seat minimums.">
<link rel="canonical" href="https://example.com/pricing">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Example">
<meta property="og:title" content="Pricing | Example">
<meta property="og:description" content="Three plans, no seat minimums.">
<meta property="og:url" content="https://example.com/pricing">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Pricing | Example">
<meta name="twitter:description" content="Three plans, no seat minimums.">
<meta name="twitter:url" content="https://example.com/pricing">Props
| Name | Type | Default | Note |
|---|---|---|---|
siteUrl | string | — | Absolute site origin, such as "https://example.com". Any relative url, canonical, or image needs it to resolve to the absolute URL Open Graph requires. |
url | string | — | This page: a root-relative path ("/pricing") or an absolute URL. In SvelteKit this is page.url.pathname, passed by your own wrapper — see the "Per-site wrapper" demo above. |
title | string | — | The page title. Composed with siteName into one string shared by <title>, og:title, and twitter:title. |
siteName | string | — | The site's name. Emits og:site_name and is the title suffix. |
titleSeparator | string | ' | ' | Sits between title and siteName. |
description | string | — | Sets the description meta tag plus og:description and twitter:description. An empty string counts as unset. |
image | string | — | Preview image, a path or an absolute URL, resolved the same way as url. |
imageAlt | string | — | Alt text for the preview image. Setting image without it prints a console warning in development. |
type | string | 'website' | Sets og:type, emitted as given with no validation. The Open Graph protocol's type list is the reference; 'article' is the other common one. |
canonical | string | url | Canonical override for a page reachable at more than one address, path or absolute. |
twitterCard | 'summary' | 'summary_large_image' | derived from image | summary_large_image once an image resolves, summary otherwise. |
children | Snippet | — | Rendered last, inside the same head block — for tags this component does not manage (twitter:site, robots, JSON-LD, and the rest), not for overriding the ones it does. |
Accessibility
This component has no ARIA, no roles, and no focus behavior. Nothing here is interactive, but two accessibility duties still apply. <title> is the page's accessible name, both in the browser tab and in a screen reader's window list, so this component never emits an empty or invented one: give it a title, a siteName, or both. og:image:alt and twitter:image:alt give the preview image a text alternative in every client that shows one, so setting image without imageAlt prints a development-only warning.
Set a page's title once, either through this component or through the page's own <svelte:head>, never both.
References: The Open Graph protocol · X: cards markup reference · Google: consolidate duplicate URLs with canonical links · WCAG 2.4.2: Page Titled