Robert Birming

Latest photo widget

  1. Bear Blog library
  2. Widgets

A widget that pulls your latest photo from a feed and shows it as a small card on your blog.1

It reads RSS, Atom, and JSON feeds, so it works with some.pics, Micro.blog, and most other places that publish photos with a feed. The card shows the photo, the date, and the caption, all linking back to the original post.

Preview

My latest photo from photos.birming.com:

Loading…

How to use

  1. Copy the snippet below and add it where you want the widget to appear.
  2. Replace the data-feed URL with your own feed. On some.pics that's https://yourname.some.pics/rss, on Micro.blog a photo category feed looks like https://yourblog.com/categories/photos/feed.json.
  3. Copy the styles and add them to your theme.

Snippet

<div id="photo-latest" data-feed="https://yourname.some.pics/rss">Loading…</div>

<script>
/* Latest photo widget | robertbirming.com */
(() => {
  const box = document.getElementById("photo-latest");
  const feedUrl = box?.dataset.feed;
  if (!feedUrl) return;

  // Plain "en" gets day-first dates: 19 October 2026
  const lang = (document.documentElement.lang || "en").replace(/^en$/, "en-GB");
  const html = (str) => new DOMParser().parseFromString(str || "", "text/html").body;

  // JSON Feed (Micro.blog and others)
  const fromJson = (data) => (data.items || []).map((it) => {
    const body = html(it.content_html);
    return {
      img: body.querySelector("img"),
      title: it.title || body.textContent.trim(),
      link: it.url,
      date: it.date_published
    };
  });

  // RSS and Atom (some.pics and others)
  const fromXml = (text) => {
    const xml = new DOMParser().parseFromString(text, "application/xml");
    return [...xml.querySelectorAll("item, entry")].map((it) => {
      const get = (sel) => it.querySelector(sel)?.textContent || "";
      const body = html(get("description") || get("content") ||
        it.getElementsByTagNameNS("*", "encoded")[0]?.textContent);
      const linkEl = it.querySelector("link");
      return {
        img: body.querySelector("img"),
        title: get("title") || body.textContent.trim(),
        link: linkEl?.getAttribute("href") || linkEl?.textContent,
        date: get("pubDate") || get("published") || get("updated")
      };
    });
  };

  fetch(feedUrl)
    .then((r) => {
      if (!r.ok) throw new Error(r.status);
      return r.text();
    })
    .then((text) => {
      const items = text.trim().startsWith("{") ? fromJson(JSON.parse(text)) : fromXml(text);
      const item = items
        .filter((it) => it.img)
        .sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0))[0];

      if (!item) {
        box.textContent = "No photos found.";
        return;
      }

      const card = document.createElement("div");
      card.className = "photo-latest-card";

      const photo = document.createElement("a");
      photo.className = "photo-latest-img-link";
      photo.href = item.link || "#";
      const img = document.createElement("img");
      img.className = "photo-latest-img";
      img.src = item.img.getAttribute("src");
      img.alt = item.img.getAttribute("alt") || item.title || "Latest photo";
      img.loading = "lazy";
      photo.append(img);
      card.append(photo);

      if (item.date) {
        const date = document.createElement("a");
        date.className = "photo-latest-date";
        date.href = item.link || "#";
        date.textContent = new Date(item.date)
          .toLocaleDateString(lang, { day: "numeric", month: "long", year: "numeric" });
        card.append(date);
      }

      if (item.title) {
        const text = document.createElement("p");
        text.className = "photo-latest-caption";
        text.textContent = item.title.trim().slice(0, 180);
        card.append(text);
      }

      box.replaceChildren(card);
    })
    .catch(() => {
      box.textContent = "Couldn’t load the latest photo right now.";
    });
})();
</script>

Styles

/* Latest photo widget | robertbirming.com */
#photo-latest {
  max-inline-size: 34rem;
  margin-block: 1.5rem;
  margin-inline: auto;
}

.photo-latest-card {
  padding-block: 1rem;
  padding-inline: 1.1rem;
  background: var(--code-background-color);
  border: 1px solid color-mix(in srgb, var(--text-color), transparent 85%);
  border-radius: 6px;
}

.photo-latest-img-link {
  display: block;
  overflow: hidden;
  border-radius: 4px;
}

.photo-latest-img {
  display: block;
  inline-size: 100%;
  aspect-ratio: 16 / 9;
  margin: 0;
  object-fit: cover;
  border-radius: 0;
}

.photo-latest-date,
.photo-latest-date:visited {
  display: block;
  margin-block-start: 1em;
  font-weight: 600;
  line-height: 1.35;
  color: var(--heading-color, var(--text-color));
  text-decoration: none;
}

@media (hover: hover) {
  .photo-latest-date:hover {
    color: var(--link-color);
  }
}

.photo-latest-caption {
  margin-block: 0.5em 0;
  font-size: calc(var(--font-scale) * 0.85);
  line-height: 1.5;
  color: color-mix(in srgb, var(--text-color), transparent 40%);
}

Notes

Happy blogging, and happy shooting.

Want more? Check out the full Bear Blog library.

  1. Requires JavaScript, available on Bear Blog's premium plan.↩