Skip to content

Handle <picture>/<source> elements in image extraction - #56

Merged
jonocodes merged 3 commits into
devfrom
claude/trusting-franklin-48gyly
Jun 22, 2026
Merged

Handle <picture>/<source> elements in image extraction#56
jonocodes merged 3 commits into
devfrom
claude/trusting-franklin-48gyly

Conversation

@jonocodes

Copy link
Copy Markdown
Owner

Summary

This PR adds proper handling for HTML <picture> elements with <source> children during image extraction. The browser gives <source> elements precedence over an <img>'s own src/srcset attributes, which was causing extracted articles to continue loading remote images instead of using the downloaded local copies.

Key Changes

  • Export extractImageUrls function to enable testing of the new functionality
  • Implement <picture>/<source> fallback logic: When an <img> element lacks its own srcset, the code now checks for srcset attributes on sibling <source> elements and uses the largest image from the first available source
  • Remove <source> elements after extraction: After processing, all <source> elements within a <picture> are removed to ensure the browser uses the rewritten <img src> pointing to downloaded images instead of the original remote URLs
  • Add comprehensive test coverage: Four new test cases verify correct behavior for:
    • Medium-style markup (real image on <img>, <source> override removed)
    • Fallback to largest <source> srcset when <img> has no srcset
    • Preference for <img> srcset over <source> elements
    • Plain <img> elements without <picture> wrapper remain untouched

Implementation Details

The solution prioritizes image sources in this order:

  1. The <img> element's own srcset (if present)
  2. The first <source> element's srcset within the parent <picture> (if no img srcset)
  3. The <img> element's src attribute (fallback)

All <source> elements are removed after extraction to prevent the browser from overriding the locally-rewritten image URLs.

https://claude.ai/code/session_01Vi4V3Z1xQ9GBCyT3uDSbwS

Image extraction only looked at <img> elements and rewrote img.src to the
downloaded local copy. But sites like Medium wrap every article image in
<picture><source srcset=...><img></picture>, and the browser gives <source>
precedence over the <img>. So even after downloading the image and rewriting
img.src, the rendered article kept loading the original remote URLs (which
fail due to hotlink/referrer/CSP protection) and the saved copies were ignored.

extractImageUrls now:
- falls back to the largest <source> srcset when the <img> has no srcset
- strips <source> siblings so the browser uses the rewritten <img src>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vi4V3Z1xQ9GBCyT3uDSbwS
@netlify

netlify Bot commented Jun 21, 2026

Copy link
Copy Markdown

Deploy Preview for savrdev ready!

Name Link
🔨 Latest commit e516365
🔍 Latest deploy log https://app.netlify.com/projects/savrdev/deploys/6a3801d96c68420008ff6540
😎 Deploy Preview https://deploy-preview-56--savrdev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

claude added 2 commits June 21, 2026 14:28
The previous fix wasn't enough for Medium. Inspecting the real proxied HTML
showed Medium's body images carry NO src on the <img> — the URL lives only in
the sibling <source srcSet>. Readability discards such "empty" <img> elements
(and their <picture>/<source>) during parsing, so by the time extractImageUrls
runs there are no article images left at all (only the author avatar, which
has a real src, survived).

Add hoistPictureSources(), called in readabilityToArticle BEFORE Readability
parses: for each <picture> whose <img> has no src, hoist the largest <source>
srcset onto the <img>. Readability then keeps the image, and the existing
extractImageUrls logic downloads it and strips the <source> siblings.

Verified against the live article HTML (fetched through the app's CORS proxy):
Readability previously kept 0 body images; after hoisting it keeps them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vi4V3Z1xQ9GBCyT3uDSbwS
Capture the background behind hoistPictureSources: why Medium images come
through empty (srcless <img>, URL only in sibling <source srcSet>, dropped by
Readability before extraction), why upstream Readability doesn't handle it
(_fixLazyImages only inspects an element's own attributes; long-open Bugzilla
1355164), and the criteria for removing the workaround later.

Adds docs/readability-lazy-picture-workaround.md and an inline pointer to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vi4V3Z1xQ9GBCyT3uDSbwS
@jonocodes
jonocodes merged commit d5698ec into dev Jun 22, 2026
6 checks passed
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.

2 participants