DEV Community

orca forge
orca forge

Posted on Originally published at forge.workstyle.tech

Automatically Generating OGP Images for Each Article on Astro Blog

📝 Originally published (in Japanese) at forge.workstyle.tech.

In this post, I’ll show you how to implement an automated pipeline for generating per-post Open Graph (OGP) images in an Astro blog, triggering image creation on every publish. I’ll also cover how to add a fallback image for posts without a dedicated graphic and include post-build validation checks.

This setup assumes you are running a blog built with Astro, you can retrieve each post’s slug, and you have an existing script to publish your posts. Specifically, for this example, the Markdown frontmatter for each article includes status, slug, title, and created fields, and the generated images are synced to the public/og/ directory within your Astro project.

I had previously written a script to generate article images, but I hadn’t integrated it into my actual publishing workflow. It wasn’t until 2026-10-01 when I looked at how my post was displayed as a card on X that I realized this gap existed.

Step 1: Generate Images for Each Article

First, prepare a script to create OGP images. In this example, the script passes the article title and date to an HTML template and uses Chrome's headless mode to render a 1200Ă—630 PNG. The design features a navy blue background with the title overlaid.

Here is an excerpt from the specified ogp-build.mjs script. It reads articles from the Vault and only targets published ones for generation. By default, it skips articles that already have an image.

const args = process.argv.slice(2);
const all = args.includes("--all");
const only = args.filter((a) => !a.startsWith("--"));

for (const f of readdirSync(BLOG)) {
  if (!f.endsWith(".md") || f.startsWith("_")) continue;

  const t = readFileSync(join(BLOG, f), "utf8");
  const meta = fm(t);
  if (meta.status !== "published") continue;

  const slug = meta.slug || f.replace(/\.md$/, "");
  if (only.length && !only.includes(slug)) continue;
  if (!all && !only.length && existsSync(join(OUT, `${slug}.png`))) continue;

  targets.push({
    slug,
    title: meta.title || slug,
    date: (meta.published_at || meta.created || "").slice(0, 10),
  });
}
Enter fullscreen mode Exit fullscreen mode

To generate the images, the article title and date are passed as query parameters to the template. In ogp-template.html, the font size adjusts based on the title length, and the date is also displayed on the image. The template dimensions are 1200Ă—630.

In this script, the source data for the images is kept within the Vault alongside the article Markdown. The output destination is Generated/Blog/og/<slug>.png. The process of copying these images to the site's public/og/ directory is handled separately from the image generation itself.

Here is an example execution. Replace the paths with those corresponding to your repository.

node /path/to/vault/_system/ogp-build.mjs
Enter fullscreen mode Exit fullscreen mode

In a standard run, only missing images are generated. Use the --all flag to regenerate images for all articles, or pass a specific slug to target a single article.

node /path/to/vault/_system/ogp-build.mjs --all
node /path/to/vault/_system/ogp-build.mjs my-article-slug
Enter fullscreen mode Exit fullscreen mode

The generation script identifies target articles via their front matter. Therefore, if your article publishing status or slug conventions differ, adjust this logic to match your specific setup. In the example script, only articles with a status of published are targeted.

Common Pitfall: Even if you run the image generation script manually, new articles added afterward will not be automatically processed. In my environment, while the image generation step itself was functioning, it was not integrated into the publishing workflow. Out of 93 published articles, only 73 had images; the 20 articles published after late September lacked individual OGP images.

Step 2: Insert image generation at the beginning of the publication flow

Once you are able to generate images, run the generation script before syncing the articles to the site. In the specified publish.sh, the following process is performed after updating the status to published but before the site synchronization:

echo "② Generating OGP images (only for ungenerated articles)"
node "$VAULT/_system/ogp-build.mjs" >/dev/null

echo "③ Syncing to site"
cd "$SITE"
npm run --silent sync
Enter fullscreen mode Exit fullscreen mode

The order is critical. We set the article to published, generate the image, and then sync to the site. This ensures that the articles intended for publication are visible to the generation process. Through synchronization, the images in the Vault are copied to the public/og/ directory of the Astro site.

Even if your publication process handles article publishing, syncing, and committing in a different order, ensure that image generation and synchronization occur after the article is marked as published but before the Astro build. If you only generate the images but forget to sync, the images will be missing from the site's public/og/ folder at the time of the build.

Common pitfall: Astro pages can still build even if images are missing. In my blog, if an article image was not found, it would fall back to the blog-placeholder-1.jpg included with the template. Consequently, the publication process wouldn't fail, and I wouldn't notice the issue until the cards started displaying "Build the web you want."

Step 3: Defining a Fallback Image for Articles Without a Custom Image

When an article lacks a specific image, relying on Astro’s default placeholder can make it easy to overlook missing generations. It’s best to prepare a common site-wide image on your end and establish it as the fallback.

In the specified BaseHead.astro, the code extracts the article's slug from the URL path to look for public/og/<slug>.png. If that file exists, it uses the article-specific image; otherwise, it falls back to referencing public/og/_site.png. Here is an excerpt of that logic:

const m = Astro.url.pathname.match(/^\/blog\/([^/]+)\/?$/);
const ogSlug = m ? m[1] : null;
const ogFile = ogSlug
  ? join(process.cwd(), "public", "og", `${ogSlug}.png`)
  : null;

const siteOg = join(process.cwd(), "public", "og", "_site.png");
const ogImage = ogFile && existsSync(ogFile)
  ? new URL(withBase(`/og/${ogSlug}.png`), Astro.site ?? Astro.url)
  : existsSync(siteOg)
    ? new URL(withBase("/og/_site.png"), Astro.site ?? Astro.url)
    : new URL(image.src, Astro.url);
Enter fullscreen mode Exit fullscreen mode

In this example, article-specific images take priority. If one isn't found, the process falls back to the site-wide common image. If that common image is also missing, it uses the image passed directly to the component. We set the determined ogImage variable for both og:image and twitter:image meta tags in the Astro component.

<meta property="og:image" content={ogImage} />
<meta name="twitter:image" content={ogImage} />
Enter fullscreen mode Exit fullscreen mode

Place the common fallback image at public/og/_site.png as well. In my setup, I used an image with the same design as the article-specific images, but featuring the site’s title to represent the site as a whole.

Common Pitfall: Using a common image as a fallback ensures that pages without article-specific images still generate convincing social media cards. In my environment, 20 posts that would have shown Astro’s default image were successfully published because the fallback prevented a hard error. Don’t stop at just providing the common image; also implement post-build inspection to catch these potential gaps.

Step 4: Checking for Fallback to Common Images After Building

Even after completing the pre-publication generation and synchronization, we need to confirm whether the article-specific images have been set in the build results. In publish.sh, we search for dist/blog/*/index.html after building.

missing=$(grep -L '/og/[^"]*\.png' dist/blog/*/index.html 2>/dev/null; grep -l '/og/_site\.png' dist/blog/*/index.html 2>/dev/null)

if [ -n "$missing" ]; then
  node "$VAULT/_system/ogp-build.mjs" >/dev/null
  npm run --silent sync >/dev/null
  npx astro build >/dev/null

  still=$(grep -l '/og/_site\.png' dist/blog/*/index.html 2>/dev/null)
  [ -n "$still" ] && osascript -e \
    'display notification "Failed to create OGP image for the article, published with the site common image" with title "Blog"'
fi
Enter fullscreen mode Exit fullscreen mode

This code is an excerpt from the specified file. In the actual file, after building, we search for article pages and if we find pages without article image specifications or pages referencing common images, we perform image generation, synchronization, and rebuilding. If the fallback to common images still remains, we send a notification to Mac.

We limit the search target to dist/blog/*/index.html to exclude the article list page and non-article pages from the inspection targets. If image inspection is also required for non-articles, add the target path according to your site structure.

The shell search depends on the shape of the URL output in HTML. If the URL changes due to the withBase setting or the deployment subpath, adjust the search string to match the generated result. Check the actual og:image in the post-build HTML and then decide on the condition to ensure you know what the inspection is looking for.

A point where you may get stuck: If the post-build HTML points to a common image, it is possible that the image generation or synchronization did not complete in time. In my blog, if a page with a common image is found, I redo the generation and synchronization and rebuild. If it still remains, I send a notification.

Step 5: Verify the Entire Publishing Workflow

Once everything is wired up, prepare a sample article and run it through the publishing workflow. You're not just checking if the image file gets created—you also need to confirm that published articles are targeted for generation, synced properly, and that the built HTML points to the article-specific image URL.

Here is the verification checklist:

  1. Make sure the article slated for publication has both a slug and a title.
  2. Run the generation script and verify that Generated/Blog/og/<slug>.png is created.
  3. Confirm that public/og/<slug>.png exists on the site after syncing.
  4. Build Astro and ensure that og:image and twitter:image inside dist/blog/<slug>/index.html point to the article's image.
  5. Verify that it falls back to public/og/_site.png when an article image is missing.
  6. Check that if the post-build inspection finds a fallback image, it triggers a regeneration and rebuild—and sends a notification if the fallback still persists.

If you rely on already-posted X cards just to check the preview, you might find yourself waiting around for site updates to reflect. X caches card metadata for a while, and the Card Validator is no longer available as an official way to force a refresh. For verification, always inspect the built HTML and image URL directly first. In my setup, the image URL returned a 404 for a brief moment right after deployment before returning a 200 a few seconds later.

Even after the image URL returns a 200, previously shared cards won't necessarily update right away. Always treat the site's build output and X's cached card preview as two separate things to verify.

Summary

Simply creating OGP images for each article doesn't guarantee they will be automatically applied to new posts. You must connect them to your deployment workflow in the order of generate, sync, and build.

In this configuration, the system uses the article image if one exists; otherwise, it falls back to a site-wide common image. After the build, we check if any article pages are still pointing to the common image. If any are found, we recreate the images and trigger a rebuild.

When verifying, check the generated PNG, the file synced to the site, and the og:image after the build in that order. Ensure your deployment flow verifies that the process—which worked once manually—is also being executed for any newly added articles.

Top comments (0)