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:typewebsite for most pages, article for blog posts.
  • twitter:card summary_large_image for 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.

Related Guides