Robert Birming

Micro.blog books

This Bearming add-on for Bear fetches and displays books from your Micro.blog JSON feed. Works with any shelf category. Recently read, currently reading, want to read, or all of them.1

The list variant is what powers my bookshelf. The grid is great for a compact "currently reading" section:

Loading...

How to use

Add the markup wherever you want the books to appear, then add the script and styles to your theme. Replace the feed URL with your own Micro.blog JSON feed URL. Add mb-books--list to the section element to switch to the list layout. Adjust data-limit to control how many books are shown.

Markup

<section class="mb-books"
         data-feed="https://yoursite.com/categories/read/feed.json"
         data-limit="12">
  <div class="mb-books-grid" aria-live="polite">
    <p>Loading...</p>
  </div>
</section>

For the list variant:

<section class="mb-books mb-books--list"
         data-feed="https://yoursite.com/categories/read/feed.json"
         data-limit="12">
  <div class="mb-books-grid" aria-live="polite">
    <p>Loading...</p>
  </div>
</section>

Script

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

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

  if (!feedUrl) { grid.innerHTML = '<p>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;
  }

  const tmp = document.createElement('div');

  function parseBook(item) {
    if (!item.content_html) return null;
    tmp.innerHTML = item.content_html;
    const img = tmp.querySelector('img.microblog_book');
    if (!img?.src) return null;
    const bookLink = tmp.querySelector('a[href*="micro.blog/books"]');
    const title = bookLink?.textContent?.trim() || 'Book';

    let author = '';
    let review = '';

    const paras = tmp.querySelectorAll('p');
    paras.forEach((p, i) => {
      if (i === 0) {
        const text = p.textContent || '';
        const byMatch = text.match(/\bby\s+([^.๐Ÿ“š\n]+)/);
        if (byMatch) author = byMatch[1].trim();
        const afterBy = text.replace(/^.*\bby\s+[^.๐Ÿ“š\n]+[.๐Ÿ“š]?\s*/, '').trim();
        if (afterBy) review = afterBy;
      } else {
        review += (review ? ' ' : '') + p.textContent.trim();
      }
    });

    review = review.replace(/๐Ÿ“š/g, '').trim();

    return {
      cover: img.src,
      title,
      author,
      review,
      url: item.url || '#',
      date: new Date(item.date_published || 0).getTime()
    };
  }

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

    if (!books.length) { grid.innerHTML = '<p>No recent books found.</p>'; return; }

    const fragment = document.createDocumentFragment();
    books.forEach(book => {
      const cell = document.createElement('div');
      cell.className = 'mb-books-item';
      cell.innerHTML =
        '<a class="mb-books-cover" href="' + book.url + '" rel="noopener" title="' + book.title + '">' +
          '<img src="' + book.cover + '" alt="' + book.title + '" loading="lazy" decoding="async">' +
        '</a>' +
        '<div class="mb-books-meta">' +
          '<a class="mb-books-title" href="' + book.url + '" rel="noopener">' + book.title + '</a>' +
          (book.author ? '<span class="mb-books-author">' + book.author + '</span>' : '') +
          (book.review ? '<p class="mb-books-review">' + book.review + '</p>' : '') +
        '</div>';
      fragment.appendChild(cell);
    });

    grid.replaceChildren(fragment);
  } catch {
    grid.innerHTML = "<p>Couldn't load books right now.</p>";
  }
})();
</script>

Styles

/* Micro.blog books | robertbirming.com */
.mb-books {
  margin-block: var(--space-block);
}

/* Grid variant */
.mb-books-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
  gap: 0.5rem;
}

.mb-books:not(.mb-books--list) .mb-books-item {
  overflow: hidden;
  height: 7.5rem;
  border: 1px solid var(--border);
}

.mb-books:not(.mb-books--list) .mb-books-item a {
  display: block;
  height: 100%;
}

.mb-books:not(.mb-books--list) .mb-books-item img {
  display: block;
  width: 100%;
  height: 100%;
  margin: 0;
  border-radius: 0;
  object-fit: cover;
  transition: transform 0.2s ease;
}

@media (hover: hover) {
  .mb-books:not(.mb-books--list) .mb-books-item a:hover img {
    transform: scale(1.04);
  }
}

/* List variant */
.mb-books--list .mb-books-grid {
  display: flex;
  flex-direction: column;
  gap: calc(var(--space-block) * 0.75);
}

.mb-books--list .mb-books-item {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  padding-block: 0.9rem;
  padding-inline: 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.mb-books--list .mb-books-cover {
  flex-shrink: 0;
  margin-block-start: 0.25rem;
}

.mb-books--list .mb-books-cover img {
  display: block;
  width: 3.75rem;
  height: auto;
  margin: 0;
  border-radius: 0;
  border: 1px solid var(--border);
}

.mb-books--list .mb-books-meta {
  min-width: 0;
}

.mb-books--list .mb-books-title {
  display: inline-block;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
}

.mb-books--list .mb-books-title:visited {
  color: var(--text);
}

.mb-books--list .mb-books-author {
  display: block;
  margin-block-start: 0.1em;
  font-size: 0.8rem;
  color: color-mix(in srgb, var(--text) 65%, var(--bg));
}

.mb-books--list .mb-books-review {
  margin-block: 0.6em 0;
  font-size: var(--font-small);
  color: color-mix(in srgb, var(--text) 85%, var(--bg));
}

.mb-books-grid > p {
  margin-block: 0;
  color: var(--muted);
}

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

Happy blogging, and keep reading.

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