Explanation

Images and CORS

How LinkMetadata analyzes preview images, marks CORS safety, and proxies images for browser cards.

Link previews usually need images from another domain. That creates two separate concerns:

  • The API needs to inspect an image server-side to learn its MIME type, dimensions, size, and final URL.
  • Browser apps need to render that image without exposing users to broken previews or CORS surprises.

Image metadata is best effort

When a page exposes an image through Open Graph or Twitter Card tags, LinkMetadata fetches the image and returns structured metadata.

{
  "image": {
    "url": "https://example.com/card.png",
    "type": "image/png",
    "width": 1200,
    "height": 630,
    "size": 84214,
    "cors_safe": false
  }
}

Some fields can be null. For example, a server may omit Content-Length, block image inspection, redirect to an unsupported response, or stream an image whose dimensions cannot be determined from the first bytes read.

What cors_safe means

cors_safe describes whether the image response includes permissive browser CORS headers. Today, LinkMetadata marks an image as CORS-safe when the image response includes:

Access-Control-Allow-Origin: *

If the field is false or missing, do not assume browser JavaScript can safely fetch, inspect, or reuse the image across origins. You can still store and display the url, but direct browser access may be limited.

How the component uses the proxy

The preview component makes this decision for you:

  • image.url is proxied unless image.cors_safe is true.
  • favicon.url is proxied only when favicon.cors_safe is explicitly false.
  • data: image URLs are used directly and are not proxied.

If an image still fails to load, the component hides the image area. If a favicon fails, only the favicon is hidden.

Proxy endpoint

Use the image proxy when you are building your own renderer and want a browser-friendly image URL.

GET https://api.linkmetadata.com/v1/cors-proxy?url={encoded image URL}

The proxy accepts only HTTP and HTTPS image URLs. It verifies the upstream Content-Type starts with image/ and checks the response bytes before streaming the image back with permissive CORS headers.

Supported proxied formats are JPEG, PNG, GIF, and WebP.

The proxy is part of the free public API and shares the same per-IP limit: 20 requests per 10 seconds, followed by a 10-second block when the limit is reached.

const imageUrl = "https://example.com/card.png";
const proxyUrl = new URL("https://api.linkmetadata.com/v1/cors-proxy");
proxyUrl.searchParams.set("url", imageUrl);

const response = await fetch(proxyUrl);

if (!response.ok) {
  throw new Error(`Image proxy failed with ${response.status}`);
}

const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);

document.querySelector("img").src = objectUrl;

For normal image tags, you can also use the proxied URL directly:

<img
  src="https://api.linkmetadata.com/v1/cors-proxy?url=https%3A%2F%2Fexample.com%2Fcard.png"
  alt="">

Proxy responses

Successful proxy responses include:

Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400
Content-Type: image/png

Common error cases include:

StatusReason
400Missing URL, invalid URL, unsupported protocol, non-image content, empty response, or unsupported image bytes.
Upstream statusThe remote image request failed, for example 404 or 403.

When to use each option

SituationRecommended approach
You use <link-metadata-preview>Let the component decide when to proxy.
You render your own card in the browserUse image.url directly when cors_safe is true; otherwise use /v1/cors-proxy.
You render server-side onlyUse the metadata URL directly unless your frontend needs browser-side CORS access.
You need dimensions or MIME typeRead image.width, image.height, and image.type from the metadata response; do not re-fetch unless your app needs to.

The proxy is for public preview images. Do not use it for private files, authenticated assets, or arbitrary non-image downloads.