Skip to content

feat: add a Photo template that uses the page's image - #97

Merged
feri-irawan merged 4 commits into
2.0from
feat/og-photo-template
Oct 17, 2026
Merged

feri-irawan merged 4 commits into
2.0from
feat/og-photo-template

Conversation

@feri-irawan

@feri-irawan feri-irawan commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

/og?template=8 fills the image with a photo and sets the title over a dark fade at the bottom, with the site's icon and address at the top. It suits blog posts and news articles that have a cover image.

The photo is the first of these that loads:

  1. The new image parameter, for example &image=https://example.com/cover.jpg.
  2. The page's og:image or twitter:image.
  3. The first large image in the page's <article> or <main>, then anywhere on the page. Lazy-loaded images are found through data-src and srcset.

Images under 200 pixels, SVGs, and fiimage's own /og images are skipped. Without a photo, the background stays dark. Other templates don't fetch photos, so they are no slower.

Avoiding self-requests

A page's og:image is often a fiimage /og URL, because that's the main way fiimage is used. If fiimage fetched it, it would render another OG image, which could fetch the same URL again, and so on.

  • /og URLs are never used as a photo or an icon.
  • Icon and photo requests carry an x-fiimage-fetch header. A request that arrives with it skips icons and photos. This also stops a loop that reaches fiimage through a redirect, including the same loop through icons, which could already happen before this change. The header is read from req.headers, because Vercel passes a plain Node request without Express's req.get().

Details

  • Fetching: photos are fetched with the same SSRF protection as icons, up to 8 MB, in parallel with the icon. They are shrunk to just cover 1200x630 and embedded as JPEG.
  • Cropping: the photo is drawn as a background-size: cover background. An <img> with object-fit: cover was cut off by resvg when the photo was much taller or wider than the card.
  • README: a Photo template section with two examples side by side, a photo passed with image and the photo found on a Wikipedia article (credited under CC BY-SA), and the image parameter in the query table.

Testing

  • Candidate order: og:image and twitter:image first, then article images. /og URLs, SVGs, small images, and data URIs are skipped. data-src and the first srcset entry are used, and there are at most 3 candidates.
  • Fetching: photos are only fetched when asked. An explicit image is tried first, a failed photo falls through to the next candidate, and photo requests carry the header. Icons can be skipped.
  • Vercel requests: the self-request check works on a request without req.get(). Running the handler with such a request failed with "Unable to generate the image." before the fix and gets past the check after it.
  • Sizing: photos are scaled down to cover 1200x630, never up.
  • Route: a non-HTTP image returns 400.
  • Render tests: all templates, with and without an icon and photo, produce a 1200x630 PNG.
  • Visual check: wide and tall photos fill the card, a 100x100 image is rejected, and the dark fallback reads well. On the preview, the Wikipedia example shows the article's photo.
  • Removing the /og skip or the request header makes the tests fail.
  • bun test: 162 pass. bun run typecheck: clean.

Try it on this PR's Vercel preview: /og?template=8&url=https://en.wikipedia.org/wiki/Komodo_dragon, or /og?template=8&url=https://github.com/feri-irawan/fiimage&image=https://images.unsplash.com/photo-1518791841217-8f162f1e1131?w=1200.

template=8 fills the Open Graph image with a photo and sets the title
over a dark fade. The photo is the first that loads of:

1. The new `image` query parameter.
2. The page's og:image or twitter:image.
3. The first large image in the page's <article> or <main>, then
   anywhere on the page, including lazy-loaded data-src and srcset.

Images under 200 pixels, SVGs, and fiimage's own /og images are skipped.
The photo is fetched with the same SSRF protection as icons, alongside
the icon, and shrunk to cover 1200x630 before rendering. Without a
photo, the background stays dark. Other templates do not fetch photos.

fiimage now marks its icon and photo requests with an x-fiimage-fetch
header. A request that carries it skips icons and photos, so a page
whose icon or image points back at fiimage cannot make it request itself
over and over.

The photo is drawn as a cover background, because resvg cuts off an
<img> that Satori crops with object-fit when the image is much taller or
wider than the card.
@codesandbox

codesandbox Bot commented Oct 17, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web Editor • VS Code • Insiders

Open Preview

@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
fiimage-yth6 Ready Ready Preview Oct 17, 2026 5:14pm UTC

On Vercel, /og receives a plain Node request, which has no Express
req.get(), so every template=8 request failed with "Unable to generate
the image.". Read the header from req.headers instead, and test the check
with a request that has no get().
Remote fetches now send "fiimage (+https://github.com/feri-irawan/fiimage)"
as the User-Agent unless the caller sets one, instead of Bun's default
"Bun/1.4.2". Some sites, such as Wikimedia, ask automated clients to
identify themselves and may block generic library agents.

The README's Photo template section shows two examples side by side: a
photo passed with image, and the photo fiimage finds on a Wikipedia
article, credited under CC BY-SA.
Remote fetches go back to Bun's default User-Agent. fiimage is a public
service that fetches any URL for anyone, so an identifying User-Agent
would point other people's traffic at the project.
@feri-irawan
feri-irawan merged commit bce98bd into 2.0 Oct 17, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview — bf3da87a Deployed Oct 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant