#2127·NewsBlur

Mastodon RSS: image alt-text replaces the real post text instead of appearing alongside it

Author: rdtbkCreated Aug 29, 2026Updated Aug 29, 2026

Summary

For Mastodon RSS feeds (https://<instance>/@<user>.rss), when a toot has an image, NewsBlur shows the image's alt-text as the story body instead of the toot's actual text. The real caption is silently dropped; the image is still shown, appended after the (wrong) alt-text.

Steps to reproduce

  1. Subscribe to any Mastodon account's RSS feed.
  2. Open a toot that has a short caption and one image with alt-text.

Expected: the toot's own text, followed by the image. Actual: the image's alt-text, followed by the image.

Example shape

xml
<item>
  <description>&lt;p&gt;Good morning!&lt;/p&gt;</description>
  <media:content url="https://example.invalid/photo.jpg" type="image/jpeg" medium="image">
    <media:description type="plain">A long descriptive sentence about what's in the photo, written for screen readers.</media:description>
  </media:content>
</item>

Real toot text: "Good morning!" — what NewsBlur shows instead: the whole alt-text sentence, then the image. This reproduces on essentially any image toot where the caption is shorter than the alt-text, which is the common case.

Root cause

utils/story_functions.py, pre_process_story() (~lines 272–292):

python
summary = entry.get("summary") or ""
content = ""
...
if entry.get("content"):
    html_items = [c for c in entry["content"] if c.get("type") == "text/html"]
    chosen = (
        max(html_items, key=lambda c: len(c.get("value", "") or ""))
        if html_items
        else max(entry["content"], key=lambda c: len(c.get("value", "") or ""))
    )
    content = chosen.get("value", "") or ""
if len(content) > len(summary):
    entry["story_content"] = content.strip()
else:
    entry["story_content"] = summary.strip()

feedparser folds a Media RSS <media:description> into entry["content"] as a separate text/plain item (already covered by an existing test for a different feed shape — apps/rss_feeds/test_rss_feeds.py, Test_PreProcessStoryContentSelection.test_picks_html_content_over_plain_media_description, which asserts entry.get("content") has 2 items when a feed has both content:encoded and media:description).

That test's fix correctly prefers text/html (the real content:encoded body) over plain-text media:description when both exist. But Mastodon toots have no content:encoded — the real text lives only in <description>entry.summary. So entry["content"] contains exactly one item: the media:description alt-text. With no html_items, the code falls back to max(entry["content"], ...), picks that alt-text item, and since it's almost always longer than a short/empty caption, it overwrites entry.summary as the story body.

Suggested fix direction

Keep the text/html-preference branch (needed for the case the existing test covers). Fix the fallback: don't let a lone text/plain entry.content item override a non-empty entry.summary purely by length.

  • Only fall back to the longest entry.content item when summary itself is empty.
  • Better: positively identify content items that correspond 1:1 to an entry.media_content[i] item (i.e. came from media:description) and exclude those from the content-vs-summary comparison — they're image metadata, not article body. This also fixes multi-image toots, where today the last media:description arbitrarily wins.
  • If the alt-text should stay visible for accessibility, append it as distinguishable markup (e.g. <figcaption>) next to the <img> insertion the code already does further down, instead of substituting it for the real body.

Relevant code

  • utils/story_functions.py:259-352 (pre_process_story)
  • apps/rss_feeds/test_rss_feeds.py:1606-1686 (Test_PreProcessStoryContentSelection) — a new case for "no content:encoded, single media:description" belongs next to it.