From 32aea5ace92c5e6d763f8a5442e644b5385a7b4f Mon Sep 17 00:00:00 2001
From: rmdes
Date: Sun, 8 Feb 2026 15:16:29 +0100
Subject: [PATCH] feat: neutralize theme for fresh deployments
Strip personal data from templates so the theme ships clean for any
deployer. Collection pages now use generatePageOnEmptyData so empty
post types show encouraging placeholders instead of 404s. Navigation
is conditional on enabled post types and installed plugins. Sidebar
widgets split into individual components with plugin-aware visibility.
Slashes page explains required plugins for root-level page creation.
Co-Authored-By: Claude Opus 4.6
---
_data/blogrollStatus.js | 34 ++
_data/cv.js | 199 +----------
_data/enabledPostTypes.js | 50 +++
_data/homepageConfig.js | 27 ++
_data/podrollStatus.js | 34 ++
_data/urlAliases.js | 8 +-
_includes/components/empty-collection.njk | 27 ++
_includes/components/sidebar.njk | 312 ++----------------
_includes/components/widgets/author-card.njk | 4 +
_includes/components/widgets/blogroll.njk | 56 ++++
_includes/components/widgets/categories.njk | 13 +
_includes/components/widgets/funkwhale.njk | 71 ++++
_includes/components/widgets/github-repos.njk | 32 ++
_includes/components/widgets/recent-posts.njk | 21 ++
.../components/widgets/social-activity.njk | 94 ++++++
_includes/layouts/base.njk | 63 ++--
_includes/layouts/home.njk | 107 +++++-
articles.njk | 4 +-
bookmarks.njk | 4 +-
feed-json.njk | 2 +-
likes.njk | 4 +-
notes.njk | 4 +-
package.json | 4 +-
photos.njk | 4 +-
replies.njk | 4 +-
reposts.njk | 4 +-
slashes.njk | 92 ++++--
27 files changed, 738 insertions(+), 540 deletions(-)
create mode 100644 _data/blogrollStatus.js
create mode 100644 _data/enabledPostTypes.js
create mode 100644 _data/homepageConfig.js
create mode 100644 _data/podrollStatus.js
create mode 100644 _includes/components/empty-collection.njk
create mode 100644 _includes/components/widgets/author-card.njk
create mode 100644 _includes/components/widgets/blogroll.njk
create mode 100644 _includes/components/widgets/categories.njk
create mode 100644 _includes/components/widgets/funkwhale.njk
create mode 100644 _includes/components/widgets/github-repos.njk
create mode 100644 _includes/components/widgets/recent-posts.njk
create mode 100644 _includes/components/widgets/social-activity.njk
diff --git a/_data/blogrollStatus.js b/_data/blogrollStatus.js
new file mode 100644
index 0000000..aee1601
--- /dev/null
+++ b/_data/blogrollStatus.js
@@ -0,0 +1,34 @@
+/**
+ * Blogroll Status Data
+ * Checks if the blogroll API backend is available at build time.
+ * Used for conditional navigation — the blogroll page itself loads data client-side.
+ */
+
+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}/blogrollapi/api/status`;
+ console.log(`[blogrollStatus] Checking API: ${url}`);
+ const data = await EleventyFetch(url, {
+ duration: "15m",
+ type: "json",
+ });
+ console.log("[blogrollStatus] API available");
+ return {
+ available: true,
+ source: "indiekit",
+ ...data,
+ };
+ } catch (error) {
+ console.log(
+ `[blogrollStatus] API unavailable: ${error.message}`
+ );
+ return {
+ available: false,
+ source: "unavailable",
+ };
+ }
+}
diff --git a/_data/cv.js b/_data/cv.js
index 576eac9..2dda1e2 100644
--- a/_data/cv.js
+++ b/_data/cv.js
@@ -1,190 +1,21 @@
/**
- * CV Data - Easy to update!
+ * CV Data — Empty defaults.
*
- * To add a new experience: Add an entry to the `experience` array
- * To add a new project: Add an entry to the `projects` array
- * To update skills: Modify the `skills` object
+ * When the indiekit-endpoint-cv plugin is installed, it serves CV data
+ * via its API endpoint and the homepage plugin renders it.
+ *
+ * Without the plugin, users can edit this file directly:
+ * - Add entries to the `experience` array
+ * - Add entries to the `projects` array
+ * - Modify the `skills` object
*/
export default {
- // Last updated date - automatically set to build time
- lastUpdated: new Date().toISOString().split("T")[0],
-
- // Work Experience - Add new positions at the TOP of the array
- experience: [
- {
- title: "Middleware Engineer",
- company: "FGTB-ABVV",
- location: "Brussels",
- startDate: "2023-11",
- endDate: null, // null = present
- type: "full-time",
- description: "Technology Specialist focusing on IT infrastructure and application delivery",
- highlights: [
- "Strategic migration of Java applications from legacy IBM Datapowers and PureApp systems",
- "Containerized application deployment on VMware Linux and OpenShift Kubernetes clusters",
- "Mastering OpenShift, Kubernetes, and Docker technologies"
- ]
- },
- {
- title: "Solution Architect",
- company: "OSINTukraine.com",
- location: "Remote",
- startDate: "2022-02",
- endDate: null,
- type: "volunteer",
- description: "Open-source intelligence (OSINT) initiative for Ukraine conflict monitoring",
- highlights: [
- "Collection, archiving, translation, analysis and dissemination of critical information",
- "Monitoring Russian Telegram channels with filtering, categorization, and archiving",
- "Sub-projects: War crimes archive, Drones research, Location-related alerts system"
- ]
- },
- {
- title: "DevOps Training",
- company: "BeCode",
- location: "Brussels",
- startDate: "2021-09",
- endDate: "2022-03",
- type: "training",
- description: "7-month intensive DevOps specialization",
- highlights: [
- "Vagrant and Ansible infrastructure as code for WordPress, Nginx, Redis",
- "Docker Swarm cluster management",
- "GitLab CI/CD with SonarQube security audits",
- "Jenkins pipelines, Python basics, Prometheus/Grafana monitoring"
- ]
- },
- {
- title: "CTO",
- company: "DigitYser",
- location: "Brussels",
- startDate: "2018-10",
- endDate: "2020-03",
- type: "full-time",
- description: "Digital flagship of tech communities in Brussels",
- highlights: [
- "Hosting infrastructure and automation",
- "Integrations with digital marketing tools",
- "Technical Event Management: Livestreaming, sound, video, photos"
- ]
- },
- {
- title: "Solution Architect",
- company: "Armada.digital",
- location: "Brussels",
- startDate: "2016-05",
- endDate: "2021-12",
- type: "freelance",
- description: "Consultancy to amplify visibility of good causes",
- highlights: [
- "Custom communication and collaboration solutions",
- "Empowering individuals and ethical businesses"
- ]
- },
- {
- title: "FactChecking Platform",
- company: "Journalistes Solidaires",
- location: "Brussels",
- startDate: "2020-03",
- endDate: "2020-05",
- type: "volunteer",
- description: "Cloudron/Docker backend for factchecking workflow",
- highlights: [
- "WordPress with custom post types for COVID-19 disinformation monitoring"
- ]
- },
- {
- title: "Event Manager",
- company: "European Data Innovation Hub",
- location: "Brussels",
- startDate: "2019-02",
- endDate: "2020-03",
- type: "full-time",
- description: "Technical event organization and management"
- },
- {
- title: "Technical Advisor",
- company: "WomenPreneur-Initiative",
- location: "Brussels",
- startDate: "2019-01",
- endDate: "2020-01",
- type: "volunteer",
- description: "Technical guidance for women-focused entrepreneurship initiative"
- },
- {
- title: "Technical Advisor",
- company: "Promote Ukraine",
- location: "Brussels",
- startDate: "2019-01",
- endDate: "2020-01",
- type: "freelance",
- description: "Technical consulting for Ukraine advocacy organization"
- }
- ],
-
- // Current/Recent Projects - Add new projects at the TOP
- projects: [
- {
- name: "OSINT Intelligence Platform",
- url: "https://osintukraine.com",
- description: "Real-time monitoring and analysis platform for open-source intelligence",
- technologies: ["Docker", "Telegram API", "Python", "PostgreSQL"],
- status: "active"
- },
- {
- name: "Indiekit Cloudron Package",
- url: "https://github.com/rmdes/indiekit-cloudron",
- description: "Cloudron-packaged IndieWeb publishing server with Eleventy frontend",
- technologies: ["Node.js", "Eleventy", "Docker", "Cloudron"],
- status: "active"
- }
- // Add more projects here as needed
- ],
-
- // Skills - Organized by category
- skills: {
- containers: ["OpenShift", "Kubernetes", "Docker", "Docker Swarm"],
- automation: ["Ansible", "Vagrant", "GitLab CI/CD", "Jenkins", "GitHub Actions"],
- monitoring: ["Prometheus", "Grafana", "OpenTelemetry"],
- systems: ["Linux Administration", "System Administration", "VMware"],
- hosting: ["Cloudron", "On-Premise", "Cloud Infrastructure"],
- web: ["Nginx", "Redis", "WordPress", "TLS/SSL", "Eleventy"],
- security: ["SonarQube", "Information Assurance", "OSINT"],
- languages: ["Python", "Bash", "JavaScript", "Node.js"]
- },
-
- // Languages spoken
- languages: [
- { name: "Portuguese", level: "Native" },
- { name: "French", level: "Fluent" },
- { name: "English", level: "Fluent" },
- { name: "Spanish", level: "Conversational" }
- ],
-
- // Education
- education: [
- {
- degree: "DevOps Training",
- institution: "BeCode",
- location: "Brussels",
- year: "2021-2022",
- description: "7-month intensive DevOps specialization"
- },
- {
- degree: "Bachelor's in Management Information Technology",
- institution: "ISLA - Instituto Superior de Gestão e Tecnologia",
- location: "Portugal",
- year: "1998-2001",
- description: "Curso Técnico Superior Profissional de Informática de Gestão"
- }
- ],
-
- // Interests
- interests: [
- "Music Production (Ableton Live, Ableton Push 3)",
- "IndieWeb & Decentralized Tech",
- "Open Source Intelligence (OSINT)",
- "Democracy & Digital Rights"
- ]
+ lastUpdated: null,
+ experience: [],
+ projects: [],
+ skills: {},
+ languages: [],
+ education: [],
+ interests: [],
};
diff --git a/_data/enabledPostTypes.js b/_data/enabledPostTypes.js
new file mode 100644
index 0000000..989bc7a
--- /dev/null
+++ b/_data/enabledPostTypes.js
@@ -0,0 +1,50 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const CONTENT_DIR = process.env.CONTENT_DIR || "/data/content";
+
+// Standard post types for any Indiekit deployment
+const ALL_POST_TYPES = [
+ { type: "article", label: "Articles", path: "/articles/", createUrl: "/posts/create?type=article" },
+ { type: "note", label: "Notes", path: "/notes/", createUrl: "/posts/create?type=note" },
+ { type: "photo", label: "Photos", path: "/photos/", createUrl: "/posts/create?type=photo" },
+ { type: "bookmark", label: "Bookmarks", path: "/bookmarks/", createUrl: "/posts/create?type=bookmark" },
+ { type: "like", label: "Likes", path: "/likes/", createUrl: "/posts/create?type=like" },
+ { type: "reply", label: "Replies", path: "/replies/", createUrl: "/posts/create?type=reply" },
+ { type: "repost", label: "Reposts", path: "/reposts/", createUrl: "/posts/create?type=repost" },
+];
+
+/**
+ * Returns the list of enabled post types.
+ *
+ * Resolution order:
+ * 1. .indiekit/post-types.json in content dir (written by Indiekit or deployer)
+ * 2. POST_TYPES env var (comma-separated: "article,note,photo")
+ * 3. All standard post types (default)
+ */
+export default function () {
+ // 1. Try config file
+ try {
+ const configPath = resolve(CONTENT_DIR, ".indiekit", "post-types.json");
+ const raw = readFileSync(configPath, "utf8");
+ const types = JSON.parse(raw);
+ if (Array.isArray(types)) {
+ // Array of type strings: ["article", "note"]
+ return ALL_POST_TYPES.filter((pt) => types.includes(pt.type));
+ }
+ // Array of objects with at least { type }
+ return types;
+ } catch {
+ // File doesn't exist — fall through
+ }
+
+ // 2. Try env var
+ const envTypes = process.env.POST_TYPES;
+ if (envTypes) {
+ const types = envTypes.split(",").map((t) => t.trim().toLowerCase());
+ return ALL_POST_TYPES.filter((pt) => types.includes(pt.type));
+ }
+
+ // 3. Default — all standard types
+ return ALL_POST_TYPES;
+}
diff --git a/_data/homepageConfig.js b/_data/homepageConfig.js
new file mode 100644
index 0000000..2c2fc35
--- /dev/null
+++ b/_data/homepageConfig.js
@@ -0,0 +1,27 @@
+/**
+ * Homepage Configuration Data
+ * Reads config from indiekit-endpoint-homepage plugin (when installed).
+ * Falls back to null — home.njk then uses the default layout.
+ *
+ * Future: The homepage plugin will write a .indiekit/homepage.json file
+ * that Eleventy watches. On change, a rebuild picks up the new config,
+ * allowing layout changes without a Docker rebuild.
+ */
+
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const CONTENT_DIR = process.env.CONTENT_DIR || "/data/content";
+
+export default function () {
+ try {
+ const configPath = resolve(CONTENT_DIR, ".indiekit", "homepage.json");
+ const raw = readFileSync(configPath, "utf8");
+ const config = JSON.parse(raw);
+ console.log("[homepageConfig] Loaded plugin config");
+ return config;
+ } catch {
+ // No homepage plugin config — this is the normal case for most deployments
+ return null;
+ }
+}
diff --git a/_data/podrollStatus.js b/_data/podrollStatus.js
new file mode 100644
index 0000000..cf117e5
--- /dev/null
+++ b/_data/podrollStatus.js
@@ -0,0 +1,34 @@
+/**
+ * Podroll Status Data
+ * Checks if the podroll API backend is available at build time.
+ * Used for conditional navigation — the podroll page itself loads data client-side.
+ */
+
+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}/podrollapi/api/status`;
+ console.log(`[podrollStatus] Checking API: ${url}`);
+ const data = await EleventyFetch(url, {
+ duration: "15m",
+ type: "json",
+ });
+ console.log("[podrollStatus] API available");
+ return {
+ available: true,
+ source: "indiekit",
+ ...data,
+ };
+ } catch (error) {
+ console.log(
+ `[podrollStatus] API unavailable: ${error.message}`
+ );
+ return {
+ available: false,
+ source: "unavailable",
+ };
+ }
+}
diff --git a/_data/urlAliases.js b/_data/urlAliases.js
index cc016ca..1478a1f 100644
--- a/_data/urlAliases.js
+++ b/_data/urlAliases.js
@@ -4,9 +4,9 @@
* Maps new URLs to their old URLs so webmentions from previous
* URL structures can be displayed on current pages.
*
- * Sources:
- * - redirects.map.rmendes (micro.blog: /YYYY/MM/DD/slug.html → /notes/...)
- * - old-blog-redirects.map.rmendes (Known/WP: /YYYY/slug → /content/...)
+ * Place redirect map files in the parent directory of this theme:
+ * - redirects.map (e.g., micro.blog: /YYYY/MM/DD/slug.html → /notes/...)
+ * - old-blog-redirects.map (e.g., Known/WP: /YYYY/slug → /content/...)
*/
import { readFileSync, existsSync } from "fs";
@@ -94,13 +94,11 @@ function findFile(candidates) {
// Try multiple possible locations for each map type
const microblogMapPath = findFile([
resolve(pkgRoot, "redirects.map"),
- resolve(pkgRoot, "redirects.map.rmendes"),
resolve(__dirname, "../../redirects.map"),
]);
const knownMapPath = findFile([
resolve(pkgRoot, "old-blog-redirects.map"),
- resolve(pkgRoot, "old-blog-redirects.map.rmendes"),
resolve(__dirname, "../../old-blog-redirects.map"),
]);
diff --git a/_includes/components/empty-collection.njk b/_includes/components/empty-collection.njk
new file mode 100644
index 0000000..1e3000e
--- /dev/null
+++ b/_includes/components/empty-collection.njk
@@ -0,0 +1,27 @@
+{# Empty collection placeholder — encourages creating content #}
+{# Usage: {% include "components/empty-collection.njk" %} with postType set before include #}
+{% set typeInfo = null %}
+{% for pt in enabledPostTypes %}
+ {% if pt.type == postType %}{% set typeInfo = pt %}{% endif %}
+{% endfor %}
+
+
+
+
No {{ title | lower }} yet
+
+ This is where your {{ title | lower }} will appear once you start creating content.
+
+ {% if typeInfo %}
+
+
+
+
+ Create your first {{ postType }}
+
+ {% endif %}
+
diff --git a/_includes/components/sidebar.njk b/_includes/components/sidebar.njk
index 6d9d57d..0c503ea 100644
--- a/_includes/components/sidebar.njk
+++ b/_includes/components/sidebar.njk
@@ -1,300 +1,26 @@
-{# Sidebar Components #}
-{# Contains: Author card (via h-card component), Bluesky feed, GitHub repos, RSS feed #}
+{# Sidebar — composed from individual widget partials #}
+{# Each widget handles its own conditional display internally, #}
+{# except API-only widgets which need a data-source guard here. #}
-{# Author Card Widget - includes the canonical h-card component #}
-
- {% include "components/h-card.njk" %}
-
+{# Author Card (h-card) — always shown #}
+{% include "components/widgets/author-card.njk" %}
-{# Social Feed Widget - Tabbed Bluesky/Mastodon #}
-{% if (blueskyFeed and blueskyFeed.length) or (mastodonFeed and mastodonFeed.length) %}
-
+{# Recent Posts (for non-blog pages) #}
+{% include "components/widgets/recent-posts.njk" %}
+
+{# Blogroll — only when backend is available #}
+{% if blogrollStatus and blogrollStatus.source == "indiekit" %}
+{% include "components/widgets/blogroll.njk" %}
{% endif %}
-{# GitHub Repos Widget #}
-{% if githubRepos and githubRepos.length %}
-
-{% endif %}
-
-{# Funkwhale Now Playing Widget #}
-{% if funkwhaleActivity and (funkwhaleActivity.nowPlaying or funkwhaleActivity.stats) %}
-
-{% endif %}
-
-{# Recent Posts Widget (for non-blog pages) #}
-{% if recentPosts and recentPosts.length %}
-
-{% endif %}
-
-{# Blogroll Widget - Dynamic loading from API #}
-
-
-
-
-{# Categories/Tags Widget #}
-{% if categories and categories.length %}
-
-{% endif %}
+{# Categories/Tags #}
+{% include "components/widgets/categories.njk" %}
diff --git a/_includes/components/widgets/author-card.njk b/_includes/components/widgets/author-card.njk
new file mode 100644
index 0000000..50a5048
--- /dev/null
+++ b/_includes/components/widgets/author-card.njk
@@ -0,0 +1,4 @@
+{# Author Card Widget - includes the canonical h-card component #}
+
+ {% include "components/h-card.njk" %}
+
diff --git a/_includes/components/widgets/blogroll.njk b/_includes/components/widgets/blogroll.njk
new file mode 100644
index 0000000..5e8222d
--- /dev/null
+++ b/_includes/components/widgets/blogroll.njk
@@ -0,0 +1,56 @@
+{# Blogroll Widget - Dynamic loading from API #}
+
+
+
diff --git a/_includes/components/widgets/categories.njk b/_includes/components/widgets/categories.njk
new file mode 100644
index 0000000..74ef98b
--- /dev/null
+++ b/_includes/components/widgets/categories.njk
@@ -0,0 +1,13 @@
+{# Categories/Tags Widget #}
+{% if categories and categories.length %}
+
+{% endif %}
diff --git a/_includes/components/widgets/funkwhale.njk b/_includes/components/widgets/funkwhale.njk
new file mode 100644
index 0000000..b513be5
--- /dev/null
+++ b/_includes/components/widgets/funkwhale.njk
@@ -0,0 +1,71 @@
+{# Funkwhale Now Playing Widget #}
+{% if funkwhaleActivity and (funkwhaleActivity.nowPlaying or funkwhaleActivity.stats) %}
+
+{% endif %}
diff --git a/_includes/components/widgets/github-repos.njk b/_includes/components/widgets/github-repos.njk
new file mode 100644
index 0000000..2907324
--- /dev/null
+++ b/_includes/components/widgets/github-repos.njk
@@ -0,0 +1,32 @@
+{# GitHub Repos Widget #}
+{% if githubRepos and githubRepos.length %}
+
+{% endif %}
diff --git a/_includes/components/widgets/recent-posts.njk b/_includes/components/widgets/recent-posts.njk
new file mode 100644
index 0000000..88bef43
--- /dev/null
+++ b/_includes/components/widgets/recent-posts.njk
@@ -0,0 +1,21 @@
+{# Recent Posts Widget (for non-blog pages) #}
+{% if recentPosts and recentPosts.length %}
+
+{% endif %}
diff --git a/_includes/components/widgets/social-activity.njk b/_includes/components/widgets/social-activity.njk
new file mode 100644
index 0000000..9439509
--- /dev/null
+++ b/_includes/components/widgets/social-activity.njk
@@ -0,0 +1,94 @@
+{# Social Feed Widget - Tabbed Bluesky/Mastodon #}
+{% if (blueskyFeed and blueskyFeed.length) or (mastodonFeed and mastodonFeed.length) %}
+
+{% endif %}
diff --git a/_includes/layouts/base.njk b/_includes/layouts/base.njk
index ff750ea..a5f4c73 100644
--- a/_includes/layouts/base.njk
+++ b/_includes/layouts/base.njk
@@ -114,7 +114,9 @@
Home
About
+ {% if collections.pages | selectattr("url", "equalto", "/now/") | list | length %}
Now
+ {% endif %}
{# Slash pages dropdown - all root pages in one menu #}
@@ -128,13 +130,24 @@
{% for item in collections.pages %}
/{{ item.fileSlug }}
{% endfor %}
+ {# Plugin pages — only show when their data source is configured #}
+ {% set hasPluginPages = (funkwhaleActivity and funkwhaleActivity.source == "indiekit") or
+ (githubActivity and githubActivity.source != "error") or
+ (lastfmActivity and lastfmActivity.source == "indiekit") or
+ (newsActivity and newsActivity.source == "indiekit") or
+ (youtubeChannel and youtubeChannel.source == "indiekit") or
+ (blogrollStatus and blogrollStatus.source == "indiekit") or
+ (podrollStatus and podrollStatus.source == "indiekit") %}
+ {% if hasPluginPages %}
-
/funkwhale
-
/github
-
/listening
-
/news
-
/podroll
-
/youtube
+ {% if blogrollStatus and blogrollStatus.source == "indiekit" %}
/blogroll {% endif %}
+ {% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}
/funkwhale {% endif %}
+ {% if githubActivity and githubActivity.source != "error" %}
/github {% endif %}
+ {% if lastfmActivity and lastfmActivity.source == "indiekit" %}
/listening {% endif %}
+ {% if newsActivity and newsActivity.source == "indiekit" %}
/news {% endif %}
+ {% if podrollStatus and podrollStatus.source == "indiekit" %}
/podroll {% endif %}
+ {% if youtubeChannel and youtubeChannel.source == "indiekit" %}
/youtube {% endif %}
+ {% endif %}
{# Blog dropdown #}
@@ -147,13 +160,9 @@
Interactions
@@ -191,7 +200,9 @@
Home
About
+ {% if collections.pages | selectattr("url", "equalto", "/now/") | list | length %}
Now
+ {% endif %}
{# Slash pages section - all root pages in one menu #}
@@ -205,13 +216,17 @@
{% for item in collections.pages %}
/{{ item.fileSlug }}
{% endfor %}
+ {# Plugin pages — only show when configured #}
+ {% if hasPluginPages %}
- /funkwhale
- /github
- /listening
- /news
- /podroll
- /youtube
+ {% if blogrollStatus and blogrollStatus.source == "indiekit" %}/blogroll {% endif %}
+ {% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}/funkwhale {% endif %}
+ {% if githubActivity and githubActivity.source != "error" %}/github {% endif %}
+ {% if lastfmActivity and lastfmActivity.source == "indiekit" %}/listening {% endif %}
+ {% if newsActivity and newsActivity.source == "indiekit" %}/news {% endif %}
+ {% if podrollStatus and podrollStatus.source == "indiekit" %}/podroll {% endif %}
+ {% if youtubeChannel and youtubeChannel.source == "indiekit" %}/youtube {% endif %}
+ {% endif %}
{# Blog section #}
@@ -224,13 +239,9 @@
Interactions
diff --git a/_includes/layouts/home.njk b/_includes/layouts/home.njk
index 73534c3..e0237e7 100644
--- a/_includes/layouts/home.njk
+++ b/_includes/layouts/home.njk
@@ -22,13 +22,17 @@ withSidebar: true
{{ site.author.title }}
+ {% if site.author.bio %}
- Hi, I geek around tech, information systems, democracy, justice, coercive groups (aka cults), and discernment.
+ {{ site.author.bio }}
+ {% endif %}
+ {% if site.description %}
- My blog serves as a repository for my thoughts, long-form writings (some still in draft), and a place where I bookmark interesting finds from the web. It's also my central hub for cross-posting to networks like Mastodon, Bluesky.
+ {{ site.description }}
Read more →
+ {% endif %}
{# Social Links #}
@@ -56,6 +60,25 @@ withSidebar: true
+{# Homepage content — three-tier fallback: #}
+{# 1. Plugin config (homepageConfig) — Phase 3, future #}
+{# 2. CV data — show experience/projects/skills #}
+{# 3. Default — show recent posts and activity #}
+
+{% set hasCvData = (cv.experience and cv.experience.length) or
+ (cv.projects and cv.projects.length) or
+ (cv.skills and (cv.skills | dictsort | length)) %}
+
+{# --- Tier 1: Plugin-driven layout (future) --- #}
+{% if homepageConfig and homepageConfig.sections %}
+{# Reserved for indiekit-endpoint-homepage plugin — will render configured sections here #}
+
+ Homepage plugin layout will render here.
+
+
+{# --- Tier 2: CV-based layout --- #}
+{% elif hasCvData %}
+
{# Work Experience Timeline - only show if data exists #}
{% if cv.experience and cv.experience.length %}
@@ -210,3 +233,83 @@ withSidebar: true
Last updated: {{ cv.lastUpdated }}
{% endif %}
+
+{# --- Tier 3: Default — recent activity when no CV and no plugin --- #}
+{% else %}
+
+{# Recent Posts #}
+{% if collections.posts and collections.posts.length %}
+
+ Recent Posts
+
+ {% for post in collections.posts | head(10) %}
+
+
+ {% if post.data.summary %}
+ {{ post.data.summary }}
+ {% endif %}
+
+
+ {{ (post.data.published or post.date) | date("MMM d, yyyy") }}
+
+ {% if post.data.postType %}
+ {{ post.data.postType }}
+ {% endif %}
+
+
+ {% endfor %}
+
+
+ View all posts
+
+
+
+{% endif %}
+
+{# Getting Started — onboarding guide for new deployments #}
+
+ Getting Started
+
+
+
+
1
+
+
Create your first post
+
+ Sign in ,
+ then visit /create
+ to publish notes, articles, bookmarks, and photos.
+
+
+
+
+
+
2
+
+
Set up syndication
+
+ Cross-post to Mastodon, Bluesky, and LinkedIn automatically.
+ Add your credentials to the .env file and restart.
+
+
+
+
+
+
3
+
+
Enable interactions
+
+ Receive likes, replies, and reposts from across the web.
+ Register at webmention.io
+ and add the token to .env as WEBMENTION_IO_TOKEN.
+
+
+
+
+
+
+{% endif %} {# end three-tier fallback #}
diff --git a/articles.njk b/articles.njk
index b72f116..50b7d64 100644
--- a/articles.njk
+++ b/articles.njk
@@ -6,6 +6,7 @@ pagination:
data: collections.articles
size: 20
alias: paginatedArticles
+ generatePageOnEmptyData: true
permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
@@ -87,6 +88,7 @@ permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{% endif %}
{% else %}
-
No articles yet.
+ {% set postType = "article" %}
+ {% include "components/empty-collection.njk" %}
{% endif %}
diff --git a/bookmarks.njk b/bookmarks.njk
index 41c888e..3ed42a1 100644
--- a/bookmarks.njk
+++ b/bookmarks.njk
@@ -6,6 +6,7 @@ pagination:
data: collections.bookmarks
size: 20
alias: paginatedBookmarks
+ generatePageOnEmptyData: true
permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
@@ -100,6 +101,7 @@ permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageN
{% endif %}
{% else %}
-
No bookmarks yet.
+ {% set postType = "bookmark" %}
+ {% include "components/empty-collection.njk" %}
{% endif %}
diff --git a/feed-json.njk b/feed-json.njk
index 43357c8..d45ed46 100644
--- a/feed-json.njk
+++ b/feed-json.njk
@@ -17,7 +17,7 @@ eleventyExcludeFromCollections: true
"language": "{{ site.locale | default('en') }}",
"authors": [
{
- "name": "{{ site.author | default('Ricardo Mendes') }}",
+ "name": "{{ site.author.name | default(site.name) }}",
"url": "{{ site.url }}/"
}
],
diff --git a/likes.njk b/likes.njk
index 1c7f299..7e25572 100644
--- a/likes.njk
+++ b/likes.njk
@@ -6,6 +6,7 @@ pagination:
data: collections.likes
size: 20
alias: paginatedLikes
+ generatePageOnEmptyData: true
permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
@@ -98,6 +99,7 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% endif %}
{% else %}
-
No likes yet.
+ {% set postType = "like" %}
+ {% include "components/empty-collection.njk" %}
{% endif %}
diff --git a/notes.njk b/notes.njk
index 65cb51d..0723f0d 100644
--- a/notes.njk
+++ b/notes.njk
@@ -6,6 +6,7 @@ pagination:
data: collections.notes
size: 20
alias: paginatedNotes
+ generatePageOnEmptyData: true
permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
@@ -84,6 +85,7 @@ permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% endif %}
{% else %}
-
No notes yet.
+ {% set postType = "note" %}
+ {% include "components/empty-collection.njk" %}
{% endif %}
diff --git a/package.json b/package.json
index 415b758..39ea7f8 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "rmendes-eleventy-site",
+ "name": "indiekit-eleventy-theme",
"version": "1.0.0",
- "description": "Personal website powered by Indiekit and Eleventy",
+ "description": "Eleventy theme for Indiekit — IndieWeb-ready personal website",
"type": "module",
"scripts": {
"build": "eleventy",
diff --git a/photos.njk b/photos.njk
index 0f99923..9c9f53b 100644
--- a/photos.njk
+++ b/photos.njk
@@ -6,6 +6,7 @@ pagination:
data: collections.photos
size: 20
alias: paginatedPhotos
+ generatePageOnEmptyData: true
permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
@@ -92,6 +93,7 @@ permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
{% endif %}
{% else %}
-
No photos yet.
+ {% set postType = "photo" %}
+ {% include "components/empty-collection.njk" %}
{% endif %}
diff --git a/replies.njk b/replies.njk
index e1950ea..8eab057 100644
--- a/replies.njk
+++ b/replies.njk
@@ -6,6 +6,7 @@ pagination:
data: collections.replies
size: 20
alias: paginatedReplies
+ generatePageOnEmptyData: true
permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
@@ -102,6 +103,7 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% endif %}
{% else %}
-
No replies yet.
+ {% set postType = "reply" %}
+ {% include "components/empty-collection.njk" %}
{% endif %}
diff --git a/reposts.njk b/reposts.njk
index a16a4d2..7162d86 100644
--- a/reposts.njk
+++ b/reposts.njk
@@ -6,6 +6,7 @@ pagination:
data: collections.reposts
size: 20
alias: paginatedReposts
+ generatePageOnEmptyData: true
permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
@@ -104,6 +105,7 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% endif %}
{% else %}
-
No reposts yet.
+ {% set postType = "repost" %}
+ {% include "components/empty-collection.njk" %}
{% endif %}
diff --git a/slashes.njk b/slashes.njk
index df4444c..d57edf1 100644
--- a/slashes.njk
+++ b/slashes.njk
@@ -11,9 +11,9 @@ permalink: /slashes/
{# Dynamic pages (created via Indiekit) #}
- {% if collections.pages.length > 0 %}
Pages
+ {% if collections.pages.length > 0 %}
{% for page in collections.pages %}
@@ -37,21 +37,65 @@ permalink: /slashes/
{% endfor %}
+ {% else %}
+
+
+ No root pages yet. To create pages like /now, /uses, or /colophon, you need two plugins:
+
+
+ @rmdes/indiekit-post-type-page — registers the "page" post type with Indiekit, using root-level URL paths (/slug instead of /type/YYYY/MM/DD/slug)
+ @rmdes/indiekit-endpoint-posts — publishing UI that sends the h=page Micropub type so pages are created at root level
+
+
+ Once both plugins are installed, "Page" appears as a post type in the Indiekit admin UI, and pages are published directly at /slug.
+
+
+ {% endif %}
- {% endif %}
- {# Activity pages (from Indiekit plugins) #}
+ {# Activity pages — only show when their plugin backend is available #}
+ {% set hasActivityPages = (funkwhaleActivity and funkwhaleActivity.source == "indiekit") or
+ (githubActivity and githubActivity.source != "error") or
+ (lastfmActivity and lastfmActivity.source == "indiekit") or
+ (newsActivity and newsActivity.source == "indiekit") or
+ (youtubeChannel and youtubeChannel.source == "indiekit") or
+ (blogrollStatus and blogrollStatus.source == "indiekit") or
+ (podrollStatus and podrollStatus.source == "indiekit") %}
+ {% if hasActivityPages %}
Activity Feeds
+ {% if blogrollStatus and blogrollStatus.source == "indiekit" %}
+
+
+ Sites I follow
+
+ {% endif %}
+ {% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}
+
+
+ Funkwhale activity
+
+ {% endif %}
+ {% if githubActivity and githubActivity.source != "error" %}
- My GitHub activity
+ GitHub activity
+ {% endif %}
+ {% if lastfmActivity and lastfmActivity.source == "indiekit" %}
Last.fm scrobbles
-
-
- My Funkwhale activity
-
-
-
- My YouTube channel
-
+ {% endif %}
+ {% if newsActivity and newsActivity.source == "indiekit" %}
RSS feed aggregator
+ {% endif %}
+ {% if podrollStatus and podrollStatus.source == "indiekit" %}
+
+
+ Podcasts I listen to
+
+ {% endif %}
+ {% if youtubeChannel and youtubeChannel.source == "indiekit" %}
+
+
+ YouTube channel
+
+ {% endif %}
+ {% endif %}
{# Inspiration section #}