Astro DB Open Graph Tags: Dynamic OG Images from Database Content

Generate dynamic Open Graph images in Astro using Astro DB data — covers server islands, getStaticPaths, and edge OG image generation.

Astro DB and Open Graph: a powerful combo

Astro DB is the official SQLite-backed database for Astro, introduced in Astro 4. It lets you store and query content (blog posts, products, events) directly in your Astro project — no external CMS required. Combined with Astro's SSR and server endpoint capabilities, you can generate dynamic Open Graph images per database record without any third-party image service.

Social crawlers only parse raw HTML — they never execute JavaScript. Astro's component-level rendering runs on the server, so OG tags rendered inside <head> of an Astro page are available in the initial HTML response before any JS runs.

Define an Astro DB table and query it

First, define your content table in db/config.ts:

// db/config.ts
import { defineDb, defineTable, column } from "astro:db";

const Post = defineTable({
  columns: {
    id:          column.number({ primaryKey: true }),
    slug:        column.text({ unique: true }),
    title:       column.text(),
    excerpt:     column.text(),
    ogImageUrl:  column.text({ optional: true }),
    publishedAt: column.date(),
  },
});

export default defineDb({ tables: { Post } });

Then query your database in a dynamic route to produce per-post OG tags:

---
// src/pages/blog/[slug].astro
import { db, Post, eq } from "astro:db";

const { slug } = Astro.params;

// Fetch the post from Astro DB
const post = await db.select().from(Post).where(eq(Post.slug, slug)).get();

if (!post) {
  return Astro.redirect("/404");
}

const ogImage = post.ogImageUrl
  ?? `https://yourdomain.com/api/og?title=${encodeURIComponent(post.title)}`;
const canonicalUrl = `https://yourdomain.com/blog/${slug}`;
---

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>{post.title} | My Blog</title>
    <meta name="description" content={post.excerpt} />

    <!-- Open Graph -->
    <meta property="og:type"        content="article" />
    <meta property="og:title"       content={post.title} />
    <meta property="og:description" content={post.excerpt} />
    <meta property="og:image"       content={ogImage} />
    <meta property="og:url"         content={canonicalUrl} />

    <!-- Twitter Card -->
    <meta name="twitter:card"        content="summary_large_image" />
    <meta name="twitter:title"       content={post.title} />
    <meta name="twitter:description" content={post.excerpt} />
    <meta name="twitter:image"       content={ogImage} />

    <link rel="canonical" href={canonicalUrl} />
  </head>
  <body>
    <article>
      <h1>{post.title}</h1>
      <p>{post.excerpt}</p>
    </article>
  </body>
</html>

getStaticPaths with Astro DB for SSG

If you deploy to a static host, export getStaticPaths to pre-render every post at build time. Astro DB runs at build time in this mode, querying all rows to generate the path list:

---
// src/pages/blog/[slug].astro (SSG mode)
import { db, Post } from "astro:db";

export async function getStaticPaths() {
  const posts = await db.select().from(Post).all();
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

interface Props {
  post: typeof Post.$inferSelect;
}

const { post } = Astro.props;
const ogImage = `https://yourdomain.com/api/og?title=${encodeURIComponent(post.title)}`;
---

<html lang="en">
  <head>
    <title>{post.title} | My Blog</title>
    <meta property="og:title"       content={post.title} />
    <meta property="og:description" content={post.excerpt} />
    <meta property="og:image"       content={ogImage} />
    <meta property="og:url"         content={`https://yourdomain.com/blog/${post.slug}`} />
    <meta name="twitter:card"       content="summary_large_image" />
    <meta name="twitter:image"      content={ogImage} />
  </head>
  <body>
    <h1>{post.title}</h1>
  </body>
</html>

Generate dynamic OG images with a server endpoint

Create a server endpoint at src/pages/api/og.ts that accepts a title query parameter and returns a PNG using Satori (or any canvas-based library):

// src/pages/api/og.ts
import type { APIRoute } from "astro";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
import fs from "node:fs";
import path from "node:path";

export const GET: APIRoute = async ({ url }) => {
  const title = url.searchParams.get("title") ?? "My Site";

  // Load a font (store in /public/fonts/)
  const fontData = fs.readFileSync(
    path.join(process.cwd(), "public/fonts/Inter-Bold.ttf")
  );

  const svg = await satori(
    {
      type: "div",
      props: {
        style: {
          background: "#0a0a0a",
          width: "100%",
          height: "100%",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          padding: "60px",
        },
        children: {
          type: "p",
          props: {
            style: { color: "white", fontSize: 60, fontWeight: 700 },
            children: title,
          },
        },
      },
    },
    {
      width: 1200,
      height: 630,
      fonts: [{ name: "Inter", data: fontData, weight: 700, style: "normal" }],
    }
  );

  const resvg = new Resvg(svg);
  const png = resvg.render().asPng();

  return new Response(png, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=31536000, immutable",
    },
  });
};

Now any post with no stored ogImageUrl will automatically get a generated OG image with its title text. The Cache-Control: immutable header ensures CDN caching while cache-busting is achievable by changing the query string.

OG tag checklist for Astro DB sites

  • Store OG image URLs in your Astro DB schema (or generate dynamically).
  • Use absolute URLs — never relative paths — for og:image.
  • Set og:type, og:url, og:title, og:description, and og:image.
  • Add twitter:card = summary_large_image explicitly.
  • Use output: "server" in astro.config.mjs for SSR deployments.
  • Test every post page with OGFixer before sharing.

Check your Astro OG tags free

Paste any Astro DB page URL into OGFixer to preview on Twitter, LinkedIn, Discord, and Slack instantly.

Related guides