Node.js Open Graph Meta Tags: Express, Fastify & Vanilla Node

Add og:title, og:image, og:description and Twitter card tags to any Node.js app — Express, Fastify, or vanilla http — with copy-paste examples.

How OG tags work in server-rendered Node apps

Open Graph tags live inside the <head> of your HTML response. In a Node.js app you control every byte of that HTML — so you set OG tags by writing the correct <meta> elements into the template or string you send back to the client. There is no magic framework — just string interpolation.

Express.js: inline head helper

// server.js (Express)
const express = require('express');
const app = express();

function ogHead({ title, description, image, url }) {
  return `
    <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}" />
  `;
}

app.get('/product/:slug', async (req, res) => {
  const product = await getProduct(req.params.slug);
  res.send(`<!DOCTYPE html>
    <html><head>
      <title>${product.name}</title>
      ${ogHead({
        title: product.name,
        description: product.tagline,
        image: 'https://yourdomain.com/og/${product.slug}.png',
        url: 'https://yourdomain.com/product/${product.slug}',
      })}
    </head><body>...</body></html>`);
});

Express + EJS or Pug template

If you use a template engine, pass OG values as locals:

// EJS layout head partial — views/partials/head.ejs
<title><%= title %></title>
<meta property="og:title"       content="<%= ogTitle || title %>" />
<meta property="og:description" content="<%= ogDescription %>" />
<meta property="og:image"       content="<%= ogImage %>" />
<meta property="og:url"         content="<%= ogUrl %>" />
<meta name="twitter:card"       content="summary_large_image" />

// route handler
res.render('product', {
  title: product.name,
  ogTitle: product.name,
  ogDescription: product.tagline,
  ogImage: `https://yourdomain.com/og/${product.slug}.png`,
  ogUrl: req.protocol + '://' + req.get('host') + req.originalUrl,
});

Fastify with @fastify/view

import Fastify from 'fastify';
import view from '@fastify/view';
import ejs from 'ejs';

const app = Fastify();
app.register(view, { engine: { ejs } });

app.get('/post/:id', async (req, reply) => {
  const post = await getPost(req.params.id);
  return reply.view('post.ejs', {
    ogTitle: post.title,
    ogDescription: post.excerpt,
    ogImage: post.coverImage,
    ogUrl: `https://yourdomain.com/post/${post.id}`,
  });
});

Generate OG images server-side with Satori or Sharp

For dynamic OG images, use Satori (renders JSX to SVG then PNG) or Sharp (image composition). Expose a /og/:slug.png route and point your og:image at it.

// /og/:slug route using @vercel/og (satori under the hood)
import satori from 'satori';
import sharp from 'sharp';
import { readFileSync } from 'fs';

app.get('/og/:slug.png', async (req, res) => {
  const post = await getPost(req.params.slug);
  const font = readFileSync('./fonts/Inter-Bold.ttf');

  const svg = await satori(
    { type: 'div', props: { style: { fontSize: 48, color: '#fff', background: '#0a0a0a', width: 1200, height: 630, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 48 }, children: post.title } },
    { width: 1200, height: 630, fonts: [{ name: 'Inter', data: font, weight: 700 }] }
  );

  const png = await sharp(Buffer.from(svg)).png().toBuffer();
  res.set('Content-Type', 'image/png').send(png);
});

Top mistakes in Node.js OG implementations

  • Relative image URLs: og:image must be an absolute URL including protocol and domain.
  • Missing og:type: Always include og:type — use website or article.
  • CDN caching stale previews: Set Cache-Control: public, max-age=3600 on og:image routes but short TTLs if content changes.
  • HTML entity escaping: Escape &, <, >, " in meta content values or scrapers will misparse them.

Verify your Node.js OG tags before going live

After implementing tags, paste your URL into OGFixer to see exactly how Twitter, Slack, Discord, and LinkedIn will render your preview — including image dimensions, missing required tags, and caching warnings.

Related guides