srmdn.

Back

Why Your og:image Doesn't Show in Social SharesBlur image

When I shared one of my posts on socials, the link preview was blank. No image, just the title and a gray box. I’d set a featured image in my CMS dashboard — it was clearly there — but social crawlers were ignoring it completely.

Turns out there were two separate bugs. They’re easy to miss because the site looks perfectly fine in a browser.

How Social Share Previews Work#

When you paste a URL into Twitter/X, iMessage, LinkedIn, or Slack, the platform’s crawler fetches that URL and reads the Open Graph meta tags in the <head>:

<meta property="og:image" content="https://example.com/image.webp" />
<meta property="twitter:image" content="https://example.com/image.webp" />
html

The crawler then fetches the image at that URL and renders the preview card. Two things can silently break this:

  1. The URL isn’t a real HTTP URL — it’s a data: URI (base64-encoded image embedded directly in the HTML)
  2. The URL is technically a URL, but it points to localhost — unreachable from the outside

Both give you the same result: no image in the preview. The crawler quietly fails and moves on.

How to Diagnose#

Before guessing, check what your page is actually serving. View source on the live page (Ctrl+U) and search for og:image:

<!-- Bug 1: base64 data URI — crawlers can't fetch this -->
<meta property="og:image" content="data:image/webp;base64,UklGRvpD..." />

<!-- Bug 2: localhost URL — unreachable from the internet -->
<meta property="og:image" content="http://localhost:4321/_astro/hero.C4SheoqF.webp" />

<!-- Correct -->
<meta property="og:image" content="https://yoursite.com/_astro/hero.C4SheoqF.webp" />
html

If you’re seeing either of the first two, read on.

Bug 1: The Base64 Image#

This shows up when your CMS stores the hero image as a base64 data URI directly in the markdown frontmatter:

---
title: My Post
heroImage: data:image/webp;base64,UklGRvpDAABXRUJQVlA4...
---
yaml

This works fine in the browser — the image renders — but when Astro processes it into an og:image tag, the full base64 string ends up as the content attribute. Social crawlers treat og:image as a URL to fetch. They won’t decode an embedded binary blob.

The Fix: Save Images as Real Files#

Extract the base64 data URI and write it to a real file on disk. In a Go backend, SavePost() is the right place to intercept it:

The saveHeroImageToDisk function parses the MIME type from the data URI, decodes the base64, and writes hero.webp (or .jpg, .png, etc.) into the post directory. The frontmatter ends up with:

heroImage: ./hero.webp
yaml

Astro’s content collection schema with image() picks up that relative path at build time, optimizes it, and outputs a proper /_astro/hero.{hash}.webp URL. That’s a real, crawlable HTTPS URL.

One more thing: your admin editor was probably uploading the image as base64 and expecting base64 back from the API. Now that the backend stores a file path, the GET /api/admin/posts/{slug} endpoint needs to read the file and return it as a data URI for the editor to display:

func (s *Server) GetPostAdminHandler(w http.ResponseWriter, r *http.Request) {
    // ...
    post, _ := fs.GetPost(s.ContentDir, slug)

    // Convert file path back to base64 for the editor
    if post.HeroImage != "" {
        post.HeroImage, _ = fs.ReadHeroImageAsDataURI(s.ContentDir, slug, post.HeroImage)
    }

    respondJSON(w, http.StatusOK, post)
}
go

The storage format (file on disk) is now separate from the API contract (base64 for the editor). Public readers get an optimized /_astro/ URL; the dashboard editor still sees the image it uploaded.

Bug 2: The Localhost URL#

This one is specific to Astro in SSR mode (output: 'server'). In SSR, Astro.url returns the internal server URL — the one Node.js sees, not the public domain:

Astro.url → http://localhost:4321/blog/my-post
plaintext

If you build your og:image URL from Astro.url, you’re embedding localhost in every social share tag on every page:

---
// ❌ Wrong — Astro.url is localhost in SSR
const socialImageURL = new URL(ogImage, Astro.url).href
// result: http://localhost:4321/_astro/hero.C4SheoqF.webp
---
astro

The Fix: Use Astro.site#

Astro gives you Astro.site, which is the canonical public URL you configured in astro.config.mjs. Build your canonical URL from that instead:

---
// ✅ Correct — canonicalURL built from Astro.site
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const socialImageURL = new URL(ogImage, canonicalURL).href
// result: https://yoursite.com/_astro/hero.C4SheoqF.webp
---
astro

The same issue applies to any other URL you construct in BaseHead.astro — canonical links, og:url, twitter:url. All of them should come from canonicalURL, not Astro.url directly:

---
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const socialImageURL = new URL(ogImage ?? config.socialCard, canonicalURL).href
---

<link rel='canonical' href={canonicalURL} />
<meta content={canonicalURL} property='og:url' />
<meta content={socialImageURL} property='og:image' />
<meta content={socialImageURL} property='twitter:image' />
astro

Verifying the Fix#

After rebuilding, view source on the live page and look for og:image:

<meta content="https://yoursite.com/_astro/hero.C4SheoqF.webp" property="og:image" />
html

If it’s a real https:// URL pointing to your domain, you’re done. You can also run it through social debugger tools — Twitter Card Validator, LinkedIn Post Inspector, or OpenGraph.xyz — though these cache aggressively and may show stale results for a while. Most have a “Scrape Again” button that forces a fresh fetch.

Gotchas#

ProblemCauseFix
Image shows in browser, not in social previewbase64 data URI in og:imageSave image as real file, store relative path
og:image URL contains localhostAstro.url used in SSR modeUse new URL(Astro.url.pathname, Astro.site)
Social debugger shows old/wrong imagePlatform cacheWait ~30 min, use “Scrape Again”
Editor shows broken image after backend changeAdmin endpoint returning file path, not data URIConvert back to base64 in admin handler

Both Bugs at Once#

Worth noting: both bugs can exist simultaneously and compound each other. A data: URI embedded in a tag that’s also been constructed from localhost is doubly broken. Fix the localhost URL first, then check whether the image content itself is valid — the failure mode for the second bug is harder to see until the URL is actually well-formed.

In my case I had both at the same time. The page looked completely fine in the browser on production. The only symptom was a blank card when sharing a link — easy to ignore if you’re not actively testing it.

Enjoyed this post?

Get Linux tips, sysadmin war stories, and new posts delivered to your inbox.

No spam. Unsubscribe anytime.

Why Your og:image Doesn't Show in Social Shares
https://srmdn.com/blog/fixing-og-image-in-social-shares
Author srmdn
Published at February 26, 2026