DEV Community

Smart Solutions
Smart Solutions

Posted on

Your image sitemap is already a social media feed

Every image sitemap I have ever written was a thing I produced for crawlers and then never read again. It turns out mine had been quietly maintaining a publishing queue for months, and I only noticed when I went to build one.

The thing I was about to build

I wanted a small job that posts one illustrated page a day to Bluesky and Mastodon: the artwork, a line of the story, and a link back to the book.

The plan was the obvious one — query the database for books, join the illustrations, pull a caption from somewhere, render a queue. A day of work, a new table, another thing to keep in sync with the site.

The thing that already existed

Then I actually opened the sitemap we publish for Google Images:

<url>
  <loc>https://thewonderbooks.com/app/comics/book/astral_echoes_scars_of_tomorrow</loc>
  <image:image>
    <image:loc>.../covers/astral_echoes_cover_large.webp</image:loc>
    <image:title>Astral Echoes: Scars of Tomorrow — book cover</image:title>
  </image:image>
  <image:image>
    <image:loc>.../illustrations/astral_echoes_page_01.webp</image:loc>
    <image:caption>The Painted Desert shimmered under twin suns, its sands
      swirling with colors unknown on Earth.</image:caption>
  </image:image>
  ...
</url>
Enter fullscreen mode Exit fullscreen mode

Per book, in one file, already public, already correct:

  • the canonical reader URL
  • the cover
  • every page illustration
  • and image:caption — which, because of how the sitemap is generated, is the sentence printed on that page of the book

That is a post. Image, copy, destination. The caption is better than anything I would have written for a social post, because it is the actual prose a reader sees.

So the "pipeline" became a fetch and a parse:

const xml = await fetch(`${SITE}/sitemaps/library-en.xml`).then(r => r.text());
const books = xml.split('<url>').slice(1).map(parseUrlBlock).filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

2,036 book pages with artwork in English, and the same again in twelve more languages. No new table, no second source of truth, and it cannot drift from the site — if a book is not published, it is not in the sitemap, so it cannot be posted.

Three things that bit me anyway

1. The sitemap is XML, so the URLs are escaped. A storage URL arrived as ?alt=media&amp;token=… and answered 403. I was unescaping captions and not hrefs. I found it by HEAD-ing every queued URL before posting — 24 checks, one bad — rather than by trusting the exit code.

2. A "cover" is not always where you think. Most covers lived under /covers/, so I keyed on the path. Some books use their page-1 illustration as the cover, marked only by its caption reading — book cover —. Those posted as if they were inside pages, using "Title — book cover — Library" as the story line. Read the caption first, the path second.

3. Link cards and image embeds are not the same post. On Bluesky I attached the artwork as an external card thumbnail and left the URL in the text. The card already shows the title, the description and the domain, so the raw URL wrapped mid-slug across two lines underneath a card that said the same thing. The card is clickable on its own. Delete the URL from the text.

All three were found by looking at the published post, not by reading logs. The logs were green for every one of them.

The general version

If you publish a sitemap with image: extensions, you already have a structured, deduplicated, always-current feed of every piece of media you own, with human-written captions attached, keyed to canonical URLs. You built it for Googlebot. It works just as well for anything else that needs "an image, a line, and a link".

Worth opening yours before you build the thing you think you need.


I build WonderBooks — twelve free illustrated libraries in thirteen languages. The sitemap above is the real one.

Top comments (0)