Skip to content

fix(seo): name JPEG in the post card image URL - #2065

Merged
feruzm merged 2 commits into
developfrom
fix/card-image-jpeg
Nov 12, 2026
Merged

feruzm merged 2 commits into
developfrom
fix/card-image-jpeg

Conversation

@feruzm

@feruzm feruzm commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Closes #2064

What

The post card image (og:image, twitter:image, oEmbed thumbnail_url) now names its format: the same proxy URL with format=jpeg instead of format=match.

Why

format=match is negotiated from the request's Accept header, but the CDN keeps one cached copy per URL. Once a browser fetched the card URL as AVIF, every link-preview crawler got that AVIF as well, and some networks (Reddit in the reported case) then show no thumbnail. A format in the URL is part of the cache key, so the card image is always a JPEG. It is also a fraction of the size of a full PNG cover, which keeps it under the size limits some networks apply. Animated sources are passed through by the proxy whatever the format.

Only the card image changes. Images on the page itself keep negotiating AVIF/WebP.

Depends on

ecency/imagehoster#77 (explicit JPEG output flattens transparency onto white instead of black) should be deployed before this reaches production, or covers with transparent areas would show a black background in previews.

Tests

  • New: the card image for a metadata cover, a body image and an already proxied cover is a /p/ URL with format=jpeg, mode=fit, width=1200, height=630; a post without an image has none. Failed before the change.
  • Full web suite: 6,370 passing. Typecheck and lint clean.

Summary by CodeRabbit

  • Improvements
    • Cover images now use JPEG formatting in entry cards when supported, while other image URLs and missing images remain unchanged.

The og:image, twitter:image and oEmbed thumbnail were Accept-negotiated
proxy URLs. The CDN keeps one copy per URL, so after a browser fetched
one as AVIF, link-preview crawlers were served AVIF too and showed no
thumbnail. A format in the URL is part of the cache key.

Closes #2064
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The entry card image helper now changes format=match to format=jpeg for valid proxied post image URLs. The specs cover JPEG card URLs, missing images, and unchanged unproxied or non-URL values.

Changes: Entry card image formatting

Layer / File(s) Summary
Format and test card images
apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts, apps/web/src/specs/app/entry-card-fields.spec.ts
The helper changes format=match to format=jpeg for valid URLs with paths starting /p/. The specs cover missing images, metadata and body images, already-proxied URLs, and unchanged unproxied or non-URL values.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: Minimal · up to b0afc

Social preview card images will use a fixed JPEG format instead of a negotiated one. No concrete merge-blocking risk was found. Deploy the imagehoster transparency fix first, as the PR description notes, to avoid black backgrounds on transparent images.

Pre-merge checks: Passed 4

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title names the change: the post card image URL now names the JPEG format. It matches the main change in the changeset, which switches the card image from format=match to format=jpeg in buildEntry…
Linked Issues check Passed Issue #2064 requires the post card image to be a proxied URL with format=jpeg, mode=fit, width=1200, height=630; no card URL with format=match; no card image for posts without an image; and the same U…
Out of Scope Changes check Passed The whole-PR diff (merge base fb0dd66 to head b0afce2) changes two files as summarized: entry-card-fields.ts and entry-card-fields.spec.ts. The helper change adds cardImageFormat and updates the Ent…
Full details: Title check

Explanation

The title names the change: the post card image URL now names the JPEG format. It matches the main change in the changeset, which switches the card image from format=match to format=jpeg in buildEntryCardFields and updates the spec. The title is specific and uses a conventional fix(seo) prefix. The changeset also covers the spec update and the no-image and pass-through cases, which the title does not need to list.

Full details: Linked Issues check

Explanation

Issue #2064 requires the post card image to be a proxied URL with format=jpeg, mode=fit, width=1200, height=630; no card URL with format=match; no card image for posts without an image; and the same URL for og:image, twitter:image, and oEmbed thumbnail_url.

Evidence at head: in apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts, buildEntryCardFields now returns image: cardImageFormat(image). cardImageFormat rewrites format=match to format=jpeg when the URL path starts with /p/. It keeps mode, width, and height unchanged, and it returns null for a missing image. Animated sources pass through the proxy, as the issue states. The spec file apps/web/src/specs/app/entry-card-fields.spec.ts covers metadata, body, and already-proxied covers, and a post without an image. Per the PR description, the new tests failed before the change.

Limits of this review: the diff shows the helper change. The summary does not show each consumer (og:image, twitter:image, oEmbed) directly. The consumer wiring is inferred from the PR description and the helper's role, and I did not inspect those routes.

The PR description's deployment dependency on ecency/imagehoster#77 is an operational note, not a coding requirement of #2064.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff (merge base fb0dd66 to head b0afce2) changes two files as summarized: entry-card-fields.ts and entry-card-fields.spec.ts. The helper change adds cardImageFormat and updates the EntryCardFields.image doc comment. The spec change updates the expected proxied-cover URL and adds image test cases. Every change maps to #2064 requirements. The PR description states that body images keep AVIF/WebP negotiation. The helper applies the rewrite only to /p/ URLs with format=match, so this matches the stated scope. No unrelated change was found.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

apps/web/src/specs/app/entry-card-fields.spec.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

I’m a rabbit with a link to share,
A JPEG card image waits there.
No cover? The field stays bare,
Other URLs pass unchanged with care.
I hop away, pleased with the pair.

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3c14ce98-41e5-4702-82a4-cb52ba007d52
Commits

Reviewing files that changed from the base of the PR and between fb0dd66 and b0afce2.

Files selected for processing: 2

  • apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts
  • apps/web/src/specs/app/entry-card-fields.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T16:47:18.074187Z 0996291 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix post card thumbnails by naming JPEG in proxy URLs

🐞 Bug fix 🧪 Tests 🕐 10-20 Minutes

Grey Divider

AI Description

• Use JPEG card URLs so CDN caching cannot serve browser-negotiated AVIF to link-preview crawlers.
• Keep on-page image negotiation unchanged; cover metadata, body images, and missing images in
 tests.
• Deploy imagehoster#77 first so transparent covers receive a white JPEG background.
Diagram

graph TD
  E["Post entry"] --> I["Image lookup"] --> B["Card fields"] --> M["OG and Twitter"] --> C[("CDN cache")]
  B --> O["oEmbed response"] --> C
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Request JPEG directly from image lookup
  • ➕ Expresses the desired format at image generation rather than rewriting the resulting URL.
  • ➖ Requires confirming the helper handles already-proxied images and preserves existing lookup behavior.

Recommendation: Keep the scoped URL transformation for this fix: it changes only eligible card URLs and leaves page images untouched. A direct JPEG lookup would be worth considering if its behavior for every image source is verified. Deploy imagehoster#77 before production to avoid black backgrounds on transparent covers.

Files changed (2) +49 / -7

Bug fix (1) +23 / -2
entry-card-fields.tsName JPEG in shared card image URLs +23/-2

Name JPEG in shared card image URLs

• Changes format=match to format=jpeg for parsed /p/ proxy URLs returned as card images. Null, unparseable, and other URLs remain unchanged; the shared result feeds Open Graph, Twitter, and oEmbed.

apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts

Tests (1) +26 / -5
entry-card-fields.spec.tsVerify JPEG card URLs across image sources +26/-5

Verify JPEG card URLs across image sources

• Updates the expected card image format and checks metadata covers, body images, and already-proxied covers for JPEG and 1200×630 fit parameters. Also verifies that posts without images retain a null card image.

apps/web/src/specs/app/entry-card-fields.spec.ts

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. New spec casts test entries through as any ✓ Resolved
Description
The new it.each case in the card image spec casts the entry fixture with entry(overrides) as any
before passing it to buildEntryCardFields. This adds a fresh use of any in a TypeScript file the
PR is changing, which the rule prohibits in new or modified lines. The cast is confined to a test,
so the impact is limited to weaker type checking of that fixture, but it sets a pattern a later
reader may copy.

2. Card image fallback branches have no test coverage ✓ Resolved
Description
cardImageFormat has a catch branch for inputs that are not parseable URLs and a
startsWith("/p/") guard that returns the image unchanged for non-proxy URLs. The new tests only
exercise the rewrite path, the null-image path, and proxied /p/ URLs with format=match, so
neither fallback is asserted. A regression that swallows a malformed image URL or rewrites the wrong
image would pass the suite unnoticed.

3. Transparent covers show black previews if shipped early 🐞 Bug ☼ Reliability
Description
cardImageFormat rewrites every /p/ card URL to format=jpeg, so any cover with an alpha channel
is flattened by the proxy. The PR says the card should not reach production before
ecency/imagehoster#77 (which changes the flattening from black to white) is deployed, but nothing in
this repo enforces that ordering, so a merge-and-deploy would ship the black-background regression
to link previews for transparent covers.
Code

apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts[R136-138]

+    const url = new URL(image);
+    if (url.pathname.startsWith("/p/") && url.searchParams.get("format") === "match") {
+      url.searchParams.set("format", "jpeg");
Evidence
The PR introduces the format change that depends on the external imagehoster fix, as the PR
description states. buildEntryCardFields calls cardImageFormat, and both
generate-entry-metadata.ts (line 64) and the oEmbed route (line 54) consume its output, so
og:image, twitter:image and thumbnail_url all change together.

apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts[133-145]
apps/web/src/app/(dynamicPages)/entry/_helpers/generate-entry-metadata.ts[64-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The card image is rewritten to `format=jpeg` before the imagehoster fix that keeps transparency (black instead of white flattening) is deployed. Transparent covers will then show a black background in link previews.
## Fix Focus Areas
- apps/web/src/app/(dynamicPages)/entry/_helpers/entry-card-fields.ts[133-145]
## Recommended Fix
Either merge this change only after ecency/imagehoster#77 is live in production, or put the `format=jpeg` rewrite behind a feature flag or config value that is turned on after the imagehoster deploy. Record the ordering requirement in the PR and in a code comment next to `cardImageFormat`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Card-image doc claims JPEG, but GIF covers stay GIF ✓ Resolved
Description
The image field comment now says the value is a 1200×630 JPEG cover image, and the PR text says
the card image is always a JPEG. cardImageFormat only rewrites the query string, so an animated
GIF cover is sent under a format=jpeg URL and the proxy returns it as a GIF, a case the spec does
not cover.

Grey Divider

Tip of the day
💡 Did you know, you can open the rule or relationship behind a finding in one click

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@greptile-apps

greptile-apps Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The changes appear safe to merge.

Summary

Card image URLs request JPEG instead of a negotiated format. Images within posts keep their existing behavior.

Since the previous review:

  • Post preview images use a fixed JPEG URL.

No new actionable issues were found.

Reviews (2) · Last reviewed commit: "test(seo): cover the card image fallback..." · Reviewed by Greptile

- An unproxied URL and a non-URL string from the image lookup come back
  unchanged.
- The new cases pass the fixture as Entry instead of any.
- The field comment says animated covers stay animated.
@feruzm
feruzm merged commit 80bd505 into develop Nov 12, 2026
9 checks passed
@feruzm
feruzm deleted the fix/card-image-jpeg branch November 12, 2026 17:19
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.

Post social previews: card image is Accept-negotiated, crawlers can be served AVIF

1 participant