ScreenshotNeo

BlogHow-to

Why Twitter Open Graph Images Are Not Showing and How to Fix Them

Fix missing X/Twitter link images by checking Open Graph tags, image access, crawler rules, duplicates, and the HTML your server actually returns.

By the ScreenshotNeo team29 September 202610 min read

Why Twitter Open Graph Images Are Not Showing and How to Fix Them

A missing image on an X (formerly Twitter) link preview usually comes from one of three places: the page does not send the intended Open Graph metadata in its initial HTML, X cannot fetch the image URL, or another tag takes precedence. Fix the problem by inspecting the raw response, testing the image as an unauthenticated request, and checking robots, firewall, CDN, and duplicate-tag rules.

This guide gives you a complete diagnostic path, working metadata examples, command-line checks, server-side rendering guidance, and fixes for common CMS and CDN configurations.

What X needs to find an image

Open Graph metadata belongs in the document <head>. The protocol defines four basic properties: og:title, og:type, og:image, and og:url. It also defines image fields for a secure URL, MIME type, dimensions, and alternative text. See the Open Graph protocol for the property definitions.

<head>
  <meta property='og:title' content='Example product page'>
  <meta property='og:type' content='website'>
  <meta property='og:url' content='https://example.com/products/widget'>
  <meta property='og:image' content='https://example.com/images/widget-share.jpg'>
  <meta property='og:image:secure_url' content='https://example.com/images/widget-share.jpg'>
  <meta property='og:image:type' content='image/jpeg'>
  <meta property='og:image:width' content='1200'>
  <meta property='og:image:height' content='630'>
  <meta property='og:image:alt' content='The Widget product on a desk'>

  <meta name='twitter:card' content='summary_large_image'>
  <meta name='twitter:title' content='Example product page'>
  <meta name='twitter:description' content='A short description of the page.'>
  <meta name='twitter:image' content='https://example.com/images/widget-share.jpg'>
</head>

The Twitter-specific tags provide card presentation and fallback values. Keep the Open Graph tags correct even when you include them. If several og:image values are sent, the first value has preference when values conflict, so an unintended earlier tag can explain why the wrong image appears.

Step 1: Inspect the HTML your server returns

Do not rely only on the browser’s Elements panel. A browser may add tags after JavaScript runs, while a crawler can consume the original HTTP response. Fetch the page without a login session and inspect the returned source.

Trace the page response and image request separately when diagnosing a missing social preview.
Trace the page response and image request separately when diagnosing a missing social preview.
curl -L --compressed -sS https://example.com/products/widget \
  -o page.html

rg -n "og:(title|type|url|image)|twitter:(card|title|description|image)" page.html

For headers and redirects, use:

curl -I -L https://example.com/products/widget

Check all of the following:

  • The response is the intended page, not a login form, bot challenge, error document, or redirect loop.
  • The tags appear inside <head> in the initial response.
  • og:image contains the exact absolute URL you intend to share.
  • og:title, og:type, and og:url identify the same page.
  • There is no earlier plugin-generated og:image that wins ordering.
  • The image URL uses HTTPS and does not depend on a browser cookie or session.

Step 2: Fetch the image independently

A correct tag cannot produce a preview if the image request fails. Request the image URL directly, without your browser’s cookies or authentication headers.

curl -L -sS -D image.headers \
  -o share-image.bin \
  https://example.com/images/widget-share.jpg

file share-image.bin
cat image.headers

Confirm that the final response is an image, not HTML. Look for a successful status, a suitable Content-Type such as image/jpeg, and a URL that does not redirect to a protected area. Also test from outside your corporate network if your CDN or WAF uses IP reputation rules.

Archived X troubleshooting guidance lists crawler blocks, robots.txt, server access-denial rules, and an image that is too large to download as possible causes. Treat that material as historical guidance rather than a current guarantee about X limits. The practical test is whether an unauthenticated crawler can retrieve the page and image reliably.

Access can fail at several layers even when the page works for you:

Layer What to inspect Typical fix
robots.txt Rules that disallow the shared page or image path Permit the paths required for public previews, then verify the deployed file
WAF or bot protection Challenge pages, 403 responses, rate limits, or user-agent blocks Allow legitimate fetches for public content and exempt image paths from interactive challenges
CDN access control Signed URLs, referer checks, country restrictions, or expiring tokens Use a stable public image URL or configure a preview-safe delivery rule
Origin server IP deny rules, authentication middleware, or incorrect MIME types Return the image directly with a success status and image content type
Hotlink protection Requests without your normal browser referer are denied Do not require a browser referer for the public share image

Review access logs for both the HTML request and the image request. A page may be allowed while its image directory is denied. Conversely, the image may load but the page may be blocked before the crawler can discover its URL.

Step 4: Remove duplicate or conflicting metadata

CMS themes and SEO plugins often emit their own social tags. View the raw response and search for every occurrence:

rg -n "property=['\"]og:image|name=['\"]twitter:image" page.html

If more than one value exists, fix the component that generates the unwanted first value. Editing a template while leaving a plugin active can create a duplicate. Also check that the canonical URL, Open Graph URL, and the URL you share use the same scheme and hostname where possible.

Step 5: Make server-rendered frameworks emit tags early

For server-rendered applications, generate metadata from the route data before sending the response. For client-only applications, add an HTML shell or server-side rendering layer that includes the tags for each URL. A browser-side script that inserts og:image after load is a practical failure mode because the initial crawler response may never contain it.

Example: minimal Express response

app.get('/products/:slug', async (req, res) => {
  const product = await loadProduct(req.params.slug);
  const title = escapeHtml(product.title);
  const image = escapeAttribute(product.shareImageUrl);
  const url = `https://example.com/products/${encodeURIComponent(product.slug)}`;

  res.type('html').send(`<!doctype html>
<html><head>
  <meta property='og:title' content='${title}'>
  <meta property='og:type' content='website'>
  <meta property='og:url' content='${url}'>
  <meta property='og:image' content='${image}'>
  <meta name='twitter:card' content='summary_large_image'>
  <meta name='twitter:image' content='${image}'>
</head><body>...</body></html>`);
});

Escape values before placing them in HTML attributes, and validate that the stored image URL is an allowed HTTPS URL. Never allow a user-controlled value to become arbitrary markup.

Step 6: Recheck after deployment

  1. Deploy the metadata and access-rule change.
  2. Fetch the page and image again with curl from an unauthenticated environment.
  3. Compare the response with the intended URL, title, and image.
  4. Share the corrected URL again and inspect the resulting card.

Cached scrapes can make a corrected page appear unchanged for a while. The available X material does not establish a current official cache interval or a guaranteed refresh command, so avoid promising a particular waiting period or relying on query-string tricks. If the issue persists, verify the live response and consult current X-owned documentation for platform-specific behavior.

Common errors and fixes

Symptom Likely cause Fix
No image, plain URL No usable og:image, inaccessible image, or crawler block Inspect raw HTML, fetch the image directly, and review access logs and robots rules
Wrong image An earlier duplicate og:image wins Remove the duplicate or reorder generated metadata so the intended value is first
Image works while logged in only Authentication, signed URL, or cookie requirement Publish a stable public image URL for previews
Image request returns HTML WAF challenge, error page, or redirect to login Allow direct image retrieval and verify the final content type
Tags visible in DevTools but not curl Tags are injected by client-side JavaScript Render them in the initial server response or use server-side rendering
Intermittent results Rate limiting, expiring URLs, or inconsistent CDN cache Use a stable URL, inspect edge logs, and remove crawler-hostile challenges from public assets
Preview remains old Cached scrape or an unchanged upstream response Confirm the live source first, then allow for cache behavior without assuming a fixed interval
Consent layers and overlays can change the rendered page a screenshot captures.
Consent layers and overlays can change the rendered page a screenshot captures.

Testing checklist for releases

  • Request every important page with redirects followed and no cookies.
  • Assert exactly one intended og:image appears before any fallback.
  • Check that the image returns an image MIME type and does not require authentication.
  • Test a page with a long title, a missing optional description, and a changed image.
  • Review robots.txt, WAF events, CDN rules, and origin logs after deployment.
  • Repeat checks for staging and production hostnames so environment-specific rules do not diverge.

Or skip the browser setup

If you need a visual check of what a public page actually renders, ScreenshotNeo can capture it through one HTTP request. Its clean-shot process accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options. This basic call captures the rendered page, which is useful for checking whether a consent layer or client-side layout is hiding the content you expect to share:

cURL

curl -G 'https://api.screenshotneo.com/v1/shot' \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/products/widget \
  -o shot.webp

Python

import requests

r = requests.get(
    'https://api.screenshotneo.com/v1/shot',
    params={
        'access_key': 'YOUR_API_KEY',
        'url': 'https://example.com/products/widget'
    },
    timeout=90,
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/products/widget'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Useful capture controls for this diagnosis

  • Wait conditions: wait for a CSS selector, a delay, or network idle when the page renders content asynchronously.
  • Custom headers, cookies, user agent, and Authorization: reproduce a permitted application state when your page needs controlled access. Do not expose secrets in client-side code.
  • Hide selectors and custom JavaScript: remove a temporary overlay or click consent controls before capture.
  • Block requests or resource types: reduce noise from ads and trackers while diagnosing page behavior.
  • Full-page or element capture: capture the whole landing page or isolate the section containing the share artwork.
  • Dark mode, device presets, viewport, and retina scale: reproduce the conditions under which your layout changes.
  • Caching with a chosen TTL: avoid repeated work while iterating on a stable URL; cache hits are identified and not billed.

ScreenshotNeo has a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account.

Performance, reliability, and cost notes

For your own metadata checks, keep image URLs stable and serve them from a cacheable CDN path. Avoid generating a new signed URL for every request unless the signing policy explicitly permits crawlers and the URL remains valid long enough for retrieval. Monitor origin and edge logs for failed image requests rather than judging success from a logged-in browser.

When using a screenshot service for visual verification, wait only for the condition your page needs. A selector wait is usually more deterministic than an arbitrary long delay; network-idle waits can be slow on pages with persistent analytics connections. Use element capture when a full page is unnecessary, and use caching when the target has not changed. ScreenshotNeo’s asynchronous jobs, signed webhooks, bulk capture of up to 100 URLs per call, usage API, OpenAPI specification, image resizing, PDF options, and signed links are available when a diagnostic workflow grows into a production pipeline.

FAQ

Do I need both Open Graph and Twitter tags?

Use Open Graph tags as the page’s core sharing metadata and include the Twitter card tags for card presentation and fallback behavior. Keep their title, description, and image values consistent.

Your browser may have cookies, credentials, a cached image, or JavaScript-generated metadata. Test the initial HTML and image URL with an unauthenticated request.

Can a robots.txt rule break the image while the page remains public?

Yes. The page and image are separate requests and can be governed by different rules. Check both paths and the relevant CDN or WAF configuration.

Should I add several og:image tags as fallbacks?

Only when you deliberately control their order. The first value has preference when Open Graph values conflict, so accidental duplicates commonly produce the wrong image.

Are old image-size limits from Twitter documentation still current?

The available guidance is archived and the research did not verify current X thresholds. Do not publish historical numbers as current requirements; use current X documentation when a specific limit matters.

How can I prove what a crawler sees?

Save the raw HTTP response, inspect the metadata in that file, fetch the image separately, and review access logs. A rendered browser screenshot can complement those checks but cannot replace them.