From 66e55af7eee16ecfb4ff5d99895ec200f3d76942 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 20 Feb 2026 15:17:32 +0100 Subject: [PATCH] feat: add configurable CV page layout with builder support CV page now reads layout config from cv-page.json when available, supporting single-column, two-column, and full-width hero layouts with configurable sections, sidebar widgets, and footer columns. Falls back to the previous hardcoded layout when no config exists. --- _data/cvPageConfig.js | 29 ++++++ _includes/components/cv-builder.njk | 121 +++++++++++++++++++++++++ _includes/components/cv-footer.njk | 26 ++++++ _includes/components/cv-sidebar.njk | 45 ++++++++++ cv.njk | 131 +++++++++++++++------------- 5 files changed, 291 insertions(+), 61 deletions(-) create mode 100644 _data/cvPageConfig.js create mode 100644 _includes/components/cv-builder.njk create mode 100644 _includes/components/cv-footer.njk create mode 100644 _includes/components/cv-sidebar.njk diff --git a/_data/cvPageConfig.js b/_data/cvPageConfig.js new file mode 100644 index 0000000..456b388 --- /dev/null +++ b/_data/cvPageConfig.js @@ -0,0 +1,29 @@ +/** + * CV Page Configuration Data + * Reads config from indiekit-endpoint-cv plugin CV page builder. + * Falls back to null — cv.njk then uses the default hardcoded layout. + * + * The CV plugin writes a .indiekit/cv-page.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", "cv-page.json"); + const raw = readFileSync(configPath, "utf8"); + const config = JSON.parse(raw); + console.log("[cvPageConfig] Loaded CV page builder config"); + return config; + } catch { + // No CV page builder config — fall back to hardcoded layout in cv.njk + return null; + } +} diff --git a/_includes/components/cv-builder.njk b/_includes/components/cv-builder.njk new file mode 100644 index 0000000..4af77bf --- /dev/null +++ b/_includes/components/cv-builder.njk @@ -0,0 +1,121 @@ +{# + 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 %} + +{# Hero — rendered at top when enabled (default: true) #} +{% if cvPageConfig.hero.enabled != false %} +
+
+ {{ site.author.name }} +
+

+ {{ site.author.name }} +

+ {% if site.author.title %} +

+ {{ site.author.title }} +

+ {% endif %} + {% if site.author.bio %} +

+ {{ site.author.bio }} +

+ {% endif %} + {% if cvPageConfig.hero.showSocial != false %} +
+ {% for link in site.social %} + + {{ link.name }} + + {% endfor %} +
+ {% 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: {{ cv.lastUpdated | date("PPP") }} +

+{% endif %} + +{# Footer — rendered after the main layout, full width #} +{% include "components/cv-footer.njk" %} diff --git a/_includes/components/cv-footer.njk b/_includes/components/cv-footer.njk new file mode 100644 index 0000000..c86957e --- /dev/null +++ b/_includes/components/cv-footer.njk @@ -0,0 +1,26 @@ +{# CV Page Builder Footer — renders footer items in a responsive 3-column grid #} +{% if cvPageConfig.footer and cvPageConfig.footer.length %} + +{% endif %} diff --git a/_includes/components/cv-sidebar.njk b/_includes/components/cv-sidebar.njk new file mode 100644 index 0000000..ff83e8e --- /dev/null +++ b/_includes/components/cv-sidebar.njk @@ -0,0 +1,45 @@ +{# CV Page Builder Sidebar — renders widgets from cvPageConfig.sidebar #} +{% if cvPageConfig.sidebar and cvPageConfig.sidebar.length %} + {% for widget in cvPageConfig.sidebar %} + {% 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" %} + + {% elif widget.type == "webmentions" %} + {% include "components/widgets/webmentions.njk" %} + {% elif widget.type == "custom-html" %} + {# Custom content widget #} + {% set wConfig = widget.config or {} %} + + {% else %} + + {% endif %} + {% endfor %} +{% endif %} diff --git a/cv.njk b/cv.njk index 2c4f7fb..e55d789 100644 --- a/cv.njk +++ b/cv.njk @@ -6,7 +6,7 @@ permalink: /cv/ pagefindIgnore: true --- -{# Standalone CV page — renders all CV sections using the same partials as the homepage builder #} +{# CV page — uses configurable layout when cvPageConfig exists, falls back to hardcoded layout #} {% set hasCvData = (cv.experience and cv.experience.length) or (cv.projects and cv.projects.length) or @@ -14,75 +14,84 @@ pagefindIgnore: true {% if hasCvData %} -{# Hero / intro #} -
-
- {{ site.author.name }} -
-

- {{ site.author.name }} -

- {% if site.author.title %} -

- {{ site.author.title }} -

- {% endif %} - {% if site.author.bio %} -

- {{ site.author.bio }} -

- {% endif %} -
- {% for link in site.social %} - +
+ {{ site.author.name }} - {{ link.name }} - - {% endfor %} +
+

+ {{ site.author.name }} +

+ {% if site.author.title %} +

+ {{ site.author.title }} +

+ {% endif %} + {% if site.author.bio %} +

+ {{ site.author.bio }} +

+ {% endif %} +
+ {% for link in site.social %} + + {{ link.name }} + + {% endfor %} +
+
-
-
-
+ -{# Experience — work-only variant #} -{% set section = { type: "cv-experience-work", config: {} } %} -{% include "components/sections/cv-experience-work.njk" ignore missing %} + {# Experience — work-only variant #} + {% set section = { type: "cv-experience-work", config: {} } %} + {% include "components/sections/cv-experience-work.njk" ignore missing %} -{# Skills — work-only variant #} -{% set section = { type: "cv-skills-work", config: {} } %} -{% include "components/sections/cv-skills-work.njk" ignore missing %} + {# Skills — work-only variant #} + {% set section = { type: "cv-skills-work", config: {} } %} + {% include "components/sections/cv-skills-work.njk" ignore missing %} -{# Work Projects (only work-related projects on the CV page) #} -{% set section = { type: "cv-projects-work", config: {} } %} -{% include "components/sections/cv-projects-work.njk" ignore missing %} + {# Work Projects (only work-related projects on the CV page) #} + {% set section = { type: "cv-projects-work", config: {} } %} + {% include "components/sections/cv-projects-work.njk" ignore missing %} -{# Education — work-only variant #} -{% set section = { type: "cv-education-work", config: {} } %} -{% include "components/sections/cv-education-work.njk" ignore missing %} + {# Education — work-only variant #} + {% set section = { type: "cv-education-work", config: {} } %} + {% include "components/sections/cv-education-work.njk" ignore missing %} -{# Languages — standalone section #} -{% set section = { type: "cv-languages", config: {} } %} -{% include "components/sections/cv-languages.njk" ignore missing %} + {# Languages — standalone section #} + {% set section = { type: "cv-languages", config: {} } %} + {% include "components/sections/cv-languages.njk" ignore missing %} -{# Interests — work-only variant #} -{% set section = { type: "cv-interests-work", config: {} } %} -{% include "components/sections/cv-interests-work.njk" ignore missing %} + {# Interests — work-only variant #} + {% set section = { type: "cv-interests-work", config: {} } %} + {% include "components/sections/cv-interests-work.njk" ignore missing %} -{# Last Updated #} -{% if cv.lastUpdated %} -

- Last updated: {% if cv.lastUpdated %}{{ cv.lastUpdated | date("PPP") }}{% endif %} -

-{% endif %} + {# Last Updated #} + {% if cv.lastUpdated %} +

+ Last updated: {{ cv.lastUpdated | date("PPP") }} +

+ {% endif %} + + {% endif %} {% else %}