← Visual design
toolshed

Homepage stream

How the homepage feed is built, filtered, and displayed, including pinning, exclusion rules, and the sidebar.

Published Maturity 🪴 Plant AI Co-created with AI Written by AI based on my ideas and direction.

The homepage is the garden’s stream: a chronological feed of content across all collections, with filter controls and a contextual sidebar. It’s built entirely at build time, no API calls on page load.

Feed eligibility rules

Not everything makes it into the stream. The feed excludes:

RuleExcluded
hub: trueProject hub posts (they live on /projects)
ai: 'generated'Fully AI-generated posts
collection === 'library'Books (they have their own /library page)
develops: <slug> (experiments and field-notes only)Project sub-files in implementation-detail collections (build notes, methodology drafts). Articles, seeds, and jottings with develops: do appear on mainstream so substantive project output stays visible.
field-notes with tag aboutMeta-posts about the garden itself
jottings with type: 'quote'Raw quotes (noise without context)

The result is a curated stream of articles, seeds, field notes (non-meta), weblinks, videos, experiments, and non-quote jottings.

Pinned article

The most recent 100% Maai article is pulled out of the feed order and rendered as a hero card at the top, visually larger, with ruled lines and a “FEATURED” rubber stamp. Co-created and assisted articles still appear in the stream but never get pinned, so the featured slot always reflects Maaike’s full-voice writing. The rest of the feed follows in date-descending order.

const pinnedArticles = mainFeedEligible
  .filter((e) => e.collection === 'articles' && (e.data as any).ai === '100% Maai')
  .slice(0, 1);

Pagination

The stream paginates at 6 items per page on mobile and 12 per page on desktop. Filter changes reset to page 1. Next/previous controls scroll to the top of the stream grid. See Pagination.

Filters

The sidebar (desktop) and mobile filter panel offer four filter groups:

  • Collection: checkboxes per collection (articles, seeds, field notes, jottings, weblinks, videos)
  • Maturity: 🌱 Draft, 🌿 Developing, 🪴 Solid, 🌳 Complete
  • AI level: ✍️ 100% Maaike, ✏️ Assisted, ✨ Co-created
  • Sort: pill bar: Date planted / Date tended

Data attributes on each card (data-collection, data-maturity, data-tags, data-date, data-updated, data-ai) power the client-side filter with no server round-trip.

The homepage sidebar is contextual:

  • Currently reading: up to 3 books from the library with status: 'reading'
  • Active projects: up to 8 posts with hub: true
  • blogroll recent posts: latest item from each blogroll site that has an RSS feed, fetched at build time

The blogroll fetch runs at build time via rss-parser:

const feedResults = await Promise.allSettled(
  blogroll
    .filter(site => Boolean(site.feed))
    .map(async (site) => {
      const feed = await parser.parseURL(site.feed);
      const latest = feed.items[0];
      return { name: site.name, postTitle, postUrl, postDate };
    })
);

Failed feeds are silently dropped (allSettled). Stale data is acceptable, it’s updated on each deploy.

Body preview

Article cards always show a 3-line body excerpt. Jotting cards show one only when ai: "100% Maai". All other collection types show the description field instead. The excerpt is extracted at build time by extractBodyPreview():

function extractBodyPreview(body, maxLen = 220) {
  return body
    .replace(/<div[^>]*class="tended"[^>]*>[\s\S]*?<\/div>/g, '')  // strip tended notes
    .replace(/<[^>]+>/g, '')                                         // strip HTML
    .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')                        // unwrap links
    // ... more cleanup
    .slice(0, maxLen) + '…';
}

Tended revision notes are specifically stripped from previews, they’re editorial metadata, not content.

Mycelium tags, relations, arguments & questions