Skip to content

Fix gray thumbnails and title-only cards in Reader related posts - #26264

Merged
crazytonyli merged 2 commits into
trunkfrom
bugfix/cmm-2593-2594-reader-related-posts
Oct 29, 2026
Merged

crazytonyli merged 2 commits into
trunkfrom
bugfix/cmm-2593-2594-reader-related-posts

Conversation

@crazytonyli

@crazytonyli crazytonyli commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes https://linear.app/a8c/issue/CMM-2593/related-posts-in-one-section-look-inconsistent
Fixes https://linear.app/a8c/issue/CMM-2594/related-post-shows-a-gray-box-for-a-video-post

To reproduce the issues: In Reader, open Text after a calendar embed and scroll to More from Art. “Embed gallery: services and sizes” shows a gray box, while “Comment Crash” shows only a title and looks like a stray heading.

  1. Ignore video embed URLs when choosing thumbnails, and hide missing images and empty excerpts.
  2. Leave out related posts that have neither an image nor an excerpt.
Before After
Related posts before the fix Related posts after the fix

Testing instructions

On an iPad in dark mode, open the post above in Reader and scroll to More from Art. Confirm that “Embed gallery: services and sizes” shows its title and excerpt without a gray box, and “Comment Crash” is absent. Check that related posts with images still show their thumbnails.

A related post whose first media is a video came with a YouTube embed URL as its featured media. The cell tried to load it as an image and showed a gray box. A post with no excerpt and no image showed only its title, which looked like a stray heading.

Only image media is used for the thumbnail now, the cell hides the parts it does not have, and posts with neither an image nor an excerpt are left out of the section.
@dangermattic

Copy link
Copy Markdown
Collaborator
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

@crazytonyli
crazytonyli requested a review from jkmassel October 28, 2026 06:21
@crazytonyli crazytonyli added this to the 27.5 milestone Oct 28, 2026
@crazytonyli
crazytonyli marked this pull request as ready for review October 28, 2026 06:22

@jkmassel jkmassel 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.

CMM-2593 and CMM-2594 are fixed ✅. On the Art post from the description, "Embed gallery: services and sizes" shows its title and excerpt with no gray box, and "Comment Crash" is gone. The new RemoteReaderSimplePostTests pass. Checked on an iPhone 18 Pro simulator (iOS 27.0) in light mode. iPad and dark mode were not checked.

CMM-2595 is not fixed. The thumbnails stay gray because image addresses that redirect are rejected. Details and a fix are in the inline comment.

Two minor things, not blocking:

  • hasContentForRelatedPostCard decides which posts are dropped, but no test covers it. An excerpt with only HTML and an image with no excerpt would be good cases.
  • The excerpt is converted to plain text twice for each post: once in the filter, once in configure(for:).

}
let featuredImageURL = post.featuredImageURL
featuredImageView.isHidden = featuredImageURL == nil
featuredImageView.downloadImage(from: featuredImageURL)

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.

This doesn't fix CMM-2595. The thumbnails in that issue still stay gray with this PR. The cause is in downloadImage, not in which address we pick.

What happens

  • Both posts in CMM-2595 have a valid featured_image, on en-blog.files.wordpress.com. This PR picks the same address as before.
  • *.files.wordpress.com answers with a 301 redirect to en.blog.wordpress.com/wp-content/uploads/….
  • URLSession follows the redirect, so response.url is the new address.
  • downloadImage(usingRequest:) only accepts the image when response?.url == url. After any redirect that's false, so it reports ImageDownloadError.urlMismatch and the view keeps its gray background.

Who is affected: any image loaded through downloadImage whose address redirects. Older WordPress.com posts use files.wordpress.com addresses, so in related posts it's mostly older WordPress.com posts.

Suggested fix

UIImageView+Networking.swift isn't part of this PR's diff, so this can't be a suggestion:

             DispatchQueue.main.async {
-                if response?.url == url {
-                    ImageCache.shared.setImage(image, forKey: url.absoluteString)
-                    handleSuccess(image, url)
-                } else {
-                    failure?(ImageDownloadError.urlMismatch)
-                }
-
+                ImageCache.shared.setImage(image, forKey: url.absoluteString)
+                handleSuccess(image, url)
                 self?.downloadTask = nil
             }

The check looks like protection against reused cells, but it compares the response with the address the same call asked for, so it never catches a reused cell. Starting a new download already cancels the old one. This changes every caller of downloadImage, not just Reader. If you'd rather keep this PR to Reader, dropping "Fixes CMM-2595" from the description works too.

How this was checked

Check Result
curl -L on both featured_image addresses 200 after 1 redirect
Scratch unit test, fake server, direct image Loads ✅
Scratch unit test, fake server, image behind a 301 Fails with urlMismatch ❌
Same test with the fix above Loads ✅ (all 38 WordPressUIUnitTests pass)
Simulator, this PR, "Stop Paying Extra to Keep Your Site Safe" → More from WordPress.com Blog Both thumbnails gray
Same build with only the check skipped Both thumbnails load
This PR Check skipped
Two related posts with gray thumbnails The same two related posts with their thumbnails loaded
Scratch test
import Foundation
import Testing
import UIKit

@testable import WordPressUI

/// Serves `/old.png` as a 301 to `/new.png`, and `/new.png` (or `/direct.png`) as a PNG.
private final class RedirectingImageProtocol: URLProtocol {
    static let png: Data = UIGraphicsImageRenderer(size: CGSize(width: 4, height: 4)).pngData { context in
        UIColor.red.setFill()
        context.fill(CGRect(x: 0, y: 0, width: 4, height: 4))
    }

    override class func canInit(with request: URLRequest) -> Bool {
        request.url?.host == "scratch-redirect.test"
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }

    override func startLoading() {
        let url = request.url!
        if url.path == "/old.png" {
            let target = URL(string: "https://scratch-redirect.test/new.png")!
            let response = HTTPURLResponse(url: url, statusCode: 301, httpVersion: "HTTP/1.1",
                                           headerFields: ["Location": target.absoluteString])!
            client?.urlProtocol(self, wasRedirectedTo: URLRequest(url: target), redirectResponse: response)
            return
        }
        let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1",
                                       headerFields: ["Content-Type": "image/png"])!
        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        client?.urlProtocol(self, didLoad: Self.png)
        client?.urlProtocolDidFinishLoading(self)
    }

    override func stopLoading() {}
}

@MainActor
@Suite(.serialized)
struct ScratchImageRedirectTests {

    private func load(_ urlString: String) async -> String {
        URLProtocol.registerClass(RedirectingImageProtocol.self)
        defer { URLProtocol.unregisterClass(RedirectingImageProtocol.self) }

        let imageView = UIImageView()
        imageView.backgroundColor = .placeholderText
        return await withCheckedContinuation { continuation in
            imageView.downloadImage(
                from: URL(string: urlString),
                success: { _ in continuation.resume(returning: "success") },
                failure: { error in continuation.resume(returning: "failure: \(String(describing: error))") }
            )
        }
    }

    @Test func directImageLoads() async {
        let result = await load("https://scratch-redirect.test/direct.png")
        #expect(result == "success")
    }

    /// `https://en-blog.files.wordpress.com/...jpg` answers 301 → `https://en.blog.wordpress.com/wp-content/uploads/...jpg`.
    @Test func redirectedImageLoads() async {
        let result = await load("https://scratch-redirect.test/old.png")
        #expect(result == "success")
    }
}

@jkmassel jkmassel 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.

Requesting changes so this doesn't merge while the description still says it fixes CMM-2595. Those thumbnails still stay gray with this PR (details, proof and a fix in #26264 (comment)).

Either change works for me:

  • Apply the fix in UIImageView+Networking.swift.
  • Drop "Fixes CMM-2595" from the description and handle it separately.

The CMM-2593 and CMM-2594 changes look good.

@crazytonyli

Copy link
Copy Markdown
Contributor Author

I incorrectly assumed 2595 is fixed, too. I have removed it and will create a separate PR for that.

@jkmassel jkmassel 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.

Approving now that CMM-2595 has moved to #26296.

Checked again at 2da1fad05d:

  • CMM-2593 and CMM-2594 are fixed ✅. We compared trunk and this branch on an iPhone 18 Pro simulator (iOS 27.0), on a real post whose related posts use YouTube embeds as their featured media. Video posts no longer show a gray box, and a title-only post is left out.
  • Every live related post we sampled has a type on its featured_media (227 posts: 144 image, 13 video, 70 empty). So requiring type == "image" doesn't drop real images. It also matches the rule in RemoteReaderPost.m.
  • iPad and dark mode were not checked.

The earlier nits still apply and don't block. One more: RemoteReaderSimplePostTests.swift changes when you run swift format on it.

@crazytonyli
crazytonyli added this pull request to the merge queue Oct 29, 2026
Merged via the queue into trunk with commit 496d8fb Oct 29, 2026
26 checks passed
@crazytonyli
crazytonyli deleted the bugfix/cmm-2593-2594-reader-related-posts branch October 29, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants