Svelte 5 Open Graph Meta Tags: Runes, SSR & OG Images

How to add og:title, og:image, og:description in Svelte 5 using the new runes API, SvelteKit head management, and server-side rendering for social crawlers.

What changed in Svelte 5 for OG tags

Svelte 5 introduced runes — a new reactivity system that replaces stores and reactive declarations. While OG tags themselves haven't changed, how you manage head metadata in SvelteKit with Svelte 5 components has some new patterns worth understanding.

The good news: SvelteKit still handles SSR perfectly, which is critical for OG tags. Social crawlers from Twitter, LinkedIn, Discord, Slack, and Facebook don't execute JavaScript — they only read the initial HTML response. SvelteKit renders your<svelte:head> content server-side, so crawlers see your OG tags immediately.

Static OG tags with svelte:head

For pages with fixed metadata, use <svelte:head> directly in your page component:

<!-- src/routes/+page.svelte (Svelte 5) -->
<svelte:head>
  <title>My SaaS — Ship faster with AI</title>
  <meta property="og:title" content="My SaaS — Ship faster with AI" />
  <meta property="og:description" content="Deploy to production in 30 seconds." />
  <meta property="og:image" content="https://mysaas.com/og/home.png" />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://mysaas.com" />
  <meta name="twitter:card" content="summary_large_image" />
</svelte:head>

<h1>Welcome to My SaaS</h1>

Dynamic OG tags with load functions

For dynamic pages like blog posts or user profiles, fetch data in a +page.server.ts load function and use it in your <svelte:head>:

// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { getPost } from '$lib/posts';

export const load: PageServerLoad = async ({ params }) => {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    image: post.ogImage || `https://mysaas.com/og/blog/${params.slug}.png`,
    slug: params.slug,
  };
};
<!-- src/routes/blog/[slug]/+page.svelte (Svelte 5 runes) -->
<script>
  let { data } = $props();
</script>

<svelte:head>
  <title>{data.title} — My SaaS Blog</title>
  <meta property="og:title" content={data.title} />
  <meta property="og:description" content={data.description} />
  <meta property="og:image" content={data.image} />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
  <meta property="og:type" content="article" />
  <meta property="og:url" content={`https://mysaas.com/blog/${data.slug}`} />
  <meta name="twitter:card" content="summary_large_image" />
</svelte:head>

<article>
  <h1>{data.title}</h1>
</article>

With Svelte 5's $props() rune, you destructure the data directly instead of using the old export let data syntax. The <svelte:head> behavior is identical.

OG images with SvelteKit endpoints

Generate dynamic OG images using a SvelteKit server endpoint with Satori or @resvg/resvg-js:

// src/routes/og/[slug].png/+server.ts
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ params }) => {
  const post = await getPost(params.slug);
  
  const svg = await satori(
    {
      type: 'div',
      props: {
        style: {
          display: 'flex',
          flexDirection: 'column',
          background: '#0a0a0a',
          width: '100%',
          height: '100%',
          padding: '60px',
        },
        children: [
          {
            type: 'div',
            props: {
              style: { fontSize: 64, fontWeight: 900, color: '#fff' },
              children: post.title,
            },
          },
        ],
      },
    },
    { width: 1200, height: 630, fonts: [/* load your font */] }
  );

  const png = new Resvg(svg).render().asPng();
  return new Response(png, {
    headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' },
  });
};

Testing OG tags in Svelte 5

After adding OG tags, verify they work:

  • Run npm run build && npm run preview and view page source — OG meta tags should be in the HTML
  • Deploy to your hosting provider and test the live URL with OGFixer
  • Check Twitter Card Validator and Facebook Sharing Debugger
  • Share the link in a private Slack/Discord channel to see the actual preview

Common Svelte 5 OG pitfalls

  • Forgetting to use a +page.server.ts load function — client-side fetches won't render in SSR HTML
  • Using $state() for OG data — runes are client-side reactive state, not SSR data; use load functions instead
  • Putting OG images behind authentication — social crawlers can't authenticate
  • Using relative URLs for og:image — always use absolute URLs with https://

Test your Svelte 5 app's OG tags

Paste your deployed SvelteKit URL to see how it looks on Twitter, Slack, Discord, and LinkedIn — with specific fixes for any issues.

Check your Svelte 5 OG tags →