Runes Meta Tags is a powerful meta tag management library for SvelteKit applications. It provides an elegant way to configure SEO meta tags, Open Graph tags for social media sharing, Twitter Cards, and more. With built-in support for deep merging, you can define default meta tags at the layout level and override them on specific pages.
pnpm i -D runes-meta-tags
or
npm i -D runes-meta-tags
or
yarn add -D runes-meta-tags
or
bun add -D runes-meta-tagsCreate a +layout.server.ts file to define your default meta tags. These will be used across all pages unless overridden.
import type { MetaProps } from 'runes-meta-tags';
export const load = ({ url }) => {
const layoutMetaTags: MetaProps = {
title: 'My Awesome Site',
description: 'Welcome to my site - we build amazing things with Svelte',
keywords: 'svelte, sveltekit, web development, javascript',
author: 'John Doe',
og: {
type: 'website',
title: 'My Awesome Site',
description: 'Welcome to my site - we build amazing things with Svelte',
url: url.href,
image: 'https://example.com/og-default.jpg',
imageAlt: 'My Awesome Site logo',
siteName: 'My Awesome Site',
imageWidth: '1200',
imageHeight: '630'
},
twitter: {
card: 'summary_large_image',
site: '@mysite',
creator: '@johndoe',
title: 'My Awesome Site',
description: 'Welcome to my site - we build amazing things with Svelte',
image: 'https://example.com/og-default.jpg',
imageAlt: 'My Awesome Site logo'
}
};
return {
layoutMetaTags
};
};deepMerge function allows page-specific meta tags to override layout defaults while preserving unmodified properties.Import MetaTags and deepMerge in your +layout.svelte. The deepMerge function intelligently combines layout and page-specific meta tags.
<script lang="ts">
import { MetaTags, deepMerge } from 'runes-meta-tags';
import { page } from '$app/stores';
let { children, data } = $props();
// Use $derived for reactive meta tags
let metaTags = $derived(
$page.data.pageMetaTags
? deepMerge(data.layoutMetaTags, $page.data.pageMetaTags)
: data.layoutMetaTags
);
</script>
<MetaTags {...metaTags} />
{@render children()} Override meta tags for specific pages by creating a +page.ts file. Only include the properties you want to change - everything else inherits from the layout.
import type { MetaProps } from 'runes-meta-tags';
export const load = ({ url }) => {
const pageMetaTags: MetaProps = {
title: 'About Us - My Awesome Site',
description: 'Learn more about our team and mission',
og: {
title: 'About Us - My Awesome Site',
description: 'Learn more about our team and mission',
url: url.href,
image: 'https://example.com/about-og.jpg'
},
twitter: {
title: 'About Us - My Awesome Site',
description: 'Learn more about our team and mission',
image: 'https://example.com/about-og.jpg'
}
};
return { pageMetaTags };
};
// Note: With deepMerge, this page will:
// โ Override: title, description, og.title, og.description, og.url, og.image
// โ Inherit: keywords, author, og.siteName, og.imageWidth, twitter.card, etc.Your page components don't need any special code - the meta tags are handled automatically!
<h1>About Us</h1>
<p>Welcome to our about page!</p>
<!-- Meta tags are automatically managed by the layout -->
<!-- No special code needed in page components --> The library provides helpful utility functions to generate consistent meta tags:
import { metaTitle, metaDescription, metaImg, splitAndCapitalize } from 'runes-meta-tags';
// Generate consistent titles based on pathname
const title = metaTitle('/blog/svelte-5', 'My Site');
// Result: "Blog Svelte 5 - My Site"
// Generate descriptions
const description = metaDescription('/blog/svelte-5', 'Learn about');
// Result: "Blog Svelte 5 - Learn about"
// Generate Open Graph images with dynamic titles
const image = metaImg('/blog/svelte-5', 'mysite.com');
// Result: "https://open-graph-vercel.vercel.app/api/mysite.com?title=Blog%20Svelte%205"
// Extract and format the last path segment
const formatted = splitAndCapitalize('/blog/my-awesome-post');
// Result: "My Awesome Post"Understanding how deepMerge works is important for effective meta tag management:
import { deepMerge } from 'runes-meta-tags';
import type { MetaProps } from 'runes-meta-tags';
// Layout meta tags (defaults)
const layoutMetaTags: MetaProps = {
title: 'My Site',
description: 'Welcome to my site',
keywords: 'svelte, runes, web',
og: {
title: 'My Site',
image: 'https://example.com/default.jpg',
siteName: 'My Site'
}
};
// Page-specific meta tags (overrides)
const pageMetaTags: MetaProps = {
title: 'About - My Site',
og: {
title: 'About - My Site',
url: 'https://example.com/about'
}
};
// Deep merge combines them intelligently
const merged = deepMerge(layoutMetaTags, pageMetaTags);
// Result:
// {
// title: 'About - My Site', // โ overridden
// description: 'Welcome to my site', // โ inherited
// keywords: 'svelte, runes, web', // โ inherited
// og: {
// title: 'About - My Site', // โ overridden
// url: 'https://example.com/about', // โ added
// image: 'https://example.com/default.jpg', // โ inherited
// siteName: 'My Site' // โ inherited
// }
// }Keywords can be defined as a string or array and are automatically inherited from layout unless overridden:
import type { MetaProps } from 'runes-meta-tags';
// In +layout.server.ts - keywords as string
const layoutMetaTags: MetaProps = {
keywords: 'svelte, sveltekit, typescript, web development'
};
// Alternative: keywords as array
const layoutMetaTagsArray: MetaProps = {
keywords: ['svelte', 'sveltekit', 'typescript', 'web development']
};
// Pages automatically inherit keywords from layout
// No need to repeat them in +page.ts unless overriding
// Override keywords for specific page
export const load = ({ url }) => {
const pageMetaTags: MetaProps = {
title: 'Blog Post',
keywords: 'blog, tutorial, svelte 5, runes' // โ overrides layout keywords
};
return { pageMetaTags };
};For blog posts and articles, use the article type with additional metadata:
import type { MetaProps } from 'runes-meta-tags';
export const load = ({ url }) => {
const pageMetaTags: MetaProps = {
title: 'Understanding Svelte 5 Runes - My Tech Blog',
description: 'A comprehensive guide to using runes in Svelte 5',
keywords: 'svelte 5, runes, reactivity, state management',
og: {
type: 'article',
title: 'Understanding Svelte 5 Runes',
description: 'A comprehensive guide to using runes in Svelte 5',
url: url.href,
image: 'https://example.com/og-images/svelte-5-runes.jpg',
article: {
publishedTime: '2024-01-15T09:00:00.000Z',
modifiedTime: '2024-01-20T14:30:00.000Z',
author: ['John Doe', 'Jane Smith'],
section: 'Web Development',
tag: ['Svelte', 'JavaScript', 'Tutorial', 'Frontend']
}
},
twitter: {
card: 'summary_large_image',
title: 'Understanding Svelte 5 Runes',
description: 'A comprehensive guide to using runes in Svelte 5',
image: 'https://example.com/og-images/svelte-5-runes.jpg'
}
};
return { pageMetaTags };
};Configure video meta tags for rich video previews:
import type { MetaProps } from 'runes-meta-tags';
export const load = ({ url }) => {
const pageMetaTags: MetaProps = {
title: 'Svelte 5 Tutorial - Video Course',
og: {
type: 'video',
title: 'Learn Svelte 5 from Scratch',
url: url.href,
video: {
url: 'https://example.com/videos/svelte-5-tutorial.mp4',
secureUrl: 'https://example.com/videos/svelte-5-tutorial.mp4',
type: 'video/mp4',
width: 1280,
height: 720
},
image: 'https://example.com/thumbnails/svelte-5-tutorial.jpg'
},
twitter: {
card: 'player',
title: 'Learn Svelte 5 from Scratch',
playerUrl: 'https://example.com/player/svelte-5-tutorial',
playerWidth: '1280',
playerHeight: '720',
image: 'https://example.com/thumbnails/svelte-5-tutorial.jpg'
}
};
return { pageMetaTags };
};Control how search engines index your pages:
import type { MetaProps } from 'runes-meta-tags';
// Simple boolean robots configuration
const allowIndexing: MetaProps = {
robots: true // Generates: <meta name="robots" content="index,follow">
};
const blockIndexing: MetaProps = {
robots: false // Generates: <meta name="robots" content="noindex,nofollow">
};
// Advanced robots configuration
const customRobots: MetaProps = {
robots: {
index: true, // Allow indexing
follow: false, // Don't follow links
nocache: true, // Don't cache this page
googleBot: 'nosnippet,notranslate' // Custom rules for Google
}
// Generates:
// <meta name="robots" content="index,nofollow,nocache">
// <meta name="googlebot" content="nosnippet,notranslate">
};
// Example: Admin page (no indexing)
export const load = () => {
const pageMetaTags: MetaProps = {
title: 'Admin Dashboard',
robots: false // Keep admin pages out of search results
};
return { pageMetaTags };
};
// Example: Draft blog post (no indexing, but follow links)
export const load = () => {
const pageMetaTags: MetaProps = {
title: 'Draft: Upcoming Features',
robots: {
index: false,
follow: true
}
};
return { pageMetaTags };
};Different Twitter Card types for various content:
import type { MetaProps } from 'runes-meta-tags';
// Summary card (default, for text content)
const summaryCard: MetaProps = {
twitter: {
card: 'summary',
title: 'Check out this article',
description: 'Learn about Svelte 5 runes',
image: 'https://example.com/square-image.jpg',
imageAlt: 'Svelte 5 logo'
}
};
// Summary card with large image (for visual content)
const summaryLargeImage: MetaProps = {
twitter: {
card: 'summary_large_image',
site: '@mysite',
creator: '@johndoe',
title: 'Amazing Photo Gallery',
description: 'Beautiful landscape photography',
image: 'https://example.com/landscape.jpg',
imageAlt: 'Stunning mountain landscape'
}
};
// Player card (for audio/video)
const playerCard: MetaProps = {
twitter: {
card: 'player',
title: 'Watch: Svelte 5 Tutorial',
description: 'Full course on Svelte 5',
playerUrl: 'https://example.com/player/video-123',
playerWidth: '1280',
playerHeight: '720',
image: 'https://example.com/video-thumbnail.jpg'
}
};
// App card (for mobile apps)
const appCard: MetaProps = {
twitter: {
card: 'app',
title: 'Download Our App',
description: 'Available on iOS and Android',
appName: 'My Awesome App',
appIdIphone: '123456789',
appIdIpad: '123456789',
appIdGooglePlay: 'com.example.app',
appCountry: 'US',
image: 'https://example.com/app-icon.jpg'
}
};Example tests for your home page meta tags:
import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
console.log(`Running ${test.info().title}`);
await page.goto('/');
});
test('index page has expected h1', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Runes Meta Tags' })).toBeVisible();
});
test('index page has expected meta title', async ({ page }) => {
await expect(page).toHaveTitle('Runes Meta Tags');
});
test('index page has expected meta description', async ({ page }) => {
const metaDescription = page.locator('meta[name="description"]');
await expect(metaDescription).toHaveAttribute('content', 'Meta tags for Runes.');
});
test('index page has expected meta keywords', async ({ page }) => {
const metaKeywords = page.locator('meta[name="keywords"]');
await expect(metaKeywords).toHaveAttribute('content', 'runes, meta, tags');
});
test('index page has expected meta og', async ({ page }) => {
const metaOgTitle = page.locator('meta[property="og:title"]');
await expect(metaOgTitle).toHaveAttribute('content', 'Runes Meta Tags');
const metaOgDescription = page.locator('meta[property="og:description"]');
await expect(metaOgDescription).toHaveAttribute('content', 'Meta tags for Runes.');
const metaOgUrl = page.locator('meta[property="og:url"]');
await expect(metaOgUrl).toHaveAttribute('content', 'http://localhost:4173/');
const metaOgImage = page.locator('meta[property="og:image"]');
await expect(metaOgImage).toHaveAttribute(
'content',
'https://open-graph-vercel.vercel.app/api/runes-meta-tags'
);
});
test('index page has expected meta twitter', async ({ page }) => {
const metaTwitterTitle = page.locator('meta[name="twitter:title"]');
await expect(metaTwitterTitle).toHaveAttribute('content', 'Runes Meta Tags');
const metaTwitterDescription = page.locator('meta[name="twitter:description"]');
await expect(metaTwitterDescription).toHaveAttribute('content', 'Meta tags for Runes.');
const metaTwitterImage = page.locator('meta[name="twitter:image"]');
await expect(metaTwitterImage).toHaveAttribute(
'content',
'https://open-graph-vercel.vercel.app/api/runes-meta-tags'
);
});
Test that page-specific meta tags override layout defaults correctly:
import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
console.log(`Running ${test.info().title}`);
await page.goto('/about');
});
test('about page has expected h1, meta title', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'About' })).toBeVisible();
});
test('about page has expected meta title', async ({ page }) => {
await expect(page).toHaveTitle('About - Runes Meta Tags');
});
test('about page has expected meta description', async ({ page }) => {
const metaDescription = page.locator('meta[name="description"]');
await expect(metaDescription).toHaveAttribute('content', 'About - Runes Meta Tags');
});
test('about page has expected meta og', async ({ page }) => {
const metaOgTitle = page.locator('meta[property="og:title"]');
await expect(metaOgTitle).toHaveAttribute('content', 'About - Runes Meta Tags');
const metaOgDescription = page.locator('meta[property="og:description"]');
await expect(metaOgDescription).toHaveAttribute('content', 'About - Runes Meta Tags');
const metaOgUrl = page.locator('meta[property="og:url"]');
await expect(metaOgUrl).toHaveAttribute('content', 'http://localhost:4173/about');
const metaOgImage = page.locator('meta[property="og:image"]');
await expect(metaOgImage).toHaveAttribute(
'content',
'https://open-graph-vercel.vercel.app/api/runes-meta-tags?title=About'
);
});
test('about page has expected meta twitter', async ({ page }) => {
const metaTwitterTitle = page.locator('meta[name="twitter:title"]');
await expect(metaTwitterTitle).toHaveAttribute('content', 'About - Runes Meta Tags');
const metaTwitterDescription = page.locator('meta[name="twitter:description"]');
await expect(metaTwitterDescription).toHaveAttribute('content', 'About - Runes Meta Tags');
const metaTwitterImage = page.locator('meta[name="twitter:image"]');
await expect(metaTwitterImage).toHaveAttribute(
'content',
'https://open-graph-vercel.vercel.app/api/runes-meta-tags?title=About'
);
});
Complete type definition for meta tag configuration:
import type { VideoType, ArticleType } from 'runes-meta-tags';
export interface MetaProps {
// Page title (appears in browser tab and search results)
title?: string;
// Page description for SEO
description?: string;
// Keywords for SEO (string or array)
keywords?: string | string[];
// Page author
author?: string;
// Viewport configuration (defaults handled automatically)
viewport?: string;
// Canonical URL (for duplicate content)
canonical?: string;
// Robots meta tag configuration
robots?:
| boolean // true = "index,follow", false = "noindex,nofollow"
| {
index?: boolean; // Allow/disallow indexing
follow?: boolean; // Allow/disallow following links
nocache?: boolean; // Disable caching
googleBot?: string; // Custom Google bot rules
};
// Open Graph (Facebook, LinkedIn, etc.)
og?: {
type?: 'website' | 'article' | 'product' | 'book' | 'profile' | 'music' | 'video';
title?: string;
description?: string;
url?: string; // Page URL (use url.href)
image?: string; // Image URL for social sharing
imageAlt?: string; // Image alt text
imageSecureUrl?: string; // HTTPS image URL
imageWidth?: string | number;
imageHeight?: string | number;
siteName?: string; // Your site name
locale?: string; // Page locale (e.g., 'en_US')
localeAlternate?: string[];// Alternate locales
video?: string | VideoType;
audio?: string;
article?: ArticleType; // For blog posts/articles
determiner?: 'a' | 'an' | 'the' | 'auto' | '';
};
// Twitter Card
twitter?: {
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
site?: string; // @username of website
creator?: string; // @username of content creator
title?: string;
description?: string;
image?: string;
imageAlt?: string;
// Player card properties
playerUrl?: string;
playerWidth?: string;
playerHeight?: string;
// App card properties
appIdIphone?: string;
appIdIpad?: string;
appIdGooglePlay?: string;
appName?: string;
appCountry?: string;
};
}+layout.server.tsdeepMerge to inherit layout meta tags on pagesog:url using url.href in page-specific +page.tsog:url to page-specific meta tagsRunesMetaTags component (use MetaTags instead)url.href for dynamic URLsMetaTags instead of RunesMetaTags. The old component name is deprecated and will be removed in v1.0.0.// Before (v0.4.x and earlier):
import { RunesMetaTags } from 'runes-meta-tags';
// After (v0.5.0+):
import { MetaTags } from 'runes-meta-tags';
// In your +layout.svelte:
// โ Old (deprecated, will be removed in v1.0.0)
<RunesMetaTags {...metaTags} />
// โ
New (recommended)
<MetaTags {...metaTags} />
// Everything else stays the same!
// The component behavior is identical, just the name changed.MetaTags component is placed in your +layout.sveltelayoutMetaTags is returned from +layout.server.tsdeepMerge is correctly combining layout and page meta tags<head> sectionurl parameter to your page's load function: export const load = ({ url }) => {...}url: url.href in the og object of pageMetaTagskeywords is defined in +layout.server.tsdeepMerge is being used in +layout.sveltepageMetaTags without keywords, they should inherit from layout