docs: add README.md and CLAUDE.md for svemagie/blog
Replaces the deleted upstream docs with svemagie-specific documentation. README.md — human-readable setup guide: stack, local dev, env vars, content structure, custom systems, deploy, repo layout, and upstream relationship. CLAUDE.md — AI working context: architecture, key custom systems (garden, AI disclosure, nested tags, unfurl, OG), template gotchas (garden-badge placement, blog.njk branch structure), deploy workflow, common tasks, and a summary of divergence from the theme upstream. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
636
CLAUDE.md
636
CLAUDE.md
@@ -1,552 +1,134 @@
|
||||
# CLAUDE.md - Indiekit Eleventy Theme
|
||||
# CLAUDE.md — svemagie/blog
|
||||
|
||||
This file provides guidance to Claude Code when working with the Indiekit Eleventy theme.
|
||||
This is Sven's personal IndieWeb blog at **blog.giersig.eu**.
|
||||
Stack: Eleventy 3 · Tailwind CSS 3 · Alpine.js · IndieKit (Micropub) · Cloudron hosting.
|
||||
|
||||
## Project Overview
|
||||
The theme lives in a separate repo (`svemagie/blog-eleventy-indiekit`, tracked as the `theme` remote). This repo is the live site and has diverged significantly from that upstream — treat them as related but independent.
|
||||
|
||||
This is a comprehensive Eleventy theme designed for IndieWeb-powered personal websites using Indiekit. It renders Micropub posts (articles, notes, photos, bookmarks, likes, replies, reposts), integrates with Indiekit endpoint plugins for enhanced functionality (CV, homepage builder, GitHub, Funkwhale, Last.fm, YouTube, RSS, Microsub, etc.), and includes full webmention support.
|
||||
|
||||
**Live Site:** https://rmendes.net
|
||||
**Used as Git submodule in:**
|
||||
- `/home/rick/code/indiekit-dev/indiekit-cloudron` (Cloudron deployment)
|
||||
- `/home/rick/code/indiekit-dev/indiekit-deploy` (Docker Compose deployment)
|
||||
|
||||
## CRITICAL: Submodule Workflow
|
||||
|
||||
**This repo is used as a Git submodule.** After ANY changes:
|
||||
|
||||
1. **Edit, commit, and push** this repo (indiekit-eleventy-theme)
|
||||
2. **Update submodule pointer** in parent repo(s):
|
||||
|
||||
```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"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
3. **Redeploy:**
|
||||
|
||||
```bash
|
||||
cd /home/rick/code/indiekit-dev/indiekit-cloudron
|
||||
make prepare # REQUIRED — copies .rmendes files to non-suffixed versions
|
||||
cloudron build --no-cache && cloudron update --app rmendes.net --no-backup
|
||||
```
|
||||
|
||||
**Common mistake:** Editing files in `indiekit-cloudron/eleventy-site/` instead of this repo. Those changes are ephemeral — always edit here.
|
||||
|
||||
## CRITICAL: Indiekit Date Handling Convention
|
||||
|
||||
**All dates MUST be stored and passed as ISO 8601 strings.** This is the universal pattern across Indiekit and ALL `@rmdes/*` plugins.
|
||||
|
||||
### The Rule
|
||||
|
||||
- **Storage (MongoDB):** Store dates as ISO strings (`new Date().toISOString()`), NEVER as JavaScript `Date` objects
|
||||
- **Controllers:** Pass date strings through to templates unchanged — NO conversion helpers, NO `formatDate()` wrappers
|
||||
- **Templates:** Use the `| date` Nunjucks filter for display formatting (e.g., `{{ value | date("PPp") }}`)
|
||||
- **Template guards:** Always wrap `| date` in `{% if value %}` to protect against null/undefined
|
||||
|
||||
### Why This Matters
|
||||
|
||||
The Nunjucks `| date` filter is `@indiekit/util`'s `formatDate()`, which calls `date-fns parseISO(string)`. It ONLY accepts ISO 8601 strings:
|
||||
|
||||
- `Date` objects → `dateString.split is not a function` (CRASH)
|
||||
- `null` / `undefined` → `Cannot read properties of undefined (reading 'match')` (CRASH)
|
||||
- Pre-formatted strings (e.g., "8 Feb 2025") → Invalid Date (WRONG OUTPUT)
|
||||
- ISO strings (e.g., `"2025-02-08T14:30:00.000Z"`) → Correctly formatted (WORKS)
|
||||
|
||||
### Correct Pattern
|
||||
|
||||
```javascript
|
||||
// _data file - store/return ISO strings
|
||||
export default async function () {
|
||||
const data = await fetch(...);
|
||||
return {
|
||||
lastSync: new Date().toISOString(), // ← ISO string
|
||||
items: data.map(item => ({
|
||||
published: item.published || null, // ← already ISO string from API
|
||||
}))
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```nunjucks
|
||||
{# Template - use | date filter, guard for null #}
|
||||
{% if lastSync %}
|
||||
{{ lastSync | date("PPp") }}
|
||||
{% endif %}
|
||||
```
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data Flow: Plugin → JSON → _data → Template
|
||||
### Core files
|
||||
- `eleventy.config.js` — monolithic config: all plugins, filters, shortcodes, collections, passthrough copies
|
||||
- `_data/site.js` — all site config driven by env vars; no hardcoded personal values in source
|
||||
- `_data/*.js` — individual data files for feeds (GitHub, Bluesky, Mastodon, Last.fm, etc.)
|
||||
- `_includes/layouts/` — page layout templates (`base.njk`, `post.njk`, etc.)
|
||||
- `_includes/components/` — reusable Nunjucks partials
|
||||
- `lib/` — build-time JS utilities (`og.js`, `unfurl-shortcode.js`, `data-fetch.js`)
|
||||
- `scripts/` — maintenance scripts (`check-upstream-widget-drift.mjs`)
|
||||
|
||||
```
|
||||
Indiekit Plugin (backend, jail node)
|
||||
→ writes JSON to content/.indiekit/*.json
|
||||
→ GitHub Action fetches homepage.json via SSH before build
|
||||
→ _data/*.js reads JSON file
|
||||
→ Nunjucks template renders data
|
||||
```
|
||||
### Content
|
||||
`content/` is a **symlink** to IndieKit's managed content directory — it is gitignored but Eleventy processes it via `setUseGitIgnore(false)`. Never edit posts in `content/` directly; they are created and updated via IndieKit's Micropub endpoint.
|
||||
|
||||
**homepage.json is fetched at deploy time** — the `deploy.yml` workflow SSHes into the server and runs `sudo bastille cmd node cat /usr/local/indiekit/content/.indiekit/homepage.json` before the Eleventy build. This ensures the build always uses the latest admin-saved config. Do not rely on the committed `content/.indiekit/homepage.json` as source of truth; it may be stale.
|
||||
Post types: `articles`, `notes`, `bookmarks`, `likes`, `replies`, `reposts`, `photos`, `pages`.
|
||||
|
||||
**Example:** CV plugin flow
|
||||
### Build output
|
||||
`_site/` — generated site, not committed. Also excluded from Eleventy processing to prevent loops.
|
||||
|
||||
1. `@rmdes/indiekit-endpoint-cv` writes `content/.indiekit/cv.json`
|
||||
2. `_data/cv.js` reads the JSON file and exports the data
|
||||
3. `cv.njk` and `_includes/components/sections/cv-*.njk` render the data
|
||||
4. Homepage builder can include CV sections via `homepageConfig.sections`
|
||||
|
||||
### Key Files by Function
|
||||
|
||||
#### Core Configuration
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `eleventy.config.js` | Eleventy configuration, plugins, filters, collections, post-build hooks |
|
||||
| `tailwind.config.js` | Tailwind CSS configuration (colors, typography) |
|
||||
| `postcss.config.js` | PostCSS pipeline (Tailwind, autoprefixer) |
|
||||
| `package.json` | Dependencies, scripts (`build`, `dev`, `build:css`) |
|
||||
|
||||
#### Data Files (_data/)
|
||||
|
||||
All `_data/*.js` files are ESM modules that export functions returning data objects. Most fetch from Indiekit plugin JSON files or external APIs.
|
||||
|
||||
| File | Data Source | Purpose |
|
||||
|------|-------------|---------|
|
||||
| `site.js` | Environment variables | Site config (name, URL, author, social links) |
|
||||
| `cv.js` | `content/.indiekit/cv.json` | CV data from `@rmdes/indiekit-endpoint-cv` |
|
||||
| `homepageConfig.js` | `content/.indiekit/homepage.json` (fetched from node jail at deploy time) | Homepage layout from `@rmdes/indiekit-endpoint-homepage` |
|
||||
| `enabledPostTypes.js` | `content/.indiekit/post-types.json` or env | List of enabled post types for navigation |
|
||||
| `urlAliases.js` | `content/.indiekit/url-aliases.json` | Legacy URL mappings for webmentions |
|
||||
| `blogrollStatus.js` | Indiekit `/blogrollapi/api/status` | Checks if blogroll plugin is available |
|
||||
| `podrollStatus.js` | Indiekit `/podroll/api/status` | Checks if podroll plugin is available |
|
||||
| `githubActivity.js` | Indiekit `/githubapi/api/*` or GitHub API | GitHub commits, stars, featured repos |
|
||||
| `githubRepos.js` | GitHub API | Starred repositories for sidebar |
|
||||
| `funkwhaleActivity.js` | Indiekit Funkwhale plugin API | Listening activity |
|
||||
| `lastfmActivity.js` | Indiekit Last.fm plugin API | Scrobbles, loved tracks |
|
||||
| `newsActivity.js` | Indiekit IndieNews plugin API | Submitted IndieNews posts |
|
||||
| `youtubeChannel.js` | YouTube Data API v3 | Channel info, latest videos, live status |
|
||||
| `blueskyFeed.js` | Bluesky AT Protocol API | Recent Bluesky posts for sidebar |
|
||||
| `mastodonFeed.js` | Mastodon API | Recent Mastodon posts for sidebar |
|
||||
|
||||
**Data Source Pattern:**
|
||||
|
||||
Most plugin-dependent data files:
|
||||
1. Try to fetch from Indiekit plugin API first
|
||||
2. Fall back to direct API (if credentials available)
|
||||
3. Return `{ source: "indiekit" | "api" | "error", ...data }`
|
||||
4. Templates check `source` to conditionally display
|
||||
|
||||
#### Layouts (_includes/layouts/)
|
||||
|
||||
| File | Used By | Features |
|
||||
|------|---------|----------|
|
||||
| `base.njk` | All pages | Base HTML shell with header, footer, nav, meta tags |
|
||||
| `home.njk` | Homepage | Always delegates to `homepage-builder.njk` (requires `content/.indiekit/homepage.json`) |
|
||||
| `post.njk` | Individual posts | h-entry microformat, Bridgy syndication, webmentions, reply context, photo gallery; includes `post-interactions.njk` → `comments.njk` → `webmentions.njk` in that order |
|
||||
| `page.njk` | Static pages | Simple content wrapper, no post metadata |
|
||||
|
||||
#### Components (_includes/components/)
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `homepage-builder.njk` | Renders plugin-configured homepage layout (single/two-column, sections, sidebar) |
|
||||
| `homepage-section.njk` | Router for section types (hero, cv-*, custom-html, recent-posts) |
|
||||
| `homepage-sidebar.njk` | Renders plugin-configured sidebar widgets |
|
||||
| `homepage-footer.njk` | Full-width footer below the main layout; renders `homepageConfig.footer` sections in a 3-column grid |
|
||||
| `sidebar.njk` | Default sidebar (author card, social activity, GitHub, Funkwhale, blogroll, categories) |
|
||||
| `blog-sidebar.njk` | Sidebar for blog/post pages (recent posts, categories) |
|
||||
| `h-card.njk` | Microformat2 h-card for author identity |
|
||||
| `reply-context.njk` | Displays reply-to/like-of/repost-of/bookmark-of context with h-cite |
|
||||
| `post-interactions.njk` | Per-post inbound webmentions (likes, reposts, replies, mentions) in card style; client-side AlpineJS, fetches from `/webmentions/api/mentions` + `/conversations/api/mentions`, hidden when empty; placed before comments |
|
||||
| `webmentions.njk` | Build-time webmention display (facepile + reply cards) + send form; shown after comments |
|
||||
| `comments.njk` | IndieAuth-gated comment form and thread; shown between post-interactions and webmentions |
|
||||
| `empty-collection.njk` | Fallback message when a post type collection is empty |
|
||||
|
||||
#### Sections (_includes/components/sections/)
|
||||
|
||||
Homepage builder sections:
|
||||
|
||||
| Section | Config Type | Purpose |
|
||||
|---------|-------------|---------|
|
||||
| `hero.njk` | `hero` | Full-width hero with avatar, name, bio, social links; carries the homepage **h-card** microformat |
|
||||
| `recent-posts.njk` | `recent-posts` | Recent posts grid (configurable maxItems, postTypes filter) |
|
||||
| `cv-experience.njk` | `cv-experience` | Work experience timeline from CV data |
|
||||
| `cv-skills.njk` | `cv-skills` | Skills with proficiency bars from CV data |
|
||||
| `cv-education.njk` | `cv-education` | Education history from CV data |
|
||||
| `cv-projects.njk` | `cv-projects` | Featured projects from CV data |
|
||||
| `cv-interests.njk` | `cv-interests` | Personal interests from CV data |
|
||||
| `custom-html.njk` | `custom-html` | Arbitrary HTML content (from admin UI) |
|
||||
|
||||
#### Widgets (_includes/components/widgets/)
|
||||
|
||||
Sidebar widgets:
|
||||
|
||||
| Widget | Data Source | Purpose |
|
||||
|--------|-------------|---------|
|
||||
| `author-card.njk` | `site.author` | h-card with avatar, bio, social links |
|
||||
| `social-activity.njk` | `blueskyFeed`, `mastodonFeed` | Recent posts from Bluesky/Mastodon |
|
||||
| `github-repos.njk` | `githubActivity`, `githubRepos` | Featured repos, recent commits |
|
||||
| `funkwhale.njk` | `funkwhaleActivity` | Now playing, listening stats |
|
||||
| `recent-posts.njk` | `collections.posts` | Recent posts list (for non-blog pages) |
|
||||
| `blogroll.njk` | Blogroll API | Recently updated blogs from OPML/Microsub |
|
||||
| `categories.njk` | `collections.categories` | Category list with post counts |
|
||||
|
||||
#### Top-Level Templates (*.njk)
|
||||
|
||||
Page templates in the root directory:
|
||||
|
||||
| Template | Permalink | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| `index.njk` | `/` | Homepage (uses `home.njk` layout) |
|
||||
| `about.njk` | `/about/` | About page with full h-card |
|
||||
| `cv.njk` | `/cv/` | CV page with all sections |
|
||||
| `blog.njk` | `/blog/` | All posts chronologically |
|
||||
| `articles.njk` | `/articles/` | Articles collection |
|
||||
| `notes.njk` | `/notes/` | Notes collection |
|
||||
| `photos.njk` | `/photos/` | Photos collection |
|
||||
| `bookmarks.njk` | `/bookmarks/` | Bookmarks collection |
|
||||
| `likes.njk` | `/likes/` | Likes collection |
|
||||
| `replies.njk` | `/replies/` | Replies collection |
|
||||
| `reposts.njk` | `/reposts/` | Reposts collection |
|
||||
| `interactions.njk` | `/interactions/` | Combined social interactions |
|
||||
| `slashes.njk` | `/slashes/` | Index of all slash pages |
|
||||
| `categories.njk` | `/categories/:slug/` | Posts by category (pagination template) |
|
||||
| `categories-index.njk` | `/categories/` | All categories index |
|
||||
| `github.njk` | `/github/` | GitHub activity page |
|
||||
| `funkwhale.njk` | `/funkwhale/` | Funkwhale listening page |
|
||||
| `listening.njk` | `/listening/` | Last.fm listening page |
|
||||
| `youtube.njk` | `/youtube/` | YouTube channel page |
|
||||
| `blogroll.njk` | `/blogroll/` | Blogroll page (client-side data fetch) |
|
||||
| `podroll.njk` | `/podroll/` | Podroll (podcast episodes) page |
|
||||
| `news.njk` | `/news/` | IndieNews submissions page |
|
||||
| `search.njk` | `/search/` | Pagefind search UI |
|
||||
| `feed.njk` | `/feed.xml` | RSS 2.0 feed |
|
||||
| `feed-json.njk` | `/feed.json` | JSON Feed 1.1 |
|
||||
| `404.njk` | `/404.html` | 404 error page |
|
||||
| `changelog.njk` | `/changelog/` | Site changelog |
|
||||
| `webmention-debug.njk` | `/webmention-debug/` | Debug page for webmentions |
|
||||
|
||||
### Eleventy Configuration Highlights
|
||||
|
||||
#### Collections
|
||||
|
||||
| Collection | Glob Pattern | Purpose |
|
||||
|------------|--------------|---------|
|
||||
| `posts` | `content/**/*.md` | All content combined |
|
||||
| `articles` | `content/articles/**/*.md` | Long-form posts |
|
||||
| `notes` | `content/notes/**/*.md` | Short status updates |
|
||||
| `photos` | `content/photos/**/*.md` | Photo posts |
|
||||
| `bookmarks` | `content/bookmarks/**/*.md` | Saved links |
|
||||
| `likes` | `content/likes/**/*.md` | Liked posts |
|
||||
| `replies` | Filtered by `inReplyTo` property | Reply posts |
|
||||
| `reposts` | Filtered by `repostOf` property | Repost posts |
|
||||
| `pages` | `content/*.md` + `content/pages/*.md` | Slash pages (/about, /now, /uses, etc.) |
|
||||
| `feed` | `content/**/*.md` (first 20) | Homepage/RSS feed |
|
||||
| `categories` | Deduplicated from all posts | Category list |
|
||||
|
||||
**Note:** `replies` and `reposts` collections are dynamically filtered by property, not by directory. Supports both camelCase (`inReplyTo`, `repostOf`) and underscore (`in_reply_to`, `repost_of`) naming.
|
||||
|
||||
#### Custom Filters
|
||||
|
||||
| Filter | Purpose | Usage |
|
||||
|--------|---------|-------|
|
||||
| `dateDisplay` | Format date as "January 1, 2025" | `{{ date \| dateDisplay }}` |
|
||||
| `isoDate` | Convert to ISO 8601 string | `{{ date \| isoDate }}` |
|
||||
| `date` | Format date with custom format | `{{ date \| date("MMM d, yyyy") }}` |
|
||||
| `truncate` | Truncate string to max length | `{{ text \| truncate(200) }}` |
|
||||
| `ogDescription` | Strip HTML, decode entities, truncate | `{{ content \| ogDescription(200) }}` |
|
||||
| `extractFirstImage` | Extract first `<img src>` from content | `{{ content \| extractFirstImage }}` |
|
||||
| `obfuscateEmail` | Convert email to HTML entities | `{{ email \| obfuscateEmail }}` |
|
||||
| `head` | Get first N items from array | `{{ array \| head(5) }}` |
|
||||
| `slugify` | Convert string to slug | `{{ name \| slugify }}` |
|
||||
| `hash` | MD5 hash of file for cache busting | `{{ '/css/style.css' \| hash }}` |
|
||||
| `timestamp` | Current Unix timestamp | `{{ '' \| timestamp }}` |
|
||||
| `webmentionsForUrl` | Filter webmentions by URL + aliases | `{{ webmentions \| webmentionsForUrl(page.url, urlAliases) }}` |
|
||||
| `webmentionsByType` | Filter by type (likes, reposts, replies) | `{{ mentions \| webmentionsByType('likes') }}` |
|
||||
| `jsonEncode` | JSON.stringify for JSON feed | `{{ value \| jsonEncode }}` |
|
||||
| `dateToRfc822` | RFC 2822 format for RSS | `{{ date \| dateToRfc822 }}` |
|
||||
|
||||
#### Plugins
|
||||
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `@11ty/eleventy-plugin-rss` | RSS feed filters (dateToRfc2822, absoluteUrl) |
|
||||
| `@11ty/eleventy-plugin-syntaxhighlight` | Syntax highlighting for code blocks |
|
||||
| `@11ty/eleventy-img` | Automatic image optimization (webp, lazy loading) |
|
||||
| `eleventy-plugin-embed-everything` | Auto-embed YouTube, Vimeo, Mastodon, Bluesky, Spotify |
|
||||
| `@chrisburnell/eleventy-cache-webmentions` | Build-time webmention caching |
|
||||
| `@quasibit/eleventy-plugin-sitemap` | Sitemap generation |
|
||||
| `html-minifier-terser` | HTML minification (production only) |
|
||||
| `pagefind` | Search indexing (post-build via eleventy.after hook) |
|
||||
|
||||
#### Transforms
|
||||
|
||||
| Transform | Purpose |
|
||||
|-----------|---------|
|
||||
| `youtube-link-to-embed` | Converts YouTube links to embeds |
|
||||
| `htmlmin` | Minifies HTML (build mode only, not watch mode) |
|
||||
| `eleventyImageTransformPlugin` | Optimizes `<img>` tags |
|
||||
|
||||
#### Post-Build Hooks (`eleventy.after`)
|
||||
|
||||
1. **Pagefind indexing** — indexes all HTML files for search
|
||||
2. **WebSub hub notification** — notifies subscribers of feed updates (/, /feed.xml, /feed.json)
|
||||
|
||||
### IndieWeb Features
|
||||
|
||||
#### Microformats2
|
||||
|
||||
- **h-card** (author identity): Name, photo, bio, location, social links with `rel="me"`. On the homepage, the h-card lives in `_includes/components/sections/hero.njk` (`h-card` on `<section>`, `p-name`/`u-url`/`u-photo`/`p-job-title`/`p-note` on inner elements). The `authorUrl` resolves from `homepageConfig.identity.url` → `site.author.url` → `site.url`.
|
||||
- **h-entry** (post markup): All post types properly marked up
|
||||
- **h-feed** (feed markup): Machine-readable post lists
|
||||
- **h-cite** (reply context): Cites external content in replies/likes/reposts
|
||||
|
||||
#### Webmentions
|
||||
|
||||
- Build-time caching via `@chrisburnell/eleventy-cache-webmentions`
|
||||
- Client-side real-time fetching via `/js/webmentions.js`
|
||||
- Displays likes, reposts, replies with avatars
|
||||
- Send webmention form on every post
|
||||
- Legacy URL support via `urlAliases` (for micro.blog and old blog URLs)
|
||||
|
||||
#### IndieAuth
|
||||
|
||||
- `rel="me"` links in `<head>` for identity verification
|
||||
- Bluesky uses `rel="me atproto"` for AT Protocol verification
|
||||
- Fediverse creator meta tag for Mastodon verification
|
||||
|
||||
#### Micropub Endpoints
|
||||
|
||||
Base layout includes `<link>` tags pointing to Indiekit endpoints:
|
||||
|
||||
```html
|
||||
<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">
|
||||
```
|
||||
|
||||
#### Bridgy Syndication
|
||||
|
||||
Posts include hidden Bridgy syndication content in `post.njk`:
|
||||
|
||||
```nunjucks
|
||||
<p class="p-summary e-bridgy-mastodon-content e-bridgy-bluesky-content hidden">
|
||||
{# Interaction posts include emoji + target URL #}
|
||||
🔖 {{ bookmarkedUrl }} - {{ description }}
|
||||
</p>
|
||||
```
|
||||
|
||||
Bridgy reads this content when syndicating to Bluesky/Mastodon. Interaction types (bookmarks, likes, replies, reposts) include emoji prefix and target URL.
|
||||
|
||||
## Code Style
|
||||
|
||||
### TypeScript/JavaScript
|
||||
|
||||
- **ESM modules:** `"type": "module"` in package.json
|
||||
- **Async data files:** `export default async function () { ... }`
|
||||
- **Data source pattern:** Return `{ source: "indiekit" | "api" | "error", ...data }`
|
||||
- **Date handling:** Always use ISO 8601 strings (`new Date().toISOString()`)
|
||||
|
||||
### Nunjucks Templates
|
||||
|
||||
- **Property name compatibility:** Support both camelCase and underscore names:
|
||||
|
||||
```nunjucks
|
||||
{% set bookmarkedUrl = bookmarkOf or bookmark_of %}
|
||||
{% set replyTo = inReplyTo or in_reply_to %}
|
||||
```
|
||||
|
||||
- **Date filter guards:** Always check for null/undefined:
|
||||
|
||||
```nunjucks
|
||||
{% if published %}
|
||||
{{ published | date("PPp") }}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
- **Markdown engine disabled:** `markdownTemplateEngine: false` to prevent parsing `{{` in content
|
||||
- **Safe filter usage:** Use `| safe` for trusted HTML content only
|
||||
- **Microformats classes:** Follow IndieWeb conventions (h-entry, p-name, dt-published, e-content, u-photo, etc.)
|
||||
|
||||
### CSS
|
||||
|
||||
- **Tailwind CSS** for all styling
|
||||
- **Dark mode:** `dark:` variants, controlled by `.dark` class on `<html>`
|
||||
- **Custom color palette:** `primary` (blue) and `surface` (neutral)
|
||||
- **Typography plugin:** `prose` classes for content rendering
|
||||
- **Responsive design:** Mobile-first, breakpoints: `sm:`, `md:`, `lg:`
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Post Type
|
||||
|
||||
1. **Create collection** in `eleventy.config.js`:
|
||||
|
||||
```javascript
|
||||
eleventyConfig.addCollection("checkins", function (collectionApi) {
|
||||
return collectionApi
|
||||
.getFilteredByGlob("content/checkins/**/*.md")
|
||||
.sort((a, b) => b.date - a.date);
|
||||
});
|
||||
```
|
||||
|
||||
2. **Create collection page** (e.g., `checkins.njk`):
|
||||
|
||||
```nunjucks
|
||||
---
|
||||
layout: layouts/page.njk
|
||||
title: Check-ins
|
||||
withBlogSidebar: true
|
||||
permalink: /checkins/
|
||||
|
||||
## Key custom systems
|
||||
|
||||
### Digital Garden (`gardenStage`)
|
||||
Posts carry a `gardenStage` front-matter value: `seedling`, `budding`, `cultivating`, or `evergreen`. Stage can also be derived from nested tags (`garden/cultivate` → `cultivating`, etc.).
|
||||
|
||||
**Garden badge component** (`_includes/components/garden-badge.njk`):
|
||||
- In post-list templates, set `gardenStage` from `post.data.gardenStage` before including, or rely on the component's own fallback.
|
||||
- The badge is included **once per post-type branch** (`{% if post.type == "article" %}...{% elif %}...{% endif %}`). Do not add it outside those branches — it will render for every post regardless of type and produce duplicate badges.
|
||||
|
||||
### AI Disclosure
|
||||
Posts declare AI involvement level in front matter (e.g. `aiCode: T1/C2`). Rendered as a badge below post content and as a hidden `.p-ai-code-level` span in list cards.
|
||||
|
||||
### Nested tags
|
||||
Categories use Obsidian-style path notation (`lang/de`, `tech/programming`). The `nestedSlugify()` function in `eleventy.config.js` preserves `/` separators during slug generation. Slugification is applied per segment.
|
||||
|
||||
### Unfurl shortcode
|
||||
`{% unfurl url %}` generates a rich link preview card with caching. Cache lives in `.cache/unfurl/`. The shortcode is registered from `lib/unfurl-shortcode.js`.
|
||||
|
||||
### OG image generation
|
||||
`lib/og.js` + `lib/og-cli.js` — generates Open Graph images at build time using Satori and resvg-js. Avatar is pulled from `AUTHOR_AVATAR` env var.
|
||||
|
||||
### Upstream drift check
|
||||
```bash
|
||||
npm run check:upstream-widgets # Report widget drift vs theme remote
|
||||
npm run check:upstream-widgets:strict # Exit 1 if any drift found
|
||||
```
|
||||
|
||||
---
|
||||
{% for post in collections.checkins %}
|
||||
{# render post #}
|
||||
{% endfor %}
|
||||
|
||||
## Templates — things to know
|
||||
|
||||
### `blog.njk`
|
||||
The main blog listing. Each post type (article, note, bookmark, like, repost, reply, photo) has its own `{% if/elif %}` branch. The AI badge and pagination are **outside** those branches at the `<li>` / `<nav>` level. Garden badge must stay **inside** each branch.
|
||||
|
||||
### `base.njk`
|
||||
Site-wide layout. Header nav uses Alpine.js for dropdowns (`x-data="{ open: false }"`). Dashboard link is auth-gated. Mobile nav mirrors desktop nav.
|
||||
|
||||
### `_data/site.js`
|
||||
All values come from env vars. The `SITE_SOCIAL` env var uses pipe-and-comma encoding: `"Name|URL|icon,Name|URL|icon"`. If not set, social links are auto-derived from feed env vars (GITHUB_USERNAME, BLUESKY_HANDLE, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Deploy workflow
|
||||
|
||||
1. Push to `main` on `github.com:svemagie/blog`
|
||||
2. GitHub Action runs `npm install && npm run build`
|
||||
3. Rsync pushes `_site/` to Cloudron server
|
||||
4. `content/.indiekit/` is excluded from `--delete` to preserve IndieKit state
|
||||
|
||||
The build runs with all env vars injected from GitHub Secrets / Cloudron app settings.
|
||||
|
||||
---
|
||||
|
||||
## Common tasks
|
||||
|
||||
**Add a new Nunjucks filter:** Register in `eleventy.config.js` with `eleventyConfig.addFilter(...)`.
|
||||
|
||||
**Add a new post type:** Create the template page + add a branch in `blog.njk` + add to `_data/enabledPostTypes.js`.
|
||||
|
||||
**Check what's drifted from theme upstream:**
|
||||
```bash
|
||||
npm run check:upstream-widgets
|
||||
```
|
||||
|
||||
3. **Add to enabledPostTypes** (optional, for nav):
|
||||
|
||||
Edit `_data/enabledPostTypes.js` or set `POST_TYPES` env var.
|
||||
|
||||
4. **Update `reply-context.njk`** if the post type has a target URL property.
|
||||
|
||||
5. **Update `post.njk` Bridgy content** if the post type needs special syndication text.
|
||||
|
||||
6. **Commit, push, and update submodule.**
|
||||
|
||||
### Adding a New Data Source
|
||||
|
||||
1. **Create `_data/newSource.js`:**
|
||||
|
||||
```javascript
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
|
||||
export default async function () {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/newapi/api/data`;
|
||||
const data = await EleventyFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
return {
|
||||
source: "indiekit",
|
||||
...data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(`[newSource] API unavailable: ${error.message}`);
|
||||
return {
|
||||
source: "error",
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
**Rebuild CSS only:**
|
||||
```bash
|
||||
npm run build:css
|
||||
```
|
||||
|
||||
2. **Use in template:**
|
||||
|
||||
```nunjucks
|
||||
{% if newSource and newSource.source == "indiekit" %}
|
||||
{% for item in newSource.items %}
|
||||
{# render item #}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
**Local dev:**
|
||||
```bash
|
||||
npm run dev # Eleventy + live reload on localhost:8080
|
||||
```
|
||||
|
||||
3. **Add status check** to base.njk navigation (if needed).
|
||||
---
|
||||
|
||||
### Adding a New Homepage Section
|
||||
## Env vars (quick ref)
|
||||
|
||||
1. **Create section template** in `_includes/components/sections/`:
|
||||
See `README.md` for the full table. Essential ones:
|
||||
|
||||
```nunjucks
|
||||
{# new-section.njk #}
|
||||
{% set sectionConfig = section.config or {} %}
|
||||
{% set maxItems = sectionConfig.maxItems or 5 %}
|
||||
|
||||
<section class="mb-8 sm:mb-12">
|
||||
<h2 class="text-xl sm:text-2xl font-bold">{{ sectionConfig.title or "New Section" }}</h2>
|
||||
{# render content #}
|
||||
</section>
|
||||
```
|
||||
SITE_URL https://blog.giersig.eu
|
||||
SITE_NAME giersig.
|
||||
AUTHOR_NAME svemagie
|
||||
SITE_LOCALE de
|
||||
ACTIVITYPUB_HANDLE svemagie
|
||||
GITHUB_USERNAME svemagie
|
||||
BLUESKY_HANDLE svemagie
|
||||
```
|
||||
|
||||
2. **Register in `homepage-section.njk`:**
|
||||
---
|
||||
|
||||
```nunjucks
|
||||
{% if section.type == "new-section" %}
|
||||
{% include "components/sections/new-section.njk" %}
|
||||
```
|
||||
## What's diverged from upstream (summary)
|
||||
|
||||
3. **Plugin integration:** The plugin that provides this section should register it via `homepageSections` in Indiekit.
|
||||
|
||||
### Debugging Webmentions
|
||||
|
||||
1. **Check build-time cache:** Look at `webmention-debug.njk` page
|
||||
2. **Check client-side fetch:** Open browser console, check for fetch requests to `/webmentions/api/mentions`
|
||||
3. **Verify target URL:** Webmentions must match exact URL (with or without trailing slash)
|
||||
4. **Check legacy URLs:** Verify `urlAliases` data includes old URLs if needed
|
||||
|
||||
### Theming and Customization
|
||||
|
||||
1. **Colors:** Edit `tailwind.config.js` → `theme.extend.colors`
|
||||
2. **Typography:** Edit `tailwind.config.js` → `theme.extend.typography`
|
||||
3. **CSS utilities:** Add custom utilities to `css/tailwind.css`
|
||||
4. **Rebuild CSS:** `npm run build:css` (or `make build:css` in parent repo)
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
1. ❌ **Forgetting to update submodule** after changes
|
||||
2. ❌ **Editing files in submodule directory** (`indiekit-cloudron/eleventy-site/`)
|
||||
3. ❌ **Using Date objects instead of ISO strings** for dates
|
||||
4. ❌ **Not guarding `| date` filters** against null/undefined
|
||||
5. ❌ **Using only underscore property names** (support both camelCase and underscore)
|
||||
6. ❌ **Using `markdownTemplateEngine: "njk"`** (breaks code samples with `{{`)
|
||||
7. ❌ **Hardcoding personal data in templates** (use environment variables)
|
||||
8. ❌ **Forgetting to run `make prepare`** before `cloudron build` (deploys stale config)
|
||||
9. ❌ **Using unsafe HTML string assignment in client-side JS** (security hooks reject it — use `createElement` + `textContent`)
|
||||
10. ❌ **Removing overrides without checking if they shadow submodule files** (causes stale data)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "dateString.split is not a function"
|
||||
|
||||
**Cause:** A Date object was passed to the `| date` filter.
|
||||
**Fix:** Store dates as ISO strings from the start: `new Date().toISOString()`
|
||||
|
||||
### Stale data in homepage/CV despite correct JSON files
|
||||
|
||||
**Cause:** Override file in `indiekit-cloudron/overrides/eleventy-site/` shadows the submodule.
|
||||
**Fix:** Delete the override file and reset submodule: `cd eleventy-site && git checkout -- _data/file.js`
|
||||
|
||||
### Webmentions not appearing
|
||||
|
||||
**Causes:**
|
||||
- Build-time cache expired (rebuild to refresh)
|
||||
- Client-side JS blocked by CSP (check console)
|
||||
- Target URL mismatch (check with/without trailing slash)
|
||||
- webmention.io down (check status)
|
||||
|
||||
**Fix:** Check `webmention-debug.njk` page, verify `webmentionsForUrl` filter is working.
|
||||
|
||||
### Plugin data not appearing in navigation
|
||||
|
||||
**Cause:** The plugin's status endpoint is unavailable or returning `source: "error"`.
|
||||
**Fix:** Check the plugin's API is running, verify environment variables are set.
|
||||
|
||||
### YouTube embeds not working
|
||||
|
||||
**Causes:**
|
||||
- URL doesn't match pattern (must be youtube.com/watch or youtu.be)
|
||||
- Link text doesn't contain "youtube" or URL (transform matches specific patterns)
|
||||
|
||||
**Fix:** Use embed plugin shortcode or raw `<iframe>` instead.
|
||||
|
||||
## Workspace Context
|
||||
|
||||
This repo is part of the Indiekit development workspace at `/home/rick/code/indiekit-dev/`. See the workspace CLAUDE.md for the full repository map and plugin architecture.
|
||||
- **Digital Garden system** — gardenStage, badges, /garden/ page, nested garden/* tags
|
||||
- **AI disclosure** — aiCode front matter, badge component, p-ai-code-level
|
||||
- **Nested tags** — Obsidian-style path categories
|
||||
- **Navigation redesign** — curated header nav with Alpine.js dropdowns; footer restructured
|
||||
- **Fedify ActivityPub** — own AP actor at `@svemagie@blog.giersig.eu`
|
||||
- **OG image generation** — Satori + resvg build-time generation
|
||||
- **Webmention self-filter** — own Bluesky account filtered from interactions
|
||||
- **Markdown Agents** — clean Markdown served to AI crawlers
|
||||
- **Mermaid diagrams** — `eleventy-plugin-mermaid` integrated
|
||||
- **Upstream drift check script** — `scripts/check-upstream-widget-drift.mjs`
|
||||
|
||||
567
README.md
567
README.md
@@ -1,498 +1,125 @@
|
||||
# Indiekit Eleventy Theme
|
||||
# giersig. — personal blog
|
||||
|
||||
A modern, IndieWeb-native Eleventy theme designed for [Indiekit](https://getindiekit.com/)-powered personal websites. Own your content, syndicate everywhere.
|
||||
Personal IndieWeb blog for [blog.giersig.eu](https://blog.giersig.eu), built with [Eleventy](https://www.11ty.dev/) and [IndieKit](https://getindiekit.com/).
|
||||
|
||||
## Features
|
||||
This is **svemagie's** instance of [`svemagie/blog-eleventy-indiekit`](https://github.com/svemagie/blog-eleventy-indiekit) (the shared theme), adapted and extended beyond the upstream.
|
||||
|
||||
### IndieWeb First
|
||||
---
|
||||
|
||||
This theme is built from the ground up for the IndieWeb:
|
||||
## Tech stack
|
||||
|
||||
- **Microformats2** markup (h-card, h-entry, h-feed, h-cite)
|
||||
- **Webmentions** via webmention.io (likes, reposts, replies)
|
||||
- **IndieAuth** with rel="me" verification
|
||||
- **Micropub** integration with Indiekit
|
||||
- **POSSE** syndication to Bluesky, Mastodon, LinkedIn, IndieNews
|
||||
- **Eleventy 3** — static site generator
|
||||
- **Tailwind CSS 3** + `@tailwindcss/typography` — styling
|
||||
- **Alpine.js** — dropdowns, interactive UI
|
||||
- **IndieKit** (Micropub) — post creation/editing via the `content/` symlink
|
||||
- **Webmentions** — via `webmention.io` + `@chrisburnell/eleventy-cache-webmentions`
|
||||
- **ActivityPub** — Fedify, handle `@svemagie@blog.giersig.eu`
|
||||
- **OG images** — generated at build time with Satori + resvg-js
|
||||
- **Pagefind** — client-side full-text search
|
||||
|
||||
### Full Post Type Support
|
||||
---
|
||||
|
||||
All IndieWeb post types via Indiekit:
|
||||
- **Articles** — Long-form blog posts with titles
|
||||
- **Notes** — Short status updates (like tweets)
|
||||
- **Photos** — Image posts with multi-photo galleries
|
||||
- **Bookmarks** — Save and share links with descriptions
|
||||
- **Likes** — Appreciate others' content
|
||||
- **Replies** — Respond to posts across the web
|
||||
- **Reposts** — Share others' content
|
||||
- **Pages** — Root-level slash pages (/about, /now, /uses)
|
||||
|
||||
### Homepage Builder
|
||||
|
||||
Dynamic, plugin-configured homepage with:
|
||||
- **Hero section** with avatar, bio, social links
|
||||
- **Recent posts** with configurable filtering
|
||||
- **CV sections** (experience, skills, education, projects, interests)
|
||||
- **Custom HTML** sections from admin UI
|
||||
- **Two-column layout** with configurable sidebar
|
||||
- **Single-column** or **full-width hero** layouts
|
||||
|
||||
### Plugin Integration
|
||||
|
||||
Integrates with custom Indiekit endpoint plugins:
|
||||
|
||||
| Plugin | Features |
|
||||
|--------|----------|
|
||||
| `@rmdes/indiekit-endpoint-homepage` | Dynamic homepage builder with admin UI |
|
||||
| `@rmdes/indiekit-endpoint-cv` | CV/resume builder with admin UI |
|
||||
| `@rmdes/indiekit-endpoint-github` | GitHub activity, commits, stars, featured repos |
|
||||
| `@rmdes/indiekit-endpoint-funkwhale` | Listening activity from Funkwhale |
|
||||
| `@rmdes/indiekit-endpoint-lastfm` | Scrobbles and loved tracks from Last.fm |
|
||||
| `@rmdes/indiekit-endpoint-youtube` | Channel info, latest videos, live status |
|
||||
| `@rmdes/indiekit-endpoint-blogroll` | OPML/Microsub blog aggregator with admin UI |
|
||||
| `@rmdes/indiekit-endpoint-podroll` | Podcast episode aggregator |
|
||||
| `@rmdes/indiekit-endpoint-rss` | RSS feed reader with MongoDB caching |
|
||||
| `@rmdes/indiekit-endpoint-microsub` | Social reader with channels and timeline |
|
||||
|
||||
### Modern Tech Stack
|
||||
|
||||
- **Eleventy 3.0** — Fast, flexible static site generator
|
||||
- **Tailwind CSS** — Utility-first styling with dark mode
|
||||
- **Alpine.js** — Lightweight JavaScript framework
|
||||
- **Pagefind** — Fast client-side search
|
||||
- **Markdown-it** — Rich markdown with auto-linking
|
||||
- **Image optimization** — Automatic WebP conversion, lazy loading
|
||||
|
||||
## Installation
|
||||
|
||||
### As a Git Submodule (Recommended)
|
||||
|
||||
This theme is designed to be used as a Git submodule in your Indiekit deployment repository:
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
# In your Indiekit deployment repo
|
||||
git submodule add https://github.com/rmdes/indiekit-eleventy-theme.git eleventy-site
|
||||
git submodule update --init --recursive
|
||||
cd eleventy-site
|
||||
npm install
|
||||
npm run dev # Eleventy dev server with live reload
|
||||
npm run build # Production build → _site/
|
||||
npm run build:css # Rebuild Tailwind CSS only
|
||||
```
|
||||
|
||||
**Why submodule?** Keeps the theme neutral (no personal data), allows upstream updates, and separates theme development from deployment.
|
||||
Copy `.env` (or set the variables below) before building. The `.env` file is not committed.
|
||||
|
||||
### Standalone Installation
|
||||
---
|
||||
|
||||
For local development or testing:
|
||||
## Environment variables
|
||||
|
||||
All site configuration is driven by env vars — no hardcoded values in source. Key ones:
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `SITE_URL` | Full URL, no trailing slash (e.g. `https://blog.giersig.eu`) |
|
||||
| `SITE_NAME` | Display name shown in header |
|
||||
| `AUTHOR_NAME` | Used in h-card, OG images, feeds |
|
||||
| `AUTHOR_BIO` | Short bio line |
|
||||
| `AUTHOR_AVATAR` | Full URL to avatar image |
|
||||
| `AUTHOR_EMAIL` | Contact email |
|
||||
| `SITE_DESCRIPTION` | Meta description |
|
||||
| `SITE_LOCALE` | BCP 47 locale (e.g. `de`) |
|
||||
| `GITHUB_USERNAME` | Enables GitHub activity feed |
|
||||
| `BLUESKY_HANDLE` | Enables Bluesky feed + webmention self-filter |
|
||||
| `MASTODON_INSTANCE` / `MASTODON_USER` | Mastodon feed + rel=me |
|
||||
| `ACTIVITYPUB_HANDLE` | ActivityPub actor name (Fedify) |
|
||||
| `SITE_SOCIAL` | Pipe-and-comma encoded social links: `Name\|URL\|icon,...` |
|
||||
| `WEBMENTION_IO_TOKEN` | webmention.io API token |
|
||||
| `LASTFM_API_KEY` / `LASTFM_USERNAME` | Listening page |
|
||||
| `SUPPORT_URL` etc. | Tip/support links in JSON Feed extension |
|
||||
| `MARKDOWN_AGENTS_ENABLED` | Serve clean Markdown to AI agents (default `true`) |
|
||||
|
||||
---
|
||||
|
||||
## Content structure
|
||||
|
||||
```
|
||||
content/ ← symlink → IndieKit-managed posts (gitignored)
|
||||
articles/
|
||||
notes/
|
||||
bookmarks/
|
||||
likes/
|
||||
replies/
|
||||
reposts/
|
||||
photos/
|
||||
pages/
|
||||
```
|
||||
|
||||
Posts are created/updated via IndieKit's Micropub endpoint — not edited manually. The `content/` directory is gitignored but processed by Eleventy (`setUseGitIgnore(false)`).
|
||||
|
||||
---
|
||||
|
||||
## Custom systems (beyond upstream)
|
||||
|
||||
### Digital Garden
|
||||
Posts can carry a `gardenStage` front-matter value (`seedling`, `budding`, `cultivating`, `evergreen`). Stages can also be derived from nested tags (`garden/cultivate`, etc.). The `garden-badge.njk` component renders a coloured pill in post lists and on individual posts. The `/garden/` page documents the stages.
|
||||
|
||||
### AI Disclosure
|
||||
Posts declare their AI involvement level in front matter. The AI badge renders below the post content and (subtly) in list cards via `.p-ai-code-level`.
|
||||
|
||||
### Nested tags
|
||||
Categories support Obsidian-style path notation (`lang/de`, `tech/programming`). The `nestedSlugify` function in `eleventy.config.js` preserves `/` separators during slug generation.
|
||||
|
||||
### Upstream drift check
|
||||
```bash
|
||||
git clone https://github.com/rmdes/indiekit-eleventy-theme.git
|
||||
cd indiekit-eleventy-theme
|
||||
npm install
|
||||
npm run check:upstream-widgets # Report widget drift vs theme remote
|
||||
npm run check:upstream-widgets:strict # Exit 1 if any drift found
|
||||
```
|
||||
|
||||
## Configuration
|
||||
---
|
||||
|
||||
**All configuration is done via environment variables** — the theme contains no hardcoded personal data.
|
||||
## Deploy
|
||||
|
||||
### Required Variables
|
||||
The site is hosted on Cloudron. Deployment is triggered by pushing to `main` — a GitHub Action builds the site and rsyncs `_site/` to the server. `content/.indiekit/` is excluded from rsync `--delete` to preserve IndieKit's internal state.
|
||||
|
||||
```bash
|
||||
# Site basics
|
||||
SITE_URL="https://your-site.com"
|
||||
SITE_NAME="Your Site Name"
|
||||
SITE_DESCRIPTION="A short description of your site"
|
||||
SITE_LOCALE="en"
|
||||
---
|
||||
|
||||
# Author info (displayed in h-card)
|
||||
AUTHOR_NAME="Your Name"
|
||||
AUTHOR_BIO="A short bio about yourself"
|
||||
AUTHOR_AVATAR="/images/avatar.jpg"
|
||||
```
|
||||
|
||||
### Social Links
|
||||
|
||||
Format: `Name|URL|icon,Name|URL|icon`
|
||||
|
||||
```bash
|
||||
SITE_SOCIAL="GitHub|https://github.com/you|github,Mastodon|https://mastodon.social/@you|mastodon,Bluesky|https://bsky.app/profile/you|bluesky"
|
||||
```
|
||||
|
||||
**Auto-generation:** If `SITE_SOCIAL` is not set, social links are automatically generated from feed credentials (GitHub, Bluesky, Mastodon, LinkedIn).
|
||||
|
||||
### Optional Author Fields
|
||||
|
||||
```bash
|
||||
AUTHOR_TITLE="Software Developer"
|
||||
AUTHOR_LOCATION="City, Country"
|
||||
AUTHOR_LOCALITY="City"
|
||||
AUTHOR_REGION="State/Province"
|
||||
AUTHOR_COUNTRY="Country"
|
||||
AUTHOR_ORG="Company Name"
|
||||
AUTHOR_PRONOUN="they/them"
|
||||
AUTHOR_CATEGORIES="IndieWeb,Open Source,Photography" # Comma-separated
|
||||
AUTHOR_KEY_URL="https://keybase.io/you/pgp_keys.asc"
|
||||
AUTHOR_EMAIL="you@example.com"
|
||||
```
|
||||
|
||||
### Social Activity Feeds
|
||||
|
||||
For sidebar social activity widgets:
|
||||
|
||||
```bash
|
||||
# Bluesky
|
||||
BLUESKY_HANDLE="you.bsky.social"
|
||||
|
||||
# Mastodon
|
||||
MASTODON_INSTANCE="https://mastodon.social"
|
||||
MASTODON_USER="your-username"
|
||||
```
|
||||
|
||||
### Plugin API Credentials
|
||||
|
||||
#### GitHub Activity
|
||||
```bash
|
||||
GITHUB_USERNAME="your-username"
|
||||
GITHUB_TOKEN="ghp_xxxx" # Personal access token (optional, increases rate limit)
|
||||
GITHUB_FEATURED_REPOS="user/repo1,user/repo2" # Comma-separated
|
||||
```
|
||||
|
||||
#### Funkwhale
|
||||
```bash
|
||||
FUNKWHALE_INSTANCE="https://your-instance.com"
|
||||
FUNKWHALE_USERNAME="your-username"
|
||||
FUNKWHALE_TOKEN="your-api-token"
|
||||
```
|
||||
|
||||
#### YouTube
|
||||
```bash
|
||||
YOUTUBE_API_KEY="your-api-key"
|
||||
YOUTUBE_CHANNELS="@channel1,@channel2" # Comma-separated handles
|
||||
```
|
||||
|
||||
#### LinkedIn
|
||||
```bash
|
||||
LINKEDIN_USERNAME="your-username"
|
||||
```
|
||||
|
||||
### Post Type Configuration
|
||||
|
||||
Control which post types appear in navigation:
|
||||
|
||||
```bash
|
||||
# Option 1: Environment variable (comma-separated)
|
||||
POST_TYPES="article,note,photo,bookmark"
|
||||
|
||||
# Option 2: JSON file (written by Indiekit or deployer)
|
||||
# Create content/.indiekit/post-types.json:
|
||||
# ["article", "note", "photo"]
|
||||
```
|
||||
|
||||
**Default:** All standard post types enabled (article, note, photo, bookmark, like, reply, repost).
|
||||
|
||||
## Directory Structure
|
||||
## Repository structure
|
||||
|
||||
```
|
||||
indiekit-eleventy-theme/
|
||||
├── _data/ # Data files
|
||||
│ ├── site.js # Site config from env vars
|
||||
│ ├── cv.js # CV data from plugin
|
||||
│ ├── homepageConfig.js # Homepage layout from plugin
|
||||
│ ├── enabledPostTypes.js # Post types for navigation
|
||||
│ ├── githubActivity.js # GitHub data (Indiekit API → GitHub API fallback)
|
||||
│ ├── funkwhaleActivity.js # Funkwhale listening activity
|
||||
│ ├── lastfmActivity.js # Last.fm scrobbles
|
||||
│ ├── youtubeChannel.js # YouTube channel info
|
||||
│ ├── blueskyFeed.js # Bluesky posts for sidebar
|
||||
│ ├── mastodonFeed.js # Mastodon posts for sidebar
|
||||
│ ├── blogrollStatus.js # Blogroll API availability check
|
||||
│ └── urlAliases.js # Legacy URL mappings for webmentions
|
||||
├── _includes/
|
||||
│ ├── layouts/
|
||||
│ │ ├── base.njk # Base HTML shell (header, footer, nav)
|
||||
│ │ ├── home.njk # Homepage layout (plugin vs default)
|
||||
│ │ ├── post.njk # Individual post (h-entry, webmentions)
|
||||
│ │ └── page.njk # Simple page layout
|
||||
│ ├── components/
|
||||
│ │ ├── homepage-builder.njk # Renders plugin homepage config
|
||||
│ │ ├── homepage-section.njk # Section router
|
||||
│ │ ├── sidebar.njk # Default sidebar
|
||||
│ │ ├── h-card.njk # Author identity card
|
||||
│ │ ├── reply-context.njk # Reply/like/repost context
|
||||
│ │ └── webmentions.njk # Webmention display + form
|
||||
│ │ ├── sections/
|
||||
│ │ │ ├── hero.njk # Homepage hero
|
||||
│ │ │ ├── recent-posts.njk # Recent posts grid
|
||||
│ │ │ ├── cv-experience.njk # Work experience timeline
|
||||
│ │ │ ├── cv-skills.njk # Skills with proficiency
|
||||
│ │ │ ├── cv-education.njk # Education history
|
||||
│ │ │ ├── cv-projects.njk # Featured projects
|
||||
│ │ │ ├── cv-interests.njk # Personal interests
|
||||
│ │ │ └── custom-html.njk # Custom HTML content
|
||||
│ │ └── widgets/
|
||||
│ │ ├── author-card.njk # Sidebar h-card
|
||||
│ │ ├── social-activity.njk # Bluesky/Mastodon feed
|
||||
│ │ ├── github-repos.njk # GitHub featured repos
|
||||
│ │ ├── funkwhale.njk # Now playing widget
|
||||
│ │ ├── blogroll.njk # Recently updated blogs
|
||||
│ │ └── categories.njk # Category list
|
||||
├── css/
|
||||
│ ├── tailwind.css # Tailwind source
|
||||
│ ├── style.css # Compiled output (generated)
|
||||
│ └── prism-theme.css # Syntax highlighting theme
|
||||
├── js/
|
||||
│ ├── webmentions.js # Client-side webmention fetcher
|
||||
│ └── admin.js # Admin auth detection (shows FAB + dashboard link)
|
||||
├── images/ # Static images
|
||||
├── *.njk # Page templates (blog, about, cv, etc.)
|
||||
├── eleventy.config.js # Eleventy configuration
|
||||
├── tailwind.config.js # Tailwind configuration
|
||||
├── postcss.config.js # PostCSS pipeline
|
||||
└── package.json # Dependencies and scripts
|
||||
_data/ JS data files (all env-var driven)
|
||||
_includes/
|
||||
layouts/ Page layout templates
|
||||
components/ Reusable Nunjucks partials
|
||||
content/ Symlink → IndieKit posts (gitignored)
|
||||
css/ Tailwind source + compiled output
|
||||
docs/plans/ Implementation plans
|
||||
images/ Static assets
|
||||
js/ Client-side scripts
|
||||
lib/ Build-time utilities (OG, unfurl)
|
||||
scripts/ Maintenance scripts
|
||||
eleventy.config.js Single monolithic Eleventy config
|
||||
```
|
||||
|
||||
## Usage
|
||||
---
|
||||
|
||||
### Development
|
||||
## Relationship to upstream
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Development server with hot reload
|
||||
npm run dev
|
||||
# → http://localhost:8080
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
# → Output to _site/
|
||||
|
||||
# Build CSS only (after Tailwind config changes)
|
||||
npm run build:css
|
||||
```
|
||||
|
||||
### Content Directory
|
||||
|
||||
The theme expects content in a `content/` directory (typically a symlink to Indiekit's content store):
|
||||
|
||||
```
|
||||
content/
|
||||
├── .indiekit/ # Plugin data files
|
||||
│ ├── homepage.json # Homepage builder config
|
||||
│ ├── cv.json # CV data
|
||||
│ └── post-types.json # Enabled post types
|
||||
├── articles/
|
||||
│ └── 2025-01-15-post.md
|
||||
├── notes/
|
||||
│ └── 2025-01-15-note.md
|
||||
├── photos/
|
||||
│ └── 2025-01-15-photo.md
|
||||
└── pages/
|
||||
└── about.md # Slash page
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Colors and Typography
|
||||
|
||||
Edit `tailwind.config.js`:
|
||||
|
||||
```javascript
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
500: "#3b82f6", // Your primary color
|
||||
600: "#2563eb",
|
||||
// ...
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Your Font", "system-ui", "sans-serif"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then rebuild CSS: `npm run build:css`
|
||||
|
||||
### Dark Mode
|
||||
|
||||
The theme includes full dark mode support with `dark:` variants. Toggle is available in header/mobile nav, syncs with system preference.
|
||||
|
||||
### Override Files
|
||||
|
||||
When using as a submodule, place override files in your parent repo:
|
||||
|
||||
```
|
||||
your-deployment-repo/
|
||||
├── overrides/
|
||||
│ └── eleventy-site/
|
||||
│ ├── _data/ # Override data files
|
||||
│ ├── images/ # Your images
|
||||
│ └── about.njk # Override templates
|
||||
└── eleventy-site/ # This theme (submodule)
|
||||
```
|
||||
|
||||
Override files are copied over the submodule during build.
|
||||
|
||||
**Warning:** Be careful with `_data/` overrides — they can shadow dynamic plugin data. Use only for truly static customizations.
|
||||
|
||||
## Plugin Integration
|
||||
|
||||
### How Plugins Provide Data
|
||||
|
||||
Indiekit plugins write JSON files to `content/.indiekit/*.json`. The theme's `_data/*.js` files read these JSON files at build time.
|
||||
|
||||
**Example flow:**
|
||||
|
||||
1. User edits CV in Indiekit admin UI (`/cv`)
|
||||
2. `@rmdes/indiekit-endpoint-cv` saves to `content/.indiekit/cv.json`
|
||||
3. Eleventy rebuild triggers (`_data/cv.js` reads the JSON file)
|
||||
4. CV sections render with new data
|
||||
|
||||
### Homepage Builder
|
||||
|
||||
The homepage builder is controlled by `@rmdes/indiekit-endpoint-homepage`:
|
||||
|
||||
1. Plugin provides admin UI at `/homepage`
|
||||
2. User configures layout, sections, sidebar widgets
|
||||
3. Plugin writes `content/.indiekit/homepage.json`
|
||||
4. Theme renders configured layout (or falls back to default)
|
||||
|
||||
**Fallback:** If no homepage plugin is installed, the theme shows a default layout (hero + recent posts + sidebar).
|
||||
|
||||
### Adding Custom Sections
|
||||
|
||||
To add a custom homepage section:
|
||||
|
||||
1. Create template in `_includes/components/sections/your-section.njk`
|
||||
2. Register in `_includes/components/homepage-section.njk`:
|
||||
|
||||
```nunjucks
|
||||
{% if section.type == "your-section" %}
|
||||
{% include "components/sections/your-section.njk" %}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
3. Plugin should register the section via `homepageSections` in Indiekit
|
||||
|
||||
## Deployment
|
||||
|
||||
### Cloudron
|
||||
|
||||
See `indiekit-cloudron` repository for Cloudron deployment with this theme as submodule.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
See `indiekit-deploy` repository for Docker Compose deployment with this theme as submodule.
|
||||
|
||||
### Static Host (Netlify, Vercel, etc.)
|
||||
|
||||
1. **Not recommended** — Indiekit needs a server for Micropub/Webmentions
|
||||
2. For static-only use (no Indiekit), set all env vars and run `npm run build`
|
||||
3. Deploy `_site/` directory
|
||||
|
||||
## Pages Included
|
||||
|
||||
| Page | URL | Description |
|
||||
|------|-----|-------------|
|
||||
| Home | `/` | Dynamic homepage (plugin or default) |
|
||||
| About | `/about/` | Full h-card with bio |
|
||||
| CV | `/cv/` | Resume with all sections |
|
||||
| Blog | `/blog/` | All posts chronologically |
|
||||
| Articles | `/articles/` | Long-form articles |
|
||||
| Notes | `/notes/` | Short status updates |
|
||||
| Photos | `/photos/` | Photo posts |
|
||||
| Bookmarks | `/bookmarks/` | Saved links |
|
||||
| Likes | `/likes/` | Liked posts |
|
||||
| Replies | `/replies/` | Responses to others |
|
||||
| Reposts | `/reposts/` | Shared content |
|
||||
| Interactions | `/interactions/` | Combined social interactions |
|
||||
| Slashes | `/slashes/` | Index of all slash pages |
|
||||
| Categories | `/categories/` | Posts by category |
|
||||
| GitHub | `/github/` | GitHub activity (if plugin enabled) |
|
||||
| Funkwhale | `/funkwhale/` | Listening history (if plugin enabled) |
|
||||
| Last.fm | `/listening/` | Last.fm scrobbles (if plugin enabled) |
|
||||
| YouTube | `/youtube/` | YouTube channel (if plugin enabled) |
|
||||
| Blogroll | `/blogroll/` | Blog aggregator (if plugin enabled) |
|
||||
| Podroll | `/podroll/` | Podcast episodes (if plugin enabled) |
|
||||
| IndieNews | `/news/` | IndieNews submissions (if plugin enabled) |
|
||||
| Search | `/search/` | Pagefind search UI |
|
||||
| RSS Feed | `/feed.xml` | RSS 2.0 feed |
|
||||
| JSON Feed | `/feed.json` | JSON Feed 1.1 |
|
||||
| Changelog | `/changelog/` | Site changelog |
|
||||
|
||||
## IndieWeb Resources
|
||||
|
||||
- [IndieWebify.me](https://indiewebify.me/) — Test your IndieWeb implementation
|
||||
- [Microformats Wiki](https://microformats.org/wiki/h-card) — Microformats2 reference
|
||||
- [webmention.io](https://webmention.io/) — Webmention service
|
||||
- [IndieAuth](https://indieauth.com/) — Authentication protocol
|
||||
- [Bridgy](https://brid.gy/) — Backfeed social interactions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Webmentions not appearing
|
||||
|
||||
**Solution:**
|
||||
1. Check `SITE_URL` matches your live domain exactly
|
||||
2. Verify webmention.io API is responding: `https://webmention.io/api/mentions?target=https://your-site.com/`
|
||||
3. Check build-time cache at `/webmention-debug/`
|
||||
4. Ensure post URLs match exactly (with/without trailing slash)
|
||||
|
||||
### Plugin data not showing
|
||||
|
||||
**Solution:**
|
||||
1. Verify the plugin is installed and running in Indiekit
|
||||
2. Check environment variables are set correctly
|
||||
3. Check `content/.indiekit/*.json` files exist and are valid JSON
|
||||
4. Rebuild Eleventy to refresh data: `npm run build`
|
||||
|
||||
### Dark mode not working
|
||||
|
||||
**Solution:**
|
||||
1. Check browser console for JavaScript errors
|
||||
2. Verify Alpine.js loaded: `<script src="...alpinejs..."></script>`
|
||||
3. Clear localStorage: `localStorage.removeItem('theme')`
|
||||
|
||||
### Search not working
|
||||
|
||||
**Solution:**
|
||||
1. Check Pagefind indexed: `_site/_pagefind/` directory exists
|
||||
2. Rebuild with search indexing: `npm run build`
|
||||
3. Check search page is not blocked by CSP headers
|
||||
|
||||
## Contributing
|
||||
|
||||
This theme is tailored for a specific Indiekit deployment but designed to be adaptable. Contributions welcome:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test with `npm run dev`
|
||||
5. Submit a pull request
|
||||
|
||||
**Guidelines:**
|
||||
- Keep theme neutral (no hardcoded personal data)
|
||||
- Use environment variables for all configuration
|
||||
- Maintain microformats2 markup
|
||||
- Test dark mode
|
||||
- Follow existing code style (ESM, Nunjucks, Tailwind)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Credits
|
||||
|
||||
- Built for [Indiekit](https://getindiekit.com/) by Paul Robert Lloyd
|
||||
- Inspired by the [IndieWeb](https://indieweb.org/) community
|
||||
- Styled with [Tailwind CSS](https://tailwindcss.com/)
|
||||
- Icons from [Heroicons](https://heroicons.com/)
|
||||
- Search by [Pagefind](https://pagefind.app/)
|
||||
- Static site generation by [Eleventy](https://11ty.dev/)
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [Indiekit](https://github.com/getindiekit/indiekit) — Micropub server
|
||||
- [indiekit-cloudron](https://github.com/rmdes/indiekit-cloudron) — Cloudron deployment
|
||||
- [indiekit-deploy](https://github.com/rmdes/indiekit-deploy) — Docker Compose deployment
|
||||
- [@rmdes/indiekit-endpoint-*](https://github.com/rmdes?tab=repositories) — Custom Indiekit plugins
|
||||
The theme remote is tracked as `theme` (`svemagie/blog-eleventy-indiekit`). This repo (`svemagie/blog`) is the live site and has diverged substantially — garden system, AI disclosure, nested tags, navigation redesign, Fedify ActivityPub, OG generation, and more. Upstream changes are selectively cherry-picked, not merged wholesale.
|
||||
|
||||
Reference in New Issue
Block a user