When I started the day—day 07 of my December Adventure—I anticipated spending it working on some period-correct graphics for my OPL-based version of Thoughts for EPOC. Little did I know that, waiting in my inbox, would be an email from Jon bestowing upon me a side-quest: the mystery of the missing images! It’s proving pretty involved, so I expect it to span multiple days.

Jon begins:

I just wanted to ping to say how much I’m enjoying your adventure days, and to beg you for images of the things you’re building.

Thank you.

Then I checked your website and lo and behold, there are images. I wonder if you know that they are absent on the feed?

Well, balls. I’m ashamed to say to say that, yes, I do know they’re absent from the feed. In fact, they’ve been missing ever since I implemented a clever approach to trying to make my website more friendly to low bandwidth connections and browsers without JavaScript. 🤦🏻‍♂️

He continues:

I looked at the feed and it does have the image reference but it’s enshrouded in mystery:

<x-image width="480.0" height="160.0">
 <x-source width="480" height="160" src="/posts/2024-12-07-december-adventure-day-06/revo/1600.png"></x-source>
 <x-source width="480" height="160" src="/posts/2024-12-07-december-adventure-day-06/revo/1200.png"></x-source>
 <x-source width="480" height="160" src="/posts/2024-12-07-december-adventure-day-06/revo/800.png"></x-source>
 <x-source width="400" height="133" src="/posts/2024-12-07-december-adventure-day-06/revo/400.png"></x-source>
 <noscript>
     <img width="400" height="133" src="/posts/2024-12-07-december-adventure-day-06/revo/400.png" />
 </noscript>
</x-image>

The mystery being the <x-source> stuff which didn’t exist when I was growing up :-), and the weird <body> tag (should that be there) and the <img> tag, the one that contains the gem, being in a <noscript> thingie.

It makes complete sense that’s a mystery: x-image is a custom HTML tag that wrote for my website and, as far as I can see, something I never wrote about or documented. Perhaps it’s time for that to change—x-image represents the end (or so I thought) of a pretty deep dive into the behavior of the different HTML image tags and something I’d appreciate feedback on.

The real issue here appears to be that the noscript that’s meant to ensure feed readers (and browsers without JavaScript) gracefully fall back to displaying a small image isn’t working.

Oh, and yes, there shouldn’t be any <body> tags int the content_html of my JSON Feed.

Anyway, I hope that helps, and please carry on!

It does indeed. Thank you. I shall.

Some Background

This website is built using InContext—my own static website builder. I started working on InContext in 2015 when the myriad static site builders we have these days were significantly less mature. The primary goal was to create a builder that would treat media files (photos, videos, audio, STL models, etc) as first class citizens rather than as an after thought to be thrown in an asset directory1; I wanted to break my dependence on mercurial companies like Google, Apple, and Flickr for hosting my photo library.

Having built pretty much every part of the pipeline that takes my writings and puts them in a web browser (or in this case a feed reader) gives me a lot of control, but also means there’s a lot of places things can go wrong.

Starting Small

I decided to start with the errant body tag. This seemed like a red herring so I wanted to eliminate it as a possibility. On closer inspection, it turns out that all inline HTML content was being wrapped with unnecessary html and body tags.

InContext uses a Lua-based templating language called Tilt2 which calls back into Swift to pull content and query documents. A simple page template looks like this:

<html>
    <head>
        <title>{{ incontext.titlecase(document.title) }}</title>
    </head>
    <body>
          <h1>{{ incontext.titlecase(document.title) }}</h1>
          {{ document.render() }}
    </body>
</html>

And the JSON Feed is created similarly, with each entry’s content_html being generated using the render method:

for _, post in ipairs(posts) do
  local item = {
      id = site.url .. post.url,
      url = site.url .. post.url
  }
  if post.content then
      do
          item.content_html = post.render()
      end
  end
  ...
  table.insert(feed.items, item)
end

This calls through to the following code in Swift:

struct DocumentContext: EvaluationContext {

    func html() throws -> String {
        let content = try SwiftSoup.parse(document.contents)
        for transform in transforms {
            try transform.transform(renderTracker: renderTracker, document: self, content: content)
        }
        return try content.html()
    }

}

With a few breakpoints, I was able to determine that somewhere between document.contents and content.html(), outer html and body tags are introduced.

document.contents represents the raw HTML content of the page (converted from Markdown) and this part of the pipeline converts the HTML to a DOM to allow for a collection of lightweight transforms to run to fix-up things like relative paths before returning it to HTML to be included by the templates. It turns out that SwiftSoup always constructs a body irrespective of the input HTML that I need to strip:

struct DocumentContext: EvaluationContext {

    func html() throws -> String {
        let content = try SwiftSoup.parseBodyFragment(document.contents)
        for transform in transforms {
            try transform.transform(renderTracker: renderTracker, document: self, content: content)
        }
        return try content.body()?.html() ?? ""
    }

}

You’ll notice I’ve also changed the SwiftSoup.parse call to SwiftSoup.parseBodyFragment—some digging into the documentation revealed that SwiftSoup.parse isn’t guaranteed to correctly preserve some tags if the input isn’t a full HTML document.

One mystery solved. 🔎


All the moving parts of InContext made progress slow—it’s all a little more nuanced than a starter-OPL program—so that’s all for day 07. I anticipate day 08 will bring more investigations into x-image in preparation for the 90s-era graphics I want to create for Thoughts.


  1. Things like Hugo’s Page Resources come pretty close these days but, back in 2015, it just wasn’t happening. 

  2. Thanks Tom