feat: add Markdown for Agents — serve clean Markdown to AI agents
Generate index.md alongside index.html for /articles/ at build time. Agents can access clean Markdown via .md URL extension or Accept: text/markdown content negotiation. Includes configurable content-signal policy (ai-train, search, ai-input) and a master on/off toggle via MARKDOWN_AGENTS_ENABLED env var.
This commit is contained in:
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ site.locale | default('en') }}">
|
||||
<head>
|
||||
{# 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 %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="generator" content="Eleventy">
|
||||
@@ -17,17 +25,13 @@
|
||||
{% set ogPhoto = ogPhoto[0] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<!-- debug:og _pageUrl={{ _pageUrl }} ogSlug={{ _ogSlug }} hasOg={{ _hasOg }} -->
|
||||
<meta property="og:title" content="{{ ogTitle }}">
|
||||
<meta property="og:site_name" content="{{ site.name }}">
|
||||
<meta property="og:url" content="{{ site.url }}{{ page.url }}">
|
||||
<meta property="og:type" content="{% if page.url == '/' %}website{% else %}article{% endif %}">
|
||||
<meta property="og:url" content="{{ site.url }}{{ _pageUrl }}">
|
||||
<meta property="og:type" content="{% if _pageUrl == '/' %}website{% else %}article{% endif %}">
|
||||
<meta property="og:description" content="{{ ogDesc }}">
|
||||
<meta name="description" content="{{ ogDesc }}">
|
||||
{# 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 %}
|
||||
<meta property="og:image" content="{% if 'http' in ogPhoto %}{{ ogPhoto }}{% else %}{{ site.url }}{% if ogPhoto[0] != '/' %}/{% endif %}{{ ogPhoto }}{% endif %}">
|
||||
{% elif image and image != "" and (image | length) > 10 %}
|
||||
@@ -98,14 +102,17 @@
|
||||
[x-show*="loading"], button[\\@click*="fetch"], button[\\@click*="loadMore"] { display: none !important; }
|
||||
</style>
|
||||
</noscript>
|
||||
<link rel="canonical" href="{{ site.url }}{{ page.url }}">
|
||||
<link rel="canonical" href="{{ site.url }}{{ _pageUrl }}">
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" title="RSS Feed">
|
||||
<link rel="alternate" type="application/json" href="/feed.json" title="JSON Feed">
|
||||
{% if site.markdownAgents.enabled and _pageUrl and _pageUrl.startsWith('/articles/') %}
|
||||
<link rel="alternate" type="text/markdown" href="{{ _pageUrl }}index.md" title="Markdown version">
|
||||
{% endif %}
|
||||
<link rel="authorization_endpoint" href="{{ site.url }}/auth">
|
||||
<link rel="token_endpoint" href="{{ site.url }}/auth/token">
|
||||
<link rel="micropub" href="{{ site.url }}/micropub">
|
||||
<link rel="microsub" href="{{ site.url }}/microsub">
|
||||
<link rel="self" href="{{ site.url }}{{ page.url }}">
|
||||
<link rel="self" href="{{ site.url }}{{ _pageUrl }}">
|
||||
<link rel="hub" href="https://websubhub.com/hub">
|
||||
<link rel="webmention" href="https://webmention.io/{{ site.webmentions.domain }}/webmention">
|
||||
<link rel="pingback" href="https://webmention.io/{{ site.webmentions.domain }}/xmlrpc">
|
||||
|
||||
37
article-markdown.njk
Normal file
37
article-markdown.njk
Normal file
@@ -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 }}
|
||||
241
docs/plans/2026-02-24-homepage-ui-ux-design.md
Normal file
241
docs/plans/2026-02-24-homepage-ui-ux-design.md
Normal file
@@ -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
|
||||
<section x-data="{ expanded: {} }">
|
||||
<!-- For each project -->
|
||||
<div class="project-card">
|
||||
<button @click="expanded[index] = !expanded[index]">
|
||||
<h3>Name</h3> <span>status</span> <span>dates</span> <chevron :class="expanded[index] && 'rotate-180'">
|
||||
</button>
|
||||
<div x-show="expanded[index]" x-transition>
|
||||
<p>description</p>
|
||||
<div>tech tags</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**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 `<h3>` 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 `<h3>` tags — the wrapper hides the inner title via CSS when the wrapper provides one, or we remove the inner `<h3>` from widget files and let the wrapper handle all titles uniformly.
|
||||
|
||||
**Recommended approach:** Remove `<h3>` 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 %}
|
||||
|
||||
<div class="widget" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : {{ defaultOpen }} }">
|
||||
<button
|
||||
class="widget-header"
|
||||
@click="open = !open; localStorage.setItem('{{ widgetKey }}', open)"
|
||||
aria-expanded="open"
|
||||
>
|
||||
<h3 class="widget-title">{{ widgetTitle }}</h3>
|
||||
<svg :class="open && 'rotate-180'" class="chevron">...</svg>
|
||||
</button>
|
||||
<div x-show="open" x-transition x-cloak>
|
||||
{% include "components/widgets/social-activity.njk" %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**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 `<article>` 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 `<article>` 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
|
||||
<article class="h-entry p-4 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-primary-400 dark:hover:border-primary-600 transition-colors">
|
||||
```
|
||||
|
||||
**After (example for repost):**
|
||||
```html
|
||||
<article class="h-entry p-4 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 border-l-3 border-l-green-400 dark:border-l-green-500 hover:border-primary-400 dark:hover:border-primary-600 transition-colors">
|
||||
```
|
||||
|
||||
**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 `<h3>` 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 `<is-land on:visible>` may not have Alpine.js available when the wrapper tries to initialize. Solution: the wrapper's `x-data` is outside `<is-land>`, so Alpine handles the toggle, and `<is-land>` handles lazy-loading the widget content inside.
|
||||
592
docs/plans/2026-02-24-homepage-ui-ux-plan.md
Normal file
592
docs/plans/2026-02-24-homepage-ui-ux-plan.md
Normal file
@@ -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 `<article>` 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 `<article>` 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 `<article>` 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 `<article>` tag using Nunjucks `{% set %}` blocks. This keeps the single `<article>` 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 `<article>` opening) with this version that sets a border class variable:
|
||||
|
||||
**Current (lines 18-19):**
|
||||
```nunjucks
|
||||
{% for post in collections.posts | head(maxItems) %}
|
||||
<article class="h-entry p-4 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-primary-400 dark:hover:border-primary-600 transition-colors">
|
||||
```
|
||||
|
||||
**After:** Insert the type detection BEFORE the `<article>` 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 %}
|
||||
|
||||
<article class="h-entry p-4 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 {{ borderClass }} hover:border-primary-400 dark:hover:border-primary-600 transition-colors">
|
||||
```
|
||||
|
||||
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 `<article>`. After this change, they're declared before the `<article>`. 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 %}
|
||||
<section class="mb-8 sm:mb-12" id="projects" x-data="{ expanded: {} }">
|
||||
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">
|
||||
{{ sectionConfig.title or "Projects" }}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{% for item in cv.projects | head(maxItems) %}
|
||||
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-primary-400 dark:hover:border-primary-600 transition-colors overflow-hidden">
|
||||
{# Summary row — always visible, clickable #}
|
||||
<button
|
||||
class="w-full p-4 flex items-center justify-between gap-2 cursor-pointer text-left hover:bg-surface-50 dark:hover:bg-surface-700/50 transition-colors"
|
||||
@click="expanded[{{ loop.index0 }}] = !expanded[{{ loop.index0 }}]"
|
||||
:aria-expanded="expanded[{{ loop.index0 }}] ? 'true' : 'false'"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<h3 class="font-semibold text-surface-900 dark:text-surface-100 truncate">
|
||||
{% if item.url %}
|
||||
<a href="{{ item.url }}" class="hover:text-primary-600 dark:hover:text-primary-400" @click.stop>{{ item.name }}</a>
|
||||
{% else %}
|
||||
{{ item.name }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% if item.status %}
|
||||
<span class="shrink-0 text-xs px-2 py-0.5 rounded-full capitalize
|
||||
{% if item.status == 'active' %}bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300
|
||||
{% elif item.status == 'maintained' %}bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300
|
||||
{% elif item.status == 'archived' %}bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-400
|
||||
{% else %}bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-400{% endif %}">
|
||||
{{ item.status }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
{% if item.startDate %}
|
||||
<span class="text-xs text-surface-500 hidden sm:inline">
|
||||
{{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<svg
|
||||
class="w-4 h-4 text-surface-400 transition-transform duration-200"
|
||||
:class="expanded[{{ loop.index0 }}] && 'rotate-180'"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{# Detail section — collapsible #}
|
||||
<div
|
||||
x-show="expanded[{{ loop.index0 }}]"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-1"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-1"
|
||||
x-cloak
|
||||
class="px-4 pb-4"
|
||||
>
|
||||
{% if item.startDate %}
|
||||
<p class="text-xs text-surface-500 mb-1 sm:hidden">
|
||||
{{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if item.description %}
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2">{{ item.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if showTechnologies and item.technologies and item.technologies.length %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for tech in item.technologies %}
|
||||
<span class="text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-400 rounded">
|
||||
{{ tech }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**Key details:**
|
||||
- `x-data="{ expanded: {} }"` on the `<section>` — object-based tracking, independent toggles
|
||||
- `@click.stop` on the project name `<a>` 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 `<h3>` 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 `<h3>` 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 #}
|
||||
<div
|
||||
class="widget-collapsible mb-4"
|
||||
x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : {{ defaultOpen }} }"
|
||||
>
|
||||
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
|
||||
<button
|
||||
class="widget-header w-full p-4"
|
||||
@click="open = !open; localStorage.setItem('{{ widgetKey }}', open)"
|
||||
:aria-expanded="open ? 'true' : 'false'"
|
||||
>
|
||||
<h3 class="widget-title font-bold text-lg">{{ widgetTitle }}</h3>
|
||||
<svg
|
||||
class="widget-chevron"
|
||||
:class="open && 'rotate-180'"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
x-cloak
|
||||
>
|
||||
{# 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" %}
|
||||
<div class="widget">
|
||||
<div id="sidebar-search"></div>
|
||||
<script>initPagefind("#sidebar-search");</script>
|
||||
</div>
|
||||
{% 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 {} %}
|
||||
<div class="widget">
|
||||
{% if wConfig.content %}
|
||||
<div class="prose dark:prose-invert prose-sm max-w-none">
|
||||
{{ wConfig.content | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Unknown widget type: {{ widget.type }} -->
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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 `<h3>` removed since the wrapper provides it
|
||||
- The `custom-html` widget's inner `<h3>` is removed — the wrapper uses `widget.config.title` or "Custom"
|
||||
- The `<is-land on:visible>` wrappers remain inside the individual widget files — the collapsible wrapper is OUTSIDE `<is-land>`, 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 <ref-of-a-widget-header> # 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. **`<is-land>` + Alpine.js interaction:** The collapsible wrapper's `x-data` is OUTSIDE `<is-land>`. Alpine.js initializes the toggle immediately. The `<is-land on:visible>` 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 `<is-land>` 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 `<h3>` — 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 `<is-land>` + `<h3>`. Now it's inline inside the collapsible wrapper with the `<h3>` removed. The `<is-land>` wrapper is preserved inside for lazy-loading Pagefind.
|
||||
|
||||
**Wait — re-reading the new sidebar template:** The search widget was changed to NOT use `<is-land>` in the inline version. Let me note this: the search widget should keep its `<is-land on:visible>` wrapper inside the collapsible content div. Update the search case to:
|
||||
```nunjucks
|
||||
{% elif widget.type == "search" %}
|
||||
<is-land on:visible>
|
||||
<div class="widget">
|
||||
<div id="sidebar-search"></div>
|
||||
<script>initPagefind("#sidebar-search");</script>
|
||||
</div>
|
||||
</is-land>
|
||||
```
|
||||
|
||||
5. **Custom HTML widget:** Similarly should keep `<is-land on:visible>` wrapper:
|
||||
```nunjucks
|
||||
{% elif widget.type == "custom-html" %}
|
||||
{% set wConfig = widget.config or {} %}
|
||||
<is-land on:visible>
|
||||
<div class="widget">
|
||||
{% if wConfig.content %}
|
||||
<div class="prose dark:prose-invert prose-sm max-w-none">
|
||||
{{ wConfig.content | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</is-land>
|
||||
```
|
||||
Reference in New Issue
Block a user