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

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 -

-
    - {% for repo in githubRepos | head(5) %} -
  • - - {{ repo.name }} - - {% if repo.description %} -

    {{ repo.description | truncate(80) }}

    - {% endif %} -
    - {% if repo.language %} - {{ repo.language }} - {% endif %} - {{ repo.stargazers_count }} stars -
    -
  • - {% endfor %} -
- - 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 +

+
    + {% for repo in githubRepos | head(5) %} +
  • + + {{ repo.name }} + + {% if repo.description %} +

    {{ repo.description | truncate(80) }}

    + {% endif %} +
    + {% if repo.language %} + {{ repo.language }} + {% endif %} + {{ repo.stargazers_count }} stars +
    +
  • + {% endfor %} +
+ + 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 @@