How to Add Dynamic OG Images to Gatsby, Remix, and SvelteKit (2026)
Gatsby, Remix, and SvelteKit each have a fundamentally different model for injecting <meta> tags — and that difference makes setting up OG images unnecessarily painful in all three. This guide covers the canonical approach for each framework and shows how to wire in a single API that works across all of them.
The Problem: No Unified Standard
Open Graph meta tags belong in <head>. Simple enough. But how you get there varies dramatically by framework:
- Gatsby uses
gatsby-plugin-react-helmetduring server-side rendering, with agatsby-ssr.jshook to inject page-level head elements. - Remix exports a
metafunction from each route file — no plugin, no component, just a plain JavaScript export. - SvelteKit uses a
+page.server.tsload function to pass data to the layout, then a<svelte:head>block to render the tags.
None of these approaches are wrong. But if you work across all three — or if your team is evaluating frameworks — you'll hit friction with each one. And that's before the harder problem: the OG image itself.
Manually creating a 1200×630 PNG for every page doesn't scale. You need dynamic images that reflect the actual page title and branding. That's where an API-based approach pays off: one URL pattern works everywhere, regardless of framework.
The Universal Solution: OGPeek API
The OGPeek API generates Open Graph images from URL parameters. You pass a title, subtitle, template, and theme — it returns a ready-to-use PNG. No headless browser, no build step, no manual exports.
The base URL structure looks like this:
https://ogpeek.com/api/v1/og
?title=YOUR_PAGE_TITLE
&subtitle=YOUR_SITE_NAME
&template=gradient
&theme=midnight
&brandColor=%23FF7A00
Every framework example below uses this same URL pattern — only the syntax for injecting it into <head> changes.
Gatsby OG Images
Prerequisites
Install gatsby-plugin-react-helmet and react-helmet if you haven't already:
npm install gatsby-plugin-react-helmet react-helmet
Add the plugin to gatsby-config.js:
// gatsby-config.js
module.exports = {
plugins: [
'gatsby-plugin-react-helmet',
// ...other plugins
],
};
Page-Level OG Tags with React Helmet
In any Gatsby page component, import Helmet and add your meta tags. The OGPeek URL is built dynamically from the page's title:
// src/pages/blog/{post.slug}.js
import React from 'react';
import { Helmet } from 'react-helmet';
import { graphql } from 'gatsby';
export default function BlogPost({ data }) {
const post = data.markdownRemark;
const title = post.frontmatter.title;
const siteUrl = 'https://yoursite.com';
// Build OGPeek URL — title comes from your CMS/GraphQL, not user input
const ogImageUrl =
'https://ogpeek.com/api/v1/og' +
'?title=' + encodeURIComponent(title) +
'&subtitle=Your+Blog+Name' +
'&template=gradient' +
'&theme=midnight' +
'&brandColor=%23FF7A00';
return (
<>
<Helmet>
<title>{title}</title>
<meta property="og:title" content={title} />
<meta property="og:type" content="article" />
<meta
property="og:url"
content={siteUrl + '/blog/' + post.fields.slug}
/>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:image" content={ogImageUrl} />
</Helmet>
<main>
<h1>{title}</h1>
<div>{post.excerpt}</div>
</main>
</>
);
}
export const query = graphql`
query($id: String!) {
markdownRemark(id: { eq: $id }) {
excerpt
fields { slug }
frontmatter { title }
}
}
`;
SSR Verification via gatsby-ssr.js
Gatsby's gatsby-ssr.js file runs during gatsby build and lets you wrap every page's HTML output. If you're building a reusable SEO component, this is where to add fallback tags:
// gatsby-ssr.js
const React = require('react');
exports.onRenderBody = ({ setHeadComponents }) => {
setHeadComponents([
React.createElement('meta', {
key: 'og-image-default',
property: 'og:image',
content:
'https://ogpeek.com/api/v1/og' +
'?title=Your+Site' +
'&template=basic' +
'&theme=midnight',
}),
]);
};
Note: Page-level <Helmet> tags override any defaults set in gatsby-ssr.js. Use gatsby-ssr.js for site-wide fallbacks only.
For a deeper walkthrough of Gatsby-specific options, see the full Gatsby OG images guide.
Remix OG Images
How the meta Export Works
Remix takes a radically different approach: each route file exports a meta function that returns an array of meta tag descriptors. No JSX, no component — just data. The runtime merges all ancestor meta exports and renders them into <head>.
Basic Route Meta Export
// app/routes/blog.$slug.tsx
import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPostBySlug(params.slug!);
if (!post) throw new Response('Not Found', { status: 404 });
return json({ post });
}
export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data) return [{ title: 'Not Found' }];
const { post } = data;
const ogImageUrl =
'https://ogpeek.com/api/v1/og' +
'?title=' + encodeURIComponent(post.title) +
'&subtitle=Your+Blog+Name' +
'&template=gradient' +
'&theme=midnight' +
'&brandColor=%23FF7A00';
return [
{ 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://yoursite.com/blog/' + post.slug },
{ property: 'og:image', content: ogImageUrl },
{ property: 'og:image:width', content: '1200' },
{ property: 'og:image:height', content: '630' },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: post.title },
{ name: 'twitter:image', content: ogImageUrl },
];
};
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
);
}
Root-Level Fallback Meta
In Remix, app/root.tsx typically defines site-wide default meta. Provide a fallback OG image there so any route without an explicit meta export still gets a valid social card:
// app/root.tsx (excerpt)
export const meta: MetaFunction = () => [
{ title: 'Your Site' },
{
property: 'og:image',
content:
'https://ogpeek.com/api/v1/og' +
'?title=Your+Site' +
'&template=basic' +
'&theme=midnight',
},
{ name: 'twitter:card', content: 'summary_large_image' },
];
Remix v2 note: The meta export receives matches as a second argument, giving you access to ancestor route data. Use it to inherit your brand name or accent color from the root loader rather than hard-coding it in every route.
See the dedicated Remix OG images guide for advanced patterns including resource routes and social share cards for user-generated content.
SvelteKit OG Images
How SvelteKit Handles Head Tags
SvelteKit uses a two-step pattern. First, a +page.server.ts (or +page.ts) load function fetches data and returns it. Second, the +page.svelte component uses a <svelte:head> block to render meta tags reactively from that data.
Step 1 — Load Function
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params }) => {
const post = await getPostBySlug(params.slug);
if (!post) throw error(404, 'Not found');
const ogImageUrl =
'https://ogpeek.com/api/v1/og' +
'?title=' + encodeURIComponent(post.title) +
'&subtitle=Your+Blog+Name' +
'&template=gradient' +
'&theme=midnight' +
'&brandColor=%23FF7A00';
return { post, ogImageUrl };
};
Step 2 — svelte:head in the Page Component
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const { post, ogImageUrl } = data;
</script>
<svelte:head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://yoursite.com/blog/{post.slug}" />
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={post.title} />
<meta name="twitter:image" content={ogImageUrl} />
</svelte:head>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
Site-Wide Fallback in +layout.svelte
For pages that don't define their own OG tags, add a default in your root layout:
<!-- src/routes/+layout.svelte -->
<svelte:head>
<meta
property="og:image"
content="https://ogpeek.com/api/v1/og?title=Your+Site&template=basic&theme=midnight"
/>
<meta name="twitter:card" content="summary_large_image" />
</svelte:head>
SSR vs. CSR: Build the OGPeek URL in +page.server.ts (not in the component script) so the meta tags are present in the HTML sent to crawlers. Social platform bots do not execute JavaScript.
For the complete SvelteKit walkthrough including adapter-specific considerations, see the SvelteKit OG images guide.
Testing Your OG Images
After deploying, verify that crawlers see the correct tags before you share any links. Two tools cover the major platforms:
Twitter Card Validator
Go to cards-dev.twitter.com/validator, paste your URL, and click Preview card. This shows exactly what Twitter will render when someone tweets your link. Look for:
- Card type:
summary_large_image - The OGPeek-generated image appearing at full 1200×630 resolution
- Your page title and description in the card text
Facebook Sharing Debugger
Go to developers.facebook.com/tools/debug and enter your URL. If you've recently updated your meta tags, click Scrape Again to clear Facebook's cache. Confirm:
og:imageresolves to your OGPeek URL- Image dimensions show
1200 × 630 - No warnings about image size or missing required tags
Quick curl Check
Before reaching for a browser tool, a quick curl confirms the tags are present in the server-rendered HTML:
curl -s https://yoursite.com/blog/your-post-slug \
| grep -i "og:image"
If the output is empty, the meta tags are being injected client-side only — move the OGPeek URL construction to your server-side load function or SSR hook.
API Parameters Reference
All three frameworks use the same OGPeek URL. Here's a quick reference of the parameters you'll use most often:
- title — Primary headline (URL-encoded). Required.
- subtitle — Secondary line (e.g. site name or category).
- template — Layout style:
basic,gradient,split,hero. - theme — Color scheme:
dark,light,midnight,forest,sunset. - brandColor — Hex color (URL-encoded). Overrides the theme accent.
Full parameter documentation is on the OGPeek API Docs page. For plan details, see the pricing page.
Start generating OG images now
No signup required for the free tier. Upgrade to remove the watermark and unlock higher request limits.
Open the playground →Conclusion
Gatsby, Remix, and SvelteKit each have their own mental model for head tag injection — but once you understand each framework's pattern, the integration is straightforward. The code above is the canonical approach for each:
- Gatsby:
react-helmetinside page components, withgatsby-ssr.jsfor site-wide fallbacks. - Remix: a
metaexport returning an array of descriptors, with a fallback inapp/root.tsx. - SvelteKit: data from
+page.server.tsrendered via<svelte:head>in the page component.
In every case, the OGPeek API reduces the image problem to a single URL. No design tools, no build-time image generation, no extra dependencies per framework. The same API call works in all three — and in any other framework you might adopt next.
Related Guides
Looking for a deeper dive into a specific framework, or want to explore other static site generators?