diff --git a/_data/site.js b/_data/site.js index 40b6063..7d3d342 100644 --- a/_data/site.js +++ b/_data/site.js @@ -127,4 +127,13 @@ export default { lightning: process.env.SUPPORT_LIGHTNING_ADDRESS || null, paymentPointer: process.env.SUPPORT_PAYMENT_POINTER || null, }, + + // Markdown for Agents — serve clean Markdown to AI agents + // Set MARKDOWN_AGENTS_ENABLED to "false" to disable entirely + markdownAgents: { + enabled: (process.env.MARKDOWN_AGENTS_ENABLED || "true").toLowerCase() === "true", + aiTrain: process.env.MARKDOWN_AGENTS_AI_TRAIN || "yes", + search: process.env.MARKDOWN_AGENTS_SEARCH || "yes", + aiInput: process.env.MARKDOWN_AGENTS_AI_INPUT || "yes", + }, }; diff --git a/_includes/layouts/base.njk b/_includes/layouts/base.njk index bc5278e..7a69877 100644 --- a/_includes/layouts/base.njk +++ b/_includes/layouts/base.njk @@ -1,6 +1,14 @@ + {# CRITICAL: Capture page.url IMMEDIATELY — Eleventy 3.x race condition (#3183) + causes page.url to change mid-render during parallel processing. + Nunjucks {% set %} captures the VALUE (not a reference), making it immune + to later mutations of the shared page object. This MUST be the first + statement in the template, before any filter calls that could yield. #} + {% set _pageUrl = page.url %} + {% set _ogSlug = (_pageUrl or "") | ogSlug %} + {% set _hasOg = _ogSlug | hasOgImage %} @@ -17,17 +25,13 @@ {% set ogPhoto = ogPhoto[0] %} {% endif %} {% endif %} + - - + + - {# Compute OG slug from page.url — NOT permalink or eleventyComputed values. - page.url may be false for pages with permalink:false (e.g., about.njk), - so guard with (page.url or ""). The ogSlug filter handles empty strings. #} - {% set _ogSlug = (page.url or "") | ogSlug %} - {% set _hasOg = _ogSlug | hasOgImage %} {% if ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10 %} {% elif image and image != "" and (image | length) > 10 %} @@ -98,14 +102,17 @@ [x-show*="loading"], button[\\@click*="fetch"], button[\\@click*="loadMore"] { display: none !important; } - + + {% if site.markdownAgents.enabled and _pageUrl and _pageUrl.startsWith('/articles/') %} + + {% endif %} - + diff --git a/article-markdown.njk b/article-markdown.njk new file mode 100644 index 0000000..3997f19 --- /dev/null +++ b/article-markdown.njk @@ -0,0 +1,37 @@ +---js +{ + pagination: { + data: "collections.articles", + size: 1, + alias: "article" + }, + permalink: function(data) { + if (!data.site.markdownAgents.enabled) return false; + return data.article.url + "index.md"; + }, + eleventyExcludeFromCollections: true +} +--- +{%- set bodyContent = article.template.frontMatter.content -%} +{%- set tokens = (bodyContent.length / 4) | round(0, "ceil") -%} +--- +title: "{{ article.data.title | replace('"', '\\"') }}" +date: {{ article.date.toISOString() }} +author: {{ site.author.name }} +url: {{ site.url }}{{ article.url }} +{%- if article.data.category %} +categories: +{%- for cat in article.data.category %} + - {{ cat }} +{%- endfor %} +{%- endif %} +{%- if article.data.description %} +description: "{{ article.data.description | replace('"', '\\"') }}" +{%- endif %} +tokens: {{ tokens }} +content_signal: ai-train={{ site.markdownAgents.aiTrain }}, search={{ site.markdownAgents.search }}, ai-input={{ site.markdownAgents.aiInput }} +--- + +# {{ article.data.title }} + +{{ bodyContent }} diff --git a/docs/plans/2026-02-24-homepage-ui-ux-design.md b/docs/plans/2026-02-24-homepage-ui-ux-design.md new file mode 100644 index 0000000..a9cc4f7 --- /dev/null +++ b/docs/plans/2026-02-24-homepage-ui-ux-design.md @@ -0,0 +1,241 @@ +# Homepage UI/UX Improvements — Design Document + +**Date:** 2026-02-24 +**Scope:** indiekit-eleventy-theme (rendering layer only) +**Status:** APPROVED + +## Context + +The homepage at rmendes.net is a data-driven page controlled by the `indiekit-endpoint-homepage` plugin. The plugin's admin UI determines which sections and sidebar widgets appear. This design addresses rendering quality improvements to three areas without changing the data model or plugin architecture. + +The homepage uses a `two-column` layout with a full-width hero, main content sections (Recent Posts, Personal Skills, Personal Interests, Personal Projects), and a sidebar with 7+ widgets (Search, Social Activity, Recent Comments, Webmentions, Blogroll, GitHub, Listening, Author h-card). + +### Design principles + +- Improve visual quality of existing templates, not content decisions +- Content selection stays with the user via the homepage plugin config +- All changes are in the Eleventy theme (Nunjucks templates + Tailwind CSS) +- Use Alpine.js for interactivity (already loaded throughout the theme) +- Respect the personal vs work data split (homepage = personal, /cv/ = work) +- Never remove IndieWeb infrastructure (h-card, webmentions, microformats) + +## Change 1: Projects Accordion + +### Problem + +The `cv-projects` section renders full paragraph descriptions for every project. On the homepage, 5 projects with multi-line descriptions dominate the page, pushing sidebar content far below the viewport. The section reads like a resume rather than a scannable overview. + +### Design + +Convert project cards from always-expanded to an accordion pattern using Alpine.js. + +**Collapsed state (default):** Single row showing: +- Project name (linked if URL exists) +- Status badge (active/maintained/archived/completed) +- Date range (e.g., "2022-02 – Present") +- Chevron toggle icon (right-aligned) + +**Expanded state (on click):** Full card content: +- Description paragraph +- Technology tags +- Smooth reveal via `x-transition` + +**File:** `_includes/components/sections/cv-projects.njk` + +**Behavior:** +- All projects start collapsed on page load +- Click anywhere on the summary row to toggle +- Multiple projects can be open simultaneously (independent toggles, not mutual exclusion) +- The 2-column grid layout is preserved — each card in the grid is independently collapsible +- Chevron rotates 180deg when expanded + +**Markup pattern:** +```html +
+ +
+ +
+

description

+
tech tags
+
+
+
+``` + +**Visual details:** +- Summary row: `flex items-center justify-between` with `cursor-pointer` +- Hover: `hover:bg-surface-50 dark:hover:bg-surface-700/50` on the summary row +- Chevron: `w-4 h-4 text-surface-400 transition-transform duration-200` +- Transition: `x-transition:enter="transition ease-out duration-200"` with opacity + translate-y + +### Impact + +Reduces vertical space of the projects section by ~70% in collapsed state. Visitors can scan project names and drill into details on interest. + +## Change 2: Sidebar Widget Collapsibility + +### Problem + +The sidebar has 7+ widgets stacked vertically, each fully expanded. The sidebar is longer than the main content area, and widgets below the fold (GitHub, Listening, Author h-card) are only reachable after significant scrolling. + +### Design + +Add a collapsible wrapper around each widget in `homepage-sidebar.njk`. Widget titles become clickable toggle buttons with a chevron indicator. Collapse state persists in `localStorage`. + +**Default state (first visit):** +- First 3 widgets in the sidebar config: **open** +- Remaining widgets: **collapsed** (title + chevron visible) + +**Return visits:** `localStorage` restores the user's last toggle state for each widget. + +**Files changed:** +- `_includes/components/homepage-sidebar.njk` — add wrapper around each widget include +- `css/tailwind.css` — add `.widget-collapsible` styles +- Individual widget files — extract `

` title to be passed as a variable OR keep title inside but hide it when the wrapper provides one + +**Architecture decision:** The wrapper approach. Rather than modifying 10+ individual widget files, the sidebar dispatcher wraps each widget include in a collapsible container. This requires knowing the widget title at the dispatcher level. + +**Title resolution:** Each widget type has a known title (Search, Social Activity, GitHub, Listening, Blogroll, etc.). The dispatcher maps `widget.type` to a display title, or uses `widget.config.title` if set. The individual widget files keep their own `

` tags — the wrapper hides the inner title via CSS when the wrapper provides one, or we remove the inner `

` from widget files and let the wrapper handle all titles uniformly. + +**Recommended approach:** Remove `

` from individual widget files and let the wrapper handle titles. This is cleaner and avoids duplicate headings. Each widget file keeps its content only. + +**Markup pattern:** +```html +{% set widgetTitle = "Social Activity" %} +{% set widgetKey = "widget-social-activity" %} +{% set defaultOpen = loop.index0 < 3 %} + +
+ +
+ {% include "components/widgets/social-activity.njk" %} +
+
+``` + +**Visual details:** +- Widget header: `flex items-center justify-between cursor-pointer` +- Chevron: `w-4 h-4 text-surface-400 transition-transform duration-200` +- No visual change when open — widget looks exactly as it does today +- When collapsed: only the header row (title + chevron) is visible, with the existing widget border/background +- Smooth transition: `x-transition:enter="transition ease-out duration-150"` + +**Widget title map:** + +| widget.type | Title | +|-------------|-------| +| search | Search | +| social-activity | Social Activity | +| github-repos | GitHub | +| funkwhale | Listening | +| recent-posts | Recent Posts | +| blogroll | Blogroll | +| feedland | FeedLand | +| categories | Categories | +| webmentions | Webmentions | +| recent-comments | Recent Comments | +| fediverse-follow | Fediverse | +| author-card | Author | +| custom-html | (from widget.config.title or "Custom") | + +### Impact + +Reduces initial sidebar scroll length. Visitors see all widget titles at a glance and expand what interests them. First-time visitors get a curated view (top 3 open), returning visitors get their preferred configuration. + +## Change 3: Post Card Color-Coded Left Borders + +### Problem + +All post cards in the `recent-posts` section use identical styling (white bg, gray border, rounded-lg). When scrolling a mixed feed of notes, reposts, replies, likes, bookmarks, and photos, the only way to distinguish post types is by reading the small icon + label text inside each card. There's no scannable visual signal at the card level. + +### Design + +Add a `border-l-3` (3px left border) to each `
` in `recent-posts.njk`, colored by post type. The colors match the existing SVG icon colors already used inside the cards. + +**Color mapping:** + +| Post Type | Left Border Color | Matches Existing | +|-----------|------------------|-----------------| +| Like | `border-l-red-400` | `text-red-500` heart icon | +| Bookmark | `border-l-amber-400` | `text-amber-500` bookmark icon | +| Repost | `border-l-green-400` | `text-green-500` repost icon | +| Reply | `border-l-primary-400` | `text-primary-500` reply icon | +| Photo | `border-l-purple-400` | `text-purple-500` camera icon | +| Article | `border-l-surface-300 dark:border-l-surface-600` | Neutral, matches header text weight | +| Note | `border-l-surface-300 dark:border-l-surface-600` | Neutral, matches header text weight | + +**File:** `_includes/components/sections/recent-posts.njk` + +**Implementation:** Add the border class to each `
` element. The template already branches by post type (like, bookmark, repost, reply, photo, article, note) so each branch gets its specific border color. + +**Before:** +```html +
+``` + +**After (example for repost):** +```html +
+``` + +**Visual details:** +- `border-l-3` (3px) is enough to be noticeable without being heavy +- The left border color is constant (doesn't change on hover) — the top/right/bottom borders still change to primary on hover +- Dark mode uses slightly brighter variants (400 in light, 500 in dark) for visibility +- `rounded-lg` still applies — the left border gets a subtle radius at top-left and bottom-left corners + +**Tailwind note:** `border-l-3` is not a default Tailwind class. Options: +1. Use `border-l-4` (4px, default Tailwind) — slightly thicker but no config change +2. Add `borderWidth: { 3: '3px' }` to `tailwind.config.js` extend — exact 3px +3. Use arbitrary value `border-l-[3px]` — works without config change + +**Recommendation:** Use `border-l-[3px]` (arbitrary value). No config change needed, exact width desired. + +### Impact + +Instant visual scanability of the feed. Visitors can quickly identify post types by color without reading text labels. The feed feels more alive and differentiated. + +## Files Modified (Summary) + +| File | Change | +|------|--------| +| `_includes/components/sections/cv-projects.njk` | Alpine.js accordion with collapsed summary rows | +| `_includes/components/sections/recent-posts.njk` | Add `border-l-[3px]` with type-specific colors to each article | +| `_includes/components/homepage-sidebar.njk` | Collapsible wrapper around each widget with localStorage persistence | +| `_includes/components/widgets/*.njk` (10+ files) | Remove `

` widget titles (moved to sidebar wrapper) | +| `css/tailwind.css` | Add `.widget-header` and `.widget-collapsible` styles | + +## Files NOT Modified + +- `tailwind.config.js` — no config changes needed (using arbitrary values) +- `_data/*.js` — no data changes +- `eleventy.config.js` — no config changes +- `indiekit-endpoint-homepage/` — no plugin changes +- `indiekit-endpoint-cv/` — no plugin changes + +## Testing + +1. Verify homepage renders correctly with all three changes +2. Test accordion open/close on projects section +3. Test sidebar collapse/expand and localStorage persistence (close browser, reopen, verify state) +4. Test dark mode for all color-coded borders +5. Test mobile responsiveness (sidebar stacks to full-width, widgets should still be collapsible) +6. Verify h-card microformat markup is preserved in the author-card widget +7. Verify the /cv/ page is unaffected (cv-projects on /cv/ uses a different template or the same template — if same, accordion applies there too, which is acceptable) +8. Visual check with playwright-cli on the live site after deployment + +## Risks + +- **Widget title extraction:** Moving titles from individual widget files to the wrapper requires updating 10+ files. Risk of missing one or breaking a title. +- **localStorage key collisions:** Using `widget-{type}` as keys. If the same widget type appears twice in the sidebar config, they'd share state. Mitigate by using `widget-{index}` or `widget-{type}-{index}`. +- **Alpine.js load order:** Widgets wrapped in `` may not have Alpine.js available when the wrapper tries to initialize. Solution: the wrapper's `x-data` is outside ``, so Alpine handles the toggle, and `` handles lazy-loading the widget content inside. diff --git a/docs/plans/2026-02-24-homepage-ui-ux-plan.md b/docs/plans/2026-02-24-homepage-ui-ux-plan.md new file mode 100644 index 0000000..61cff4b --- /dev/null +++ b/docs/plans/2026-02-24-homepage-ui-ux-plan.md @@ -0,0 +1,592 @@ +# Homepage UI/UX Improvements — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Improve homepage scannability by adding post card color-coded borders, collapsible project accordion, and collapsible sidebar widgets. + +**Architecture:** Three independent rendering changes in the Eleventy theme's Nunjucks templates + Tailwind CSS. No data model changes. Alpine.js handles all interactivity (already loaded). localStorage persists sidebar widget collapse state. + +**Tech Stack:** Nunjucks templates, Tailwind CSS (arbitrary values), Alpine.js, localStorage + +**Design doc:** `docs/plans/2026-02-24-homepage-ui-ux-design.md` + +--- + +## Task 1: Post Card Color-Coded Left Borders + +**Files:** +- Modify: `_includes/components/sections/recent-posts.njk` + +This is the simplest change — add a `border-l-[3px]` class with a type-specific color to each `
` element. The template already branches by post type (like, bookmark, repost, reply, photo, article, note), so each branch gets its own color. + +**Color mapping (from design doc):** +| Post Type | Classes | +|-----------|---------| +| Like | `border-l-[3px] border-l-red-400 dark:border-l-red-500` | +| Bookmark | `border-l-[3px] border-l-amber-400 dark:border-l-amber-500` | +| Repost | `border-l-[3px] border-l-green-400 dark:border-l-green-500` | +| Reply | `border-l-[3px] border-l-primary-400 dark:border-l-primary-500` | +| Photo | `border-l-[3px] border-l-purple-400 dark:border-l-purple-500` | +| Article | `border-l-[3px] border-l-surface-300 dark:border-l-surface-600` | +| Note | `border-l-[3px] border-l-surface-300 dark:border-l-surface-600` | + +### Step 1: Implement the color-coded borders + +The `
` tag on **line 19** is shared by ALL post types. The type detection happens INSIDE the article (lines 22-27 set variables, lines 28-226 branch by type). Since we need different border colors per type, we must move the `
` tag inside each branch, OR use a Nunjucks variable to set the border class before the article opens. + +**Approach:** Set a border class variable before the `
` tag using Nunjucks `{% set %}` blocks. This keeps the single `
` tag and avoids duplicating it 7 times. + +In `_includes/components/sections/recent-posts.njk`, replace the block from line 18 through line 28 (the `{% for %}`, type detection variables, and `
` opening) with this version that sets a border class variable: + +**Current (lines 18-19):** +```nunjucks + {% for post in collections.posts | head(maxItems) %} +
+``` + +**After:** Insert the type detection BEFORE the `
` tag, set a `borderClass` variable, and add it to the article's class list: + +```nunjucks + {% for post in collections.posts | head(maxItems) %} + + {# Detect post type for color-coded left border #} + {% set likedUrl = post.data.likeOf or post.data.like_of %} + {% set bookmarkedUrl = post.data.bookmarkOf or post.data.bookmark_of %} + {% set repostedUrl = post.data.repostOf or post.data.repost_of %} + {% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %} + {% set hasPhotos = post.data.photo and post.data.photo.length %} + + {% if likedUrl %} + {% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %} + {% elif bookmarkedUrl %} + {% set borderClass = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} + {% elif repostedUrl %} + {% set borderClass = "border-l-[3px] border-l-green-400 dark:border-l-green-500" %} + {% elif replyToUrl %} + {% set borderClass = "border-l-[3px] border-l-primary-400 dark:border-l-primary-500" %} + {% elif hasPhotos %} + {% set borderClass = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %} + {% else %} + {% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %} + {% endif %} + +
+``` + +Then **remove** the duplicate type detection variables that currently exist inside the article (lines 22-26), since they've been moved above. The rest of the template still uses these same variable names in the `{% if likedUrl %}` / `{% elif %}` branches, so those continue to work — the variables are already set. + +**Important:** The type detection variables (`likedUrl`, `bookmarkedUrl`, `repostedUrl`, `replyToUrl`, `hasPhotos`) are currently declared on lines 22-26 inside the `
`. After this change, they're declared before the `
`. Since they're still within the same `{% for %}` loop scope, all subsequent `{% if %}` checks on lines 28+ continue to reference them correctly. Remove lines 22-26 to avoid redeclaring the same variables. + +### Step 2: Build and verify + +Run: +```bash +cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme +npm run build +``` + +Expected: Build completes with exit 0, no template errors. + +### Step 3: Visual verification with playwright-cli + +```bash +playwright-cli open https://rmendes.net +playwright-cli snapshot +``` + +Verify: Post cards in "Recent Posts" section have colored left borders (red for likes, green for reposts, etc.). Take a screenshot for evidence. + +### Step 4: Commit + +```bash +cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme +git add _includes/components/sections/recent-posts.njk +git commit -m "feat: add color-coded left borders to post cards by type" +``` + +--- + +## Task 2: Projects Accordion + +**Files:** +- Modify: `_includes/components/sections/cv-projects.njk` + +Convert the always-expanded 2-column project cards grid into an Alpine.js accordion. Each card shows a collapsed summary row (name + status badge + date range + chevron) and expands on click to reveal description + technology tags. + +### Step 1: Implement the accordion + +Replace the entire content of `_includes/components/sections/cv-projects.njk` with the accordion version. + +**Current behavior:** Lines 16-58 render a `grid grid-cols-1 sm:grid-cols-2 gap-4` with each card showing name, status, dates, description, and tech tags all at once. + +**New behavior:** Same grid layout, but each card has: +- A clickable summary row (always visible): project name (linked if URL), status badge, date range, chevron icon +- A collapsible detail section (hidden by default): description + tech tags, revealed with `x-show` + `x-transition` + +**Full replacement for `cv-projects.njk`:** + +```nunjucks +{# + CV Projects Section - collapsible project cards (accordion) + Data fetched from /cv/data.json via homepage plugin +#} + +{% set sectionConfig = section.config or {} %} +{% set maxItems = sectionConfig.maxItems or 10 %} +{% set showTechnologies = sectionConfig.showTechnologies if sectionConfig.showTechnologies is defined else true %} + +{% if cv and cv.projects and cv.projects.length %} +
+

+ {{ sectionConfig.title or "Projects" }} +

+ +
+ {% for item in cv.projects | head(maxItems) %} +
+ {# Summary row — always visible, clickable #} + + + {# Detail section — collapsible #} +
+ {% if item.startDate %} +

+ {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %} +

+ {% endif %} + + {% if item.description %} +

{{ item.description }}

+ {% endif %} + + {% if showTechnologies and item.technologies and item.technologies.length %} +
+ {% for tech in item.technologies %} + + {{ tech }} + + {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} +
+
+{% endif %} +``` + +**Key details:** +- `x-data="{ expanded: {} }"` on the `
` — object-based tracking, independent toggles +- `@click.stop` on the project name `` link — prevents the button click handler from firing when clicking the link +- Date range shown in summary row on `sm:` screens, and duplicated inside the collapsible detail for mobile (`sm:hidden`) +- `x-cloak` hides detail sections during Alpine.js initialization +- `x-transition` with opacity + translate-y for smooth reveal +- Chevron rotates 180deg via `:class="expanded[index] && 'rotate-180'"` +- `aria-expanded` attribute for accessibility + +### Step 2: Build and verify + +```bash +cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme +npm run build +``` + +Expected: Exit 0. + +### Step 3: Visual verification with playwright-cli + +```bash +playwright-cli open https://rmendes.net +playwright-cli snapshot +``` + +Verify: Projects section shows collapsed cards with name + status + date + chevron. Click a project card to expand — description and tech tags appear with smooth animation. + +### Step 4: Commit + +```bash +cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme +git add _includes/components/sections/cv-projects.njk +git commit -m "feat: convert projects section to collapsible accordion" +``` + +--- + +## Task 3: Sidebar Widget Collapsibility + +**Files:** +- Modify: `_includes/components/homepage-sidebar.njk` — add collapsible wrapper +- Modify: `css/tailwind.css` — add `.widget-header` and `.widget-collapsible` styles +- Modify: 10 widget files — remove `

` titles (moved to sidebar wrapper) + +This is the most complex change. The sidebar dispatcher wraps each widget in a collapsible Alpine.js container. The wrapper provides the `

` title + chevron toggle, and a CSS rule hides the inner widget title to avoid duplication. + +### Step 1: Add CSS classes for widget collapsibility + +In `css/tailwind.css`, add these classes inside the existing `@layer components` block (after the `.widget-title` rule, around line 293): + +```css + /* Collapsible widget wrapper */ + .widget-header { + @apply flex items-center justify-between cursor-pointer; + } + + .widget-header .widget-title { + @apply mb-0; + } + + .widget-chevron { + @apply w-4 h-4 text-surface-400 transition-transform duration-200 shrink-0; + } + + /* Hide inner widget titles when the collapsible wrapper provides one */ + .widget-collapsible .widget .widget-title { + @apply hidden; + } + + /* Hide FeedLand's custom title in collapsible wrapper */ + .widget-collapsible .widget .fl-title { + @apply hidden; + } +``` + +### Step 2: Rewrite homepage-sidebar.njk with collapsible wrapper + +Replace the entire content of `_includes/components/homepage-sidebar.njk`. + +**Widget title map** (from design doc): + +| widget.type | Title | +|-------------|-------| +| search | Search | +| social-activity | Social Activity | +| github-repos | GitHub | +| funkwhale | Listening | +| recent-posts | Recent Posts | +| blogroll | Blogroll | +| feedland | FeedLand | +| categories | Categories | +| webmentions | Webmentions | +| recent-comments | Recent Comments | +| fediverse-follow | Fediverse | +| author-card | Author | +| custom-html | (from widget.config.title or "Custom") | + +**New `homepage-sidebar.njk`:** + +```nunjucks +{# Homepage Builder Sidebar — renders widgets from homepageConfig.sidebar #} +{# Each widget is wrapped in a collapsible container with localStorage persistence #} +{% if homepageConfig.sidebar and homepageConfig.sidebar.length %} + {% for widget in homepageConfig.sidebar %} + + {# Resolve widget title #} + {% if widget.type == "search" %}{% set widgetTitle = "Search" %} + {% elif widget.type == "social-activity" %}{% set widgetTitle = "Social Activity" %} + {% elif widget.type == "github-repos" %}{% set widgetTitle = "GitHub" %} + {% elif widget.type == "funkwhale" %}{% set widgetTitle = "Listening" %} + {% elif widget.type == "recent-posts" %}{% set widgetTitle = "Recent Posts" %} + {% elif widget.type == "blogroll" %}{% set widgetTitle = "Blogroll" %} + {% elif widget.type == "feedland" %}{% set widgetTitle = "FeedLand" %} + {% elif widget.type == "categories" %}{% set widgetTitle = "Categories" %} + {% elif widget.type == "webmentions" %}{% set widgetTitle = "Webmentions" %} + {% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %} + {% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %} + {% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %} + {% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %} + {% else %}{% set widgetTitle = widget.type %} + {% endif %} + + {% set widgetKey = "widget-" + widget.type + "-" + loop.index0 %} + {% set defaultOpen = "true" if loop.index0 < 3 else "false" %} + + {# Collapsible wrapper — Alpine.js handles toggle, localStorage persists state #} +
+
+ + +
+ {# Widget content — inner .widget provides padding, inner title hidden by CSS #} + {% if widget.type == "author-card" %} + {% include "components/widgets/author-card.njk" %} + {% elif widget.type == "social-activity" %} + {% include "components/widgets/social-activity.njk" %} + {% elif widget.type == "github-repos" %} + {% include "components/widgets/github-repos.njk" %} + {% elif widget.type == "funkwhale" %} + {% include "components/widgets/funkwhale.njk" %} + {% elif widget.type == "recent-posts" %} + {% include "components/widgets/recent-posts.njk" %} + {% elif widget.type == "blogroll" %} + {% include "components/widgets/blogroll.njk" %} + {% elif widget.type == "feedland" %} + {% include "components/widgets/feedland.njk" %} + {% elif widget.type == "categories" %} + {% include "components/widgets/categories.njk" %} + {% elif widget.type == "search" %} +
+ + +
+ {% elif widget.type == "webmentions" %} + {% include "components/widgets/webmentions.njk" %} + {% elif widget.type == "recent-comments" %} + {% include "components/widgets/recent-comments.njk" %} + {% elif widget.type == "fediverse-follow" %} + {% include "components/widgets/fediverse-follow.njk" %} + {% elif widget.type == "custom-html" %} + {% set wConfig = widget.config or {} %} +
+ {% if wConfig.content %} +
+ {{ wConfig.content | safe }} +
+ {% endif %} +
+ {% else %} + + {% endif %} +
+
+
+ + {% endfor %} +{% endif %} +``` + +**Key architecture decisions:** +- The wrapper provides the outer card styling (`bg-white`, `rounded-lg`, `border`, `shadow-sm`) and the title + chevron +- The inner widget files keep their `.widget` class, but the inner title is hidden via CSS `.widget-collapsible .widget .widget-title { display: none; }` +- The `search` widget was previously inline in the sidebar — it's now included directly (no separate file), with the inner `

` removed since the wrapper provides it +- The `custom-html` widget's inner `

` is removed — the wrapper uses `widget.config.title` or "Custom" +- The `` wrappers remain inside the individual widget files — the collapsible wrapper is OUTSIDE ``, so the toggle works immediately even before the lazy-loaded content initializes +- `widgetKey` uses `widget.type + "-" + loop.index0` to avoid localStorage key collisions if the same widget type appears twice +- First 3 widgets open by default (`loop.index0 < 3`), rest collapsed + +**Note on widget `.widget` class and double borders:** The wrapper div already has `bg-white rounded-lg border shadow-sm`, and the inner `.widget` class also has those styles. To avoid double borders/shadows, we need to neutralize the inner `.widget` styling when inside `.widget-collapsible`. Add this to the CSS in Step 1: + +```css + /* Neutralize inner widget card styling when inside collapsible wrapper */ + .widget-collapsible .widget { + @apply border-0 shadow-none rounded-none mb-0 bg-transparent; + } +``` + +### Step 3: Build and verify + +```bash +cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme +npm run build +``` + +Expected: Exit 0. + +### Step 4: Visual verification with playwright-cli + +```bash +playwright-cli open https://rmendes.net +playwright-cli snapshot +``` + +Verify: +- All sidebar widgets have a title + chevron header +- First 3 widgets are expanded, remaining are collapsed +- Click a collapsed widget title → it expands smoothly +- Click an expanded widget title → it collapses +- No duplicate titles visible (inner titles hidden) +- No double borders or shadows on widget cards + +### Step 5: Test localStorage persistence + +```bash +playwright-cli click # Toggle a widget +playwright-cli eval "localStorage.getItem('widget-social-activity-1')" +playwright-cli close +playwright-cli open https://rmendes.net +playwright-cli snapshot +``` + +Verify: The widget you toggled retains its state after page reload. + +### Step 6: Verify dark mode + +```bash +playwright-cli eval "document.documentElement.classList.add('dark')" +playwright-cli screenshot --filename=dark-mode-sidebar +``` + +Verify: Widget headers, chevrons, and collapsed/expanded states look correct in dark mode. + +### Step 7: Verify mobile responsiveness + +```bash +playwright-cli resize 375 812 +playwright-cli snapshot +``` + +Verify: Sidebar stacks below main content, widgets are still collapsible. + +### Step 8: Commit + +```bash +cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme +git add _includes/components/homepage-sidebar.njk css/tailwind.css +git commit -m "feat: add collapsible sidebar widgets with localStorage persistence" +``` + +--- + +## Task 4: Deploy and Final Verification + +**Files:** None (deployment commands only) + +### Step 1: Push theme repo + +```bash +cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme +git push origin main +``` + +### Step 2: Update submodule in indiekit-cloudron + +```bash +cd /home/rick/code/indiekit-dev/indiekit-cloudron +git submodule update --remote eleventy-site +git add eleventy-site +git commit -m "chore: update eleventy-site submodule (homepage UI/UX improvements)" +git push origin main +``` + +### Step 3: Build and deploy + +```bash +cd /home/rick/code/indiekit-dev/indiekit-cloudron +make prepare +cloudron build --no-cache && cloudron update --app rmendes.net --no-backup +``` + +### Step 4: Final visual verification on live site + +```bash +playwright-cli open https://rmendes.net +playwright-cli screenshot --filename=homepage-final +playwright-cli snapshot +``` + +Verify all three changes are live: +1. Post cards have color-coded left borders +2. Projects section is collapsible (all collapsed by default) +3. Sidebar widgets are collapsible (first 3 open, rest collapsed) +4. Dark mode works for all changes +5. h-card (Author widget) is present and contains proper microformat markup + +--- + +## Risk Mitigation Notes + +1. **`` + Alpine.js interaction:** The collapsible wrapper's `x-data` is OUTSIDE ``. Alpine.js initializes the toggle immediately. The `` inside widget files handles lazy-loading the widget content. This means the toggle button works before the widget content loads — expanding an unloaded widget triggers `` visibility, which then loads the content. + +2. **FeedLand widget:** Uses custom `fl-title` instead of `widget-title`. The CSS rule `.widget-collapsible .widget .fl-title { display: none; }` handles this case. + +3. **Author card widget:** Has no inner `

` — it just includes `h-card.njk`. The CSS hiding rule won't find anything to hide, which is fine. The wrapper provides "Author" as the title. + +4. **Search widget:** Was previously inline in the sidebar with its own `` + `

`. Now it's inline inside the collapsible wrapper with the `

` removed. The `` wrapper is preserved inside for lazy-loading Pagefind. + + **Wait — re-reading the new sidebar template:** The search widget was changed to NOT use `` in the inline version. Let me note this: the search widget should keep its `` wrapper inside the collapsible content div. Update the search case to: + ```nunjucks + {% elif widget.type == "search" %} + +
+ + +
+
+ ``` + +5. **Custom HTML widget:** Similarly should keep `` wrapper: + ```nunjucks + {% elif widget.type == "custom-html" %} + {% set wConfig = widget.config or {} %} + +
+ {% if wConfig.content %} +
+ {{ wConfig.content | safe }} +
+ {% endif %} +
+
+ ```