Svelte & SvelteKit Open Graph Meta Tags: Complete Setup Guide
How to add og:title, og:image, og:description, and Twitter card tags to SvelteKit apps using svelte:head, +layout.svelte, and dynamic metadata for blog posts.
SvelteKit vs plain Svelte for OG tags
Plain Svelte compiles to a client-side bundle. Social crawlers (Twitter, LinkedIn, Discord) don't run JavaScript, so they'll see your page before any JS executes. That means meta tags injected via JavaScript at runtime will be invisible.
SvelteKit solves this by rendering pages server-side or at build time. Using <svelte:head> inside a SvelteKit page component, your OG tags appear in the raw HTML — exactly what crawlers need.
Adding OG tags with svelte:head
In any SvelteKit page (+page.svelte), use the <svelte:head> block to inject meta tags:
<!-- src/routes/+page.svelte --> <svelte:head> <title>My Page Title</title> <meta name="description" content="My page description." /> <!-- Open Graph --> <meta property="og:title" content="My Page Title" /> <meta property="og:description" content="My page description." /> <meta property="og:image" content="https://example.com/og.jpg" /> <meta property="og:url" content="https://example.com" /> <meta property="og:type" content="website" /> <!-- Twitter Card --> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="My Page Title" /> <meta name="twitter:description" content="My page description." /> <meta name="twitter:image" content="https://example.com/og.jpg" /> </svelte:head> <main> <!-- page content --> </main>
Dynamic OG tags for blog posts
Load data in +page.server.ts (or +page.ts for universal load) and pass it to the page component as props:
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const post = await fetchPost(params.slug); // your data fetcher
return { post };
};<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const { post } = data;
</script>
<svelte:head>
<title>{post.title}</title>
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={post.coverImage} />
<meta property="og:url" content={`https://example.com/blog/${post.slug}`} />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />
</svelte:head>
<article>
<h1>{post.title}</h1>
<!-- content -->
</article>Site-wide defaults in +layout.svelte
Add fallback OG tags in your root layout so every page has baseline metadata:
<!-- src/routes/+layout.svelte --> <svelte:head> <meta property="og:site_name" content="My App" /> <meta property="og:type" content="website" /> <meta property="og:image" content="https://example.com/og/default.jpg" /> <meta name="twitter:card" content="summary_large_image" /> </svelte:head> <slot />
Page-level <svelte:head> tags override layout defaults for the same property names.
Common SvelteKit OG tag mistakes
- Using Svelte without SvelteKit: client-only apps can't inject crawlable OG tags. Migrate to SvelteKit or add a prerender step.
- Relative og:image URLs: always use absolute HTTPS URLs for images.
- Adapter mismatch: if deploying to a static host with
adapter-static, ensure all dynamic routes have prerender enabled or useadapter-node/adapter-vercelfor true SSR. - Missing og:url: always include a canonical URL to avoid duplicate content issues.
Check your SvelteKit OG tags → Paste your URL into OGFixer to preview how your link looks on Twitter, LinkedIn, Discord, and Slack.