Robert Birming

Micro.blog gallery

This Bearming add-on for Bear fetches and displays your latest photos from a Micro.blog JSON feed in a responsive square grid. Click any photo to open it in a lightbox with navigation and a link back to the original post.1

Preview

Head over to my photos page to see it in action.

How to use

Add the markup wherever you want the gallery to appear, then add the script and styles to your theme. Replace the feed URL with your own Micro.blog photos feed URL. Adjust data-limit to control how many photos are shown.

Markup

<section class="mb-gallery"
         data-feed="https://yoursite.com/photos/index.json"
         data-limit="12">
  <div class="mb-gallery-list" aria-live="polite">
    <p class="bearming-panel-empty">Loading photos…</p>
  </div>
</section>

Script

<script>
/* Micro.blog gallery | robertbirming.com */
(async () => {
  const root = document.querySelector('.mb-gallery');
  if (!root) return;

  const list = root.querySelector('.mb-gallery-list');
  const feedUrl = root.getAttribute('data-feed');
  const limit = parseInt(root.getAttribute('data-limit') || '12', 10);

  if (!feedUrl) { list.innerHTML = '<p class="bearming-panel-empty">Feed not configured.</p>'; return; }

  async function fetchItems(url, max) {
    const items = [];
    let next = url;
    while (next && items.length < max) {
      try {
        const res = await fetch(next);
        if (!res.ok) break;
        const data = await res.json();
        items.push(...(data.items || []));
        next = data.next_url || null;
      } catch { break; }
    }
    return items.slice(0, max);
  }

  const tmp = document.createElement('div');
  function parsePhoto(item) {
    let src = item.image || null;
    let alt = '';
    if (item.content_html) {
      tmp.innerHTML = item.content_html;
      const img = tmp.querySelector('img');
      if (!src) src = img?.src || null;
      alt = img?.alt || '';
    }
    if (!src) return null;
    const thumbnail = item._microblog?.thumbnail_url || src;
    return {
      src,
      thumbnail,
      alt: alt || 'Photo',
      url: item.url || '#',
      date: new Date(item.date_published || 0).getTime()
    };
  }

  const overlay = document.createElement('div');
  overlay.className = 'mb-lightbox';
  overlay.setAttribute('role', 'dialog');
  overlay.setAttribute('aria-modal', 'true');
  overlay.setAttribute('aria-label', 'Photo lightbox');
  overlay.hidden = true;
  overlay.innerHTML =
    '<button class="mb-lightbox-close" aria-label="Close">&times;</button>' +
    '<button class="mb-lightbox-prev" aria-label="Previous">&#8592;</button>' +
    '<div class="mb-lightbox-media">' +
      '<img class="mb-lightbox-img" src="" alt="">' +
      '<a class="mb-lightbox-link" href="#" target="_blank" rel="noopener">View post</a>' +
    '</div>' +
    '<button class="mb-lightbox-next" aria-label="Next">&#8594;</button>';
  document.body.appendChild(overlay);

  const lbImg   = overlay.querySelector('.mb-lightbox-img');
  const lbLink  = overlay.querySelector('.mb-lightbox-link');
  const lbClose = overlay.querySelector('.mb-lightbox-close');
  const lbPrev  = overlay.querySelector('.mb-lightbox-prev');
  const lbNext  = overlay.querySelector('.mb-lightbox-next');

  let current = 0;
  let photos = [];
  let lastFocused = null;

  function showLb(i) {
    current = (i + photos.length) % photos.length;
    lbImg.src = photos[current].src;
    lbImg.alt = photos[current].alt;
    lbLink.href = photos[current].url;
    lastFocused = document.activeElement;
    overlay.hidden = false;
    document.body.style.overflow = 'hidden';
    lbClose.focus();
  }

  function closeLb() {
    overlay.hidden = true;
    document.body.style.overflow = '';
    lastFocused?.focus();
  }

  lbClose.addEventListener('click', closeLb);
  lbPrev.addEventListener('click', () => showLb(current - 1));
  lbNext.addEventListener('click', () => showLb(current + 1));
  overlay.addEventListener('click', e => { if (e.target === overlay) closeLb(); });
  document.addEventListener('keydown', e => {
    if (overlay.hidden) return;
    if (e.key === 'Escape') closeLb();
    if (e.key === 'ArrowLeft') showLb(current - 1);
    if (e.key === 'ArrowRight') showLb(current + 1);
  });

  try {
    const raw = await fetchItems(feedUrl, limit * 2);
    photos = raw
      .map(parsePhoto)
      .filter(Boolean)
      .sort((a, b) => b.date - a.date)
      .slice(0, limit);

    if (!photos.length) { list.innerHTML = '<p class="bearming-panel-empty">No photos found.</p>'; return; }

    list.innerHTML = '';
    photos.forEach((photo, i) => {
      const item = document.createElement('div');
      item.className = 'mb-gallery-item';
      const btn = document.createElement('button');
      btn.setAttribute('aria-label', photo.alt);
      btn.onclick = () => showLb(i);
      const img = document.createElement('img');
      img.src = photo.thumbnail;
      img.alt = photo.alt;
      img.loading = 'lazy';
      img.decoding = 'async';
      btn.appendChild(img);
      item.appendChild(btn);
      list.appendChild(item);
    });
  } catch {
    list.innerHTML = '<p class="bearming-panel-empty">Couldn\'t load photos right now.</p>';
  }
})();
</script>

Styles

/* Micro.blog gallery | robertbirming.com */
.mb-gallery-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 8px;
  margin-block-start: var(--space-block);
}

.mb-gallery-item button {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

.mb-gallery-item img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius);
  margin-block: 0;
}

@media (hover: hover) {
  .mb-gallery-item img {
    transition: opacity 0.15s ease;
  }

  .mb-gallery-item button:hover img {
    opacity: 0.85;
  }
}

.mb-lightbox {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 10%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.mb-lightbox[hidden] {
  display: none;
}

.mb-lightbox-media {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.8rem;
}

.mb-lightbox-img {
  max-width: min(90vw, 900px);
  max-height: 85vh;
  width: auto;
  height: auto;
  border-radius: var(--radius);
  margin-block: 0;
}

.mb-lightbox-link {
  font-size: var(--font-small);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.4rem 0.9rem;
  text-decoration: none;
}

.mb-lightbox-close,
.mb-lightbox-prev,
.mb-lightbox-next {
  position: fixed;
  padding: 0.5rem 0.9rem;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}

.mb-lightbox-close { top: 1.2rem; right: 1.2rem; }
.mb-lightbox-prev  { left: 1.2rem; top: 50%; transform: translateY(-50%); }
.mb-lightbox-next  { right: 1.2rem; top: 50%; transform: translateY(-50%); }

Want more? Check out all available Bearming add-ons.

  1. Built for the Bearming theme. Using a different theme? Add the Bearming tokens to make them work with your setup.