Files
blog-eleventy-indiekit/_includes/layouts/home.njk
Ricardo d9e99fa9cc feat: add homepage builder components for Phase 3
- homepage-builder.njk: master section renderer for configured sections
- sections/hero.njk: hero section with avatar, name, title, bio, social
- sections/recent-posts.njk: configurable recent posts section
- sections/custom-html.njk: freeform HTML content block
- Wire home.njk Tier 1 to include homepage-builder when config exists

Part of indiekit-endpoint-homepage plugin integration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 15:55:08 +01:00

313 lines
15 KiB
Plaintext

---
layout: layouts/base.njk
withSidebar: true
---
{# Hero Section #}
<section class="mb-8 sm:mb-12">
<div class="flex flex-col sm:flex-row gap-6 sm:gap-8 items-start">
{# Avatar #}
<img
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
class="w-24 h-24 sm:w-32 sm:h-32 rounded-full object-cover shadow-lg flex-shrink-0"
loading="eager"
>
{# Introduction #}
<div class="flex-1 min-w-0">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold text-surface-900 dark:text-surface-100 mb-2">
{{ site.author.name }}
</h1>
<p class="text-lg sm:text-xl text-primary-600 dark:text-primary-400 mb-3 sm:mb-4">
{{ site.author.title }}
</p>
{% if site.author.bio %}
<p class="text-base sm:text-lg text-surface-700 dark:text-surface-300 mb-4">
{{ site.author.bio }}
</p>
{% endif %}
{% if site.description %}
<p class="text-base sm:text-lg text-surface-700 dark:text-surface-300 mb-4 sm:mb-6">
{{ site.description }}
<a href="/about/" class="text-primary-600 dark:text-primary-400 hover:underline font-medium">Read more &rarr;</a>
</p>
{% endif %}
{# Social Links #}
<div class="flex flex-wrap gap-3">
{% for link in site.social %}
<a
href="{{ link.url }}"
rel="{{ link.rel }} noopener"
class="inline-flex items-center gap-2 px-3 py-2 text-sm bg-surface-100 dark:bg-surface-800 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors"
target="_blank"
>
{% if link.icon == "github" %}
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
{% elif link.icon == "linkedin" %}
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path></svg>
{% elif link.icon == "bluesky" %}
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"></path></svg>
{% elif link.icon == "mastodon" %}
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.668 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12v6.406z"></path></svg>
{% endif %}
<span class="text-sm font-medium">{{ link.name }}</span>
</a>
{% endfor %}
</div>
</div>
</div>
</section>
{# Homepage content — three-tier fallback: #}
{# 1. Plugin config (homepageConfig) — Phase 3, future #}
{# 2. CV data — show experience/projects/skills #}
{# 3. Default — show recent posts and activity #}
{% set hasCvData = (cv.experience and cv.experience.length) or
(cv.projects and cv.projects.length) or
(cv.skills and (cv.skills | dictsort | length)) %}
{# --- Tier 1: Plugin-driven layout --- #}
{% if homepageConfig and homepageConfig.sections %}
{% include "components/homepage-builder.njk" %}
{# --- Tier 2: CV-based layout --- #}
{% elif hasCvData %}
{# Work Experience Timeline - only show if data exists #}
{% if cv.experience and cv.experience.length %}
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Experience</h2>
<div class="timeline">
{% for job in cv.experience %}
<article class="timeline-item">
<div class="flex flex-wrap items-baseline gap-2 mb-2">
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-100">
{{ job.title }}
</h3>
<span class="text-primary-600 dark:text-primary-400">@ {{ job.company }}</span>
{% if job.type != "full-time" %}
<span class="text-xs px-2 py-0.5 bg-surface-200 dark:bg-surface-700 rounded">{{ job.type }}</span>
{% endif %}
</div>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2">
<time datetime="{{ job.startDate }}">{{ job.startDate }}</time> -
{% if job.endDate %}
<time datetime="{{ job.endDate }}">{{ job.endDate }}</time>
{% else %}
Present
{% endif %}
· {{ job.location }}
</p>
{% if job.description %}
<p class="text-surface-700 dark:text-surface-300 mb-2">{{ job.description }}</p>
{% endif %}
{% if job.highlights %}
<ul class="list-disc list-inside text-sm text-surface-600 dark:text-surface-400 space-y-1">
{% for highlight in job.highlights %}
<li>{{ highlight }}</li>
{% endfor %}
</ul>
{% endif %}
</article>
{% endfor %}
</div>
</section>
{% endif %}
{# Projects Section - only show if data exists #}
{% if cv.projects and cv.projects.length %}
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Projects</h2>
<div class="grid gap-3 sm:gap-4 md:grid-cols-2">
{% for project in cv.projects %}
<article class="p-4 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<h3 class="font-semibold text-surface-900 dark:text-surface-100 mb-1">
{% if project.url %}
<a href="{{ project.url }}" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank" rel="noopener">
{{ project.name }}
</a>
{% else %}
{{ project.name }}
{% endif %}
</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-3">{{ project.description }}</p>
<div class="flex flex-wrap gap-2">
{% for tech in project.technologies %}
<span class="skill-badge">{{ tech }}</span>
{% endfor %}
</div>
{% if project.status == "active" %}
<span class="inline-block mt-3 text-xs px-2 py-0.5 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">Active</span>
{% endif %}
</article>
{% endfor %}
</div>
</section>
{% endif %}
{# Skills Section - only show if data exists #}
{% if cv.skills and (cv.skills | dictsort | length) %}
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Skills</h2>
<div class="grid gap-4 sm:gap-6 md:grid-cols-2">
{% for category, skills in cv.skills %}
<div>
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
{{ category }}
</h3>
<div class="flex flex-wrap gap-2">
{% for skill in skills %}
<span class="skill-badge">{{ skill }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{# Education & Languages - only show if data exists #}
{% if (cv.education and cv.education.length) or (cv.languages and cv.languages.length) %}
<section class="mb-8 sm:mb-12 grid gap-6 sm:gap-8 md:grid-cols-2">
{# Education #}
{% if cv.education and cv.education.length %}
<div>
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-100 mb-6">Education</h2>
{% for edu in cv.education %}
<article class="mb-4 last:mb-0">
<h3 class="font-semibold text-surface-900 dark:text-surface-100">{{ edu.degree }}</h3>
<p class="text-primary-600 dark:text-primary-400">{{ edu.institution }}</p>
<p class="text-sm text-surface-600 dark:text-surface-400">{{ edu.year }} · {{ edu.location }}</p>
</article>
{% endfor %}
</div>
{% endif %}
{# Languages #}
{% if cv.languages and cv.languages.length %}
<div>
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-100 mb-6">Languages</h2>
<ul class="space-y-2">
{% for lang in cv.languages %}
<li class="flex justify-between items-center">
<span class="text-surface-900 dark:text-surface-100">{{ lang.name }}</span>
<span class="text-sm text-surface-600 dark:text-surface-400">{{ lang.level }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</section>
{% endif %}
{# Interests - only show if data exists #}
{% if cv.interests and cv.interests.length %}
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Interests</h2>
<div class="flex flex-wrap gap-2">
{% for interest in cv.interests %}
<span class="skill-badge">{{ interest }}</span>
{% endfor %}
</div>
</section>
{% endif %}
{# Last Updated - only show if CV has content #}
{% if cv.lastUpdated and (cv.experience.length or cv.projects.length) %}
<p class="text-sm text-surface-500 text-center">
Last updated: {{ cv.lastUpdated }}
</p>
{% endif %}
{# --- Tier 3: Default — recent activity when no CV and no plugin --- #}
{% else %}
{# Recent Posts #}
{% if collections.posts and collections.posts.length %}
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Recent Posts</h2>
<div class="space-y-4">
{% for post in collections.posts | head(10) %}
<article class="p-4 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-primary-400 dark:hover:border-primary-600 transition-colors">
<h3 class="font-semibold text-surface-900 dark:text-surface-100 mb-1">
<a href="{{ post.url }}" class="hover:text-primary-600 dark:hover:text-primary-400">
{{ post.data.title or post.data.name or "Untitled" }}
</a>
</h3>
{% if post.data.summary %}
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2 line-clamp-2">{{ post.data.summary }}</p>
{% endif %}
<div class="flex items-center gap-3 text-xs text-surface-500">
<time datetime="{{ post.data.published or post.date }}">
{{ (post.data.published or post.date) | date("MMM d, yyyy") }}
</time>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded">{{ post.data.postType }}</span>
{% endif %}
</div>
</article>
{% endfor %}
</div>
<a href="/blog/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-4 inline-flex items-center gap-1">
View all posts
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</a>
</section>
{% endif %}
{# Getting Started — onboarding guide for new deployments #}
<section class="mb-8 sm:mb-12 p-6 bg-primary-50 dark:bg-primary-900/20 rounded-lg border border-primary-200 dark:border-primary-800">
<h2 class="text-xl font-bold text-surface-900 dark:text-surface-100 mb-4">Getting Started</h2>
<div class="space-y-4 text-surface-700 dark:text-surface-300">
<div class="flex gap-3">
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-primary-600 text-white text-sm flex items-center justify-center font-bold">1</span>
<div>
<strong class="text-surface-900 dark:text-surface-100">Create your first post</strong>
<p class="text-sm mt-1">
<a href="/session/login" class="text-primary-600 dark:text-primary-400 hover:underline">Sign in</a>,
then visit <a href="/create" class="text-primary-600 dark:text-primary-400 hover:underline">/create</a>
to publish notes, articles, bookmarks, and photos.
</p>
</div>
</div>
<div class="flex gap-3">
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-primary-600 text-white text-sm flex items-center justify-center font-bold">2</span>
<div>
<strong class="text-surface-900 dark:text-surface-100">Set up syndication</strong>
<p class="text-sm mt-1">
Cross-post to Mastodon, Bluesky, and LinkedIn automatically.
Add your credentials to the <code class="text-xs bg-surface-200 dark:bg-surface-700 px-1 py-0.5 rounded">.env</code> file and restart.
</p>
</div>
</div>
<div class="flex gap-3">
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-primary-600 text-white text-sm flex items-center justify-center font-bold">3</span>
<div>
<strong class="text-surface-900 dark:text-surface-100">Enable interactions</strong>
<p class="text-sm mt-1">
Receive likes, replies, and reposts from across the web.
Register at <a href="https://webmention.io" class="text-primary-600 dark:text-primary-400 hover:underline" target="_blank" rel="noopener">webmention.io</a>
and add the token to <code class="text-xs bg-surface-200 dark:bg-surface-700 px-1 py-0.5 rounded">.env</code> as <code class="text-xs bg-surface-200 dark:bg-surface-700 px-1 py-0.5 rounded">WEBMENTION_IO_TOKEN</code>.
</p>
</div>
</div>
</div>
</section>
{% endif %} {# end three-tier fallback #}