2026-07-03

Teasing RSS feeds with LLMs


TL; DR: I'm using LLMs to generate short "teasers" for blog posts and essays, which I embed in images and then post to feeds that I follow on BlueSky and Mastodon.

A bit pathetically, I devote much of my energy to the challenge of how can we resurrect the high-quality "commonwealth of letters" we briefly enjoyed in the pre-social-media blogosphere?

Implicit in the question is that something about "social media" undermined the environment in which the fast-paced, promiscuous, long-form, and ultimately progress-making conversations of the old blogosphere could thrive. Other events, obviously, contributed to the collapse of the conversational communities that briefly made the blogosphere an extraordinary epistemological institution. Google murdered Google Reader, and in practical terms that was the end of the blogosphere. But Google only murdered Google Reader because it saw social media cannibalizing the functions of RSS, and thought it could gain more eyeballs and sell more ads if attention migrated — ideally to its own doomed Google+, but anywhere other than typically ad-free full-content RSS feeds.

I don't pretend to know what the but-for alt history looks like in which Google Reader is celebrated and maintained rather than canned, but I do know Twitter and Facebook provided the alternative attentional equilibrium into which the internet collapsed when Reader was canned, from which it has not since been able to recover. Had "Web 2.0" social media not existed, I suspect attention would have migrated from Google Reader to the many excellent alternative RSS apps that existed then and that still do exist.

Nevertheless, "microblogging" was invented, and won't be uninvented. Our situation is much worse than it was in 2013, when Twitter was mostly reverse chronological and Facebook was mostly posts by your friends. Contemporary X, TikTok, Instagram, and YouTube are algorithmic social media, devices tuned to draw our attention with no regard for the intellectual or social value (or toxicity) of the "content" our attention would be drawn to. We are surrounded by the informational equivalent of Cheetos, all day, all the time, in our palms and our pockets. Reading long-form essays has come to feel like homework, like eating your vegetables, in comparison to watching short-form videos or scrolling through punchy microblog feeds that simulate chatting with friends.

I don't have a solution for this, but I am susceptible to it. I too now spend a far greater share of my time on BlueSky or Mastodon than I do in my favorite RSS reader.

To counter this, one thing I've done for a while is let the microblogging platforms do double duty as a kind of serendipitous RSS reader. Inoreader, my actual RSS reader, allows me to organize the (hundreds? thousands?) of feeds I notionally follow into folders, and then to view the folders like individual feeds. I manage my massive universe of feeds by monitoring a couple of "high priority" folders, and making decisions about rotating feeds into or out of those folders. Inoreader lets me view those folders themselves as RSS feeds. I use my own unify-rss utility to combine those into a single feed, and then I use feedletter to publish the title, author, and URL of each item to feeds on BlueSky and Mastodon.

The hope is I will be more likely to see interesting stuff that crosses my feeds, and that putting actual blog posts a single click away from the places where my attention most often lies will lead me to read more of them.

But I'm attentionally lazy. More often than not, just the title and author of an essay is not enticing enough. So, I thought, in our brave new world, why not have LLMs write little teasers that might intrigue and encourage me?

Length restrictions on BlueSky and Mastodon are too tight to reliably include meaningful summaries. But they both allow one to supply images (and are much less restrictive about the length of alt-text attached to those images). So, I thought, I'll just place my teasers on images. feedletter already posted images described with <media:content> elements in RSS items as attachments to Mastodon. It would be easy to add this functionality for BlueSky posting as well.

Working with Claude code, I mostly "vibecoded" an "indexcard" library that accepts HTML text and some CSS and generates an image containing the contents of a div, using flyingsaucer to do the rendering. There are some quirks, and it took some messing around to get text antialiased and at sufficient resolution to be crisp. But it works well enough!

I asked Claude to write code to hit Claude, in order to generate the teasers. It's pretty straightforward, trivial stuff. The code Claude generated proved a bit brittle. I modified the MessageResponse to be more tolerant of different kinds of valid responses than what Claude generated initially, and added the more forgiving timeouts. Unsurprisingly I've played around quite a bit with the teaser-generating prompt.

Note: If you are copying and pasting from the code below, note that I've replaced the pipe characters with a look alike (U+00&C => U+FF5C) to overcome a markdown rendering glitch. You'll want to replace the vertical bars with ordinary pipe characters.

 object Anthropic:
   // largely Claude-written code to access Claude...

   val anthropicApiVersion = "2023-06-01"

   // val endpointUrl = "https://api.anthropic.com/v1/messages"
   val endpointUrl = "https://ws-a9bz3tqrw5scqnbl.eu-central-1.maas.aliyuncs.com/apps/anthropic/v1/messages"

   val readTimeout    = 100000
   val connectTimeout = 100000

   case class Message(role: String, content: String) derives ReadWriter
   case class MessagesRequest(model: String, max_tokens: Int, cache_control : Map[String,String], messages: List[Message]) derives ReadWriter
   case class MessagesResponse(content: List[Map[String,String]]) derives ReadWriter

   def requestArticleTeaser( url : String ) : String =
     val articleText = requests.get(url).text()
     val prompt =
       s"""Below you will find an article as HTML text. Please write a
          |concise "teaser" for the article, of no more than 150 words.
          |Keep it punchy: 150 words is a max, but shorter is fine, don't add unnecessary padding.
          |The teaser should describe the subject of the article, but not fully summarize it,
          |leaving readers to check out the article to know where it lands and to experience the writing.
          |Return the teaser in plain text, as a single paragraph, with no preamble:
          |
          |${articleText}
          |
          |""".stripMargin

     val requestBody = MessagesRequest(
       model         = model, // model is defined externally
       max_tokens    = 1024,
       cache_control = Map("type" -> "ephemeral"),
       messages      = List(Message("user", prompt))
     )

     val response = requests.post(
       endpointUrl,
       headers = Map(
         "x-api-key"         -> claudeApiKey,            // <-- key sent as a header
         "anthropic-version" -> anthropicApiVersion,
         "content-type"      -> "application/json"
       ),
       data = write(requestBody),
       readTimeout = readTimeout,
       connectTimeout = connectTimeout
     )

     val parsed = read[MessagesResponse](response.text())

     parsed.content
       .find(_.get("type") == Some("text"))
       .flatMap(_.get("text"))
       .getOrElse(throw new RuntimeException("No text block in response"))
 end Anthropic

Price considerations have had me tumbling through different models:

 //val model = "claude-sonnet-4-6" // very expensive
 //val model = "claude-haiku-4-5"  // fine teasers, but still expensive

 //val model = "qwen3.7-plus"      // fine teasers
 val model = "qwen3.6-flash"       // cheaper, let's see!

When I switched to qwen, I also switched from Anthropic to Alibaba Cloud, but they offer an Anthropic-compatible API, so that wasn't a lot of work. (It was here I had to make the Claude-generated response object a bit more tolerant, though. Qwen provided type="thinking" objects that broke the original, very prescriptive, response format.)

I think that running this with Qwen3.6-flash on Alibaba Cloud will cost me $1-$2 a day. Claude Sonnet would be around $10 a day, way too much. Long-term, probably $1 to $2 per day is still too much. I'll be looking for more economical ways to summarize. (Please let me know if you have suggestions.)

For each RSS item, once a teaser is generated, I use indexcard to generate an image, and the original teaser text to generate the image's alt-text. I save those into a folder that my webserver is configured to statically serve. Then I use a scala-xml RewriteRule to yield a version of each XML <item> with the image embedded, or the <item> unchanged if anything went wrong.

Once teasers and alts have been generated, they are reused. We only create new teasers when generating XML if no teaser can be found for the item's link URL.

You can check out the full application here.

You can check out — and even follow — the resulting BlueSky and Mastodon feeds.

(For the moment, this is expensive to run, so no promises re how long these feeds will keep embedding teasers!)