Latest photo widget
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
- Copy the snippet below and add it where you want the widget to appear.
- Replace the
data-feedURL with your own feed. On some.pics that'shttps://yourname.some.pics/rss, on Micro.blog a photo category feed looks likehttps://yourblog.com/categories/photos/feed.json. - 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
- The feed has to allow other sites to read it. some.pics and Micro.blog do. If the widget only says it couldn't load, that's the most likely reason.
- The widget shows the most recent post that has an image, so text-only posts in the same feed are skipped.
Happy blogging, and happy shooting.
Want more? Check out the full Bear Blog library.
Requires JavaScript, available on Bear Blog's premium plan.↩