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) %} -
-

Social Activity

+{# Social Activity — Bluesky/Mastodon feeds #} +{% include "components/widgets/social-activity.njk" %} - {# Tab buttons #} -
- {% if blueskyFeed and blueskyFeed.length %} - - {% endif %} - {% if mastodonFeed and mastodonFeed.length %} - - {% endif %} -
+{# GitHub Repos #} +{% include "components/widgets/github-repos.njk" %} - {# Bluesky Tab Content #} - {% if blueskyFeed and blueskyFeed.length %} -
- - - View on Bluesky - - -
- {% endif %} +{# Funkwhale — Now Playing / Listening Stats #} +{% include "components/widgets/funkwhale.njk" %} - {# Mastodon Tab Content #} - {% if mastodonFeed and mastodonFeed.length %} -
- - - View on Mastodon - - -
- {% endif %} -
+{# 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 %} -
-

- - GitHub Projects -

- - - View all repositories - -
-{% endif %} - -{# Funkwhale Now Playing Widget #} -{% if funkwhaleActivity and (funkwhaleActivity.nowPlaying or funkwhaleActivity.stats) %} -
-

- - - - Listening -

- - {# Now Playing / Recently Played #} - {% if funkwhaleActivity.nowPlaying and funkwhaleActivity.nowPlaying.track %} -
- {% if funkwhaleActivity.nowPlaying.status == 'now-playing' %} -
- - - - - - Now Playing -
- {% elif funkwhaleActivity.nowPlaying.status == 'recently-played' %} -
Recently Played
- {% endif %} - -
- {% if funkwhaleActivity.nowPlaying.coverUrl %} - - {% endif %} -
-

- {% if funkwhaleActivity.nowPlaying.trackUrl %} - - {{ funkwhaleActivity.nowPlaying.track }} - - {% else %} - {{ funkwhaleActivity.nowPlaying.track }} - {% endif %} -

-

{{ funkwhaleActivity.nowPlaying.artist }}

-
-
-
- {% endif %} - - {# Quick Stats #} - {% if funkwhaleActivity.stats and funkwhaleActivity.stats.summary %} - {% set stats = funkwhaleActivity.stats.summary.all %} -
-
- {{ stats.totalPlays or 0 }} - plays -
-
- {{ stats.uniqueArtists or 0 }} - artists -
-
- {{ stats.totalDurationFormatted or '0m' }} - listened -
-
- {% endif %} - - - View full listening history - - -
-{% endif %} - -{# Recent Posts Widget (for non-blog pages) #} -{% if recentPosts and recentPosts.length %} -
-

Recent Posts

- - - View all posts - -
-{% endif %} - -{# Blogroll Widget - Dynamic loading from API #} -
-

- - - - Blogroll -

- - - -
- No blogs loaded yet. -
- - - View all blogs - - -
- - - -{# Categories/Tags Widget #} -{% if categories and categories.length %} -
-

Categories

-
- {% for category in categories %} - - {{ category }} - - {% endfor %} -
-
-{% 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 #} +
+

+ + + + Blogroll +

+ + + +
+ No blogs loaded yet. +
+ + + View all blogs + + +
+ + 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 %} +
+

Categories

+
+ {% for category in categories %} + + {{ category }} + + {% endfor %} +
+
+{% 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) %} +
+

+ + + + Listening +

+ + {# Now Playing / Recently Played #} + {% if funkwhaleActivity.nowPlaying and funkwhaleActivity.nowPlaying.track %} +
+ {% if funkwhaleActivity.nowPlaying.status == 'now-playing' %} +
+ + + + + + Now Playing +
+ {% elif funkwhaleActivity.nowPlaying.status == 'recently-played' %} +
Recently Played
+ {% endif %} + +
+ {% if funkwhaleActivity.nowPlaying.coverUrl %} + + {% endif %} +
+

+ {% if funkwhaleActivity.nowPlaying.trackUrl %} + + {{ funkwhaleActivity.nowPlaying.track }} + + {% else %} + {{ funkwhaleActivity.nowPlaying.track }} + {% endif %} +

+

{{ funkwhaleActivity.nowPlaying.artist }}

+
+
+
+ {% endif %} + + {# Quick Stats #} + {% if funkwhaleActivity.stats and funkwhaleActivity.stats.summary %} + {% set stats = funkwhaleActivity.stats.summary.all %} +
+
+ {{ stats.totalPlays or 0 }} + plays +
+
+ {{ stats.uniqueArtists or 0 }} + artists +
+
+ {{ stats.totalDurationFormatted or '0m' }} + listened +
+
+ {% endif %} + + + View full listening history + + +
+{% 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 %} +
+

+ + GitHub Projects +

+ + + View all repositories + +
+{% 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 %} +
+

Recent Posts

+ + + View all posts + +
+{% 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) %} +
+

Social Activity

+ + {# Tab buttons #} +
+ {% if blueskyFeed and blueskyFeed.length %} + + {% endif %} + {% if mastodonFeed and mastodonFeed.length %} + + {% endif %} +
+ + {# Bluesky Tab Content #} + {% if blueskyFeed and blueskyFeed.length %} +
+ + + View on Bluesky + + +
+ {% endif %} + + {# Mastodon Tab Content #} + {% if mastodonFeed and mastodonFeed.length %} +
+ + + View on Mastodon + + +
+ {% endif %} +
+{% 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 @@