From f4c02fdc936d09bf542eb67e0397d4bfbc12611d Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:25:02 +0100 Subject: [PATCH] chore: remove theme/ upstream sync directory theme/ was an upstream reference copy, explicitly ignored by Eleventy (eleventy.config.js ignores.add("theme")). Not used in the build. Co-Authored-By: Claude Sonnet 4.6 --- theme/_data/blogrollStatus.js | 34 - theme/_data/blueskyFeed.js | 74 - theme/_data/conversationMentions.js | 14 - theme/_data/cv.js | 84 - theme/_data/cvPageConfig.js | 80 - theme/_data/cvPageConfigStatic.js | 6 - theme/_data/cvStatic.js | 250 - theme/_data/eleventyComputed.js | 71 - theme/_data/enabledPostTypes.js | 50 - theme/_data/funkwhaleActivity.js | 139 - theme/_data/githubActivity.js | 291 - theme/_data/githubRepos.js | 48 - theme/_data/githubStarred.js | 44 - theme/_data/homepageConfig.js | 29 - theme/_data/lastfmActivity.js | 99 - theme/_data/mastodonFeed.js | 111 - theme/_data/newsActivity.js | 99 - theme/_data/podrollStatus.js | 34 - theme/_data/recentComments.js | 24 - theme/_data/site.js | 139 - theme/_data/urlAliases.js | 155 - theme/_data/whereCheckins.js | 455 -- theme/_data/youtubeChannel.js | 206 - theme/_includes/components/blog-sidebar.njk | 315 - theme/_includes/components/comments.njk | 109 - theme/_includes/components/cv-builder.njk | 169 - theme/_includes/components/cv-footer.njk | 26 - theme/_includes/components/cv-sidebar.njk | 45 - .../_includes/components/empty-collection.njk | 27 - .../_includes/components/fediverse-modal.njk | 81 - .../components/funkwhale-stats-content.njk | 66 - theme/_includes/components/h-card.njk | 115 - .../_includes/components/homepage-builder.njk | 86 - .../_includes/components/homepage-footer.njk | 26 - .../_includes/components/homepage-section.njk | 56 - .../_includes/components/homepage-sidebar.njk | 147 - theme/_includes/components/icon.njk | 67 - .../_includes/components/post-navigation.njk | 103 - theme/_includes/components/reply-context.njk | 74 - .../components/sections/custom-html.njk | 18 - .../sections/cv-education-personal.njk | 2 - .../components/sections/cv-education-work.njk | 2 - .../components/sections/cv-education.njk | 88 - .../sections/cv-experience-personal.njk | 2 - .../sections/cv-experience-work.njk | 2 - .../components/sections/cv-experience.njk | 48 - .../sections/cv-interests-personal.njk | 2 - .../components/sections/cv-interests-work.njk | 2 - .../components/sections/cv-interests.njk | 50 - .../components/sections/cv-languages.njk | 21 - .../sections/cv-projects-personal.njk | 124 - .../components/sections/cv-projects-work.njk | 124 - .../components/sections/cv-projects.njk | 114 - .../sections/cv-skills-personal.njk | 2 - .../components/sections/cv-skills-work.njk | 2 - .../components/sections/cv-skills.njk | 50 - .../components/sections/featured-posts.njk | 259 - theme/_includes/components/sections/hero.njk | 68 - .../components/sections/posting-activity.njk | 24 - .../components/sections/recent-posts.njk | 338 - theme/_includes/components/sidebar.njk | 261 - theme/_includes/components/social-icon.njk | 131 - theme/_includes/components/webmentions.njk | 206 - .../widgets/author-card-compact.njk | 43 - .../components/widgets/author-card.njk | 6 - .../_includes/components/widgets/blogroll.njk | 110 - .../components/widgets/categories.njk | 15 - .../components/widgets/fediverse-follow.njk | 41 - .../_includes/components/widgets/feedland.njk | 383 - .../components/widgets/funkwhale.njk | 115 - .../components/widgets/github-repos.njk | 316 - .../components/widgets/post-categories.njk | 21 - .../components/widgets/post-navigation.njk | 66 - .../components/widgets/recent-comments.njk | 27 - .../components/widgets/recent-posts-blog.njk | 108 - .../components/widgets/recent-posts.njk | 117 - theme/_includes/components/widgets/search.njk | 10 - theme/_includes/components/widgets/share.njk | 31 - .../components/widgets/social-activity.njk | 121 - .../components/widgets/subscribe.njk | 20 - theme/_includes/components/widgets/toc.njk | 59 - .../components/widgets/webmentions.njk | 168 - theme/_includes/layouts/base.njk | 551 -- theme/_includes/layouts/fullwidth.njk | 25 - theme/_includes/layouts/home.njk | 175 - theme/_includes/layouts/page.njk | 136 - theme/_includes/layouts/post.njk | 263 - theme/_includes/layouts/where.njk | 117 - theme/about.njk | 69 - theme/articles.njk | 100 - theme/blog.njk | 389 - theme/bookmarks.njk | 109 - theme/categories-index.njk | 30 - theme/categories.njk | 83 - theme/category-feed-json.njk | 70 - theme/category-feed.njk | 50 - theme/changelog.njk | 234 - theme/chardonsbleus.njk | 28 - theme/css/critical.css | 57 - theme/css/lite-yt-embed.css | 95 - theme/css/prism-theme.css | 201 - theme/css/tailwind.css | 986 --- theme/cv.njk | 146 - theme/digest-feed.njk | 31 - theme/digest-index.njk | 83 - theme/digest.njk | 168 - theme/eleventy.config.js | 1327 ---- theme/featured.njk | 186 - theme/feed-json.njk | 67 - theme/feed.njk | 47 - theme/funkwhale.njk | 269 - theme/github.njk | 270 - theme/graph.njk | 18 - theme/images/default-avatar.svg | 5 - theme/images/favicon.svg | 4 - theme/images/og-default.png | Bin 39412 -> 0 bytes theme/images/rick.jpg | Bin 152493 -> 0 bytes theme/index.njk | 8 - theme/interactions.njk | 533 -- theme/interactive/architecture.html | 1162 --- theme/js/admin.js | 67 - theme/js/comments.js | 151 - theme/js/fediverse-interact.js | 144 - theme/js/lightbox.js | 80 - theme/js/save-later.js | 51 - theme/js/share-post.js | 86 - theme/js/time-difference.js | 91 - theme/js/vendor/alpine-collapse.min.js | 1 - theme/js/vendor/alpine.min.js | 5 - theme/js/vendor/lite-yt-embed.js | 225 - theme/js/webmentions.js | 467 -- theme/lib/og-cli.js | 19 - theme/lib/og.js | 344 - theme/lib/unfurl-shortcode.js | 174 - theme/likes.njk | 110 - theme/listening.njk | 508 -- theme/news.njk | 461 -- theme/notes.njk | 97 - theme/package-lock.json | 6452 ----------------- theme/package.json | 46 - theme/podroll.njk | 386 - theme/postcss.config.js | 6 - theme/readlater.njk | 250 - theme/replies.njk | 139 - theme/reposts.njk | 115 - theme/search.njk | 39 - theme/slashes.njk | 162 - theme/starred.njk | 446 -- theme/tailwind.config.js | 100 - theme/webmention-debug.njk | 124 - theme/youtube.njk | 262 - 151 files changed, 27465 deletions(-) delete mode 100644 theme/_data/blogrollStatus.js delete mode 100644 theme/_data/blueskyFeed.js delete mode 100644 theme/_data/conversationMentions.js delete mode 100644 theme/_data/cv.js delete mode 100644 theme/_data/cvPageConfig.js delete mode 100644 theme/_data/cvPageConfigStatic.js delete mode 100644 theme/_data/cvStatic.js delete mode 100644 theme/_data/eleventyComputed.js delete mode 100644 theme/_data/enabledPostTypes.js delete mode 100644 theme/_data/funkwhaleActivity.js delete mode 100644 theme/_data/githubActivity.js delete mode 100644 theme/_data/githubRepos.js delete mode 100644 theme/_data/githubStarred.js delete mode 100644 theme/_data/homepageConfig.js delete mode 100644 theme/_data/lastfmActivity.js delete mode 100644 theme/_data/mastodonFeed.js delete mode 100644 theme/_data/newsActivity.js delete mode 100644 theme/_data/podrollStatus.js delete mode 100644 theme/_data/recentComments.js delete mode 100644 theme/_data/site.js delete mode 100644 theme/_data/urlAliases.js delete mode 100644 theme/_data/whereCheckins.js delete mode 100644 theme/_data/youtubeChannel.js delete mode 100644 theme/_includes/components/blog-sidebar.njk delete mode 100644 theme/_includes/components/comments.njk delete mode 100644 theme/_includes/components/cv-builder.njk delete mode 100644 theme/_includes/components/cv-footer.njk delete mode 100644 theme/_includes/components/cv-sidebar.njk delete mode 100644 theme/_includes/components/empty-collection.njk delete mode 100644 theme/_includes/components/fediverse-modal.njk delete mode 100644 theme/_includes/components/funkwhale-stats-content.njk delete mode 100644 theme/_includes/components/h-card.njk delete mode 100644 theme/_includes/components/homepage-builder.njk delete mode 100644 theme/_includes/components/homepage-footer.njk delete mode 100644 theme/_includes/components/homepage-section.njk delete mode 100644 theme/_includes/components/homepage-sidebar.njk delete mode 100644 theme/_includes/components/icon.njk delete mode 100644 theme/_includes/components/post-navigation.njk delete mode 100644 theme/_includes/components/reply-context.njk delete mode 100644 theme/_includes/components/sections/custom-html.njk delete mode 100644 theme/_includes/components/sections/cv-education-personal.njk delete mode 100644 theme/_includes/components/sections/cv-education-work.njk delete mode 100644 theme/_includes/components/sections/cv-education.njk delete mode 100644 theme/_includes/components/sections/cv-experience-personal.njk delete mode 100644 theme/_includes/components/sections/cv-experience-work.njk delete mode 100644 theme/_includes/components/sections/cv-experience.njk delete mode 100644 theme/_includes/components/sections/cv-interests-personal.njk delete mode 100644 theme/_includes/components/sections/cv-interests-work.njk delete mode 100644 theme/_includes/components/sections/cv-interests.njk delete mode 100644 theme/_includes/components/sections/cv-languages.njk delete mode 100644 theme/_includes/components/sections/cv-projects-personal.njk delete mode 100644 theme/_includes/components/sections/cv-projects-work.njk delete mode 100644 theme/_includes/components/sections/cv-projects.njk delete mode 100644 theme/_includes/components/sections/cv-skills-personal.njk delete mode 100644 theme/_includes/components/sections/cv-skills-work.njk delete mode 100644 theme/_includes/components/sections/cv-skills.njk delete mode 100644 theme/_includes/components/sections/featured-posts.njk delete mode 100644 theme/_includes/components/sections/hero.njk delete mode 100644 theme/_includes/components/sections/posting-activity.njk delete mode 100644 theme/_includes/components/sections/recent-posts.njk delete mode 100644 theme/_includes/components/sidebar.njk delete mode 100644 theme/_includes/components/social-icon.njk delete mode 100644 theme/_includes/components/webmentions.njk delete mode 100644 theme/_includes/components/widgets/author-card-compact.njk delete mode 100644 theme/_includes/components/widgets/author-card.njk delete mode 100644 theme/_includes/components/widgets/blogroll.njk delete mode 100644 theme/_includes/components/widgets/categories.njk delete mode 100644 theme/_includes/components/widgets/fediverse-follow.njk delete mode 100644 theme/_includes/components/widgets/feedland.njk delete mode 100644 theme/_includes/components/widgets/funkwhale.njk delete mode 100644 theme/_includes/components/widgets/github-repos.njk delete mode 100644 theme/_includes/components/widgets/post-categories.njk delete mode 100644 theme/_includes/components/widgets/post-navigation.njk delete mode 100644 theme/_includes/components/widgets/recent-comments.njk delete mode 100644 theme/_includes/components/widgets/recent-posts-blog.njk delete mode 100644 theme/_includes/components/widgets/recent-posts.njk delete mode 100644 theme/_includes/components/widgets/search.njk delete mode 100644 theme/_includes/components/widgets/share.njk delete mode 100644 theme/_includes/components/widgets/social-activity.njk delete mode 100644 theme/_includes/components/widgets/subscribe.njk delete mode 100644 theme/_includes/components/widgets/toc.njk delete mode 100644 theme/_includes/components/widgets/webmentions.njk delete mode 100644 theme/_includes/layouts/base.njk delete mode 100644 theme/_includes/layouts/fullwidth.njk delete mode 100644 theme/_includes/layouts/home.njk delete mode 100644 theme/_includes/layouts/page.njk delete mode 100644 theme/_includes/layouts/post.njk delete mode 100644 theme/_includes/layouts/where.njk delete mode 100644 theme/about.njk delete mode 100644 theme/articles.njk delete mode 100644 theme/blog.njk delete mode 100644 theme/bookmarks.njk delete mode 100644 theme/categories-index.njk delete mode 100644 theme/categories.njk delete mode 100644 theme/category-feed-json.njk delete mode 100644 theme/category-feed.njk delete mode 100644 theme/changelog.njk delete mode 100644 theme/chardonsbleus.njk delete mode 100644 theme/css/critical.css delete mode 100644 theme/css/lite-yt-embed.css delete mode 100644 theme/css/prism-theme.css delete mode 100644 theme/css/tailwind.css delete mode 100644 theme/cv.njk delete mode 100644 theme/digest-feed.njk delete mode 100644 theme/digest-index.njk delete mode 100644 theme/digest.njk delete mode 100644 theme/eleventy.config.js delete mode 100644 theme/featured.njk delete mode 100644 theme/feed-json.njk delete mode 100644 theme/feed.njk delete mode 100644 theme/funkwhale.njk delete mode 100644 theme/github.njk delete mode 100644 theme/graph.njk delete mode 100644 theme/images/default-avatar.svg delete mode 100644 theme/images/favicon.svg delete mode 100644 theme/images/og-default.png delete mode 100644 theme/images/rick.jpg delete mode 100644 theme/index.njk delete mode 100644 theme/interactions.njk delete mode 100644 theme/interactive/architecture.html delete mode 100644 theme/js/admin.js delete mode 100644 theme/js/comments.js delete mode 100644 theme/js/fediverse-interact.js delete mode 100644 theme/js/lightbox.js delete mode 100644 theme/js/save-later.js delete mode 100644 theme/js/share-post.js delete mode 100644 theme/js/time-difference.js delete mode 100644 theme/js/vendor/alpine-collapse.min.js delete mode 100644 theme/js/vendor/alpine.min.js delete mode 100644 theme/js/vendor/lite-yt-embed.js delete mode 100644 theme/js/webmentions.js delete mode 100644 theme/lib/og-cli.js delete mode 100644 theme/lib/og.js delete mode 100644 theme/lib/unfurl-shortcode.js delete mode 100644 theme/likes.njk delete mode 100644 theme/listening.njk delete mode 100644 theme/news.njk delete mode 100644 theme/notes.njk delete mode 100644 theme/package-lock.json delete mode 100644 theme/package.json delete mode 100644 theme/podroll.njk delete mode 100644 theme/postcss.config.js delete mode 100644 theme/readlater.njk delete mode 100644 theme/replies.njk delete mode 100644 theme/reposts.njk delete mode 100644 theme/search.njk delete mode 100644 theme/slashes.njk delete mode 100644 theme/starred.njk delete mode 100644 theme/tailwind.config.js delete mode 100644 theme/webmention-debug.njk delete mode 100644 theme/youtube.njk diff --git a/theme/_data/blogrollStatus.js b/theme/_data/blogrollStatus.js deleted file mode 100644 index aee1601..0000000 --- a/theme/_data/blogrollStatus.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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/theme/_data/blueskyFeed.js b/theme/_data/blueskyFeed.js deleted file mode 100644 index 00e604b..0000000 --- a/theme/_data/blueskyFeed.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Bluesky Feed Data - * Fetches recent posts from Bluesky using the AT Protocol API - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -export default async function () { - const rawHandle = (process.env.BLUESKY_HANDLE || "") - .trim() - .replace(/^@+/, ""); - const handle = - rawHandle && !rawHandle.includes(".") && !rawHandle.startsWith("did:") - ? `${rawHandle}.bsky.social` - : rawHandle; - - if (!handle) { - return []; - } - - try { - // Get the author's feed using public API (no auth needed for public posts) - const feedUrl = `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${handle}&limit=10`; - - const response = await EleventyFetch(feedUrl, { - duration: "15m", // Cache for 15 minutes - type: "json", - fetchOptions: { - headers: { - Accept: "application/json", - }, - }, - }); - - if (!response.feed) { - console.log("No Bluesky feed found for handle:", handle); - return []; - } - - // Transform the feed into a simpler format - return response.feed.map((item) => { - // Extract rkey from AT URI (at://did:plc:xxx/app.bsky.feed.post/rkey) - const rkey = item.post.uri.split("/").pop(); - const postUrl = `https://bsky.app/profile/${item.post.author.handle}/post/${rkey}`; - - return { - text: item.post.record.text, - createdAt: item.post.record.createdAt, - uri: item.post.uri, - url: postUrl, - cid: item.post.cid, - author: { - handle: item.post.author.handle, - displayName: item.post.author.displayName, - avatar: item.post.author.avatar, - }, - likeCount: item.post.likeCount || 0, - repostCount: item.post.repostCount || 0, - replyCount: item.post.replyCount || 0, - // Extract any embedded links or images - embed: item.post.embed - ? { - type: item.post.embed.$type, - images: item.post.embed.images || [], - external: item.post.embed.external || null, - } - : null, - }; - }); - } catch (error) { - console.error("Error fetching Bluesky feed:", error.message); - return []; - } -} diff --git a/theme/_data/conversationMentions.js b/theme/_data/conversationMentions.js deleted file mode 100644 index 1652f37..0000000 --- a/theme/_data/conversationMentions.js +++ /dev/null @@ -1,14 +0,0 @@ -import EleventyFetch from "@11ty/eleventy-fetch"; - -export default async function () { - try { - const data = await EleventyFetch( - "http://127.0.0.1:8080/conversations/api/mentions?per-page=10000", - { duration: "15m", type: "json" } - ); - return data.children || []; - } catch (e) { - console.log(`[conversationMentions] API unavailable: ${e.message}`); - return []; - } -} diff --git a/theme/_data/cv.js b/theme/_data/cv.js deleted file mode 100644 index 10f0e8e..0000000 --- a/theme/_data/cv.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * CV Data - * - * API-first for split backend/frontend deployments: - * - Try Indiekit public API (`/cvapi/data.json`, fallback `/cv/data.json`) - * - Fallback to local plugin file (`content/.indiekit/cv.json`) - * - * Returns empty defaults if neither source is available. - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; -import { readFileSync } from "node:fs"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import cvStatic from "./cvStatic.js"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const INDIEKIT_URL = - process.env.INDIEKIT_URL || process.env.SITE_URL || "https://example.com"; - -const EMPTY_CV = { - lastUpdated: null, - experience: [], - projects: [], - skills: {}, - skillTypes: {}, - languages: [], - education: [], - interests: {}, - interestTypes: {}, -}; - -async function fetchFromIndiekit(path) { - const urls = [ - `${INDIEKIT_URL}/cvapi/${path}`, - `${INDIEKIT_URL}/cv/${path}`, - ]; - - for (const url of urls) { - try { - console.log(`[cv] Fetching from Indiekit: ${url}`); - const data = await EleventyFetch(url, { - duration: "15m", - type: "json", - }); - console.log(`[cv] Indiekit ${path} success via ${url}`); - return data; - } catch (error) { - console.log(`[cv] Indiekit API unavailable at ${url}: ${error.message}`); - } - } - - return null; -} - -function readLocalCvFile() { - try { - const cvPath = resolve(__dirname, "..", "content", ".indiekit", "cv.json"); - const raw = readFileSync(cvPath, "utf8"); - const data = JSON.parse(raw); - console.log("[cv] Loaded CV data from local plugin file"); - return data; - } catch { - return null; - } -} - -export default async function () { - const apiData = await fetchFromIndiekit("data.json"); - if (apiData && typeof apiData === "object") { - return { ...EMPTY_CV, ...apiData }; - } - - const localData = readLocalCvFile(); - if (localData && typeof localData === "object") { - return { ...EMPTY_CV, ...localData }; - } - - if (cvStatic && typeof cvStatic === "object") { - return { ...EMPTY_CV, ...cvStatic }; - } - - return EMPTY_CV; -} diff --git a/theme/_data/cvPageConfig.js b/theme/_data/cvPageConfig.js deleted file mode 100644 index 0776008..0000000 --- a/theme/_data/cvPageConfig.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * CV Page Configuration Data - * - * API-first for split backend/frontend deployments: - * - Try Indiekit public API (`/cvapi/page.json`, fallback `/cv/page.json`) - * - Fallback to local plugin file (`content/.indiekit/cv-page.json`) - * - * Falls back to null so cv.njk can use the hardcoded default layout. - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; -import { readFileSync } from "node:fs"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import cvPageConfigStatic from "./cvPageConfigStatic.js"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const INDIEKIT_URL = - process.env.INDIEKIT_URL || process.env.SITE_URL || "https://example.com"; - -async function fetchFromIndiekit(path) { - const urls = [ - `${INDIEKIT_URL}/cvapi/${path}`, - `${INDIEKIT_URL}/cv/${path}`, - ]; - - for (const url of urls) { - try { - console.log(`[cvPageConfig] Fetching from Indiekit: ${url}`); - const data = await EleventyFetch(url, { - duration: "15m", - type: "json", - }); - console.log(`[cvPageConfig] Indiekit ${path} success via ${url}`); - return data; - } catch (error) { - console.log( - `[cvPageConfig] Indiekit API unavailable at ${url}: ${error.message}` - ); - } - } - - return null; -} - -function readLocalConfigFile() { - try { - const configPath = resolve( - __dirname, - "..", - "content", - ".indiekit", - "cv-page.json" - ); - const raw = readFileSync(configPath, "utf8"); - const config = JSON.parse(raw); - console.log("[cvPageConfig] Loaded local CV page builder config"); - return config; - } catch { - return null; - } -} - -export default async function () { - const apiConfig = await fetchFromIndiekit("page.json"); - if (apiConfig && typeof apiConfig === "object") { - return apiConfig; - } - - const localConfig = readLocalConfigFile(); - if (localConfig && typeof localConfig === "object") { - return localConfig; - } - - if (cvPageConfigStatic && typeof cvPageConfigStatic === "object") { - return cvPageConfigStatic; - } - - return null; -} diff --git a/theme/_data/cvPageConfigStatic.js b/theme/_data/cvPageConfigStatic.js deleted file mode 100644 index f40e810..0000000 --- a/theme/_data/cvPageConfigStatic.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Repository-managed CV page config fallback. - * Keep `null` to use the default layout in cv.njk. - */ - -export default null; diff --git a/theme/_data/cvStatic.js b/theme/_data/cvStatic.js deleted file mode 100644 index 9a46e41..0000000 --- a/theme/_data/cvStatic.js +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Repository-managed CV fallback data. - * Edit this file to maintain CV content without the backend CV plugin. - */ - -export default { - lastUpdated: "2026-03-08T12:00:00.000Z", - experience: [ - { - title: "Public Relations Specialist", - company: "Landratsamt Garmisch-Partenkirchen", - location: "Garmisch-Partenkirchen, Germany", - startDate: "2024", - endDate: null, - type: "full-time", - experienceType: "work", - description: - "Spearheaded strategic communication initiatives, improved public outreach, and strengthened stakeholder relationships.", - highlights: [ - "Media campaigns", - "Press releases", - "Stakeholder communication", - "Public image management", - ], - }, - { - title: "Social Worker", - company: "BIB Augsburg", - location: "Garmisch-Partenkirchen, Germany", - startDate: "2021", - endDate: "2024", - type: "full-time", - experienceType: "work", - description: - "Provided social support and counseling with a focus on integration, empowerment, and crisis intervention.", - highlights: [ - "Social integration", - "Crisis intervention", - "Family support", - "Agency collaboration", - ], - }, - { - title: "Founder and Communication Consultant", - company: "Textbureau & Communication Giersig", - location: "Konstanz and Murnau, Germany", - startDate: "2014", - endDate: "2023-12", - type: "self-employed", - experienceType: "work", - description: - "Built and ran a communication agency delivering content, editing, and strategic communication across industries.", - highlights: [ - "Agency leadership", - "Project management", - "Client engagement", - "Content creation", - "Editorial work", - ], - }, - { - title: "Store Manager", - company: "Jack Wolfskin", - location: "Murnau, Germany", - startDate: "2017", - endDate: "2021", - type: "full-time", - experienceType: "work", - description: - "Managed daily store operations, team leadership, and sales strategy with consistent growth and strong customer service outcomes.", - highlights: [ - "Retail operations", - "Team leadership", - "Sales strategy", - "Customer satisfaction", - ], - }, - { - title: "Sales Professional", - company: "Jack Wolfskin and Vaude", - location: "Konstanz, Germany", - startDate: "2012", - endDate: "2017", - type: "full-time", - experienceType: "work", - description: - "Built strong customer relationships and product expertise in outdoor retail while improving sales performance.", - highlights: [ - "Customer service", - "Product consulting", - "Sales performance", - "Brand representation", - ], - }, - { - title: "Research Assistant", - company: "University of Constance", - location: "Konstanz, Germany", - startDate: "2007", - endDate: "2012", - type: "part-time", - experienceType: "work", - description: - "Supported sociological research projects, contributed to publications, and tutored students in qualitative and quantitative methods.", - highlights: [ - "Research design", - "Data analysis", - "Academic publications", - "Methods tutoring", - ], - }, - { - title: "UNIX Consultant", - company: "Netzwerk2000 and Emprise Network Consulting", - location: "Waiblingen and Stuttgart, Germany", - startDate: "1998", - endDate: "2002", - type: "full-time", - experienceType: "work", - description: - "Delivered UNIX consulting in networked environments, covering system administration, troubleshooting, and technical support.", - highlights: [ - "UNIX system administration", - "Troubleshooting", - "Technical support", - "Large network environments", - ], - }, - ], - projects: [ - { - name: "Communication Agency Development", - url: "", - description: - "Built and scaled an independent communication agency and delivered tailored messaging for clients across industries.", - technologies: [ - "Project management", - "Client communication", - "Messaging frameworks", - "Editorial production", - ], - status: "completed", - projectType: "work", - startDate: "2014", - endDate: "2023-12", - }, - { - name: "Public Outreach and Media Work", - url: "", - description: - "Designed and executed public communication initiatives for social topics in district administration.", - technologies: [ - "Public relations", - "Press writing", - "Media coordination", - "Stakeholder relations", - ], - status: "active", - projectType: "work", - startDate: "2024", - endDate: null, - }, - { - name: "Social Integration Support Programs", - url: "", - description: - "Worked with families, young people, and refugees through social support and integration-focused services.", - technologies: [ - "Counseling", - "Case coordination", - "Community partnerships", - "Intercultural communication", - ], - status: "completed", - projectType: "work", - startDate: "2021", - endDate: "2024", - }, - ], - skills: { - Communication: [ - "Strategic communication", - "Public relations", - "Press releases", - "Media campaigns", - "Content creation", - ], - Research: [ - "Ethnographic perspective", - "Qualitative methods", - "Quantitative methods", - "Sociological analysis", - ], - "Social Work": [ - "Social integration", - "Crisis intervention", - "Counseling", - "Community collaboration", - ], - Operations: ["Project management", "Team leadership", "Adaptability", "Client engagement"], - }, - skillTypes: { - Communication: "work", - Research: "work", - "Social Work": "work", - Operations: "work", - }, - languages: [ - { name: "German", level: "native" }, - { name: "English", level: "fluent" }, - ], - education: [ - { - degree: "B.A. Sociology (Minor Subjects: Politics and Administration, Gender Studies)", - institution: "University of Constance", - location: "Constance, Germany", - startDate: "2007", - endDate: "2018", - educationType: "work", - description: "Academic training in sociology with a strong methodological and social-theory foundation.", - }, - { - degree: "A Level / Abitur", - institution: "Kolping-Kolleg Stuttgart", - location: "Stuttgart, Germany", - startDate: "2003", - endDate: "2007", - educationType: "work", - description: "General university entrance qualification.", - }, - ], - interests: { - "Professional Focus": [ - "Social issues", - "Community communication", - "Ethnographic inquiry", - "Grassroots movements", - ], - Personal: [ - "Long-distance hiking in the Alps", - "Designing and sewing ultralight hiking gear", - "Running blogs", - "Minimal techno", - ], - }, - interestTypes: { - "Professional Focus": "work", - Personal: "work", - }, -}; diff --git a/theme/_data/eleventyComputed.js b/theme/_data/eleventyComputed.js deleted file mode 100644 index 7ef08a2..0000000 --- a/theme/_data/eleventyComputed.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Computed data resolved during the data cascade. - * - * Eleventy 3.x parallel rendering causes `page.url`, `page.fileSlug`, - * and `page.inputPath` to return values from OTHER pages being processed - * concurrently. This affects both templates and eleventyComputed functions. - * - * IMPORTANT: Only `permalink` is computed here, because it reads from the - * file's own frontmatter data (per-file, immune to race conditions). - * OG image lookups are done in templates using the `permalink` data value - * and Nunjucks filters (see base.njk). - * - * NEVER use `page.url`, `page.fileSlug`, or `page.inputPath` here. - * - * See: https://github.com/11ty/eleventy/issues/3183 - */ - -const normalizeOutputPermalink = (permalink) => { - if (typeof permalink !== "string") return permalink; - - // Convert accidental absolute URLs to Eleventy output paths. - if (/^https?:\/\//i.test(permalink)) { - try { - const { pathname } = new URL(permalink); - if (!pathname) return "/"; - return pathname.endsWith("/") ? pathname : `${pathname}/`; - } catch { - return permalink; - } - } - - return permalink; -}; - -export default { - eleventyComputed: { - // Compute permalink from file path for posts without explicit frontmatter permalink. - // Pattern: content/{type}/{yyyy}-{MM}-{dd}-{slug}.md → /{type}/{yyyy}/{MM}/{dd}/{slug}/ - permalink: (data) => { - // Convert stale /content/ permalinks from pre-beta.37 posts to canonical format - const explicitPermalink = normalizeOutputPermalink(data.permalink); - - if (explicitPermalink && typeof explicitPermalink === "string") { - const contentMatch = explicitPermalink.match( - /^\/content\/([^/]+)\/(\d{4})-(\d{2})-(\d{2})-(.+?)\/?$/ - ); - if (contentMatch) { - const [, type, year, month, day, slug] = contentMatch; - return `/${type}/${year}/${month}/${day}/${slug}/`; - } - // Valid non-/content/ permalink — use as-is - return explicitPermalink; - } - - // No frontmatter permalink — compute from file path - // NOTE: data.page.inputPath may be wrong due to parallel rendering, - // but posts without frontmatter permalink are rare (only pre-beta.37 edge cases) - const inputPath = data.page?.inputPath || ""; - const match = inputPath.match( - /content\/([^/]+)\/(\d{4})-(\d{2})-(\d{2})-(.+)\.md$/ - ); - if (match) { - const [, type, year, month, day, slug] = match; - return `/${type}/${year}/${month}/${day}/${slug}/`; - } - - // For non-matching files (pages, root files), let Eleventy decide - return data.permalink; - }, - }, -}; diff --git a/theme/_data/enabledPostTypes.js b/theme/_data/enabledPostTypes.js deleted file mode 100644 index 989bc7a..0000000 --- a/theme/_data/enabledPostTypes.js +++ /dev/null @@ -1,50 +0,0 @@ -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/theme/_data/funkwhaleActivity.js b/theme/_data/funkwhaleActivity.js deleted file mode 100644 index 63f2af3..0000000 --- a/theme/_data/funkwhaleActivity.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Funkwhale Activity Data - * Fetches from Indiekit's endpoint-funkwhale public API - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -const INDIEKIT_URL = - process.env.INDIEKIT_URL || process.env.SITE_URL || "https://example.com"; -const FUNKWHALE_INSTANCE = process.env.FUNKWHALE_INSTANCE || ""; -const DEFAULT_FETCH_CACHE_DURATION = "5m"; -const LISTENING_FETCH_CACHE_DURATION = - (process.env.LISTENING_FETCH_CACHE_DURATION || "").trim() || DEFAULT_FETCH_CACHE_DURATION; -const FUNKWHALE_FETCH_CACHE_DURATION = - (process.env.FUNKWHALE_FETCH_CACHE_DURATION || "").trim() || LISTENING_FETCH_CACHE_DURATION; - -/** - * Fetch from Indiekit's public Funkwhale API endpoint - */ -async function fetchFromIndiekit(endpoint) { - const urls = [ - `${INDIEKIT_URL}/funkwhale/api/${endpoint}`, - `${INDIEKIT_URL}/funkwhaleapi/api/${endpoint}`, - ]; - - for (const url of urls) { - try { - console.log(`[funkwhaleActivity] Fetching from Indiekit: ${url}`); - const data = await EleventyFetch(url, { - duration: FUNKWHALE_FETCH_CACHE_DURATION, - type: "json", - }); - console.log(`[funkwhaleActivity] Indiekit ${endpoint} success via ${url}`); - return data; - } catch (error) { - console.log( - `[funkwhaleActivity] Indiekit API unavailable for ${endpoint} at ${url}: ${error.message}` - ); - } - } - - return null; -} - -/** - * Format duration in seconds to human-readable string - */ -function formatDuration(seconds) { - if (!seconds || seconds < 0) return "0:00"; - - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - - if (hours > 24) { - const days = Math.floor(hours / 24); - return `${days}d`; - } - - if (hours > 0) { - return `${hours}h ${minutes}m`; - } - - return `${minutes}m`; -} - -export default async function () { - try { - console.log("[funkwhaleActivity] Fetching Funkwhale data..."); - console.log( - `[funkwhaleActivity] EleventyFetch cache duration: ${FUNKWHALE_FETCH_CACHE_DURATION}` - ); - - // Fetch all data from Indiekit API - const [nowPlaying, listenings, favorites, stats] = await Promise.all([ - fetchFromIndiekit("now-playing"), - fetchFromIndiekit("listenings"), - fetchFromIndiekit("favorites"), - fetchFromIndiekit("stats"), - ]); - - // Check if we got data - const hasData = nowPlaying || listenings?.listenings?.length || stats?.summary; - - if (!hasData) { - console.log("[funkwhaleActivity] No data available from Indiekit"); - return { - nowPlaying: null, - listenings: [], - favorites: [], - stats: null, - instanceUrl: FUNKWHALE_INSTANCE, - source: "unavailable", - }; - } - - console.log("[funkwhaleActivity] Using Indiekit API data"); - - // Format stats with human-readable durations - let formattedStats = null; - if (stats?.summary) { - formattedStats = { - ...stats, - summary: { - all: { - ...stats.summary.all, - totalDurationFormatted: formatDuration(stats.summary.all?.totalDuration || 0), - }, - month: { - ...stats.summary.month, - totalDurationFormatted: formatDuration(stats.summary.month?.totalDuration || 0), - }, - week: { - ...stats.summary.week, - totalDurationFormatted: formatDuration(stats.summary.week?.totalDuration || 0), - }, - }, - }; - } - - return { - nowPlaying: nowPlaying || null, - listenings: listenings?.listenings || [], - favorites: favorites?.favorites || [], - stats: formattedStats, - instanceUrl: FUNKWHALE_INSTANCE, - source: "indiekit", - }; - } catch (error) { - console.error("[funkwhaleActivity] Error:", error.message); - return { - nowPlaying: null, - listenings: [], - favorites: [], - stats: null, - instanceUrl: FUNKWHALE_INSTANCE, - source: "error", - }; - } -} diff --git a/theme/_data/githubActivity.js b/theme/_data/githubActivity.js deleted file mode 100644 index 0693786..0000000 --- a/theme/_data/githubActivity.js +++ /dev/null @@ -1,291 +0,0 @@ -/** - * GitHub Activity Data - * Fetches from Indiekit's endpoint-github public API - * Falls back to direct GitHub API if Indiekit is unavailable - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -const GITHUB_USERNAME = process.env.GITHUB_USERNAME || ""; -const INDIEKIT_URL = process.env.SITE_URL || "https://example.com"; - -// Fallback featured repos if Indiekit API unavailable (from env: comma-separated) -const FALLBACK_FEATURED_REPOS = process.env.GITHUB_FEATURED_REPOS?.split(",").filter(Boolean) || []; - -/** - * Fetch from Indiekit's public GitHub API endpoint - */ -async function fetchFromIndiekit(endpoint) { - const urls = [ - `${INDIEKIT_URL}/github/api/${endpoint}`, - `${INDIEKIT_URL}/githubapi/api/${endpoint}`, - ]; - - for (const url of urls) { - try { - console.log(`[githubActivity] Fetching from Indiekit: ${url}`); - const data = await EleventyFetch(url, { - duration: "15m", - type: "json", - }); - console.log(`[githubActivity] Indiekit ${endpoint} success via ${url}`); - return data; - } catch (error) { - console.log( - `[githubActivity] Indiekit API unavailable for ${endpoint} at ${url}: ${error.message}` - ); - } - } - - return null; -} - -/** - * Fetch from GitHub API directly - */ -async function fetchFromGitHub(endpoint) { - const url = `https://api.github.com${endpoint}`; - const headers = { - Accept: "application/vnd.github.v3+json", - "User-Agent": "Eleventy-Site", - }; - - if (process.env.GITHUB_TOKEN) { - headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; - } - - return await EleventyFetch(url, { - duration: "15m", - type: "json", - fetchOptions: { headers }, - }); -} - -/** - * Truncate text with ellipsis - */ -function truncate(text, maxLength = 80) { - if (!text || text.length <= maxLength) return text || ""; - return text.slice(0, maxLength - 1) + "..."; -} - -/** - * Extract commits from push events - */ -function extractCommits(events) { - if (!Array.isArray(events)) return []; - - return events - .filter((event) => event.type === "PushEvent") - .flatMap((event) => - (event.payload?.commits || []).map((commit) => ({ - sha: commit.sha.slice(0, 7), - message: truncate(commit.message.split("\n")[0]), - url: `https://github.com/${event.repo.name}/commit/${commit.sha}`, - repo: event.repo.name, - repoUrl: `https://github.com/${event.repo.name}`, - date: event.created_at, - })) - ) - .slice(0, 10); -} - -/** - * Extract PRs/Issues from events - */ -function extractContributions(events) { - if (!Array.isArray(events)) return []; - - return events - .filter( - (event) => - (event.type === "PullRequestEvent" || event.type === "IssuesEvent") && - event.payload?.action === "opened" - ) - .map((event) => { - const item = event.payload.pull_request || event.payload.issue; - return { - type: event.type === "PullRequestEvent" ? "pr" : "issue", - title: truncate(item?.title), - url: item?.html_url, - repo: event.repo.name, - repoUrl: `https://github.com/${event.repo.name}`, - number: item?.number, - date: event.created_at, - }; - }) - .slice(0, 10); -} - -/** - * Format starred repos - */ -function formatStarred(repos) { - if (!Array.isArray(repos)) return []; - - return repos.map((repo) => ({ - name: repo.full_name, - description: truncate(repo.description, 120), - url: repo.html_url, - stars: repo.stargazers_count, - language: repo.language, - topics: repo.topics?.slice(0, 5) || [], - })); -} - -/** - * Fetch featured repos directly from GitHub (fallback) - */ -async function fetchFeaturedFromGitHub(repoList) { - const featured = []; - - for (const repoFullName of repoList) { - try { - const repo = await fetchFromGitHub(`/repos/${repoFullName}`); - let commits = []; - try { - const commitsData = await fetchFromGitHub( - `/repos/${repoFullName}/commits?per_page=5` - ); - commits = commitsData.map((c) => ({ - sha: c.sha.slice(0, 7), - message: truncate(c.commit.message.split("\n")[0]), - url: c.html_url, - date: c.commit.author.date, - })); - } catch (e) { - console.log(`[githubActivity] Could not fetch commits for ${repoFullName}`); - } - - featured.push({ - fullName: repo.full_name, - name: repo.name, - description: repo.description, - url: repo.html_url, - stars: repo.stargazers_count, - forks: repo.forks_count, - language: repo.language, - isPrivate: repo.private, - commits, - }); - } catch (error) { - console.log(`[githubActivity] Could not fetch ${repoFullName}: ${error.message}`); - } - } - - return featured; -} - -/** - * Fetch commits directly from user's recently pushed repos - * Fallback when events API doesn't include commit details - */ -async function fetchCommitsFromRepos(username, limit = 10) { - try { - const repos = await fetchFromGitHub( - `/users/${username}/repos?sort=pushed&per_page=5` - ); - - if (!Array.isArray(repos) || repos.length === 0) { - return []; - } - - const allCommits = []; - for (const repo of repos.slice(0, 5)) { - try { - const repoCommits = await fetchFromGitHub( - `/repos/${repo.full_name}/commits?per_page=5` - ); - for (const c of repoCommits) { - allCommits.push({ - sha: c.sha.slice(0, 7), - message: truncate(c.commit?.message?.split("\n")[0]), - url: c.html_url, - repo: repo.full_name, - repoUrl: repo.html_url, - date: c.commit?.author?.date, - }); - } - } catch { - // Skip repos we can't access - } - } - - // Sort by date and limit - return allCommits - .sort((a, b) => new Date(b.date) - new Date(a.date)) - .slice(0, limit); - } catch (error) { - console.log(`[githubActivity] Could not fetch commits from repos: ${error.message}`); - return []; - } -} - -export default async function () { - try { - console.log("[githubActivity] Fetching GitHub data..."); - - // Try Indiekit public API first - const [indiekitStars, indiekitCommits, indiekitContributions, indiekitActivity, indiekitFeatured] = - await Promise.all([ - fetchFromIndiekit("stars"), - fetchFromIndiekit("commits"), - fetchFromIndiekit("contributions"), - fetchFromIndiekit("activity"), - fetchFromIndiekit("featured"), - ]); - - // Check if Indiekit API is available - const hasIndiekitData = - indiekitStars?.stars || - indiekitCommits?.commits || - indiekitFeatured?.featured; - - if (hasIndiekitData) { - console.log("[githubActivity] Using Indiekit API data"); - return { - stars: indiekitStars?.stars || [], - commits: indiekitCommits?.commits || [], - contributions: indiekitContributions?.contributions || [], - activity: indiekitActivity?.activity || [], - featured: indiekitFeatured?.featured || [], - source: "indiekit", - }; - } - - // Fallback to direct GitHub API - console.log("[githubActivity] Falling back to GitHub API"); - - const [events, starred, featured] = await Promise.all([ - fetchFromGitHub(`/users/${GITHUB_USERNAME}/events/public?per_page=50`), - fetchFromGitHub(`/users/${GITHUB_USERNAME}/starred?per_page=20&sort=created`), - fetchFeaturedFromGitHub(FALLBACK_FEATURED_REPOS), - ]); - - // Try to extract commits from events first - let commits = extractCommits(events || []); - - // If events API didn't have commits, fetch directly from repos - if (commits.length === 0 && GITHUB_USERNAME) { - console.log("[githubActivity] Events API returned no commits, fetching from repos"); - commits = await fetchCommitsFromRepos(GITHUB_USERNAME, 10); - } - - return { - stars: formatStarred(starred || []), - commits, - contributions: extractContributions(events || []), - featured, - source: "github", - }; - } catch (error) { - console.error("[githubActivity] Error:", error.message); - return { - stars: [], - commits: [], - contributions: [], - featured: [], - source: "error", - }; - } -} diff --git a/theme/_data/githubRepos.js b/theme/_data/githubRepos.js deleted file mode 100644 index 1b8fe5f..0000000 --- a/theme/_data/githubRepos.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * GitHub Repos Data - * Fetches public repositories from GitHub API - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -export default async function () { - const username = process.env.GITHUB_USERNAME || ""; - - try { - // Fetch public repos, sorted by updated date - const url = `https://api.github.com/users/${username}/repos?sort=updated&per_page=10&type=owner`; - - const repos = await EleventyFetch(url, { - duration: "1h", // Cache for 1 hour - type: "json", - fetchOptions: { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "Eleventy-Site", - }, - }, - }); - - // Filter and transform repos - return repos - .filter((repo) => !repo.fork && !repo.private) // Exclude forks and private repos - .map((repo) => ({ - name: repo.name, - full_name: repo.full_name, - description: repo.description, - html_url: repo.html_url, - homepage: repo.homepage, - language: repo.language, - stargazers_count: repo.stargazers_count, - forks_count: repo.forks_count, - open_issues_count: repo.open_issues_count, - topics: repo.topics || [], - updated_at: repo.updated_at, - created_at: repo.created_at, - })) - .slice(0, 10); // Limit to 10 repos - } catch (error) { - console.error("Error fetching GitHub repos:", error.message); - return []; - } -} diff --git a/theme/_data/githubStarred.js b/theme/_data/githubStarred.js deleted file mode 100644 index ca5cf71..0000000 --- a/theme/_data/githubStarred.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * GitHub Starred Repos Metadata - * Fetches the starred API response (cached 15min) to extract totalCount. - * Only totalCount is passed to Eleventy's data cascade — the full star - * list is discarded after parsing, keeping build memory low. - * The starred page fetches all data client-side via Alpine.js. - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -const INDIEKIT_URL = process.env.SITE_URL || "https://example.com"; - -export default async function () { - try { - const urls = [ - `${INDIEKIT_URL}/github/api/starred/all`, - `${INDIEKIT_URL}/githubapi/api/starred/all`, - ]; - - for (const url of urls) { - try { - const response = await EleventyFetch(url, { - duration: "15m", - type: "json", - }); - - return { - totalCount: response.totalCount || 0, - buildDate: new Date().toISOString(), - }; - } catch (error) { - console.log(`[githubStarred] Could not fetch ${url}: ${error.message}`); - } - } - - throw new Error("No GitHub starred endpoint responded"); - } catch (error) { - console.log(`[githubStarred] Could not fetch starred count: ${error.message}`); - return { - totalCount: 0, - buildDate: new Date().toISOString(), - }; - } -} diff --git a/theme/_data/homepageConfig.js b/theme/_data/homepageConfig.js deleted file mode 100644 index ed5b2a7..0000000 --- a/theme/_data/homepageConfig.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default function () { - try { - // Resolve via the content/ symlink relative to the Eleventy project - const configPath = resolve(__dirname, "..", "content", ".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/theme/_data/lastfmActivity.js b/theme/_data/lastfmActivity.js deleted file mode 100644 index 7be3ff0..0000000 --- a/theme/_data/lastfmActivity.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Last.fm Activity Data - * Fetches from Indiekit's endpoint-lastfm public API - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -const INDIEKIT_URL = - process.env.INDIEKIT_URL || process.env.SITE_URL || "https://example.com"; -const LASTFM_USERNAME = process.env.LASTFM_USERNAME || ""; -const DEFAULT_FETCH_CACHE_DURATION = "5m"; -const LISTENING_FETCH_CACHE_DURATION = - (process.env.LISTENING_FETCH_CACHE_DURATION || "").trim() || DEFAULT_FETCH_CACHE_DURATION; -const LASTFM_FETCH_CACHE_DURATION = - (process.env.LASTFM_FETCH_CACHE_DURATION || "").trim() || LISTENING_FETCH_CACHE_DURATION; - -/** - * Fetch from Indiekit's public Last.fm API endpoint - */ -async function fetchFromIndiekit(path) { - const urls = [ - `${INDIEKIT_URL}/lastfmapi/api/${path}`, - `${INDIEKIT_URL}/lastfm/api/${path}`, - ]; - - for (const url of urls) { - try { - console.log(`[lastfmActivity] Fetching from Indiekit: ${url}`); - const data = await EleventyFetch(url, { - duration: LASTFM_FETCH_CACHE_DURATION, - type: "json", - }); - console.log(`[lastfmActivity] Indiekit ${path} success via ${url}`); - return data; - } catch (error) { - console.log( - `[lastfmActivity] Indiekit API unavailable for ${path} at ${url}: ${error.message}` - ); - } - } - - return null; -} - -export default async function () { - try { - console.log("[lastfmActivity] Fetching Last.fm data..."); - console.log( - `[lastfmActivity] EleventyFetch cache duration: ${LASTFM_FETCH_CACHE_DURATION}` - ); - - // Fetch all data from Indiekit API - const [nowPlaying, scrobbles, loved, stats] = await Promise.all([ - fetchFromIndiekit("now-playing"), - fetchFromIndiekit("scrobbles?period=alltime&limit=10"), - fetchFromIndiekit("loved?limit=10"), - fetchFromIndiekit("stats?period=alltime"), - ]); - - // Check if we got data - const hasData = nowPlaying || scrobbles?.scrobbles?.length || stats?.summary; - - if (!hasData) { - console.log("[lastfmActivity] No data available from Indiekit"); - return { - nowPlaying: null, - scrobbles: [], - loved: [], - stats: null, - username: LASTFM_USERNAME, - profileUrl: LASTFM_USERNAME ? `https://www.last.fm/user/${LASTFM_USERNAME}` : null, - source: "unavailable", - }; - } - - console.log("[lastfmActivity] Using Indiekit API data"); - - return { - nowPlaying: nowPlaying || null, - scrobbles: scrobbles?.scrobbles || [], - loved: loved?.loved || [], - stats: stats || null, - username: LASTFM_USERNAME, - profileUrl: LASTFM_USERNAME ? `https://www.last.fm/user/${LASTFM_USERNAME}` : null, - source: "indiekit", - }; - } catch (error) { - console.error("[lastfmActivity] Error:", error.message); - return { - nowPlaying: null, - scrobbles: [], - loved: [], - stats: null, - username: LASTFM_USERNAME, - profileUrl: null, - source: "error", - }; - } -} diff --git a/theme/_data/mastodonFeed.js b/theme/_data/mastodonFeed.js deleted file mode 100644 index 4255b48..0000000 --- a/theme/_data/mastodonFeed.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Mastodon Feed Data - * Fetches recent posts from Mastodon using the public API - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -export default async function () { - const instance = ( - process.env.MASTODON_URL || - process.env.MASTODON_INSTANCE || - "" - ) - .replace(/^https?:\/\//, "") - .replace(/\/+$/, ""); - const username = ( - process.env.MASTODON_USER || - process.env.MASTODON_USERNAME || - "" - ).trim().replace(/^@+/, ""); - - if (!instance || !username) { - console.log("[mastodonFeed] MASTODON_URL/MASTODON_USER not set, skipping"); - return []; - } - - try { - // First, look up the account ID - const lookupUrl = `https://${instance}/api/v1/accounts/lookup?acct=${username}`; - - const account = await EleventyFetch(lookupUrl, { - duration: "1h", // Cache account lookup for 1 hour - type: "json", - fetchOptions: { - headers: { - Accept: "application/json", - }, - }, - }); - - if (!account || !account.id) { - console.log("Mastodon account not found:", username); - return []; - } - - // Fetch recent statuses (excluding replies and boosts for cleaner feed) - const statusesUrl = `https://${instance}/api/v1/accounts/${account.id}/statuses?limit=10&exclude_replies=true&exclude_reblogs=true`; - - const statuses = await EleventyFetch(statusesUrl, { - duration: "15m", // Cache for 15 minutes - type: "json", - fetchOptions: { - headers: { - Accept: "application/json", - }, - }, - }); - - if (!statuses || !Array.isArray(statuses)) { - console.log("No Mastodon statuses found for:", username); - return []; - } - - // Transform statuses into a simpler format - return statuses.map((status) => ({ - id: status.id, - url: status.url, - text: stripHtml(status.content), - htmlContent: status.content, - createdAt: status.created_at, - author: { - username: status.account.username, - displayName: status.account.display_name || status.account.username, - avatar: status.account.avatar, - url: status.account.url, - }, - favouritesCount: status.favourites_count || 0, - reblogsCount: status.reblogs_count || 0, - repliesCount: status.replies_count || 0, - // Media attachments - media: status.media_attachments - ? status.media_attachments.map((m) => ({ - type: m.type, - url: m.url, - previewUrl: m.preview_url, - description: m.description, - })) - : [], - })); - } catch (error) { - console.error("Error fetching Mastodon feed:", error.message); - return []; - } -} - -// Simple HTML stripper for plain text display -function stripHtml(html) { - if (!html) return ""; - return html - .replace(//gi, " ") - .replace(/<\/p>/gi, " ") - .replace(/<[^>]+>/g, "") - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/ /g, " ") - .replace(/\s+/g, " ") - .trim(); -} diff --git a/theme/_data/newsActivity.js b/theme/_data/newsActivity.js deleted file mode 100644 index 47499b8..0000000 --- a/theme/_data/newsActivity.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * News/RSS Activity Data - * Fetches from Indiekit's endpoint-rss public API - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -const INDIEKIT_URL = process.env.SITE_URL || "https://example.com"; - -/** - * Fetch from Indiekit's public RSS API endpoint - */ -async function fetchFromIndiekit(endpoint) { - try { - const url = `${INDIEKIT_URL}/rssapi/api/${endpoint}`; - console.log(`[newsActivity] Fetching from Indiekit: ${url}`); - const data = await EleventyFetch(url, { - duration: "15m", - type: "json", - }); - console.log(`[newsActivity] Indiekit ${endpoint} success`); - return data; - } catch (error) { - console.log( - `[newsActivity] Indiekit API unavailable for ${endpoint}: ${error.message}` - ); - return null; - } -} - -export default async function () { - try { - console.log("[newsActivity] Fetching RSS feed data..."); - - // Fetch all data from Indiekit API - const [itemsRes, feedsRes, statusRes] = await Promise.all([ - fetchFromIndiekit("items?limit=50"), - fetchFromIndiekit("feeds"), - fetchFromIndiekit("status"), - ]); - - // Check if we got data - const hasData = itemsRes?.items?.length || feedsRes?.feeds?.length; - - if (!hasData) { - console.log("[newsActivity] No data available from Indiekit"); - return { - items: [], - feeds: [], - status: null, - lastUpdated: null, - source: "unavailable", - }; - } - - console.log( - `[newsActivity] Got ${itemsRes?.items?.length || 0} items from ${feedsRes?.feeds?.length || 0} feeds` - ); - - // Create a map of feed IDs to feed info for quick lookup - const feedMap = new Map(); - for (const feed of feedsRes?.feeds || []) { - feedMap.set(feed.id, feed); - } - - // Enhance items with additional feed info - const items = (itemsRes?.items || []).map((item) => { - const feed = feedMap.get(item.feedId); - return { - ...item, - feedInfo: feed - ? { - title: feed.title, - siteUrl: feed.siteUrl, - imageUrl: feed.imageUrl, - } - : null, - }; - }); - - return { - items, - feeds: feedsRes?.feeds || [], - pagination: itemsRes?.pagination || null, - status: statusRes || null, - lastUpdated: statusRes?.lastSync || new Date().toISOString(), - source: "indiekit", - }; - } catch (error) { - console.error("[newsActivity] Error:", error.message); - return { - items: [], - feeds: [], - status: null, - lastUpdated: null, - source: "error", - }; - } -} diff --git a/theme/_data/podrollStatus.js b/theme/_data/podrollStatus.js deleted file mode 100644 index cf117e5..0000000 --- a/theme/_data/podrollStatus.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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/theme/_data/recentComments.js b/theme/_data/recentComments.js deleted file mode 100644 index bdd1ede..0000000 --- a/theme/_data/recentComments.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Recent Comments Data - * Fetches the 5 most recent comments at build time for the sidebar widget. - */ - -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}/comments/api/comments?limit=5`; - console.log(`[recentComments] Fetching: ${url}`); - const data = await EleventyFetch(url, { - duration: "15m", - type: "json", - }); - console.log(`[recentComments] Got ${(data.children || []).length} comments`); - return data.children || []; - } catch (error) { - console.log(`[recentComments] Unavailable: ${error.message}`); - return []; - } -} diff --git a/theme/_data/site.js b/theme/_data/site.js deleted file mode 100644 index 7d3d342..0000000 --- a/theme/_data/site.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Site configuration for Eleventy - * - * Configure via environment variables in Cloudron app settings. - * All values have sensible defaults for initial deployment. - */ - -// Parse social links from env (format: "name|url|icon,name|url|icon") -function parseSocialLinks(envVar) { - if (!envVar) return []; - return envVar.split(",").map((link) => { - const [name, url, icon] = link.split("|").map((s) => s.trim()); - // Bluesky requires "me atproto" for verification - const rel = url.includes("bsky.app") ? "me atproto" : "me"; - return { name, url, rel, icon: icon || name.toLowerCase() }; - }); -} - -// Get fediverse handle for fediverse:creator meta tag -// Prefers the site's own ActivityPub identity over external Mastodon account -function getFediverseCreator() { - // Primary: site's own ActivityPub actor (canonical fediverse identity) - const apHandle = process.env.ACTIVITYPUB_HANDLE; - if (apHandle) { - const domain = (process.env.SITE_URL || "https://example.com").replace(/^https?:\/\//, ""); - return `@${apHandle}@${domain}`; - } - // Fallback: external Mastodon account (syndication target) - const instance = process.env.MASTODON_INSTANCE?.replace("https://", "") || ""; - const user = process.env.MASTODON_USER || ""; - if (instance && user) { - return `@${user}@${instance}`; - } - return ""; -} - -// Auto-generate social links from feed config when SITE_SOCIAL is not set -function buildSocialFromFeeds() { - const links = []; - const github = process.env.GITHUB_USERNAME; - if (github) { - links.push({ name: "GitHub", url: `https://github.com/${github}`, rel: "me", icon: "github" }); - } - const bskyHandle = process.env.BLUESKY_HANDLE; - if (bskyHandle) { - links.push({ name: "Bluesky", url: `https://bsky.app/profile/${bskyHandle}`, rel: "me atproto", icon: "bluesky" }); - } - const mastoInstance = process.env.MASTODON_INSTANCE?.replace("https://", ""); - const mastoUser = process.env.MASTODON_USER; - if (mastoInstance && mastoUser) { - links.push({ name: "Mastodon", url: `https://${mastoInstance}/@${mastoUser}`, rel: "me", icon: "mastodon" }); - } - const linkedin = process.env.LINKEDIN_USERNAME; - if (linkedin) { - links.push({ name: "LinkedIn", url: `https://linkedin.com/in/${linkedin}`, rel: "me", icon: "linkedin" }); - } - const apHandle = process.env.ACTIVITYPUB_HANDLE; - if (apHandle) { - const siteUrl = process.env.SITE_URL || "https://example.com"; - links.push({ name: "ActivityPub", url: `${siteUrl}/activitypub/users/${apHandle}`, rel: "me", icon: "activitypub" }); - } - return links; -} - -// site.url: no trailing slash — used as URL base for path concatenation ({{ site.url }}/path) -// site.me / site.author.url: trailing slash — Mastodon rel="me" requires exact match -const siteUrlBase = (process.env.SITE_URL || "https://example.com").replace(/\/$/, ""); -const siteUrlWithSlash = siteUrlBase + "/"; - -export default { - // Basic site info - name: process.env.SITE_NAME || "My IndieWeb Blog", - url: siteUrlBase, - me: siteUrlWithSlash, - locale: process.env.SITE_LOCALE || "en", - description: - process.env.SITE_DESCRIPTION || - "An IndieWeb-powered blog with Micropub support", - - // Author info (shown in h-card, about page, etc.) - author: { - name: process.env.AUTHOR_NAME || "Blog Author", - url: siteUrlWithSlash, - avatar: process.env.AUTHOR_AVATAR || "/images/default-avatar.svg", - title: process.env.AUTHOR_TITLE || "", - bio: process.env.AUTHOR_BIO || "Welcome to my IndieWeb blog.", - location: process.env.AUTHOR_LOCATION || "", - locality: process.env.AUTHOR_LOCALITY || "", - region: process.env.AUTHOR_REGION || "", - country: process.env.AUTHOR_COUNTRY || "", - org: process.env.AUTHOR_ORG || "", - pronoun: process.env.AUTHOR_PRONOUN || "", - categories: process.env.AUTHOR_CATEGORIES?.split(",").map(s => s.trim()) || [], - keyUrl: process.env.AUTHOR_KEY_URL || "", - email: process.env.AUTHOR_EMAIL || "", - }, - - // Social links (for rel="me" and h-card) - // Set SITE_SOCIAL env var as: "GitHub|https://github.com/user|github,Mastodon|https://mastodon.social/@user|mastodon" - // Falls back to auto-generating from feed config (GITHUB_USERNAME, BLUESKY_HANDLE, etc.) - social: parseSocialLinks(process.env.SITE_SOCIAL).length > 0 - ? parseSocialLinks(process.env.SITE_SOCIAL) - : buildSocialFromFeeds(), - - // Feed integrations (usernames for data fetching) - feeds: { - github: process.env.GITHUB_USERNAME || "", - bluesky: process.env.BLUESKY_HANDLE || "", - mastodon: { - instance: process.env.MASTODON_INSTANCE?.replace("https://", "") || "", - username: process.env.MASTODON_USER || "", - }, - }, - - // Webmentions configuration - webmentions: { - domain: process.env.SITE_URL?.replace("https://", "").replace("http://", "") || "example.com", - }, - - // Fediverse creator for meta tag (e.g., @rick@rmendes.net) - fediverseCreator: getFediverseCreator(), - - // Support/monetization configuration (used in _textcasting JSON Feed extension) - support: { - url: process.env.SUPPORT_URL || null, - stripe: process.env.SUPPORT_STRIPE_URL || null, - lightning: process.env.SUPPORT_LIGHTNING_ADDRESS || null, - paymentPointer: process.env.SUPPORT_PAYMENT_POINTER || null, - }, - - // Markdown for Agents — serve clean Markdown to AI agents - // Set MARKDOWN_AGENTS_ENABLED to "false" to disable entirely - markdownAgents: { - enabled: (process.env.MARKDOWN_AGENTS_ENABLED || "true").toLowerCase() === "true", - aiTrain: process.env.MARKDOWN_AGENTS_AI_TRAIN || "yes", - search: process.env.MARKDOWN_AGENTS_SEARCH || "yes", - aiInput: process.env.MARKDOWN_AGENTS_AI_INPUT || "yes", - }, -}; diff --git a/theme/_data/urlAliases.js b/theme/_data/urlAliases.js deleted file mode 100644 index 1478a1f..0000000 --- a/theme/_data/urlAliases.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * URL Aliases for Webmention Recovery - * - * Maps new URLs to their old URLs so webmentions from previous - * URL structures can be displayed on current pages. - * - * 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"; -import { resolve, dirname } from "path"; -import { fileURLToPath } from "url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const siteUrl = process.env.SITE_URL || "https://example.com"; - -/** - * Parse a redirect map file into URL mappings - * Format: old_path new_path; - */ -function parseRedirectMap(filePath) { - const aliases = {}; - - if (!existsSync(filePath)) { - console.log(`[urlAliases] File not found: ${filePath}`); - return aliases; - } - - try { - const content = readFileSync(filePath, "utf-8"); - const lines = content.split("\n").filter((line) => { - const trimmed = line.trim(); - return trimmed && !trimmed.startsWith("#"); - }); - - for (const line of lines) { - // Format: /old/path /new/path; - const match = line.match(/^(\S+)\s+(\S+);?$/); - if (match) { - const [, oldPath, newPath] = match; - // Normalize paths (remove trailing slashes, ensure leading slash) - const normalizedNew = newPath.replace(/;$/, "").replace(/\/$/, ""); - const normalizedOld = oldPath.replace(/\/$/, ""); - - // Map new URL → array of old URLs - if (!aliases[normalizedNew]) { - aliases[normalizedNew] = []; - } - aliases[normalizedNew].push(normalizedOld); - } - } - } catch (error) { - console.error(`[urlAliases] Error parsing ${filePath}:`, error.message); - } - - return aliases; -} - -/** - * Merge multiple alias maps - */ -function mergeAliases(...maps) { - const merged = {}; - for (const map of maps) { - for (const [newUrl, oldUrls] of Object.entries(map)) { - if (!merged[newUrl]) { - merged[newUrl] = []; - } - merged[newUrl].push(...oldUrls); - } - } - return merged; -} - -// Parse redirect maps from /app/pkg (Docker) or parent directory (local dev) -// In Docker: eleventy-site is at /app/pkg/eleventy-site, maps are at /app/pkg/ -// In local dev: maps might be at ../ -const pkgRoot = resolve(__dirname, "../.."); - -// Helper to find first existing file -function findFile(candidates) { - for (const path of candidates) { - if (existsSync(path)) { - console.log(`[urlAliases] Found: ${path}`); - return path; - } - } - console.log(`[urlAliases] No file found in: ${candidates.join(", ")}`); - return null; -} - -// Try multiple possible locations for each map type -const microblogMapPath = findFile([ - resolve(pkgRoot, "redirects.map"), - resolve(__dirname, "../../redirects.map"), -]); - -const knownMapPath = findFile([ - resolve(pkgRoot, "old-blog-redirects.map"), - resolve(__dirname, "../../old-blog-redirects.map"), -]); - -const microblogAliases = microblogMapPath ? parseRedirectMap(microblogMapPath) : {}; -const knownAliases = knownMapPath ? parseRedirectMap(knownMapPath) : {}; - -const allAliases = mergeAliases(microblogAliases, knownAliases); - -// Log summary -const totalMappings = Object.keys(allAliases).length; -const totalOldUrls = Object.values(allAliases).reduce((sum, urls) => sum + urls.length, 0); -console.log(`[urlAliases] Loaded ${totalMappings} URL mappings with ${totalOldUrls} old URLs`); - -export default { - // The merged alias map: new URL → [old URLs] - aliases: allAliases, - - // Site URL for building absolute URLs - siteUrl, - - /** - * Get all URLs (old and new) that should be checked for webmentions - * @param {string} url - Current page URL (relative) - * @returns {string[]} - Array of absolute URLs to check - */ - getAllUrls(url) { - const normalizedUrl = url.replace(/\/$/, ""); - const urls = [ - `${siteUrl}${url}`, - `${siteUrl}${normalizedUrl}`, - ]; - - // Add old URL variations - const oldUrls = allAliases[normalizedUrl] || []; - for (const oldUrl of oldUrls) { - urls.push(`${siteUrl}${oldUrl}`); - // Also try with trailing slash - urls.push(`${siteUrl}${oldUrl}/`); - } - - // Deduplicate - return [...new Set(urls)]; - }, - - /** - * Get just the old URLs for a given new URL - * @param {string} url - Current page URL (relative) - * @returns {string[]} - Array of old relative URLs - */ - getOldUrls(url) { - const normalizedUrl = url.replace(/\/$/, ""); - return allAliases[normalizedUrl] || []; - }, -}; diff --git a/theme/_data/whereCheckins.js b/theme/_data/whereCheckins.js deleted file mode 100644 index 3ffb0cd..0000000 --- a/theme/_data/whereCheckins.js +++ /dev/null @@ -1,455 +0,0 @@ -/** - * Where/Checkin data - * - * Reads local check-ins created by this site's Micropub endpoint. - * Supports OwnYourSwarm JSON mode and simple mode payloads once they are - * written to local markdown content. - */ - -import matter from "gray-matter"; -import { existsSync, readdirSync, readFileSync } from "node:fs"; -import { extname, join, relative } from "node:path"; -import { fileURLToPath } from "node:url"; - -function resolveContentDir() { - const candidates = ["../content", "../../content"].map((value) => - fileURLToPath(new URL(value, import.meta.url)) - ); - return candidates.find((dirPath) => existsSync(dirPath)) || candidates[0]; -} - -const CONTENT_DIR = resolveContentDir(); -const SWARM_HOST = "swarmapp.com"; - -function first(value) { - if (Array.isArray(value)) return value[0]; - return value; -} - -function asArray(value) { - if (value === null || value === undefined || value === "") return []; - return Array.isArray(value) ? value : [value]; -} - -function asText(value) { - if (value === null || value === undefined) return ""; - if (typeof value === "string") return value; - if (typeof value === "number") return String(value); - if (value instanceof Date) return value.toISOString(); - if (typeof value === "object") { - if (typeof value.value === "string") return value.value; - if (typeof value.text === "string") return value.text; - if (typeof value.url === "string") return value.url; - } - return ""; -} - -function asNumber(value) { - const raw = first(asArray(value)); - const num = Number(raw); - return Number.isFinite(num) ? num : null; -} - -function uniqueStrings(values) { - return [...new Set(values.filter(Boolean))]; -} - -function joinLocation(locality, region, country) { - return [locality, region, country].filter(Boolean).join(", "); -} - -function toRelativePath(filePath) { - return relative(CONTENT_DIR, filePath).replace(/\\/g, "/"); -} - -function walkMarkdownFiles(dirPath) { - const files = []; - - for (const entry of readdirSync(dirPath, { withFileTypes: true })) { - const fullPath = join(dirPath, entry.name); - if (entry.isDirectory()) { - files.push(...walkMarkdownFiles(fullPath)); - continue; - } - - if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") { - files.push(fullPath); - } - } - - return files; -} - -function toPropertiesObject(value) { - if (!value || typeof value !== "object") return {}; - if (value.properties && typeof value.properties === "object") { - return value.properties; - } - return value; -} - -function getEntryProperties(frontmatter) { - return toPropertiesObject(frontmatter.properties); -} - -function isSwarmUrl(url) { - return asText(url).toLowerCase().includes(SWARM_HOST); -} - -function parseGeoUri(value) { - const raw = asText(value).trim(); - const match = raw.match(/^geo:\s*([-+]?\d+(?:\.\d+)?),\s*([-+]?\d+(?:\.\d+)?)/i); - if (!match) { - return { - latitude: null, - longitude: null, - }; - } - - const latitude = Number(match[1]); - const longitude = Number(match[2]); - - return { - latitude: Number.isFinite(latitude) ? latitude : null, - longitude: Number.isFinite(longitude) ? longitude : null, - }; -} - -function extractSimpleModeVenueName(contentValue) { - const text = asText(contentValue).trim(); - if (!text) return ""; - - const match = text.match(/^Checked in (?:at|to)\s+(.+?)(?:\.\s|$)/i); - return match ? match[1].trim() : ""; -} - -function parsePersonCard(card) { - if (!card || typeof card !== "object") return null; - const props = toPropertiesObject(card); - - const urls = asArray(props.url).map((url) => asText(url)).filter(Boolean); - const photos = asArray(props.photo).map((photo) => asText(photo)).filter(Boolean); - - return { - name: - asText(first(asArray(props.name))) || - asText(props.firstName) || - "", - url: urls[0] || "", - urls, - photo: photos[0] || "", - }; -} - -function parseCategory(categoryValues) { - const tags = []; - const people = []; - - for (const value of categoryValues) { - if (typeof value === "string") { - tags.push(value.trim()); - continue; - } - - if (!value || typeof value !== "object") continue; - - const type = Array.isArray(value.type) ? value.type : []; - const looksLikePersonTag = - type.includes("h-card") || - Boolean(value.properties) || - value.name !== undefined || - value.url !== undefined; - - if (looksLikePersonTag) { - const person = parsePersonCard(value); - if (person && (person.name || person.url)) { - people.push(person); - } - } - } - - const normalizedTags = uniqueStrings(tags).filter( - (tag) => !["where", "slashpage"].includes(tag.toLowerCase()) - ); - - return { - tags: normalizedTags, - people, - }; -} - -function isCheckinFrontmatter(frontmatter, relativePath) { - if (relativePath === "pages/where.md") return false; - - const properties = getEntryProperties(frontmatter); - - const checkinValue = - properties.checkin ?? - frontmatter.checkin ?? - frontmatter["check-in"]; - - const syndicationValues = asArray(properties.syndication ?? frontmatter.syndication) - .map((value) => asText(value)) - .filter(Boolean); - - const categories = asArray(properties.category ?? frontmatter.category) - .map((value) => asText(value).toLowerCase()) - .filter(Boolean); - - const locationValue = properties.location ?? frontmatter.location; - const geoCoords = parseGeoUri(first(asArray(locationValue))); - - const hasCheckinField = checkinValue !== undefined; - const hasSwarmSyndication = syndicationValues.some((url) => isSwarmUrl(url)); - const hasLocationField = locationValue !== undefined; - const hasCoordinates = - properties.latitude !== undefined || - properties.longitude !== undefined || - frontmatter.latitude !== undefined || - frontmatter.longitude !== undefined || - (geoCoords.latitude !== null && geoCoords.longitude !== null); - - const hasCheckinCategory = - categories.includes("where") || - categories.includes("checkin") || - categories.includes("swarm"); - - return hasCheckinField || hasSwarmSyndication || (hasCheckinCategory && (hasLocationField || hasCoordinates)); -} - -function normalizeCheckin(frontmatter, relativePath) { - const properties = getEntryProperties(frontmatter); - - const checkinValue = first( - asArray(properties.checkin ?? frontmatter.checkin ?? frontmatter["check-in"]) - ); - const locationValue = first(asArray(properties.location ?? frontmatter.location)); - - const checkinProps = toPropertiesObject(checkinValue); - const locationProps = toPropertiesObject(locationValue); - const locationGeo = parseGeoUri(locationValue); - - const venueUrlsFromCard = asArray(checkinProps.url).map((url) => asText(url)).filter(Boolean); - const venueUrlFromSimpleMode = - typeof checkinValue === "string" ? checkinValue : asText(checkinValue?.value); - const venueUrls = uniqueStrings( - venueUrlFromSimpleMode ? [venueUrlFromSimpleMode, ...venueUrlsFromCard] : venueUrlsFromCard - ); - - const venueUrl = venueUrls[0] || ""; - const venueWebsiteUrl = venueUrls[1] || ""; - const venueSocialUrl = venueUrls[2] || ""; - - const contentValue = first(asArray(properties.content ?? frontmatter.content)); - const simpleModeVenueName = extractSimpleModeVenueName(contentValue); - - const name = - asText(first(asArray(checkinProps.name))) || - simpleModeVenueName || - asText(frontmatter.title) || - "Unknown place"; - - const locality = - asText(first(asArray(checkinProps.locality))) || - asText(first(asArray(locationProps.locality))) || - asText(properties.locality) || - asText(frontmatter.locality); - const region = - asText(first(asArray(checkinProps.region))) || - asText(first(asArray(locationProps.region))) || - asText(properties.region) || - asText(frontmatter.region); - const country = - asText(first(asArray(checkinProps["country-name"]))) || - asText(first(asArray(locationProps["country-name"]))) || - asText(properties["country-name"]) || - asText(frontmatter["country-name"]); - const postalCode = - asText(first(asArray(checkinProps["postal-code"]))) || - asText(first(asArray(locationProps["postal-code"]))) || - asText(properties["postal-code"]) || - asText(frontmatter["postal-code"]); - - const latitude = - asNumber(checkinProps.latitude) ?? - asNumber(locationProps.latitude) ?? - asNumber(properties.latitude) ?? - asNumber(frontmatter.latitude) ?? - locationGeo.latitude; - const longitude = - asNumber(checkinProps.longitude) ?? - asNumber(locationProps.longitude) ?? - asNumber(properties.longitude) ?? - asNumber(frontmatter.longitude) ?? - locationGeo.longitude; - - const published = - asText(first(asArray(properties.published ?? frontmatter.published))) || - asText(frontmatter.date); - - const syndicationUrls = asArray(properties.syndication ?? frontmatter.syndication) - .map((url) => asText(url)) - .filter(Boolean); - const syndication = - syndicationUrls.find((url) => isSwarmUrl(url)) || - syndicationUrls[0] || - ""; - - const visibility = asText( - first(asArray(properties.visibility ?? frontmatter.visibility)) - ).toLowerCase(); - - const categoryValues = asArray(properties.category ?? frontmatter.category); - const category = parseCategory(categoryValues); - - const checkedInByValue = first( - asArray( - properties["checked-in-by"] ?? - frontmatter["checked-in-by"] ?? - frontmatter.checkedInBy - ) - ); - const checkedInBy = parsePersonCard(checkedInByValue); - - const addedPhotos = - frontmatter.add && typeof frontmatter.add === "object" - ? asArray(frontmatter.add.photo) - : []; - const photoValues = [ - ...asArray(properties.photo ?? frontmatter.photo), - ...addedPhotos, - ]; - const photos = uniqueStrings( - photoValues - .map((photo) => { - if (typeof photo === "string") return photo; - if (photo && typeof photo === "object") { - return asText(photo.url || photo.value || photo.src || ""); - } - return ""; - }) - .filter(Boolean) - ); - - if (!checkinValue && !syndication && latitude === null && longitude === null) { - return null; - } - - const mapUrl = - latitude !== null && longitude !== null - ? `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=16/${latitude}/${longitude}` - : ""; - - const coordinatesText = - latitude !== null && longitude !== null - ? `${latitude.toFixed(5)}, ${longitude.toFixed(5)}` - : ""; - - const locationText = joinLocation(locality, region, country); - const timestamp = published ? Date.parse(published) || 0 : 0; - const permalink = asText(frontmatter.permalink); - const id = syndication || permalink || `${relativePath}-${published || "unknown"}`; - - return { - id, - sourcePath: relativePath, - published, - timestamp, - syndication, - visibility, - isPrivate: visibility === "private", - name, - photos, - tags: category.tags, - taggedPeople: category.people, - checkedInBy, - venueUrl, - venueWebsiteUrl, - venueSocialUrl, - locality, - region, - country, - postalCode, - locationText, - latitude, - longitude, - coordinatesText, - mapUrl, - }; -} - -function normalizeCheckins(items) { - const seen = new Set(); - const checkins = []; - - for (const item of items) { - if (!item) continue; - if (seen.has(item.id)) continue; - seen.add(item.id); - checkins.push(item); - } - - return checkins.sort((a, b) => b.timestamp - a.timestamp); -} - -export default async function () { - const checkedAt = new Date().toISOString(); - const errors = []; - - let filePaths = []; - - try { - filePaths = walkMarkdownFiles(CONTENT_DIR); - } catch (error) { - const message = `[whereCheckins] Unable to scan local content: ${error.message}`; - console.log(message); - return { - source: "local-endpoint", - available: false, - checkedAt, - scannedFiles: 0, - checkins: [], - errors: [message], - stats: { - total: 0, - withCoordinates: 0, - }, - }; - } - - const items = []; - - for (const filePath of filePaths) { - const relativePath = toRelativePath(filePath); - - try { - const raw = readFileSync(filePath, "utf-8"); - const frontmatter = matter(raw).data || {}; - - if (!isCheckinFrontmatter(frontmatter, relativePath)) continue; - - const checkin = normalizeCheckin(frontmatter, relativePath); - if (checkin) items.push(checkin); - } catch (error) { - errors.push(`[whereCheckins] Skipped ${relativePath}: ${error.message}`); - } - } - - const checkins = normalizeCheckins(items); - const withCoordinates = checkins.filter( - (item) => item.latitude !== null && item.longitude !== null - ).length; - - return { - source: "local-endpoint", - available: checkins.length > 0, - checkedAt, - scannedFiles: filePaths.length, - checkins, - errors, - stats: { - total: checkins.length, - withCoordinates, - }, - }; -} diff --git a/theme/_data/youtubeChannel.js b/theme/_data/youtubeChannel.js deleted file mode 100644 index 7fbf461..0000000 --- a/theme/_data/youtubeChannel.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * YouTube Channel Data - * Fetches from Indiekit's endpoint-youtube public API - * Supports single or multiple channels - */ - -import EleventyFetch from "@11ty/eleventy-fetch"; - -const INDIEKIT_URL = process.env.SITE_URL || "https://example.com"; - -/** - * Fetch from Indiekit's public YouTube API endpoint - */ -async function fetchFromIndiekit(endpoint) { - try { - const url = `${INDIEKIT_URL}/youtubeapi/api/${endpoint}`; - console.log(`[youtubeChannel] Fetching from Indiekit: ${url}`); - const data = await EleventyFetch(url, { - duration: "5m", - type: "json", - }); - console.log(`[youtubeChannel] Indiekit ${endpoint} success`); - return data; - } catch (error) { - console.log( - `[youtubeChannel] Indiekit API unavailable for ${endpoint}: ${error.message}` - ); - return null; - } -} - -/** - * Format large numbers with locale separators - */ -function formatNumber(num) { - if (!num) return "0"; - return new Intl.NumberFormat().format(num); -} - -/** - * Format view count with K/M suffix for compact display - */ -function formatViewCount(num) { - if (!num) return "0"; - if (num >= 1000000) { - return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M"; - } - if (num >= 1000) { - return (num / 1000).toFixed(1).replace(/\\.0$/, "") + "K"; - } - return num.toString(); -} - -/** - * Format relative time from ISO date string - */ -function formatRelativeTime(dateString) { - if (!dateString) return ""; - const date = new Date(dateString); - const now = new Date(); - const diffMs = now - date; - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return "Today"; - if (diffDays === 1) return "Yesterday"; - if (diffDays < 7) return `${diffDays} days ago`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`; - if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`; - return `${Math.floor(diffDays / 365)} years ago`; -} - -/** - * Format channel data with computed fields - */ -function formatChannel(channel) { - if (!channel) return null; - return { - ...channel, - subscriberCountFormatted: formatNumber(channel.subscriberCount), - videoCountFormatted: formatNumber(channel.videoCount), - viewCountFormatted: formatNumber(channel.viewCount), - url: `https://www.youtube.com/channel/${channel.id}`, - }; -} - -/** - * Format video data with computed fields - */ -function formatVideo(video) { - return { - ...video, - viewCountFormatted: formatViewCount(video.viewCount), - relativeTime: formatRelativeTime(video.publishedAt), - }; -} - -export default async function () { - try { - console.log("[youtubeChannel] Fetching YouTube data..."); - - // Fetch all data from Indiekit API - const [channelData, videosData, liveData] = await Promise.all([ - fetchFromIndiekit("channel"), - fetchFromIndiekit("videos"), - fetchFromIndiekit("live"), - ]); - - // Check if we got data - const hasData = - channelData?.channel || - channelData?.channels?.length || - videosData?.videos?.length; - - if (!hasData) { - console.log("[youtubeChannel] No data available from Indiekit"); - return { - channel: null, - channels: [], - videos: [], - videosByChannel: {}, - liveStatus: null, - liveStatuses: [], - isMultiChannel: false, - source: "unavailable", - }; - } - - console.log("[youtubeChannel] Using Indiekit API data"); - - // Determine if multi-channel mode - const isMultiChannel = !!(channelData?.channels && channelData.channels.length > 1); - - // Format channels - let channels = []; - let channel = null; - - if (isMultiChannel) { - channels = (channelData.channels || []).map(formatChannel).filter(Boolean); - channel = channels[0] || null; - } else { - channel = formatChannel(channelData?.channel); - channels = channel ? [channel] : []; - } - - // Format videos - const videos = (videosData?.videos || []).map(formatVideo); - - // Group videos by channel if multi-channel - let videosByChannel = {}; - if (isMultiChannel && videosData?.videosByChannel) { - for (const [channelName, channelVideos] of Object.entries(videosData.videosByChannel)) { - videosByChannel[channelName] = (channelVideos || []).map(formatVideo); - } - } else if (channel) { - videosByChannel[channel.configName || channel.title] = videos; - } - - // Format live status - let liveStatus = null; - let liveStatuses = []; - - if (liveData) { - if (isMultiChannel && liveData.liveStatuses) { - liveStatuses = liveData.liveStatuses; - // Find first live or upcoming - const live = liveStatuses.find((s) => s.isLive); - const upcoming = liveStatuses.find((s) => s.isUpcoming && !s.isLive); - liveStatus = { - isLive: !!live, - isUpcoming: !live && !!upcoming, - stream: live?.stream || upcoming?.stream || null, - }; - } else { - liveStatus = { - isLive: liveData.isLive || false, - isUpcoming: liveData.isUpcoming || false, - stream: liveData.stream || null, - }; - liveStatuses = [{ ...liveStatus, channelConfigName: channel?.configName }]; - } - } - - return { - channel, - channels, - videos, - videosByChannel, - liveStatus, - liveStatuses, - isMultiChannel, - source: "indiekit", - }; - } catch (error) { - console.error("[youtubeChannel] Error:", error.message); - return { - channel: null, - channels: [], - videos: [], - videosByChannel: {}, - liveStatus: null, - liveStatuses: [], - isMultiChannel: false, - source: "error", - }; - } -} diff --git a/theme/_includes/components/blog-sidebar.njk b/theme/_includes/components/blog-sidebar.njk deleted file mode 100644 index d22ebc0..0000000 --- a/theme/_includes/components/blog-sidebar.njk +++ /dev/null @@ -1,315 +0,0 @@ -{# Blog Sidebar - Shown on individual post pages #} -{# Data-driven when homepageConfig.blogPostSidebar is configured, otherwise falls back to default widgets #} -{# Each widget is wrapped in a collapsible container with localStorage persistence #} -{% from "components/icon.njk" import icon %} - -{% set isArticlePost = (postType == "article") or (page.url and page.url.startsWith('/articles/') and page.url != '/articles/') %} -{% set showArticleToc = isArticlePost %} - -{% if homepageConfig and homepageConfig.blogPostSidebar and homepageConfig.blogPostSidebar.length %} - {# === Data-driven mode: render configured widgets === #} - {% set hasConfiguredToc = '"toc"' in (homepageConfig.blogPostSidebar | dump) %} - {% for widget in homepageConfig.blogPostSidebar %} - - {# Resolve widget title #} - {% if widget.type == "search" %}{% set widgetTitle = "Search" %} - {% elif widget.type == "social-activity" %}{% set widgetTitle = "Social Activity" %} - {% elif widget.type == "github-repos" %}{% set widgetTitle = "GitHub" %} - {% elif widget.type == "funkwhale" %}{% set widgetTitle = "Listening" %} - {% elif widget.type == "recent-posts" %}{% set widgetTitle = "Recent Posts" %} - {% elif widget.type == "blogroll" %}{% set widgetTitle = "Blogroll" %} - {% elif widget.type == "feedland" %}{% set widgetTitle = "FeedLand" %} - {% elif widget.type == "categories" %}{% set widgetTitle = "Categories" %} - {% elif widget.type == "webmentions" %}{% set widgetTitle = "Webmentions" %} - {% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %} - {% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %} - {% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %} - {% elif widget.type == "author-card-compact" %}{% set widgetTitle = "Author" %} - {% elif widget.type == "subscribe" %}{% set widgetTitle = "Subscribe" %} - {% elif widget.type == "toc" %}{% set widgetTitle = "Table of Contents" %} - {% elif widget.type == "post-categories" %}{% set widgetTitle = "Categories" %} - {% elif widget.type == "share" %}{% set widgetTitle = "Share" %} - {% elif widget.type == "ai-usage" %}{% set widgetTitle = "AI Transparency" %} - {% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %} - {% else %}{% set widgetTitle = widget.type %} - {% endif %} - - {# Resolve widget icon and accent border #} - {% if widget.type == "social-activity" %} - {% set widgetIcon = "globe" %}{% set widgetIconClass = "w-5 h-5 text-[#0085ff]" %}{% set widgetBorder = "border-l-[3px] border-l-[#0085ff]" %} - {% elif widget.type == "github-repos" %} - {% set widgetIcon = "github" %}{% set widgetIconClass = "w-5 h-5 text-surface-800 dark:text-surface-200" %}{% set widgetBorder = "border-l-[3px] border-l-surface-400 dark:border-l-surface-500" %} - {% elif widget.type == "funkwhale" %} - {% set widgetIcon = "headphones" %}{% set widgetIconClass = "w-5 h-5 text-purple-500" %}{% set widgetBorder = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %} - {% elif widget.type == "blogroll" %} - {% set widgetIcon = "book-open" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif widget.type == "feedland" %} - {% set widgetIcon = "rss" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif widget.type == "subscribe" %} - {% set widgetIcon = "rss" %}{% set widgetIconClass = "w-5 h-5 text-orange-500" %}{% set widgetBorder = "border-l-[3px] border-l-orange-400 dark:border-l-orange-500" %} - {% elif widget.type == "fediverse-follow" %} - {% set widgetIcon = "user-plus" %}{% set widgetIconClass = "w-5 h-5 text-[#a730b8]" %}{% set widgetBorder = "border-l-[3px] border-l-[#a730b8]" %} - {% elif widget.type == "author-card" or widget.type == "author-card-compact" %} - {% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "recent-posts" %} - {% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "categories" or widget.type == "post-categories" %} - {% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "recent-comments" %} - {% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "search" %} - {% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "webmentions" %} - {% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "ai-usage" %} - {% set widgetIcon = "zap" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif widget.type == "toc" %} - {% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "share" %} - {% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% else %} - {% set widgetIcon = "" %}{% set widgetIconClass = "" %}{% set widgetBorder = "" %} - {% endif %} - - {% set widgetKey = "post-widget-" + widget.type + "-" + loop.index0 %} - {% set defaultOpen = "true" if loop.index0 < 3 else "false" %} - - {# Collapsible wrapper — Alpine.js handles toggle, localStorage persists state #} -
-
- - -
- {# Widget content — inner .widget provides padding, inner title hidden by CSS #} - {% if widget.type == "author-card-compact" %} - {% include "components/widgets/author-card-compact.njk" %} - {% elif widget.type == "author-card" %} - {% include "components/widgets/author-card.njk" %} - {% elif widget.type == "toc" %} - {% include "components/widgets/toc.njk" %} - {% elif widget.type == "post-categories" %} - {% include "components/widgets/post-categories.njk" %} - {% elif widget.type == "recent-posts" %} - {% include "components/widgets/recent-posts-blog.njk" %} - {% elif widget.type == "webmentions" %} - {% include "components/widgets/webmentions.njk" %} - {% elif widget.type == "share" %} - {% include "components/widgets/share.njk" %} - {% elif widget.type == "subscribe" %} - {% include "components/widgets/subscribe.njk" %} - {% elif widget.type == "social-activity" %} - {% include "components/widgets/social-activity.njk" %} - {% elif widget.type == "github-repos" %} - {% include "components/widgets/github-repos.njk" %} - {% elif widget.type == "funkwhale" %} - {% include "components/widgets/funkwhale.njk" %} - {% elif widget.type == "blogroll" %} - {% include "components/widgets/blogroll.njk" %} - {% elif widget.type == "feedland" %} - {% include "components/widgets/feedland.njk" %} - {% elif widget.type == "categories" %} - {% include "components/widgets/categories.njk" %} - {% elif widget.type == "recent-comments" %} - {% include "components/widgets/recent-comments.njk" %} - {% elif widget.type == "search" %} - {% include "components/widgets/search.njk" %} - {% elif widget.type == "fediverse-follow" %} - {% include "components/widgets/fediverse-follow.njk" %} - {% elif widget.type == "ai-usage" %} - {% include "components/widgets/ai-usage.njk" ignore missing %} - {% elif widget.type == "custom-html" %} - {% set wConfig = widget.config or {} %} - -
- {% if wConfig.content %} -
- {{ wConfig.content | safe }} -
- {% endif %} -
-
- {% else %} - - {% endif %} -
-
-
- - {% if showArticleToc and not hasConfiguredToc and (widget.type == "author-card" or widget.type == "author-card-compact") %} - {% set widgetKey = "post-widget-toc-article" %} -
-
- -
- {% include "components/widgets/toc.njk" %} -
-
-
- {% endif %} - - {% endfor %} -{% else %} - {# === Fallback: aligned with rmendes.net article sidebar === #} - - {# Author #} - {% set widgetKey = "post-widget-author-card-compact-0" %} -
-
- -
- {% include "components/widgets/author-card-compact.njk" %} -
-
-
- - {% if showArticleToc %} - {# Table of Contents (articles only) #} - {% set widgetKey = "post-widget-toc-article" %} -
-
- -
- {% include "components/widgets/toc.njk" %} -
-
-
- {% endif %} - - {# Post Categories #} - {% set widgetKey = "post-widget-post-categories-2" %} -
-
- -
- {% include "components/widgets/post-categories.njk" %} -
-
-
- - {# Share #} - {% set widgetKey = "post-widget-share-1" %} -
-
- -
- {% include "components/widgets/share.njk" %} -
-
-
- - {# Social Activity #} - {% set widgetKey = "post-widget-social-activity-3" %} -
-
- -
- {% include "components/widgets/social-activity.njk" %} -
-
-
- - {# Listening #} - {% set widgetKey = "post-widget-funkwhale-4" %} -
-
- -
- {% include "components/widgets/funkwhale.njk" %} -
-
-
- - {# Blogroll #} - {% if blogrollStatus and blogrollStatus.source == "indiekit" %} - {% set widgetKey = "post-widget-blogroll-5" %} -
-
- -
- {% include "components/widgets/blogroll.njk" %} -
-
-
- {% endif %} - - {# Subscribe #} - {% set widgetKey = "post-widget-subscribe-6" %} -
-
- -
- {% include "components/widgets/subscribe.njk" %} -
-
-
- - {# Fediverse #} - {% set widgetKey = "post-widget-fediverse-follow-7" %} -
-
- -
- {% include "components/widgets/fediverse-follow.njk" %} -
-
-
-{% endif %} diff --git a/theme/_includes/components/comments.njk b/theme/_includes/components/comments.njk deleted file mode 100644 index ddf074c..0000000 --- a/theme/_includes/components/comments.njk +++ /dev/null @@ -1,109 +0,0 @@ -{# Comments section — shown on post pages before webmentions #} -{# Collapsed when empty, auto-opens when comments exist #} -{% set absoluteUrl = site.url + page.url %} - - -
- -
- -

- Comments - -

- -
- -
- {# Status messages #} -
- -
- - {# Sign-in form (shown when not authenticated) #} -
-

Sign in with your website to comment:

-
-
- - -
- -
-
- - {# Comment form (shown when authenticated) #} -
-
- Signed in as - - -
- -
- -
- - -
-
-
- - {# Comment list #} -
- - - - - -
-
-
-
-
diff --git a/theme/_includes/components/cv-builder.njk b/theme/_includes/components/cv-builder.njk deleted file mode 100644 index 3699f73..0000000 --- a/theme/_includes/components/cv-builder.njk +++ /dev/null @@ -1,169 +0,0 @@ -{# - CV Page Builder - renders configured layout, sections, and sidebar - from cvPageConfig (written by indiekit-endpoint-cv plugin) -#} - -{% set layout = cvPageConfig.layout or "single-column" %} -{% set hasSidebar = cvPageConfig.sidebar and cvPageConfig.sidebar.length %} - -{# CV identity — check cvPageConfig.identity first, fall back to site.author #} -{% set cvId = cvPageConfig.identity if (cvPageConfig and cvPageConfig.identity) else {} %} -{% set authorName = cvId.name or site.author.name %} -{% set authorAvatar = cvId.avatar or site.author.avatar %} -{% set authorTitle = cvId.title or site.author.title %} -{% set authorBio = cvId.bio or site.author.bio %} -{% set authorDescription = cvId.description or '' %} -{% set socialLinks = cvId.social if (cvId.social and cvId.social.length) else site.social %} -{% set cvLocality = cvId.locality or site.author.locality %} -{% set cvCountry = cvId.country or site.author.country %} -{% set cvOrg = cvId.org or site.author.org %} -{% set cvUrl = cvId.url or '' %} -{% set cvEmail = cvId.email or site.author.email %} -{% set cvKeyUrl = cvId.keyUrl or site.author.keyUrl %} - -{# Hero — rendered at top when enabled (default: true) #} -{% if cvPageConfig.hero.enabled != false %} -
-
- {{ authorName }} -
-

- {{ authorName }} -

- {% if authorTitle %} -

- {{ authorTitle }} -

- {% endif %} - {% if authorBio %} -

- {{ authorBio }} -

- {% endif %} - {% if authorDescription %} -
- - More about me ↓ - -

- {{ authorDescription }} -

-
- {% endif %} - {% from "components/social-icon.njk" import socialIcon, socialIconColorClass %} - {% if cvPageConfig.hero.showSocial != false and socialLinks %} -
- {% for link in socialLinks %} - - {{ socialIcon(link.icon, "w-5 h-5") }} - {{ link.name }} - - {% endfor %} -
- {% endif %} - {# Contact details — location, organization, website, email, PGP #} - {% if cvLocality or cvCountry or cvOrg or cvUrl or cvEmail or cvKeyUrl %} -
- {% if cvLocality or cvCountry %} - {% if cvLocality %}{{ cvLocality }}{% endif %}{% if cvLocality and cvCountry %}, {% endif %}{% if cvCountry %}{{ cvCountry }}{% endif %} - {% endif %} - {% if cvOrg %} - {{ cvOrg }} - {% endif %} - {% if cvUrl %} - {{ cvUrl | replace("https://", "") | replace("http://", "") }} - {% endif %} - {% if cvEmail %} - {{ cvEmail }} - {% endif %} - {% if cvKeyUrl %} - PGP Key - {% endif %} -
- {% endif %} -
-
-
-{% endif %} - -{# Layout wrapper #} -{% if layout == "single-column" %} - - {# Single column — no sidebar, full width sections #} -
- {% for section in cvPageConfig.sections %} - {% include "components/homepage-section.njk" %} - {% endfor %} -
- -{% elif layout == "two-column" and hasSidebar %} - - {# Two column — sections + sidebar #} -
-
-
- {% for section in cvPageConfig.sections %} - {% include "components/homepage-section.njk" %} - {% endfor %} -
-
- -
- -{% elif layout == "full-width-hero" %} - - {# Full width hero (already rendered above), then two-column below #} - {% if hasSidebar %} -
-
-
- {% for section in cvPageConfig.sections %} - {% include "components/homepage-section.njk" %} - {% endfor %} -
-
- -
- {% else %} -
- {% for section in cvPageConfig.sections %} - {% include "components/homepage-section.njk" %} - {% endfor %} -
- {% endif %} - -{% else %} - - {# Fallback — two-column without sidebar, or unknown layout #} -
- {% for section in cvPageConfig.sections %} - {% include "components/homepage-section.njk" %} - {% endfor %} -
- -{% endif %} - -{# Last Updated #} -{% if cv.lastUpdated %} -

- Last updated: -

-{% endif %} - -{# Footer — rendered after the main layout, full width #} -{% include "components/cv-footer.njk" %} diff --git a/theme/_includes/components/cv-footer.njk b/theme/_includes/components/cv-footer.njk deleted file mode 100644 index c86957e..0000000 --- a/theme/_includes/components/cv-footer.njk +++ /dev/null @@ -1,26 +0,0 @@ -{# CV Page Builder Footer — renders footer items in a responsive 3-column grid #} -{% if cvPageConfig.footer and cvPageConfig.footer.length %} -
-
- {% for section in cvPageConfig.footer %} - {% if section.type == "custom-html" %} - {% set sectionConfig = section.config or {} %} -
- {% if sectionConfig.title %} -

{{ sectionConfig.title }}

- {% endif %} - {% if sectionConfig.content %} -
- {{ sectionConfig.content | safe }} -
- {% endif %} -
- {% else %} -
- {% include "components/homepage-section.njk" %} -
- {% endif %} - {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/cv-sidebar.njk b/theme/_includes/components/cv-sidebar.njk deleted file mode 100644 index 5b4d164..0000000 --- a/theme/_includes/components/cv-sidebar.njk +++ /dev/null @@ -1,45 +0,0 @@ -{# CV Page Builder Sidebar — renders widgets from cvPageConfig.sidebar #} -{% if cvPageConfig.sidebar and cvPageConfig.sidebar.length %} - {% for widget in cvPageConfig.sidebar %} - {% if widget.type == "recent-comments" or widget.type == "categories" or widget.type == "post-categories" %} - {# Hidden sidebar widgets by request #} - {% elif widget.type == "author-card" %} - {% include "components/widgets/author-card.njk" %} - {% elif widget.type == "social-activity" %} - {% include "components/widgets/social-activity.njk" %} - {% elif widget.type == "github-repos" %} - {% include "components/widgets/github-repos.njk" %} - {% elif widget.type == "funkwhale" %} - {% include "components/widgets/funkwhale.njk" %} - {% elif widget.type == "recent-posts" %} - {% include "components/widgets/recent-posts.njk" %} - {% elif widget.type == "blogroll" %} - {% include "components/widgets/blogroll.njk" %} - {% elif widget.type == "feedland" %} - {% include "components/widgets/feedland.njk" %} - {% elif widget.type == "categories" %} - {% include "components/widgets/categories.njk" %} - {% elif widget.type == "search" %} - {% include "components/widgets/search.njk" %} - {% elif widget.type == "webmentions" %} - {% include "components/widgets/webmentions.njk" %} - {% elif widget.type == "custom-html" %} - {# Custom content widget #} - {% set wConfig = widget.config or {} %} - -
- {% if wConfig.title %} -

{{ wConfig.title }}

- {% endif %} - {% if wConfig.content %} -
- {{ wConfig.content | safe }} -
- {% endif %} -
-
- {% else %} - - {% endif %} - {% endfor %} -{% endif %} diff --git a/theme/_includes/components/empty-collection.njk b/theme/_includes/components/empty-collection.njk deleted file mode 100644 index 8198853..0000000 --- a/theme/_includes/components/empty-collection.njk +++ /dev/null @@ -1,27 +0,0 @@ -{# 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/theme/_includes/components/fediverse-modal.njk b/theme/_includes/components/fediverse-modal.njk deleted file mode 100644 index 30699e0..0000000 --- a/theme/_includes/components/fediverse-modal.njk +++ /dev/null @@ -1,81 +0,0 @@ -{# Shared fediverse instance picker modal #} -{# Used by post.njk (interact), fediverse-follow.njk (follow), share.njk (share) #} -{# Requires: modalTitle, modalDescription variables set before include #} - diff --git a/theme/_includes/components/funkwhale-stats-content.njk b/theme/_includes/components/funkwhale-stats-content.njk deleted file mode 100644 index b7123a2..0000000 --- a/theme/_includes/components/funkwhale-stats-content.njk +++ /dev/null @@ -1,66 +0,0 @@ -{# Stats Summary Cards #} -{% if summary %} -
-
- {{ summary.totalPlays or 0 }} - Plays -
-
- {{ summary.uniqueTracks or 0 }} - Tracks -
-
- {{ summary.uniqueArtists or 0 }} - Artists -
-
- {{ summary.totalDurationFormatted or '0m' }} - Listened -
-
-{% endif %} - -{# Top Artists #} -{% if topArtists and topArtists.length %} -
-

Top Artists

-
- {% for artist in topArtists | head(5) %} -
- {{ loop.index }} - {{ artist.name }} - {{ artist.playCount }} plays -
- {% endfor %} -
-
-{% endif %} - -{# Top Albums #} -{% if topAlbums and topAlbums.length %} -
-

Top Albums

-
- {% for album in topAlbums | head(5) %} -
- {% if album.coverUrl %} - - {% else %} -
- - - -
- {% endif %} -

{{ album.title }}

-

{{ album.artist }}

-

{{ album.playCount }} plays

-
- {% endfor %} -
-
-{% endif %} - -{% if not summary and not topArtists and not topAlbums %} -

No statistics available for this period.

-{% endif %} diff --git a/theme/_includes/components/h-card.njk b/theme/_includes/components/h-card.njk deleted file mode 100644 index dd1d4a7..0000000 --- a/theme/_includes/components/h-card.njk +++ /dev/null @@ -1,115 +0,0 @@ -{# h-card - IndieWeb identity microformat #} -{# See: https://microformats.org/wiki/h-card #} -{# - This is the canonical h-card component for the site. - Include in sidebar widgets, author cards, etc. -#} -{% set id = homepageConfig.identity if (homepageConfig and homepageConfig.identity) else {} %} -{% set authorName = id.name if (id.name is defined) else site.author.name %} -{% set authorAvatar = id.avatar if (id.avatar is defined) else site.author.avatar %} -{% set authorTitle = id.title if (id.title is defined) else site.author.title %} -{% set authorBio = id.bio if (id.bio is defined) else site.author.bio %} -{% set authorUrl = id.url if (id.url is defined and id.url) else site.author.url %} -{% set authorPronoun = id.pronoun if (id.pronoun is defined) else site.author.pronoun %} -{% set authorLocality = id.locality if (id.locality is defined) else site.author.locality %} -{% set authorCountry = id.country if (id.country is defined) else site.author.country %} -{% set authorLocation = id.location if (id.location is defined) else site.author.location %} -{% set authorOrg = id.org if (id.org is defined) else site.author.org %} -{% set authorEmail = id.email if (id.email is defined) else site.author.email %} -{% set authorKeyUrl = id.keyUrl if (id.keyUrl is defined) else site.author.keyUrl %} -{% set authorCategories = id.categories if (id.categories is defined) else site.author.categories %} -{% set socialLinks = id.social if (id.social is defined) else site.social %} - -
- {# Hidden u-photo for reliable microformat parsing (some parsers struggle with img inside links) #} - - -
- -
- - {{ authorName }} - - {% if authorPronoun %} - ({{ authorPronoun }}) - {% endif %} - {% if authorTitle %} -

{{ authorTitle }}

- {% endif %} - {# Structured address #} -

- {% if authorLocality %} - {{ authorLocality }}{% if authorCountry %}, {% endif %} - {% endif %} - {% if authorCountry %} - {{ authorCountry }} - {% endif %} - {# Fallback to legacy location field #} - {% if not authorLocality and authorLocation %} - {{ authorLocation }} - {% endif %} -

-
-
- - {# Bio #} - {% if authorBio %} -

{{ authorBio }}

- {% endif %} - - {# Organization #} - {% if authorOrg %} -

- {{ authorOrg }} -

- {% endif %} - - {# Email and PGP Key #} -
- {% if authorEmail %} - {# Display text obfuscated to deter spam harvesters; href kept plain for browser compatibility #} - - ✉️ {{ authorEmail | obfuscateEmail | safe }} - - {% endif %} - {% if authorKeyUrl %} - - 🔐 PGP Key - - {% endif %} -
- - {# Categories / Skills #} - {% if authorCategories and authorCategories.length %} -
- {% for category in authorCategories %} - {{ category }} - {% endfor %} -
- {% endif %} - - {# Social links with rel="me" - critical for IndieWeb identity verification #} - {% from "components/social-icon.njk" import socialIcon %} - {% if socialLinks and socialLinks.length %} - - {% endif %} -
diff --git a/theme/_includes/components/homepage-builder.njk b/theme/_includes/components/homepage-builder.njk deleted file mode 100644 index b0ba263..0000000 --- a/theme/_includes/components/homepage-builder.njk +++ /dev/null @@ -1,86 +0,0 @@ -{# - Homepage Builder - renders configured layout, sections, and sidebar - from homepageConfig (written by indiekit-endpoint-homepage plugin) -#} - -{% set layout = homepageConfig.layout or "two-column" %} -{% set hasSidebar = homepageConfig.sidebar and homepageConfig.sidebar.length %} - -{# Hero — rendered before layout wrapper when enabled #} -{% if homepageConfig.hero and homepageConfig.hero.enabled %} - {% include "components/sections/hero.njk" %} -{% endif %} - -{# Layout wrapper #} -{% if layout == "single-column" %} - - {# Single column — no sidebar, full width sections #} -
- {% for section in homepageConfig.sections %} - {% if section.type != "hero" %} - {% include "components/homepage-section.njk" %} - {% endif %} - {% endfor %} -
- -{% elif layout == "two-column" and hasSidebar %} - - {# Two column — sections + sidebar #} -
-
-
- {% for section in homepageConfig.sections %} - {% if section.type != "hero" %} - {% include "components/homepage-section.njk" %} - {% endif %} - {% endfor %} -
-
- -
- -{% elif layout == "full-width-hero" %} - - {# Full width hero (already rendered above), then two-column below #} - {% if hasSidebar %} -
-
-
- {% for section in homepageConfig.sections %} - {% if section.type != "hero" %} - {% include "components/homepage-section.njk" %} - {% endif %} - {% endfor %} -
-
- -
- {% else %} -
- {% for section in homepageConfig.sections %} - {% if section.type != "hero" %} - {% include "components/homepage-section.njk" %} - {% endif %} - {% endfor %} -
- {% endif %} - -{% else %} - - {# Fallback — two-column without sidebar, or unknown layout #} -
- {% for section in homepageConfig.sections %} - {% if section.type != "hero" %} - {% include "components/homepage-section.njk" %} - {% endif %} - {% endfor %} -
- -{% endif %} - -{# Footer — rendered after the main layout, full width #} -{% include "components/homepage-footer.njk" %} diff --git a/theme/_includes/components/homepage-footer.njk b/theme/_includes/components/homepage-footer.njk deleted file mode 100644 index d6f917b..0000000 --- a/theme/_includes/components/homepage-footer.njk +++ /dev/null @@ -1,26 +0,0 @@ -{# Homepage Builder Footer — renders footer items in a responsive 3-column grid #} -{% if homepageConfig.footer and homepageConfig.footer.length %} -
-
- {% for section in homepageConfig.footer %} - {% if section.type == "custom-html" %} - {% set sectionConfig = section.config or {} %} -
- {% if sectionConfig.title %} -

{{ sectionConfig.title }}

- {% endif %} - {% if sectionConfig.content %} -
- {{ sectionConfig.content | safe }} -
- {% endif %} -
- {% else %} -
- {% include "components/homepage-section.njk" %} -
- {% endif %} - {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/homepage-section.njk b/theme/_includes/components/homepage-section.njk deleted file mode 100644 index e7b8ee0..0000000 --- a/theme/_includes/components/homepage-section.njk +++ /dev/null @@ -1,56 +0,0 @@ -{# Homepage Section Dispatcher — maps section.type to the right partial #} -{% if section.type == "featured-posts" %} - {% include "components/sections/featured-posts.njk" %} -{% elif section.type == "recent-posts" %} - {% include "components/sections/recent-posts.njk" %} -{% elif section.type == "custom-html" %} - {% include "components/sections/custom-html.njk" %} -{% elif section.type == "cv-experience" %} - {% include "components/sections/cv-experience.njk" ignore missing %} -{% elif section.type == "cv-projects" %} - {% include "components/sections/cv-projects.njk" ignore missing %} -{% elif section.type == "cv-projects-personal" %} - {% include "components/sections/cv-projects-personal.njk" ignore missing %} -{% elif section.type == "cv-projects-work" %} - {% include "components/sections/cv-projects-work.njk" ignore missing %} -{% elif section.type == "cv-skills" %} - {% include "components/sections/cv-skills.njk" ignore missing %} -{% elif section.type == "cv-education" %} - {% include "components/sections/cv-education.njk" ignore missing %} -{% elif section.type == "cv-interests" %} - {% include "components/sections/cv-interests.njk" ignore missing %} -{% elif section.type == "cv-experience-personal" %} - {% include "components/sections/cv-experience-personal.njk" ignore missing %} -{% elif section.type == "cv-experience-work" %} - {% include "components/sections/cv-experience-work.njk" ignore missing %} -{% elif section.type == "cv-education-personal" %} - {% include "components/sections/cv-education-personal.njk" ignore missing %} -{% elif section.type == "cv-education-work" %} - {% include "components/sections/cv-education-work.njk" ignore missing %} -{% elif section.type == "cv-skills-personal" %} - {% include "components/sections/cv-skills-personal.njk" ignore missing %} -{% elif section.type == "cv-skills-work" %} - {% include "components/sections/cv-skills-work.njk" ignore missing %} -{% elif section.type == "cv-interests-personal" %} - {% include "components/sections/cv-interests-personal.njk" ignore missing %} -{% elif section.type == "cv-interests-work" %} - {% include "components/sections/cv-interests-work.njk" ignore missing %} -{% elif section.type == "cv-languages" %} - {% include "components/sections/cv-languages.njk" ignore missing %} -{% elif section.type == "blogroll" %} - {% include "components/sections/blogroll.njk" ignore missing %} -{% elif section.type == "podroll" %} - {% include "components/sections/podroll.njk" ignore missing %} -{% elif section.type == "github-activity" %} - {% include "components/sections/github-activity.njk" ignore missing %} -{% elif section.type == "youtube" %} - {% include "components/sections/youtube.njk" ignore missing %} -{% elif section.type == "funkwhale" %} - {% include "components/sections/funkwhale.njk" ignore missing %} -{% elif section.type == "lastfm" %} - {% include "components/sections/lastfm.njk" ignore missing %} -{% elif section.type == "posting-activity" %} - {% include "components/sections/posting-activity.njk" ignore missing %} -{% else %} - -{% endif %} diff --git a/theme/_includes/components/homepage-sidebar.njk b/theme/_includes/components/homepage-sidebar.njk deleted file mode 100644 index 9601cd5..0000000 --- a/theme/_includes/components/homepage-sidebar.njk +++ /dev/null @@ -1,147 +0,0 @@ -{# Homepage Builder Sidebar — renders widgets from homepageConfig.sidebar #} -{# Each widget is wrapped in a collapsible container with localStorage persistence #} -{% from "components/icon.njk" import icon %} - -{% if homepageConfig.sidebar and homepageConfig.sidebar.length %} - {% for widget in homepageConfig.sidebar %} - {% set isHomepageAuthorWidget = widget.type == "author-card" or widget.type == "author-card-compact" %} - {% if not isHomepageAuthorWidget %} - - {# Resolve widget title #} - {% if widget.type == "search" %}{% set widgetTitle = "Search" %} - {% elif widget.type == "social-activity" %}{% set widgetTitle = "Social Activity" %} - {% elif widget.type == "github-repos" %}{% set widgetTitle = "GitHub" %} - {% elif widget.type == "funkwhale" %}{% set widgetTitle = "Listening" %} - {% elif widget.type == "recent-posts" %}{% set widgetTitle = "Recent Posts" %} - {% elif widget.type == "blogroll" %}{% set widgetTitle = "Blogroll" %} - {% elif widget.type == "feedland" %}{% set widgetTitle = "FeedLand" %} - {% elif widget.type == "categories" %}{% set widgetTitle = "Categories" %} - {% elif widget.type == "webmentions" %}{% set widgetTitle = "Webmentions" %} - {% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %} - {% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %} - {% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %} - {% elif widget.type == "ai-usage" %}{% set widgetTitle = "AI Transparency" %} - {% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %} - {% else %}{% set widgetTitle = widget.type %} - {% endif %} - - {# Resolve widget icon and accent border #} - {% if widget.type == "social-activity" %} - {% set widgetIcon = "globe" %}{% set widgetIconClass = "w-5 h-5 text-[#0085ff]" %}{% set widgetBorder = "border-l-[3px] border-l-[#0085ff]" %} - {% elif widget.type == "github-repos" %} - {% set widgetIcon = "github" %}{% set widgetIconClass = "w-5 h-5 text-surface-800 dark:text-surface-200" %}{% set widgetBorder = "border-l-[3px] border-l-surface-400 dark:border-l-surface-500" %} - {% elif widget.type == "funkwhale" %} - {% set widgetIcon = "headphones" %}{% set widgetIconClass = "w-5 h-5 text-purple-500" %}{% set widgetBorder = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %} - {% elif widget.type == "blogroll" %} - {% set widgetIcon = "book-open" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif widget.type == "feedland" %} - {% set widgetIcon = "rss" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif widget.type == "subscribe" %} - {% set widgetIcon = "rss" %}{% set widgetIconClass = "w-5 h-5 text-orange-500" %}{% set widgetBorder = "border-l-[3px] border-l-orange-400 dark:border-l-orange-500" %} - {% elif widget.type == "fediverse-follow" %} - {% set widgetIcon = "user-plus" %}{% set widgetIconClass = "w-5 h-5 text-[#a730b8]" %}{% set widgetBorder = "border-l-[3px] border-l-[#a730b8]" %} - {% elif widget.type == "author-card" %} - {% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "recent-posts" %} - {% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "categories" %} - {% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "recent-comments" %} - {% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "search" %} - {% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "webmentions" %} - {% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "ai-usage" %} - {% set widgetIcon = "zap" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% else %} - {% set widgetIcon = "" %}{% set widgetIconClass = "" %}{% set widgetBorder = "" %} - {% endif %} - - {% set widgetKey = "widget-" + widget.type + "-" + loop.index0 %} - {% set defaultOpen = "true" if loop.index0 < 3 else "false" %} - - {# Collapsible wrapper — Alpine.js handles toggle, localStorage persists state #} -
-
- - -
- {# Widget content — inner .widget provides padding, inner title hidden by CSS #} - {% if widget.type == "author-card" %} - {% include "components/widgets/author-card.njk" %} - {% elif widget.type == "social-activity" %} - {% include "components/widgets/social-activity.njk" %} - {% elif widget.type == "github-repos" %} - {% include "components/widgets/github-repos.njk" %} - {% elif widget.type == "funkwhale" %} - {% include "components/widgets/funkwhale.njk" %} - {% elif widget.type == "recent-posts" %} - {% include "components/widgets/recent-posts.njk" %} - {% elif widget.type == "blogroll" %} - {% include "components/widgets/blogroll.njk" %} - {% elif widget.type == "feedland" %} - {% include "components/widgets/feedland.njk" %} - {% elif widget.type == "categories" %} - {% include "components/widgets/categories.njk" %} - {% elif widget.type == "search" %} - {% include "components/widgets/search.njk" %} - {% elif widget.type == "webmentions" %} - {% include "components/widgets/webmentions.njk" %} - {% elif widget.type == "recent-comments" %} - {% include "components/widgets/recent-comments.njk" %} - {% elif widget.type == "fediverse-follow" %} - {% include "components/widgets/fediverse-follow.njk" %} - {% elif widget.type == "ai-usage" %} - {% include "components/widgets/ai-usage.njk" ignore missing %} - {% elif widget.type == "custom-html" %} - {% set wConfig = widget.config or {} %} - -
- {% if wConfig.content %} -
- {{ wConfig.content | safe }} -
- {% endif %} -
-
- {% else %} - - {% endif %} -
-
-
- - {% endif %} - - {% endfor %} -{% endif %} diff --git a/theme/_includes/components/icon.njk b/theme/_includes/components/icon.njk deleted file mode 100644 index d538ddd..0000000 --- a/theme/_includes/components/icon.njk +++ /dev/null @@ -1,67 +0,0 @@ -{# - Centralized UI icon macro - Usage: {% from "components/icon.njk" import icon %} - {{ icon("heart", "w-5 h-5 text-red-500") }} - - All icons use stroke-width="2" unless they are filled icons. - Default size: w-5 h-5 (override via cssClass parameter) -#} - -{% macro icon(name, cssClass) %} -{% set cls = cssClass or "w-5 h-5" %} -{%- if name == "heart" -%} - -{%- elif name == "bookmark" -%} - -{%- elif name == "repost" -%} - -{%- elif name == "reply" -%} - -{%- elif name == "camera" -%} - -{%- elif name == "article" -%} - -{%- elif name == "note" -%} - -{%- elif name == "music" -%} - -{%- elif name == "tag" -%} - -{%- elif name == "rss" -%} - -{%- elif name == "chat" -%} - -{%- elif name == "user" -%} - -{%- elif name == "search" -%} - -{%- elif name == "star" -%} - -{%- elif name == "external-link" -%} - -{%- elif name == "chevron-down" -%} - -{%- elif name == "chevron-right" -%} - -{%- elif name == "globe" -%} - -{%- elif name == "github" -%} - -{%- elif name == "list" -%} - -{%- elif name == "share" -%} - -{%- elif name == "book-open" -%} - -{%- elif name == "headphones" -%} - -{%- elif name == "mail" -%} - -{%- elif name == "podcast" -%} - -{%- elif name == "user-plus" -%} - -{%- else -%} - -{%- endif -%} -{% endmacro %} diff --git a/theme/_includes/components/post-navigation.njk b/theme/_includes/components/post-navigation.njk deleted file mode 100644 index 0103704..0000000 --- a/theme/_includes/components/post-navigation.njk +++ /dev/null @@ -1,103 +0,0 @@ -{# Post Navigation - Previous/Next (image-first, inspired by zachleat.com) #} -{% set _prevPost = collections.posts | previousInCollection(page) %} -{% set _nextPost = collections.posts | nextInCollection(page) %} - -{% if _prevPost or _nextPost %} - -{% endif %} diff --git a/theme/_includes/components/reply-context.njk b/theme/_includes/components/reply-context.njk deleted file mode 100644 index bfa2781..0000000 --- a/theme/_includes/components/reply-context.njk +++ /dev/null @@ -1,74 +0,0 @@ -{# Reply Context Component #} -{# Displays rich context for replies, likes, reposts, and bookmarks #} -{# Uses h-cite microformat for citing external content #} -{# Includes unfurl card for rich link preview (OpenGraph metadata) #} - -{# Support both camelCase (Indiekit Eleventy preset) and underscore (legacy) property names #} -{% set replyTo = inReplyTo or in_reply_to %} -{% set likedUrl = likeOf or like_of %} -{% set repostedUrl = repostOf or repost_of %} -{% set bookmarkedUrl = bookmarkOf or bookmark_of %} - -{% if replyTo or likedUrl or repostedUrl or bookmarkedUrl %} - -{% endif %} diff --git a/theme/_includes/components/sections/custom-html.njk b/theme/_includes/components/sections/custom-html.njk deleted file mode 100644 index 5045160..0000000 --- a/theme/_includes/components/sections/custom-html.njk +++ /dev/null @@ -1,18 +0,0 @@ -{# - Custom HTML Section - freeform HTML/markdown content block - Rendered by homepage-builder when custom-html section is configured -#} - -{% set sectionConfig = section.config or {} %} - -
- {% if sectionConfig.title %} -

- {{ sectionConfig.title }} -

- {% endif %} - -
- {{ sectionConfig.content | safe }} -
-
diff --git a/theme/_includes/components/sections/cv-education-personal.njk b/theme/_includes/components/sections/cv-education-personal.njk deleted file mode 100644 index fd937a7..0000000 --- a/theme/_includes/components/sections/cv-education-personal.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "personal" %} -{% include "components/sections/cv-education.njk" %} diff --git a/theme/_includes/components/sections/cv-education-work.njk b/theme/_includes/components/sections/cv-education-work.njk deleted file mode 100644 index 6d2e3ea..0000000 --- a/theme/_includes/components/sections/cv-education-work.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "work" %} -{% include "components/sections/cv-education.njk" %} diff --git a/theme/_includes/components/sections/cv-education.njk b/theme/_includes/components/sections/cv-education.njk deleted file mode 100644 index 3ff1ecf..0000000 --- a/theme/_includes/components/sections/cv-education.njk +++ /dev/null @@ -1,88 +0,0 @@ -{# - CV Education Section - collapsible education cards (accordion) - Data fetched from /cv/data.json via homepage plugin - Each card gets a distinct color via cycling palette -#} - -{% set hasEducation = cv and cv.education and cv.education.length %} - -{% if hasEducation %} -
-

- {{ section.config.title or "Education" }} -

- -
- {% for item in cv.education %} - {% if not filterType or item.educationType == filterType or not item.educationType %} - {% set ci = loop.index0 % 8 %} -
- {# Summary row — always visible, clickable #} - - - {# Detail section — collapsible #} -
- {% if item.startDate %} -

- {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %} -

- {% elif item.year %} -

{{ item.year }}

- {% endif %} - - {% if item.description %} -

{{ item.description }}

- {% endif %} -
-
- {% endif %} - {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/cv-experience-personal.njk b/theme/_includes/components/sections/cv-experience-personal.njk deleted file mode 100644 index 7d4c858..0000000 --- a/theme/_includes/components/sections/cv-experience-personal.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "personal" %} -{% include "components/sections/cv-experience.njk" %} diff --git a/theme/_includes/components/sections/cv-experience-work.njk b/theme/_includes/components/sections/cv-experience-work.njk deleted file mode 100644 index 57b91b3..0000000 --- a/theme/_includes/components/sections/cv-experience-work.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "work" %} -{% include "components/sections/cv-experience.njk" %} diff --git a/theme/_includes/components/sections/cv-experience.njk b/theme/_includes/components/sections/cv-experience.njk deleted file mode 100644 index b9ad71d..0000000 --- a/theme/_includes/components/sections/cv-experience.njk +++ /dev/null @@ -1,48 +0,0 @@ -{# - CV Experience Section - work experience timeline - Data fetched from /cv/data.json via homepage plugin -#} - -{% set sectionConfig = section.config or {} %} -{% set maxItems = sectionConfig.maxItems or 10 %} -{% set showHighlights = sectionConfig.showHighlights if sectionConfig.showHighlights is defined else true %} - -{% if cv and cv.experience and cv.experience.length %} -
-

- {{ sectionConfig.title or "Experience" }} -

- -
- {% for item in cv.experience | head(maxItems) %} - {% if not filterType or item.experienceType == filterType or not item.experienceType %} -
-
-

{{ item.title }}

-

- {{ item.company }}{% if item.location %} · {{ item.location }}{% endif %} - {% if item.type %} · {{ item.type }}{% endif %} -

- {% if item.startDate %} -

- {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %} -

- {% endif %} - {% if item.description %} -

{{ item.description }}

- {% endif %} - {% if showHighlights and item.highlights and item.highlights.length %} -
- {% for h in item.highlights %} - - {{ h }} - - {% endfor %} -
- {% endif %} -
- {% endif %} - {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/cv-interests-personal.njk b/theme/_includes/components/sections/cv-interests-personal.njk deleted file mode 100644 index 0d053a3..0000000 --- a/theme/_includes/components/sections/cv-interests-personal.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "personal" %} -{% include "components/sections/cv-interests.njk" %} diff --git a/theme/_includes/components/sections/cv-interests-work.njk b/theme/_includes/components/sections/cv-interests-work.njk deleted file mode 100644 index 7cbe270..0000000 --- a/theme/_includes/components/sections/cv-interests-work.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "work" %} -{% include "components/sections/cv-interests.njk" %} diff --git a/theme/_includes/components/sections/cv-interests.njk b/theme/_includes/components/sections/cv-interests.njk deleted file mode 100644 index 997f791..0000000 --- a/theme/_includes/components/sections/cv-interests.njk +++ /dev/null @@ -1,50 +0,0 @@ -{# - CV Interests Section - interests grouped by category - Data fetched from /cv/data.json via homepage plugin - Each family gets a distinct color via cycling palette -#} - -{% if cv and cv.interests and (cv.interests | dictsort | length) %} -
-

- {{ section.config.title or "Interests" }} -

- -
- {% for category, items in cv.interests %} - {% if not filterType or (cv.interestTypes and cv.interestTypes[category] == filterType) or not cv.interestTypes or not cv.interestTypes[category] %} - {# Cycle through 8 distinct colors per family using loop.index0 #} - {% set ci = loop.index0 % 8 %} -
-

- {{ category }} -

-
- {% for interest in items %} - {% if ci == 0 %} - - {% elif ci == 1 %} - - {% elif ci == 2 %} - - {% elif ci == 3 %} - - {% elif ci == 4 %} - - {% elif ci == 5 %} - - {% elif ci == 6 %} - - {% elif ci == 7 %} - - {% endif %} - {{ interest }} - - {% endfor %} -
-
- {% endif %} - {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/cv-languages.njk b/theme/_includes/components/sections/cv-languages.njk deleted file mode 100644 index 438905f..0000000 --- a/theme/_includes/components/sections/cv-languages.njk +++ /dev/null @@ -1,21 +0,0 @@ -{# - CV Languages Section - Data fetched from /cv/data.json via homepage plugin -#} - -{% if cv and cv.languages and cv.languages.length %} -
-

- {{ section.config.title or "Languages" }} -

- -
- {% for lang in cv.languages %} -
- {{ lang.name }} - {{ lang.level }} -
- {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/cv-projects-personal.njk b/theme/_includes/components/sections/cv-projects-personal.njk deleted file mode 100644 index e3e6ab0..0000000 --- a/theme/_includes/components/sections/cv-projects-personal.njk +++ /dev/null @@ -1,124 +0,0 @@ -{# - CV Personal Projects Section - collapsible project cards (accordion) - Filters projects by projectType == "personal" (or unset) - Data fetched from /cv/data.json via homepage plugin -#} - -{% set sectionConfig = section.config or {} %} -{% set maxItems = sectionConfig.maxItems or 10 %} -{% set showTechnologies = sectionConfig.showTechnologies if sectionConfig.showTechnologies is defined else true %} - -{% set personalProjects = [] %} -{% if cv and cv.projects %} - {% for item in cv.projects %} - {% if item.projectType == "personal" or not item.projectType %} - {% set personalProjects = (personalProjects.push(item), personalProjects) %} - {% endif %} - {% endfor %} -{% endif %} - -{% if personalProjects.length %} -
-

- {{ sectionConfig.title or "Personal Projects" }} -

- -
- {% for item in personalProjects | head(maxItems) %} - {% set ci = loop.index0 % 8 %} -
- {# Summary row — always visible, clickable #} - - - {# Detail section — collapsible #} -
- {% if item.startDate %} -

- {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %} -

- {% endif %} - - {% if item.description %} -

{{ item.description }}

- {% endif %} - - {% if showTechnologies and item.technologies and item.technologies.length %} -
- {% for tech in item.technologies %} - - {{ tech }} - - {% endfor %} -
- {% endif %} -
-
- {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/cv-projects-work.njk b/theme/_includes/components/sections/cv-projects-work.njk deleted file mode 100644 index 47d7014..0000000 --- a/theme/_includes/components/sections/cv-projects-work.njk +++ /dev/null @@ -1,124 +0,0 @@ -{# - CV Work Projects Section - collapsible project cards (accordion) - Filters projects by projectType == "work" - Data fetched from /cv/data.json via homepage plugin -#} - -{% set sectionConfig = section.config or {} %} -{% set maxItems = sectionConfig.maxItems or 10 %} -{% set showTechnologies = sectionConfig.showTechnologies if sectionConfig.showTechnologies is defined else true %} - -{% set workProjects = [] %} -{% if cv and cv.projects %} - {% for item in cv.projects %} - {% if item.projectType == "work" %} - {% set workProjects = (workProjects.push(item), workProjects) %} - {% endif %} - {% endfor %} -{% endif %} - -{% if workProjects.length %} -
-

- {{ sectionConfig.title or "Work Projects" }} -

- -
- {% for item in workProjects | head(maxItems) %} - {% set ci = loop.index0 % 8 %} -
- {# Summary row — always visible, clickable #} - - - {# Detail section — collapsible #} -
- {% if item.startDate %} -

- {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %} -

- {% endif %} - - {% if item.description %} -

{{ item.description }}

- {% endif %} - - {% if showTechnologies and item.technologies and item.technologies.length %} -
- {% for tech in item.technologies %} - - {{ tech }} - - {% endfor %} -
- {% endif %} -
-
- {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/cv-projects.njk b/theme/_includes/components/sections/cv-projects.njk deleted file mode 100644 index 99cc152..0000000 --- a/theme/_includes/components/sections/cv-projects.njk +++ /dev/null @@ -1,114 +0,0 @@ -{# - CV Projects Section - collapsible project cards (accordion) - Data fetched from /cv/data.json via homepage plugin -#} - -{% set sectionConfig = section.config or {} %} -{% set maxItems = sectionConfig.maxItems or 10 %} -{% set showTechnologies = sectionConfig.showTechnologies if sectionConfig.showTechnologies is defined else true %} - -{% if cv and cv.projects and cv.projects.length %} -
-

- {{ sectionConfig.title or "Projects" }} -

- -
- {% for item in cv.projects | head(maxItems) %} - {% set ci = loop.index0 % 8 %} -
- {# Summary row — always visible, clickable #} - - - {# Detail section — collapsible #} -
- {% if item.startDate %} -

- {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %} -

- {% endif %} - - {% if item.description %} -

{{ item.description }}

- {% endif %} - - {% if showTechnologies and item.technologies and item.technologies.length %} -
- {% for tech in item.technologies %} - - {{ tech }} - - {% endfor %} -
- {% endif %} -
-
- {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/cv-skills-personal.njk b/theme/_includes/components/sections/cv-skills-personal.njk deleted file mode 100644 index baa52be..0000000 --- a/theme/_includes/components/sections/cv-skills-personal.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "personal" %} -{% include "components/sections/cv-skills.njk" %} diff --git a/theme/_includes/components/sections/cv-skills-work.njk b/theme/_includes/components/sections/cv-skills-work.njk deleted file mode 100644 index cc1332a..0000000 --- a/theme/_includes/components/sections/cv-skills-work.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% set filterType = "work" %} -{% include "components/sections/cv-skills.njk" %} diff --git a/theme/_includes/components/sections/cv-skills.njk b/theme/_includes/components/sections/cv-skills.njk deleted file mode 100644 index 6b55cde..0000000 --- a/theme/_includes/components/sections/cv-skills.njk +++ /dev/null @@ -1,50 +0,0 @@ -{# - CV Skills Section - skills grouped by category - Data fetched from /cv/data.json via homepage plugin - Each family gets a distinct color via cycling palette -#} - -{% if cv and cv.skills and (cv.skills | dictsort | length) %} -
-

- {{ section.config.title or "Skills" }} -

- -
- {% for category, items in cv.skills %} - {% if not filterType or (cv.skillTypes and cv.skillTypes[category] == filterType) or not cv.skillTypes or not cv.skillTypes[category] %} - {# Cycle through 8 distinct colors per family using loop.index0 #} - {% set ci = loop.index0 % 8 %} -
-

- {{ category }} -

-
- {% for skill in items %} - {% if ci == 0 %} - - {% elif ci == 1 %} - - {% elif ci == 2 %} - - {% elif ci == 3 %} - - {% elif ci == 4 %} - - {% elif ci == 5 %} - - {% elif ci == 6 %} - - {% elif ci == 7 %} - - {% endif %} - {{ skill }} - - {% endfor %} -
-
- {% endif %} - {% endfor %} -
-
-{% endif %} diff --git a/theme/_includes/components/sections/featured-posts.njk b/theme/_includes/components/sections/featured-posts.njk deleted file mode 100644 index 730fdbe..0000000 --- a/theme/_includes/components/sections/featured-posts.njk +++ /dev/null @@ -1,259 +0,0 @@ -{# - Featured Posts Section - displays curated posts with `featured: true` frontmatter - Rendered by homepage-builder when featured-posts section is configured - Supports type-aware rendering for articles, notes, likes, bookmarks, reposts, replies, photos -#} - -{% set sectionConfig = section.config or {} %} -{% set maxItems = sectionConfig.maxItems or 6 %} -{% set showSummary = sectionConfig.showSummary if sectionConfig.showSummary is defined else true %} - -{% if collections.featuredPosts and collections.featuredPosts.length %} -
-

- - {{ sectionConfig.title or "Featured" }} -

- -
- {% for post in collections.featuredPosts | head(maxItems) %} - {# Detect post type from frontmatter properties #} - {% set likedUrl = post.data.likeOf or post.data.like_of %} - {% set bookmarkedUrl = post.data.bookmarkOf or post.data.bookmark_of %} - {% set repostedUrl = post.data.repostOf or post.data.repost_of %} - {% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %} - {% set hasPhotos = post.data.photo and post.data.photo.length %} - - {# Determine border color by post type #} - {% set borderClass = "" %} - {% if likedUrl %} - {% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %} - {% elif bookmarkedUrl %} - {% set borderClass = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif repostedUrl %} - {% set borderClass = "border-l-[3px] border-l-green-400 dark:border-l-green-500" %} - {% elif replyToUrl %} - {% set borderClass = "border-l-[3px] border-l-sky-400 dark:border-l-sky-500" %} - {% elif hasPhotos %} - {% set borderClass = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %} - {% else %} - {% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %} - {% endif %} - -
- - {% if likedUrl %} - {# ── Like card ── #} -
-
- -
-
-
- Liked - -
- {{ likedUrl | unfurlCard | safe }} - - {{ likedUrl }} - - {% if post.templateContent %} -
- {{ post.templateContent | safe }} -
- {% endif %} - Permalink -
-
- - {% elif bookmarkedUrl %} - {# ── Bookmark card ── #} -
-
- -
-
-
- Bookmarked - -
- {% if post.data.title %} -

- {{ post.data.title }} -

- {% endif %} - {{ bookmarkedUrl | unfurlCard | safe }} - - {{ bookmarkedUrl }} - - {% if post.templateContent %} -
- {{ post.templateContent | safe }} -
- {% endif %} - Permalink -
-
- - {% elif repostedUrl %} - {# ── Repost card ── #} -
-
- -
-
-
- Reposted - -
- {{ repostedUrl | unfurlCard | safe }} - - {{ repostedUrl }} - - {% if post.templateContent %} -
- {{ post.templateContent | safe }} -
- {% endif %} - Permalink -
-
- - {% elif replyToUrl %} - {# ── Reply card ── #} -
-
- -
-
-
- In reply to - -
- {{ replyToUrl | unfurlCard | safe }} - - {{ replyToUrl }} - - {% if post.templateContent %} -
- {{ post.templateContent | safe }} -
- {% endif %} - Permalink -
-
- - {% elif hasPhotos %} - {# ── Photo card ── #} -
-
- -
-
-
- Photo - -
- - {% if post.templateContent %} -
- {{ post.templateContent | safe }} -
- {% endif %} - Permalink -
-
- - {% elif post.data.title %} - {# ── Article/Page card ── #} -

- - {{ post.data.title }} - -

- {% if showSummary and post.templateContent %} -

- {{ post.templateContent | striptags | truncate(250) }} -

- {% endif %} -
- - {% if post.data.postType %} - - {{ post.data.postType }} - - {% endif %} -
- - {% else %} - {# ── Note card ── #} -
- - - - {% if post.data.postType %} - - {{ post.data.postType }} - - {% endif %} -
- {% if post.templateContent %} -
- {{ post.templateContent | safe }} -
- {% endif %} - - Permalink - - {% endif %} - -
- {% endfor %} -
- - {% if collections.featuredPosts.length > maxItems %} - - {% endif %} -
-{% endif %} diff --git a/theme/_includes/components/sections/hero.njk b/theme/_includes/components/sections/hero.njk deleted file mode 100644 index e8ff3df..0000000 --- a/theme/_includes/components/sections/hero.njk +++ /dev/null @@ -1,68 +0,0 @@ -{# - Hero Section - author intro with avatar, name, title, bio - Rendered by homepage-builder when hero is enabled -#} - -{% set heroConfig = homepageConfig.hero or {} %} -{% set id = homepageConfig.identity if (homepageConfig and homepageConfig.identity) else {} %} -{% set authorName = id.name if (id.name is defined) else site.author.name %} -{% set authorAvatar = id.avatar if (id.avatar is defined) else site.author.avatar %} -{% set authorTitle = id.title if (id.title is defined) else site.author.title %} -{% set authorBio = id.bio if (id.bio is defined) else site.author.bio %} -{% set siteDescription = id.description if (id.description is defined) else site.description %} -{% set socialLinks = id.social if (id.social is defined) else site.social %} - -
-
- {# Avatar #} - {% if heroConfig.showAvatar != false %} - {{ authorName }} - {% endif %} - - {# Introduction #} -
-

- {{ authorName }} -

- {% if authorTitle %} -

- {{ authorTitle }} -

- {% endif %} - {% if authorBio %} -

- {{ authorBio }} -

- {% endif %} - {% if siteDescription %} -

- {{ siteDescription }} - Read more → -

- {% endif %} - - {# Social Links #} - {% from "components/social-icon.njk" import socialIcon, socialIconColorClass %} - {% if heroConfig.showSocial != false and socialLinks %} -
- {% for link in socialLinks %} - - {{ socialIcon(link.icon, "w-5 h-5") }} - {{ link.name }} - - {% endfor %} -
- {% endif %} -
-
-
diff --git a/theme/_includes/components/sections/posting-activity.njk b/theme/_includes/components/sections/posting-activity.njk deleted file mode 100644 index 976dba5..0000000 --- a/theme/_includes/components/sections/posting-activity.njk +++ /dev/null @@ -1,24 +0,0 @@ -{# Posting Activity Section — configurable post-graph contribution grid #} -{% set sectionConfig = section.config or {} %} -{% set graphTitle = sectionConfig.title or "Posting Activity" %} - -{% if collections.posts and collections.posts.length %} -
-

- {{ graphTitle }} -

- {% set graphOptions = {} %} - {% if sectionConfig.years and sectionConfig.years.length %} - {% set graphOptions = { only: sectionConfig.years } %} - {% elif sectionConfig.limit %} - {% set graphOptions = { limit: sectionConfig.limit } %} - {% endif %} - {% postGraph collections.posts, graphOptions %} - - View full history - - - - -
-{% endif %} diff --git a/theme/_includes/components/sections/recent-posts.njk b/theme/_includes/components/sections/recent-posts.njk deleted file mode 100644 index 36c85cf..0000000 --- a/theme/_includes/components/sections/recent-posts.njk +++ /dev/null @@ -1,338 +0,0 @@ -{# - Recent Posts Section - displays latest posts from any collection - Rendered by homepage-builder when recent-posts section is configured - Redesigned to match the visual language used on /blog/ -#} - -{% set sectionConfig = section.config or {} %} -{% set maxItems = sectionConfig.maxItems or 5 %} -{% set showSummary = sectionConfig.showSummary if sectionConfig.showSummary is defined else true %} -{% set primaryPosts = collections.posts if (collections and collections.posts) else [] %} -{% set fallbackRecentPosts = collections.recentPosts if (collections and collections.recentPosts) else [] %} -{% set listedPosts = primaryPosts | excludeUnlistedPosts %} -{% if not (listedPosts and listedPosts.length) %} - {% set listedPosts = fallbackRecentPosts | excludeUnlistedPosts %} -{% endif %} - -{% if listedPosts and listedPosts.length %} -
-

- {{ sectionConfig.title or "Recent Posts" }} -

- -
    - {% for post in listedPosts | head(maxItems) %} - {# Detect post type from frontmatter properties #} - {% set likedUrl = post.data.likeOf or post.data.like_of %} - {% set bookmarkedUrl = post.data.bookmarkOf or post.data.bookmark_of %} - {% set repostedUrl = post.data.repostOf or post.data.repost_of %} - {% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %} - {% set hasPhotos = post.data.photo and post.data.photo.length %} - - {% set borderClass = "" %} - {% if likedUrl %} - {% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %} - {% elif bookmarkedUrl %} - {% set borderClass = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif repostedUrl %} - {% set borderClass = "border-l-[3px] border-l-green-400 dark:border-l-green-500" %} - {% elif replyToUrl %} - {% set borderClass = "border-l-[3px] border-l-sky-400 dark:border-l-sky-500" %} - {% elif hasPhotos %} - {% set borderClass = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %} - {% elif post.data.title %} - {% set borderClass = "border-l-[3px] border-l-indigo-400 dark:border-l-indigo-500" %} - {% else %} - {% set borderClass = "border-l-[3px] border-l-teal-400 dark:border-l-teal-500" %} - {% endif %} - -
  • - {% if likedUrl %} - {# Like #} -
    -
    - -
    -
    - - {{ likedUrl | unfurlCard | safe }} - - {{ likedUrl }} - - {% if post.templateContent %} -
    - {{ post.templateContent | safe }} -
    - {% endif %} - Permalink -
    -
    - - {% elif bookmarkedUrl %} - {# Bookmark #} -
    -
    - -
    -
    - - {% if post.data.title %} -

    - {{ post.data.title }} -

    - {% endif %} - {{ bookmarkedUrl | unfurlCard | safe }} - - {{ bookmarkedUrl }} - - {% if post.templateContent %} -
    - {{ post.templateContent | safe }} -
    - {% endif %} - Permalink -
    -
    - - {% elif repostedUrl %} - {# Repost #} -
    -
    - -
    -
    - - {{ repostedUrl | unfurlCard | safe }} - - {{ repostedUrl }} - - {% if post.templateContent %} -
    - {{ post.templateContent | safe }} -
    - {% endif %} - Permalink -
    -
    - - {% elif replyToUrl %} - {# Reply #} -
    -
    - -
    -
    - - {{ replyToUrl | unfurlCard | safe }} - - {{ replyToUrl }} - - {% if post.templateContent %} -
    - {{ post.templateContent | safe }} -
    - {% endif %} - Permalink -
    -
    - - {% elif hasPhotos %} - {# Photo #} -
    -
    - -
    -
    - - - {% if post.templateContent %} -
    - {{ post.templateContent | safe }} -
    - {% endif %} - Permalink -
    -
    - - {% elif post.data.title and (post.data.title | trim) %} - {# Article #} -
    -

    - - {{ post.data.title }} - -

    - -
    - {% if showSummary and post.templateContent %} -

    - {{ post.templateContent | striptags | truncate(250) }} -

    - {% endif %} - - Read more → - - - {% else %} - {# Note #} -
    - - - - {% if post.data.category %} - - {% if post.data.category is string %} - {{ post.data.category }} - {% else %} - {% for cat in post.data.category %} - {{ cat }} - {% endfor %} - {% endif %} - - {% endif %} -
    -
    - {% if post.content and post.content.html %} - {{ post.content.html | safe }} - {% else %} - {{ post.templateContent | safe }} - {% endif %} -
    - - {% endif %} -
  • - {% endfor %} -
- - {% if sectionConfig.showViewAll != false %} - - {{ sectionConfig.viewAllText or "View all posts" }} - - - - - {% endif %} -
-{% endif %} diff --git a/theme/_includes/components/sidebar.njk b/theme/_includes/components/sidebar.njk deleted file mode 100644 index 78b02ff..0000000 --- a/theme/_includes/components/sidebar.njk +++ /dev/null @@ -1,261 +0,0 @@ -{# Sidebar — for blog listing pages (/blog/, /notes/, /articles/...) #} -{# Data-driven when homepageConfig.blogListingSidebar is configured, otherwise falls back to default widgets #} -{# Each widget is wrapped in a collapsible container with localStorage persistence #} -{% from "components/icon.njk" import icon %} - -{% if homepageConfig and homepageConfig.blogListingSidebar and homepageConfig.blogListingSidebar.length %} - {# === Data-driven mode: render configured widgets === #} - {% for widget in homepageConfig.blogListingSidebar %} - {% set isHomepageAuthorWidget = page.url == "/" and (widget.type == "author-card" or widget.type == "author-card-compact") %} - {% if not isHomepageAuthorWidget %} - - {# Resolve widget title #} - {% if widget.type == "search" %}{% set widgetTitle = "Search" %} - {% elif widget.type == "social-activity" %}{% set widgetTitle = "Social Activity" %} - {% elif widget.type == "github-repos" %}{% set widgetTitle = "GitHub" %} - {% elif widget.type == "funkwhale" %}{% set widgetTitle = "Listening" %} - {% elif widget.type == "recent-posts" %}{% set widgetTitle = "Recent Posts" %} - {% elif widget.type == "blogroll" %}{% set widgetTitle = "Blogroll" %} - {% elif widget.type == "feedland" %}{% set widgetTitle = "FeedLand" %} - {% elif widget.type == "categories" %}{% set widgetTitle = "Categories" %} - {% elif widget.type == "webmentions" %}{% set widgetTitle = "Webmentions" %} - {% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %} - {% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %} - {% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %} - {% elif widget.type == "author-card-compact" %}{% set widgetTitle = "Author" %} - {% elif widget.type == "subscribe" %}{% set widgetTitle = "Subscribe" %} - {% elif widget.type == "ai-usage" %}{% set widgetTitle = "AI Transparency" %} - {% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %} - {% else %}{% set widgetTitle = widget.type %} - {% endif %} - - {# Resolve widget icon and accent border #} - {% if widget.type == "social-activity" %} - {% set widgetIcon = "globe" %}{% set widgetIconClass = "w-5 h-5 text-[#0085ff]" %}{% set widgetBorder = "border-l-[3px] border-l-[#0085ff]" %} - {% elif widget.type == "github-repos" %} - {% set widgetIcon = "github" %}{% set widgetIconClass = "w-5 h-5 text-surface-800 dark:text-surface-200" %}{% set widgetBorder = "border-l-[3px] border-l-surface-400 dark:border-l-surface-500" %} - {% elif widget.type == "funkwhale" %} - {% set widgetIcon = "headphones" %}{% set widgetIconClass = "w-5 h-5 text-purple-500" %}{% set widgetBorder = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %} - {% elif widget.type == "blogroll" %} - {% set widgetIcon = "book-open" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif widget.type == "feedland" %} - {% set widgetIcon = "rss" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% elif widget.type == "subscribe" %} - {% set widgetIcon = "rss" %}{% set widgetIconClass = "w-5 h-5 text-orange-500" %}{% set widgetBorder = "border-l-[3px] border-l-orange-400 dark:border-l-orange-500" %} - {% elif widget.type == "fediverse-follow" %} - {% set widgetIcon = "user-plus" %}{% set widgetIconClass = "w-5 h-5 text-[#a730b8]" %}{% set widgetBorder = "border-l-[3px] border-l-[#a730b8]" %} - {% elif widget.type == "author-card" or widget.type == "author-card-compact" %} - {% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "recent-posts" %} - {% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "categories" %} - {% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "recent-comments" %} - {% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "search" %} - {% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "webmentions" %} - {% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %} - {% elif widget.type == "ai-usage" %} - {% set widgetIcon = "zap" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %} - {% else %} - {% set widgetIcon = "" %}{% set widgetIconClass = "" %}{% set widgetBorder = "" %} - {% endif %} - - {% set widgetKey = "listing-widget-" + widget.type + "-" + loop.index0 %} - {% set defaultOpen = "true" if loop.index0 < 3 else "false" %} - - {# Collapsible wrapper — Alpine.js handles toggle, localStorage persists state #} -
-
- - -
- {# Widget content — inner .widget provides padding, inner title hidden by CSS #} - {% if widget.type == "author-card" %} - {% include "components/widgets/author-card.njk" %} - {% elif widget.type == "author-card-compact" %} - {% include "components/widgets/author-card-compact.njk" %} - {% elif widget.type == "social-activity" %} - {% include "components/widgets/social-activity.njk" %} - {% elif widget.type == "github-repos" %} - {% include "components/widgets/github-repos.njk" %} - {% elif widget.type == "funkwhale" %} - {% include "components/widgets/funkwhale.njk" %} - {% elif widget.type == "recent-posts" %} - {% include "components/widgets/recent-posts.njk" %} - {% elif widget.type == "blogroll" %} - {% if blogrollStatus and blogrollStatus.source == "indiekit" %} - {% include "components/widgets/blogroll.njk" %} - {% endif %} - {% elif widget.type == "feedland" %} - {% include "components/widgets/feedland.njk" %} - {% elif widget.type == "categories" %} - {% include "components/widgets/categories.njk" %} - {% elif widget.type == "subscribe" %} - {% include "components/widgets/subscribe.njk" %} - {% elif widget.type == "recent-comments" %} - {% include "components/widgets/recent-comments.njk" %} - {% elif widget.type == "search" %} - {% include "components/widgets/search.njk" %} - {% elif widget.type == "webmentions" %} - {% include "components/widgets/webmentions.njk" %} - {% elif widget.type == "fediverse-follow" %} - {% include "components/widgets/fediverse-follow.njk" %} - {% elif widget.type == "ai-usage" %} - {% include "components/widgets/ai-usage.njk" ignore missing %} - {% elif widget.type == "custom-html" %} - {% set wConfig = widget.config or {} %} - -
- {% if wConfig.content %} -
- {{ wConfig.content | safe }} -
- {% endif %} -
-
- {% else %} - - {% endif %} -
-
-
- - {% endif %} - - {% endfor %} -{% else %} - {# === Fallback: aligned with rmendes.net default sidebar === #} - - {# Recent Posts #} - {% set widgetKey = "widget-recent-posts-0" %} -
-
- -
- {% include "components/widgets/recent-posts.njk" %} -
-
-
- - {# Social Activity #} - {% set widgetKey = "widget-social-activity-1" %} -
-
- -
- {% include "components/widgets/social-activity.njk" %} -
-
-
- - {# Blogroll #} - {% if blogrollStatus and blogrollStatus.source == "indiekit" %} - {% set widgetKey = "widget-blogroll-2" %} -
-
- -
- {% include "components/widgets/blogroll.njk" %} -
-
-
- {% endif %} - - {# GitHub #} - {% set widgetKey = "widget-github-repos-3" %} -
-
- -
- {% include "components/widgets/github-repos.njk" %} -
-
-
- - {# Listening #} - {% set widgetKey = "widget-funkwhale-4" %} -
-
- -
- {% include "components/widgets/funkwhale.njk" %} -
-
-
- - {% if page.url != "/" %} - {# Author #} - {% set widgetKey = "widget-author-card-5" %} -
-
- -
- {% include "components/widgets/author-card.njk" %} -
-
-
- {% endif %} - - {# Fediverse #} - {% set widgetKey = "widget-fediverse-follow-6" %} -
-
- -
- {% include "components/widgets/fediverse-follow.njk" %} -
-
-
-{% endif %} diff --git a/theme/_includes/components/social-icon.njk b/theme/_includes/components/social-icon.njk deleted file mode 100644 index 54638b2..0000000 --- a/theme/_includes/components/social-icon.njk +++ /dev/null @@ -1,131 +0,0 @@ -{# - Social Icon Macro - Usage: {% from "components/social-icon.njk" import socialIcon, socialIconColorClass %} - {{ socialIcon("github", "w-5 h-5") }} - {{ socialIcon("github", "w-5 h-5") }} - - SVG paths sourced from Simple Icons (simpleicons.org) - CC0 1.0 Universal - All icons render at 24x24 viewBox with fill="currentColor" - Brand colors from official brand guidelines -#} - -{# Returns Tailwind color classes for an icon's brand color (light + dark) #} -{% macro socialIconColorClass(name) %} -{%- if name == "activitypub" -%}text-[#f1027e] -{%- elif name == "github" -%}text-[#181717] dark:text-[#e6edf3] -{%- elif name == "gitlab" -%}text-[#FC6D26] -{%- elif name == "forgejo" -%}text-[#609926] -{%- elif name == "codeberg" -%}text-[#2185D0] -{%- elif name == "mastodon" -%}text-[#6364FF] -{%- elif name == "bluesky" -%}text-[#0085FF] -{%- elif name == "pixelfed" -%}text-[#6C42C9] -{%- elif name == "linkedin" -%}text-[#0A66C2] -{%- elif name == "twitter" -%}text-[#000000] dark:text-[#e7e9ea] -{%- elif name == "threads" -%}text-[#000000] dark:text-[#f5f5f5] -{%- elif name == "youtube" -%}text-[#FF0000] -{%- elif name == "twitch" -%}text-[#9146FF] -{%- elif name == "spotify" -%}text-[#1DB954] -{%- elif name == "bandcamp" -%}text-[#629aa9] -{%- elif name == "soundcloud" -%}text-[#FF5500] -{%- elif name == "rss" -%}text-[#F26522] -{%- elif name == "discord" -%}text-[#5865F2] -{%- elif name == "signal" -%}text-[#3A76F0] -{%- elif name == "telegram" -%}text-[#26A5E4] -{%- elif name == "matrix" -%}text-[#000000] dark:text-[#e6e6e6] -{%- elif name == "reddit" -%}text-[#FF4500] -{%- elif name == "hackernews" -%}text-[#FF6600] -{%- elif name == "funkwhale" -%}text-[#0D47A1] -{%- elif name == "lastfm" -%}text-[#D51007] -{%- elif name == "peertube" -%}text-[#F1680D] -{%- elif name == "bookwyrm" -%}text-[#002200] dark:text-[#78b578] -{%- elif name == "indieweb" -%}text-[#FF5C00] -{%- elif name == "email" -%}text-surface-600 dark:text-surface-400 -{%- elif name == "website" -%}text-surface-600 dark:text-surface-400 -{%- elif name == "keybase" -%}text-[#33A0FF] -{%- elif name == "orcid" -%}text-[#A6CE39] -{%- elif name == "flickr" -%}text-[#0063DC] -{%- elif name == "xmpp" -%}text-[#002B5C] dark:text-[#5badff] -{%- elif name == "sourcehut" -%}text-[#000000] dark:text-[#e0e0e0] -{%- elif name == "facebook" -%}text-[#0866FF] -{%- elif name == "instagram" -%}text-[#E4405F] -{%- else -%}text-surface-600 dark:text-surface-400 -{%- endif -%} -{% endmacro %} - -{% macro socialIcon(name, cssClass) %} -{%- if name == "github" -%} - -{%- elif name == "gitlab" -%} - -{%- elif name == "forgejo" -%} - -{%- elif name == "codeberg" -%} - -{%- elif name == "sourcehut" -%} - -{%- elif name == "linkedin" -%} - -{%- elif name == "bluesky" -%} - -{%- elif name == "mastodon" -%} - -{%- elif name == "activitypub" -%} - -{%- elif name == "pixelfed" -%} - -{%- elif name == "twitter" -%} - -{%- elif name == "facebook" -%} - -{%- elif name == "instagram" -%} - -{%- elif name == "threads" -%} - -{%- elif name == "youtube" -%} - -{%- elif name == "twitch" -%} - -{%- elif name == "flickr" -%} - -{%- elif name == "spotify" -%} - -{%- elif name == "bandcamp" -%} - -{%- elif name == "soundcloud" -%} - -{%- elif name == "rss" -%} - -{%- elif name == "matrix" -%} - -{%- elif name == "discord" -%} - -{%- elif name == "signal" -%} - -{%- elif name == "telegram" -%} - -{%- elif name == "xmpp" -%} - -{%- elif name == "reddit" -%} - -{%- elif name == "hackernews" -%} - -{%- elif name == "keybase" -%} - -{%- elif name == "orcid" -%} - -{%- elif name == "indieweb" -%} - -{%- elif name == "website" -%} - -{%- elif name == "email" -%} - -{%- elif name == "funkwhale" -%} - -{%- elif name == "lastfm" -%} - -{%- elif name == "peertube" -%} - -{%- elif name == "bookwyrm" -%} - -{%- endif -%} -{% endmacro %} diff --git a/theme/_includes/components/webmentions.njk b/theme/_includes/components/webmentions.njk deleted file mode 100644 index 971984b..0000000 --- a/theme/_includes/components/webmentions.njk +++ /dev/null @@ -1,206 +0,0 @@ -{# Webmentions Component #} -{# Displays likes, reposts, and replies for a post #} -{# Also checks legacy URLs from micro.blog and old blog for historical webmentions #} -{# Client-side JS supplements build-time data with real-time fetches #} - -{% set mentions = webmentions | webmentionsForUrl(page.url, urlAliases, conversationMentions) %} -{% set absoluteUrl = site.url + page.url %} -{% set buildTimestamp = "" | timestamp %} - -{# Data container for client-side JS to fetch new webmentions #} - - -{% if mentions.length %} -
-

- Webmentions ({{ mentions.length }}) -

- - {# Likes #} - {% set likes = mentions | webmentionsByType('likes') %} - {% if likes.length %} -
-

- {{ likes.length }} Like{% if likes.length != 1 %}s{% endif %} -

- -
- {% for like in likes %} - - {{ like.author.name }} - - {% endfor %} -
-
-
- {% endif %} - - {# Reposts #} - {% set reposts = mentions | webmentionsByType('reposts') %} - {% if reposts.length %} -
-

- {{ reposts.length }} Repost{% if reposts.length != 1 %}s{% endif %} -

- -
- {% for repost in reposts %} - - {{ repost.author.name }} - - {% endfor %} -
-
-
- {% endif %} - - {# Bookmarks #} - {% set bookmarks = mentions | webmentionsByType('bookmarks') %} - {% if bookmarks.length %} -
-

- {{ bookmarks.length }} Bookmark{% if bookmarks.length != 1 %}s{% endif %} -

- -
- {% for bookmark in bookmarks %} - - {{ bookmark.author.name }} - - {% endfor %} -
-
-
- {% endif %} - - {# Replies #} - {% set replies = mentions | webmentionsByType('replies') %} - {% if replies.length %} -
-

- {{ replies.length }} Repl{% if replies.length != 1 %}ies{% else %}y{% endif %} -

- -
- {% endif %} - - {# Other mentions #} - {% set otherMentions = mentions | webmentionsByType('mentions') %} - {% if otherMentions.length %} -
-

- {{ otherMentions.length }} Mention{% if otherMentions.length != 1 %}s{% endif %} -

- -
- {% endif %} -
-{% endif %} - -{# Webmention send form — collapsed by default #} -
- - - Send a Webmention - -
-

- Have you written a response to this post? Send a webmention by entering your post URL below. -

-
- - - -
-
-
diff --git a/theme/_includes/components/widgets/author-card-compact.njk b/theme/_includes/components/widgets/author-card-compact.njk deleted file mode 100644 index f213550..0000000 --- a/theme/_includes/components/widgets/author-card-compact.njk +++ /dev/null @@ -1,43 +0,0 @@ -{# Author Compact Card - h-card microformat (compact version for blog sidebars) #} -{% set id = homepageConfig.identity if (homepageConfig and homepageConfig.identity) else {} %} -{% set authorName = id.name if (id.name is defined) else site.author.name %} -{% set authorAvatar = id.avatar if (id.avatar is defined) else site.author.avatar %} -{% set authorTitle = id.title if (id.title is defined) else site.author.title %} -{% set authorUrl = id.url if (id.url is defined and id.url) else site.author.url %} -{% set authorLocality = id.locality if (id.locality is defined) else site.author.locality %} -{% set authorCountry = id.country if (id.country is defined) else site.author.country %} -{% set authorBio = id.bio if (id.bio is defined) else site.author.bio %} -{% set authorEmail = id.email if (id.email is defined) else site.author.email %} -{% set authorOrg = id.org if (id.org is defined) else site.author.org %} - - -
-
- {# Hidden u-photo for reliable microformat parsing #} - - -
- - {{ authorName }} - - {% if authorTitle %} -

{{ authorTitle }}

- {% endif %} - {% if authorLocality %} -

{{ authorLocality }}{% if authorCountry %}, {{ authorCountry }}{% endif %}

- {% endif %} -
-
- {# Hidden but present for microformat completeness #} - {% if authorBio %}{% endif %} - {% if authorEmail %}{% endif %} - {% if authorOrg %}{% endif %} -
-
diff --git a/theme/_includes/components/widgets/author-card.njk b/theme/_includes/components/widgets/author-card.njk deleted file mode 100644 index f205d58..0000000 --- a/theme/_includes/components/widgets/author-card.njk +++ /dev/null @@ -1,6 +0,0 @@ -{# Author Card Widget - includes the canonical h-card component #} - -
- {% include "components/h-card.njk" %} -
-
diff --git a/theme/_includes/components/widgets/blogroll.njk b/theme/_includes/components/widgets/blogroll.njk deleted file mode 100644 index 572cff0..0000000 --- a/theme/_includes/components/widgets/blogroll.njk +++ /dev/null @@ -1,110 +0,0 @@ -{# Blogroll Widget - Dynamic loading from API with source tabs #} - -
-

- - - - Blogroll -

- - {# Source tabs - only shown when multiple sources exist #} -
- -
- -
    - -
- -
- No blogs loaded yet. -
- - - View all blogs - - -
- - -
diff --git a/theme/_includes/components/widgets/categories.njk b/theme/_includes/components/widgets/categories.njk deleted file mode 100644 index b071191..0000000 --- a/theme/_includes/components/widgets/categories.njk +++ /dev/null @@ -1,15 +0,0 @@ -{# Categories/Tags Widget #} -{% if categories and categories.length %} - -
-

Categories

-
- {% for category in categories %} - - {{ category }} - - {% endfor %} -
-
-
-{% endif %} diff --git a/theme/_includes/components/widgets/fediverse-follow.njk b/theme/_includes/components/widgets/fediverse-follow.njk deleted file mode 100644 index b99dbb3..0000000 --- a/theme/_includes/components/widgets/fediverse-follow.njk +++ /dev/null @@ -1,41 +0,0 @@ -{# Fediverse Follow Me Widget — uses the fediverseInteract Alpine.js component #} -{# Requires fediverse-interact.js loaded in base.njk (already present) #} -{# Determines actor URI from site social links: prefers self-hosted AP, falls back to Mastodon #} - -{% set id = homepageConfig.identity if (homepageConfig and homepageConfig.identity) else {} %} -{% set socialLinks = id.social if (id.social is defined) else site.social %} - -{% set actorUrl = "" %} -{% for link in socialLinks %} - {% if link.icon == "activitypub" and not actorUrl %} - {% set actorUrl = link.url %} - {% endif %} -{% endfor %} -{% if not actorUrl %} - {% for link in socialLinks %} - {% if link.icon == "mastodon" and not actorUrl %} - {% set actorUrl = link.url %} - {% endif %} - {% endfor %} -{% endif %} - -{% if actorUrl %} - -
-

Follow Me

-

Follow me from your fediverse instance.

- - - Follow on the Fediverse - - {% set modalTitle = "Follow on the Fediverse" %} - {% set modalDescription = "Choose your instance to follow this account." %} - {% include "components/fediverse-modal.njk" %} -
-
-{% endif %} diff --git a/theme/_includes/components/widgets/feedland.njk b/theme/_includes/components/widgets/feedland.njk deleted file mode 100644 index 5a4b1c3..0000000 --- a/theme/_includes/components/widgets/feedland.njk +++ /dev/null @@ -1,383 +0,0 @@ -{# FeedLand Widget - Matches Dave Winer's blogroll.js visual rendering #} -{# Uses Alpine.js + blogroll API instead of jQuery + external blogroll.js #} - - - - - - -
-
- {# Title + menu #} -
-
- -
- - -
- - {# Sort links #} -
- Title - When -
- - {# Feed list — pure divs, no table #} - - - {# Footer #} - -
-
- - -
diff --git a/theme/_includes/components/widgets/funkwhale.njk b/theme/_includes/components/widgets/funkwhale.njk deleted file mode 100644 index 0f9bbac..0000000 --- a/theme/_includes/components/widgets/funkwhale.njk +++ /dev/null @@ -1,115 +0,0 @@ -{# Listening Widget — combined Funkwhale + Last.fm recent tracks #} -{% set hasListening = (funkwhaleActivity and (funkwhaleActivity.nowPlaying or funkwhaleActivity.listenings.length)) or (lastfmActivity and (lastfmActivity.nowPlaying or lastfmActivity.scrobbles.length)) %} -{% if hasListening %} - -
-

- - - - Listening -

- - {# Now Playing — show if either source is actively playing #} - {% set fwNow = funkwhaleActivity.nowPlaying if funkwhaleActivity and funkwhaleActivity.nowPlaying and funkwhaleActivity.nowPlaying.status == 'now-playing' else null %} - {% set lfmNow = lastfmActivity.nowPlaying if lastfmActivity and lastfmActivity.nowPlaying and lastfmActivity.nowPlaying.status == 'now-playing' else null %} - - {% if fwNow or lfmNow %} - {% set np = fwNow or lfmNow %} - {% set npSource = "Funkwhale" if fwNow else "Last.fm" %} - {% set npColor = "purple" if fwNow else "red" %} -
-
- - - - - - Now Playing - ({{ npSource }}) -
-
- {% if np.coverUrl %} - - {% endif %} -
-

- {% if np.trackUrl %} - {{ np.track }} - {% else %} - {{ np.track }} - {% endif %} -

-

{{ np.artist }}

-
-
-
- {% endif %} - - {# Recent tracks — 2 from each source #} -
    - {% if funkwhaleActivity and funkwhaleActivity.listenings.length %} - {% for listening in funkwhaleActivity.listenings | head(2) %} -
  • - {% if listening.coverUrl %} - - {% else %} -
    - - - -
    - {% endif %} -
    -

    - {% if listening.trackUrl %} - {{ listening.track }} - {% else %} - {{ listening.track }} - {% endif %} -

    -

    {{ listening.artist }} - Funkwhale -

    -
    -
  • - {% endfor %} - {% endif %} - - {% if lastfmActivity and lastfmActivity.scrobbles.length %} - {% for scrobble in lastfmActivity.scrobbles | head(2) %} -
  • - {% if scrobble.coverUrl %} - - {% else %} -
    - - - -
    - {% endif %} -
    -

    - {% if scrobble.trackUrl %} - {{ scrobble.track }} - {% else %} - {{ scrobble.track }} - {% endif %} - {% if scrobble.loved %}{% endif %} -

    -

    {{ scrobble.artist }} - Last.fm -

    -
    -
  • - {% endfor %} - {% endif %} -
- - - View full listening history - - -
-
-{% endif %} diff --git a/theme/_includes/components/widgets/github-repos.njk b/theme/_includes/components/widgets/github-repos.njk deleted file mode 100644 index 69c46be..0000000 --- a/theme/_includes/components/widgets/github-repos.njk +++ /dev/null @@ -1,316 +0,0 @@ -{# GitHub Activity Widget - Tabbed Commits/Repos/Featured/PRs with live API data #} - -{% set ghFallbackCommits = githubActivity.commits if githubActivity and githubActivity.commits else [] %} -{% set ghFallbackFeatured = githubActivity.featured if githubActivity and githubActivity.featured else [] %} -{% set ghFallbackContributions = githubActivity.contributions if githubActivity and githubActivity.contributions else [] %} -{% set ghFallbackRepos = githubRepos if githubRepos else [] %} -{% set id = homepageConfig.identity if (homepageConfig and homepageConfig.identity) else {} %} -{% set socialLinks = id.social if (id.social is defined) else site.social %} -{% set githubProfileUrl = "" %} -{% for link in socialLinks %} - {% if not githubProfileUrl and (link.icon == "github" or "github.com/" in link.url) %} - {% set githubProfileUrl = link.url %} - {% endif %} -{% endfor %} -{% if not githubProfileUrl and site.feeds.github %} - {% set githubProfileUrl = "https://github.com/" + site.feeds.github %} -{% endif %} -
-

- - GitHub -

- - {# Tab buttons — order: Commits, Repos, Featured, PRs #} -
- - - - -
- - {# Tab content — fixed height container to prevent layout shift #} -
- - {# Loading state #} -
- Loading... -
- - {# Commits Tab #} -
-
    - -
-
No recent commits.
-
- - {# Repos Tab #} -
-
    - -
-
No repositories found.
-
- - {# Featured Tab #} -
-
    - -
-
No featured projects.
-
- - {# PRs Tab #} -
-
    - -
-
No recent PRs or issues.
-
- -
- - {# Footer link #} - {% if githubProfileUrl %} - - View on GitHub - - - {% endif %} -
- - -
diff --git a/theme/_includes/components/widgets/post-categories.njk b/theme/_includes/components/widgets/post-categories.njk deleted file mode 100644 index 0d015d5..0000000 --- a/theme/_includes/components/widgets/post-categories.njk +++ /dev/null @@ -1,21 +0,0 @@ -{# Categories for This Post #} -{% if category %} - -
-

Categories

-
- {% if category is string %} - - {{ category }} - - {% else %} - {% for cat in category %} - - {{ cat }} - - {% endfor %} - {% endif %} -
-
-
-{% endif %} diff --git a/theme/_includes/components/widgets/post-navigation.njk b/theme/_includes/components/widgets/post-navigation.njk deleted file mode 100644 index f0b0155..0000000 --- a/theme/_includes/components/widgets/post-navigation.njk +++ /dev/null @@ -1,66 +0,0 @@ -{# Post Navigation Widget - Previous/Next #} -{# Uses previousInCollection/nextInCollection filters to find adjacent posts #} -{% set _prevPost = collections.posts | previousInCollection(page) %} -{% set _nextPost = collections.posts | nextInCollection(page) %} - -{% if _prevPost or _nextPost %} - -
-

More Posts

-
- {% if _prevPost %} - - {% endif %} - {% if _nextPost %} -
- Next - {% set _likedUrl = _nextPost.data.likeOf or _nextPost.data.like_of %} - {% set _bookmarkedUrl = _nextPost.data.bookmarkOf or _nextPost.data.bookmark_of %} - {% set _repostedUrl = _nextPost.data.repostOf or _nextPost.data.repost_of %} - {% set _replyToUrl = _nextPost.data.inReplyTo or _nextPost.data.in_reply_to %} - - {% if _likedUrl %} - - Liked {{ _likedUrl | replace("https://", "") | truncate(35) }} - {% elif _bookmarkedUrl %} - - {{ _nextPost.data.title or ("Bookmarked " + (_bookmarkedUrl | replace("https://", "") | truncate(30))) }} - {% elif _repostedUrl %} - - Reposted {{ _repostedUrl | replace("https://", "") | truncate(35) }} - {% elif _replyToUrl %} - - Reply to {{ _replyToUrl | replace("https://", "") | truncate(35) }} - {% else %} - {{ _nextPost.data.title or _nextPost.data.name or (_nextPost.templateContent | striptags | truncate(50)) or "Note" }} - {% endif %} - -
- {% endif %} -
-
-
-{% endif %} diff --git a/theme/_includes/components/widgets/recent-comments.njk b/theme/_includes/components/widgets/recent-comments.njk deleted file mode 100644 index 9e0b78a..0000000 --- a/theme/_includes/components/widgets/recent-comments.njk +++ /dev/null @@ -1,27 +0,0 @@ -{# Recent Comments Widget — sidebar #} -{% if recentComments and recentComments.length %} - -
-

Recent Comments

-
    - {% for comment in recentComments %} -
  • -
    - {% if comment.author and comment.author.photo %} - {{ comment.author.name }} - {% endif %} -
    - {{ comment.author.name or "Anonymous" }} -

    {{ comment.content.text | truncate(80) }}

    - {% if comment["comment-target"] %} - View post - {% endif %} -
    -
    -
  • - {% endfor %} -
-
-
-{% endif %} diff --git a/theme/_includes/components/widgets/recent-posts-blog.njk b/theme/_includes/components/widgets/recent-posts-blog.njk deleted file mode 100644 index eb04095..0000000 --- a/theme/_includes/components/widgets/recent-posts-blog.njk +++ /dev/null @@ -1,108 +0,0 @@ -{# Recent Posts Widget — type-aware, for blog/post sidebars #} -{# Uses collections.posts directly (all post types, not just recentPosts collection) #} -{% set listedPosts = collections.posts | excludeUnlistedPosts %} -{% if listedPosts and listedPosts.length %} - -
-

Recent Posts

- - - View all posts - -
-
-{% endif %} diff --git a/theme/_includes/components/widgets/recent-posts.njk b/theme/_includes/components/widgets/recent-posts.njk deleted file mode 100644 index 82e8b0d..0000000 --- a/theme/_includes/components/widgets/recent-posts.njk +++ /dev/null @@ -1,117 +0,0 @@ -{# Recent Posts Widget (sidebar) - compact type-aware list #} -{% set recentPosts = recentPosts or collections.recentPosts %} -{% set listedRecentPosts = recentPosts | excludeUnlistedPosts %} -{% if listedRecentPosts and listedRecentPosts.length %} - -
-

Recent Posts

- - - View all posts - -
-
-{% endif %} diff --git a/theme/_includes/components/widgets/search.njk b/theme/_includes/components/widgets/search.njk deleted file mode 100644 index c547241..0000000 --- a/theme/_includes/components/widgets/search.njk +++ /dev/null @@ -1,10 +0,0 @@ -{# Search Widget — redirects to /search/?q=query #} -
- - -
diff --git a/theme/_includes/components/widgets/share.njk b/theme/_includes/components/widgets/share.njk deleted file mode 100644 index 0c8d9b1..0000000 --- a/theme/_includes/components/widgets/share.njk +++ /dev/null @@ -1,31 +0,0 @@ -{# Share Widget #} -{% set shareText = title + " " + site.url + page.url %} - -
-

Share

-
- - - - - - - - {% set modalTitle = "Share on Mastodon / Fediverse" %} - {% set modalDescription = "Choose your instance to share this post." %} - {% include "components/fediverse-modal.njk" %} - -
-
-
diff --git a/theme/_includes/components/widgets/social-activity.njk b/theme/_includes/components/widgets/social-activity.njk deleted file mode 100644 index e6a2e69..0000000 --- a/theme/_includes/components/widgets/social-activity.njk +++ /dev/null @@ -1,121 +0,0 @@ -{# Social Feed Widget - Tabbed Bluesky/Mastodon #} -{% if (blueskyFeed and blueskyFeed.length) or (mastodonFeed and mastodonFeed.length) %} -{% set id = homepageConfig.identity if (homepageConfig and homepageConfig.identity) else {} %} -{% set socialLinks = id.social if (id.social is defined) else site.social %} - -{% set blueskyProfileUrl = "" %} -{% set mastodonProfileUrl = "" %} -{% for link in socialLinks %} - {% if not blueskyProfileUrl and (link.icon == "bluesky" or "bsky.app/profile/" in link.url) %} - {% set blueskyProfileUrl = link.url %} - {% endif %} - {% if not mastodonProfileUrl and (link.icon == "mastodon" or "@" in link.url) %} - {% set mastodonProfileUrl = link.url %} - {% endif %} -{% endfor %} -{% if not blueskyProfileUrl and site.feeds.bluesky %} - {% set blueskyProfileUrl = "https://bsky.app/profile/" + site.feeds.bluesky %} -{% endif %} -{% if not mastodonProfileUrl and site.feeds.mastodon.instance and site.feeds.mastodon.username %} - {% set mastodonProfileUrl = "https://" + site.feeds.mastodon.instance + "/@" + site.feeds.mastodon.username %} -{% endif %} - - -{% set defaultSocialTab = "mastodon" if mastodonFeed and mastodonFeed.length else "bluesky" %} -
-

Social Activity

- - {# Tab buttons #} -
- {% if blueskyFeed and blueskyFeed.length %} - - {% endif %} - {% if mastodonFeed and mastodonFeed.length %} - - {% endif %} -
- - {# Bluesky Tab Content #} - {% if blueskyFeed and blueskyFeed.length %} -
- - {% if blueskyProfileUrl %} - - View on Bluesky - - - {% endif %} -
- {% endif %} - - {# Mastodon Tab Content #} - {% if mastodonFeed and mastodonFeed.length %} -
- - {% if mastodonProfileUrl %} - - View on Mastodon - - - {% endif %} -
- {% endif %} -
-
-{% endif %} diff --git a/theme/_includes/components/widgets/subscribe.njk b/theme/_includes/components/widgets/subscribe.njk deleted file mode 100644 index 61f4005..0000000 --- a/theme/_includes/components/widgets/subscribe.njk +++ /dev/null @@ -1,20 +0,0 @@ -{# Subscribe Widget #} - - - diff --git a/theme/_includes/components/widgets/toc.njk b/theme/_includes/components/widgets/toc.njk deleted file mode 100644 index aaffdc3..0000000 --- a/theme/_includes/components/widgets/toc.njk +++ /dev/null @@ -1,59 +0,0 @@ -{# Table of Contents Widget (for articles with headings) #} - -
-

Contents

- -
-
diff --git a/theme/_includes/components/widgets/webmentions.njk b/theme/_includes/components/widgets/webmentions.njk deleted file mode 100644 index ae51b4d..0000000 --- a/theme/_includes/components/widgets/webmentions.njk +++ /dev/null @@ -1,168 +0,0 @@ -{# Recent Webmentions Widget - site-wide inbound/outbound activity #} -{# Uses client-side fetch from /webmentions/api/mentions (same as /interactions page) #} -{# Outbound tab uses Eleventy collections (likes, replies, bookmarks, reposts) #} - -
-

- - - - Webmentions -

- - {# Tab buttons #} -
- - -
- - {# === Inbound tab (client-side fetched) === #} -
- {# Loading #} -
-
-
- - {# Mentions list #} -
- -
- - {# Empty #} -

No webmentions received yet.

- - {# Error #} -

- - {# Link to full interactions page #} - -
- - {# === Outbound tab (from Eleventy collections) === #} -
- - -
-
- - -
diff --git a/theme/_includes/layouts/base.njk b/theme/_includes/layouts/base.njk deleted file mode 100644 index a6a4401..0000000 --- a/theme/_includes/layouts/base.njk +++ /dev/null @@ -1,551 +0,0 @@ - - - - {# OG image resolution handled by og-fix transform in eleventy.config.js - to bypass Eleventy 3.x parallel rendering race condition (#3183). - Template outputs __OG_IMAGE_PLACEHOLDER__ and __TWITTER_CARD_PLACEHOLDER__ - which the transform replaces using the correct slug derived from outputPath. #} - - - - {% if title %}{{ title }} - {% endif %}{{ site.name }} - - {# OpenGraph meta tags #} - {% set ogTitle = title | default(site.name) %} - {% set ogDesc = description | default(content | ogDescription(200)) | default(site.description) %} - {# Normalize photo - could be array for multi-photo posts #} - {% set ogPhoto = photo %} - {% if ogPhoto %} - {% if ogPhoto[0] and (ogPhoto[0] | length) > 10 %} - {% set ogPhoto = ogPhoto[0] %} - {% endif %} - {% endif %} - - - - - - - {% if ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10 %} - - {% elif image and image != "" and (image | length) > 10 %} - - {% else %} - - {% endif %} - - - - - {# Twitter Card meta tags #} - {% set hasExplicitImage = (ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10) or (image and image != "" and (image | length) > 10) %} - - - - {% if ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10 %} - - {% elif image and image != "" and (image | length) > 10 %} - - {% else %} - - {% endif %} - - {# Favicon #} - - - - {# Critical CSS — inlined for fast first paint #} - - {# Defer full stylesheet — loads after first paint #} - - - - - - - - - - {# Alpine.js components — MUST load before Alpine core (Alpine.data() registration via alpine:init) #} - - - - - - - {# Graceful no-JS fallback: show content that Alpine would normally control #} - - - - - - {% if site.markdownAgents.enabled and page.url and page.url.startsWith('/articles/') and page.url != '/articles/' %} - - {% endif %} - {% if category and page.url and page.url.startsWith('/categories/') and page.url != '/categories/' %} - - - {% endif %} - - - - - - - - - - {# Fediverse creator meta tag for Mastodon verification #} - {% if site.fediverseCreator %} - - {% endif %} - - {# IndieAuth rel="me" links for identity verification #} - {# Note: Bluesky links use "me atproto" for verification #} - {% for social in site.social %} - - {% endfor %} - - - - - -
- {% if withSidebar and page.url == "/" and homepageConfig and homepageConfig.sections %} - {# Homepage: builder controls its own layout and sidebar #} - {{ content | safe }} - {% elif withSidebar %} -
-
- {{ content | safe }} -
- -
- {% elif withBlogSidebar %} -
-
- {{ content | safe }} -
- -
- {% else %} - {{ content | safe }} - {% endif %} -
- - - - {# Island architecture - lazy hydration for widgets #} - - {# Relative date display - progressively enhances