Skip to content

Fall back to the original image before the default image - #4007

Merged
gumaerc merged 3 commits into
mainfrom
cg/nextjs-image-opt-fallback-to-original
Sep 29, 2026
Merged

gumaerc merged 3 commits into
mainfrom
cg/nextjs-image-opt-fallback-to-original

Conversation

@gumaerc

@gumaerc gumaerc commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

Part of https://github.com/mitodl/hq/issues/10240 (follow-up to #3983)

Description (What does it do?)

Since image optimization went live, some resources show the default image even though their real image loads fine in a browser. The optimizer fetches remote images server-side, and some hosts block that. climate.mit.edu (behind Akamai) answers the optimizer with 403: the production nextjs pod logs had 1,047 of these across 48 images between 14:05 and 16:41 UTC on 2026-09-28. useImageWithFallback went straight from the failed optimized image to the default.

The fallback chain is now: optimized image, then the original loaded directly by the browser (next/image with unoptimized), then the default image. This covers any host that blocks server-side fetches, with no list of domains to maintain.

  • useImageWithFallback returns unoptimized alongside src and onError, and every image that uses it passes that through: the resource cards (via a new imageUnoptimized prop on BaseLearningResourceCard), the resource drawer image, MitxOnlineResourceCard, and the product page sidebar image.
  • Next.js already serves some images unoptimized, such as SVGs and data: URLs. For those the retry would render the same <img> and never fire another error, so onError checks the failed element's src and skips straight to the default.
  • VideoCard had its own copy of the old optimized-then-placeholder logic; it now uses the hook.
  • The byImageSrc test queries now accept their nextJsOriginalSrc option in TypeScript. It already worked at runtime, but the error functions were typed with the default single-argument signature.

How can this be tested?

  • yarn test covers the chain: new unit tests for the hook, and the existing fallback tests in the card, list card, drawer, MITx Online card and product page tests now check each step.
  • In the browser, with Chrome DevTools, block the URL pattern */_next/image* (Network request blocking) and load a search page. Card images should load from their original URLs instead of /_next/image. Also block an image's source host and it should show the default image. (I haven't run this check; the chain is covered by the tests above.)
  • After release, https://learn.mit.edu/search?q=Geothermal%20Energy should show the climate.mit.edu image on the "Geothermal Energy" card instead of the default image.

Additional Context

  • Images the optimizer rejects for size (over the 20MB maximumResponseBody cap) now load the full original instead of the default image. That's what they did before optimization was turned on.
  • Images that don't use the hook don't get the retry yet: news and article listing images (which hide themselves on error), and a few with no error handler at all (FeaturedVideo, MoreFromPlaylist, testimonial avatars, organization logos, instructor photos, the homepage news section). None of these had an original-image fallback before this either. That's left for a follow-up.
  • Each blocked image still makes one failing /_next/image request per page view before the browser loads the original, and Next doesn't cache failed fetches, so those requests reach the pods. Caching /_next/image 4xx responses briefly at Fastly would stop that; that's a separate change in ol-infrastructure.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings September 28, 2026 22:12
@gumaerc
gumaerc requested a review from a team as a code owner September 28, 2026 22:12
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

OpenAPI Changes

No changes detected

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The passive stage reset can briefly load a newly supplied source unoptimized or fetch it twice.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds direct-browser image loading between Next.js optimization failure and the default fallback.

Changes:

  • Extends useImageWithFallback with optimized, original, and fallback stages.
  • Propagates unoptimized through affected image components.
  • Updates query typings and fallback tests.
File Description
frontends/​ol-utilities/​src/​hooks/​useImageWithFallback.ts Implements the fallback state machine.
frontends/​ol-utilities/​src/​hooks/​useImageWithFallback.test.ts Tests fallback stages and source changes.
frontends/​ol-test-utilities/​src/​domQueries/​byImageSrc.ts Types image-query options.
frontends/​ol-components/​src/​components/​LearningResourceCard/​LearningResourceListCard.tsx Passes unoptimized state to list cards.
frontends/​ol-components/​src/​components/​LearningResourceCard/​LearningResourceListCard.test.tsx Tests list-card fallback sequence.
frontends/​ol-components/​src/​components/​LearningResourceCard/​LearningResourceCard.tsx Passes unoptimized state to cards.
frontends/​ol-components/​src/​components/​LearningResourceCard/​LearningResourceCard.test.tsx Tests card and SVG fallbacks.
frontends/​ol-components/​src/​components/​BaseLearningResourceCard/​BaseLearningResourceCard.tsx Adds image optimization control.
frontends/​main/​src/​page-components/​LearningResourceExpanded/​CallToActionSection.tsx Applies fallback behavior to drawer images.
frontends/​main/​src/​page-components/​LearningResourceExpanded/​CallToActionSection.test.tsx Tests drawer fallback sequence.
frontends/​main/​src/​app-pages/​VideoPlaylistCollectionPage/​VideoCard.tsx Reuses the shared fallback hook.
frontends/​main/​src/​app-pages/​ProductPages/​ProductPageTemplate.tsx Applies fallback behavior to sidebar images.
frontends/​main/​src/​app-pages/​ProductPages/​ProductPageTemplate.test.tsx Tests sidebar fallback sequence.
frontends/​main/​src/​app-pages/​ProductPages/​MitxOnlineResourceCard.tsx Applies fallback behavior to MITx cards.
frontends/​main/​src/​app-pages/​ProductPages/​MitxOnlineResourceCard.test.tsx Tests MITx card fallback sequences.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread frontends/ol-utilities/src/hooks/useImageWithFallback.ts Outdated
@ChristopherChudzicki ChristopherChudzicki self-assigned this Sep 29, 2026
gumaerc and others added 2 commits September 29, 2026 12:21
The image optimizer fetches remote images server-side, and some hosts
block that (e.g. bot protection answering 403) while still serving the
same image to browsers. useImageWithFallback went straight from the
failed optimized image to the default image, so those resources lost
their images. It now retries the original with next/image's
`unoptimized` first, and only falls back to the default if that fails
too.

Every image using the hook passes the new `unoptimized` value through.
Also type byImageSrc's error functions with their real arguments so the
queries accept the `nextJsOriginalSrc` option in TypeScript.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Next.js serves some images unoptimized regardless of the prop, e.g.
SVGs and data: URLs. For those, the "original" retry rendered the same
<img>, no second error fired, and the image stayed broken instead of
falling back to the default. onError now checks the failed element's
src and goes straight to the fallback when it was already the original.

Also move VideoCard's hand-rolled optimized-then-placeholder fallback
onto useImageWithFallback so its thumbnails get the original-image retry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gumaerc
gumaerc force-pushed the cg/nextjs-image-opt-fallback-to-original branch from 876b27e to 827b35b Compare September 29, 2026 16:21

@ChristopherChudzicki ChristopherChudzicki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 But with one suggestion that I do think is worth doing.

Comment thread frontends/ol-utilities/src/hooks/useImageWithFallback.ts Outdated
Replace the useEffect that reset the stage when src changed with state
that records which src failed. Any other src starts at its initial
stage in the same render, so a new src can no longer render once with
the previous src's stage (e.g. loading unoptimized, or twice). Adds a
test that records every render to check this.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gumaerc
gumaerc merged commit b98285b into main Sep 29, 2026
14 checks passed
@gumaerc
gumaerc deleted the cg/nextjs-image-opt-fallback-to-original branch September 29, 2026 17:48
This was referenced Sep 29, 2026
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.

3 participants