Preact Open Graph Meta Tags: Add OG to Your Preact App
How to add og:title, og:image, og:description, and Twitter card meta tags to Preact applications — using preact-helmet, static HTML, or SSR with preact/render-to-string.
Why Preact OG tags need special attention
Preact is a 3 kB alternative to React with the same modern API. Like React, it renders components to a virtual DOM — but social crawlers (Twitterbot, LinkedInBot, Discordbot) don't execute JavaScript. They fetch raw HTML and parse only what's already in the <head>. If your Preact app is a client-side SPA, those crawlers will see an empty <head> with no OG tags.
The solution depends on your architecture: static pre-render, server-side rendering with preact/render-to-string, or a static HTML shell with hard-coded meta tags for your primary routes.
Option 1: preact-helmet for SSR / prerendering
preact-helmet is the Preact port of react-helmet. It manages document head state from inside your component tree and works correctly when used with server-side rendering or static pre-rendering.
npm install preact-helmet
// src/components/SeoHead.jsx
import { Helmet } from 'preact-helmet';
export function SeoHead({ title, description, image, url }) {
return (
<Helmet>
<title>{title}</title>
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
</Helmet>
);
}
// src/routes/Post.jsx
import { SeoHead } from '../components/SeoHead';
export default function Post({ post }) {
return (
<>
<SeoHead
title={post.title}
description={post.excerpt}
image={`https://yourdomain.com/og/${post.slug}.png`}
url={`https://yourdomain.com/post/${post.slug}`}
/>
<article>{/* ... */}</article>
</>
);
}Important: preact-helmet only injects meta tags into the DOM client-side unless you call Helmet.renderStatic() during server-side rendering. For crawlers, you must pre-render.
Option 2: SSR with preact/render-to-string
For full SSR, use preact/render-to-string (or the newer preact-render-to-string package) with an Express or Node.js server. Render the component tree to a string, extract Helmet data, and inject it into your HTML template.
// server.js
import express from 'express';
import { render } from 'preact-render-to-string';
import { Helmet } from 'preact-helmet';
import { h } from 'preact';
import App from './src/App.jsx';
const server = express();
server.get('*', async (req, res) => {
// Render the component tree to string
const body = render(h(App, { url: req.url }));
// Extract helmet-injected head tags
const helmet = Helmet.renderStatic();
res.send(`<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
</head>
<body>
<div id="app">${body}</div>
<script type="module" src="/bundle.js"></script>
</body>
</html>`);
});
server.listen(3000);Option 3: Static HTML shell with hardcoded meta tags
If your Preact app has a small number of key shareable pages (e.g., a landing page and a few product pages), the simplest solution is to serve per-route static HTML files with the correct OG tags already in the <head>. The Preact bundle hydrates on top.
<!-- public/index.html (landing page) -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Preact App — Build Fast UIs</title>
<meta property="og:type" content="website" />
<meta property="og:title" content="My Preact App — Build Fast UIs" />
<meta property="og:description" content="A 3kB alternative to React with the same API." />
<meta property="og:image" content="https://yourdomain.com/og/home.png" />
<meta property="og:url" content="https://yourdomain.com/" />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/bundle.js"></script>
</body>
</html>For dynamic routes (blog posts, user profiles), you need a server that injects the right meta tags before sending the HTML — the static shell approach only works for static pages.
Option 4: Preact + Vite prerendering with vite-prerender
If you're using Vite with Preact, you can use vite-plugin-prerender or @preact/preset-vite with prerendering enabled. This statically renders each known route at build time, embedding your Helmet meta tags into each page's HTML file.
// vite.config.js
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
export default defineConfig({
plugins: [
preact({
prerender: {
enabled: true,
renderTarget: '#app',
additionalPrerenderRoutes: ['/about', '/blog', '/pricing'],
previewMiddlewareEnabled: true,
previewMiddlewareFallback: '/404',
},
}),
],
});Essential OG tag checklist for Preact
- og:title — Page or post title. Keep under 60 characters.
- og:description — 1–2 sentence summary. Keep under 200 characters.
- og:image — Absolute URL to a 1200×630 px PNG/JPG. Must be HTTPS.
- og:url — Canonical URL of the page. No trailing slash variance.
- og:type —
websitefor most pages,articlefor blog posts. - twitter:card —
summary_large_imagefor large preview cards.
Test your OG tags free
Paste any URL into OGFixer to see exactly how your link previews look on Twitter, LinkedIn, Discord, and Slack.