refactor: single h-card component with variant support

- h-card.njk is now the single source of truth
- sidebar.njk includes h-card with variant='full'
- blog-sidebar.njk includes h-card with variant='compact'
- Removed duplicate h-card code from both sidebars
This commit is contained in:
Ricardo
2026-01-30 16:51:29 +01:00
parent 721f37d23f
commit d6640d515d
3 changed files with 92 additions and 182 deletions

View File

@@ -1,28 +1,10 @@
{# Blog Sidebar - Shown on individual post pages #}
{# Contains: Author compact card, Related posts, Categories, Recent posts #}
{# Author Compact Card - h-card microformat (compact version) #}
{# Author Compact Card - includes h-card component #}
<div class="widget">
<div class="h-card p-author flex items-center gap-3">
<a href="{{ site.author.url }}" class="u-url u-uid" rel="me">
<img
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
class="u-photo w-12 h-12 rounded-full object-cover"
loading="lazy"
>
</a>
<div>
<a href="{{ site.author.url }}" class="u-url p-name font-medium text-surface-900 dark:text-surface-100 hover:text-primary-600 dark:hover:text-primary-400">
{{ site.author.name }}
</a>
<p class="p-job-title text-xs text-surface-500">{{ site.author.title }}</p>
{% if site.author.locality %}
<p class="p-locality text-xs text-surface-500">{{ site.author.locality }}{% if site.author.country %}, <span class="p-country-name">{{ site.author.country }}</span>{% endif %}</p>
{% endif %}
</div>
</div>
<p class="p-note text-sm text-surface-600 dark:text-surface-400 mt-2 hidden">{{ site.author.bio }}</p>
{% set hcardVariant = "compact" %}
{% include "components/h-card.njk" %}
</div>
{# Post Navigation Widget - Previous/Next #}

View File

@@ -1,116 +1,106 @@
{# h-card - IndieWeb identity microformat #}
{# h-card - Single source of truth for IndieWeb identity microformat #}
{# See: https://microformats.org/wiki/h-card #}
{#
Usage:
{% include "components/h-card.njk" %} {# defaults to full #}
{% set hcardVariant = "full" %}{% include "components/h-card.njk" %}
{% set hcardVariant = "compact" %}{% include "components/h-card.njk" %}
#}
{% set variant = hcardVariant | default("full") %}
<div class="h-card p-author" itemscope itemtype="http://schema.org/Person">
{# Avatar - visible for h-card validation #}
<a href="{{ site.author.url }}" class="u-url u-uid" rel="me" itemprop="url">
<img
class="u-photo w-16 h-16 rounded-full object-cover"
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
loading="lazy"
itemprop="image"
>
</a>
{# Name #}
<span class="p-name font-semibold text-surface-900 dark:text-surface-100" itemprop="name">
{{ site.author.name }}
</span>
{# Pronouns #}
{% if site.author.pronoun %}
<span class="p-pronoun text-surface-500 dark:text-surface-400 text-sm">({{ site.author.pronoun }})</span>
{% endif %}
{# Job title #}
{% if site.author.title %}
<span class="p-job-title text-surface-600 dark:text-surface-400 text-sm" itemprop="jobTitle">
{{ site.author.title }}
</span>
{% endif %}
{% if variant == "full" %}
{# ===== FULL VARIANT - Homepage sidebar ===== #}
<div class="flex items-center gap-4">
<a href="{{ site.author.url }}" class="u-url u-uid" rel="me">
<img
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
class="u-photo w-16 h-16 rounded-full object-cover"
loading="lazy"
itemprop="image"
>
</a>
<div>
<a href="{{ site.author.url }}" class="u-url p-name font-bold text-lg block hover:text-primary-600 dark:hover:text-primary-400" itemprop="name">
{{ site.author.name }}
</a>
{% if site.author.pronoun %}
<span class="p-pronoun text-xs text-surface-500">({{ site.author.pronoun }})</span>
{% endif %}
<p class="p-job-title text-sm text-surface-600 dark:text-surface-400" itemprop="jobTitle">{{ site.author.title }}</p>
{# Structured address #}
<p class="p-adr h-adr text-sm text-surface-500 dark:text-surface-500" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
{% if site.author.locality %}
<span class="p-locality" itemprop="addressLocality">{{ site.author.locality }}</span>{% if site.author.country %}, {% endif %}
{% endif %}
{% if site.author.country %}
<span class="p-country-name" itemprop="addressCountry">{{ site.author.country }}</span>
{% endif %}
{# Fallback to legacy location #}
{% if not site.author.locality and site.author.location %}
<span class="p-locality">{{ site.author.location }}</span>
{% endif %}
</p>
</div>
</div>
{# Bio #}
<p class="p-note mt-3 text-sm text-surface-700 dark:text-surface-300" itemprop="description">{{ site.author.bio }}</p>
{# Organization #}
{% if site.author.org %}
<span class="p-org text-surface-600 dark:text-surface-400 text-sm" itemprop="worksFor">
@ {{ site.author.org }}
</span>
{% endif %}
{# Bio / Note #}
{% if site.author.bio %}
<p class="p-note text-surface-700 dark:text-surface-300 text-sm mt-2" itemprop="description">
{{ site.author.bio }}
<p class="mt-2 text-sm text-surface-600 dark:text-surface-400">
<span class="p-org" itemprop="worksFor">{{ site.author.org }}</span>
</p>
{% endif %}
{# Location - structured address #}
<div class="p-adr h-adr text-surface-500 dark:text-surface-400 text-sm mt-1" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
{% if site.author.locality %}
<span class="p-locality" itemprop="addressLocality">{{ site.author.locality }}</span>{% if site.author.region or site.author.country %}, {% endif %}
{# Email and PGP Key #}
<div class="mt-2 flex flex-wrap gap-3 text-sm">
{% if site.author.email %}
<a href="mailto:{{ site.author.email }}" class="u-email text-primary-600 dark:text-primary-400 hover:underline" itemprop="email">
{{ site.author.email }}
</a>
{% endif %}
{% if site.author.region %}
<span class="p-region" itemprop="addressRegion">{{ site.author.region }}</span>{% if site.author.country %}, {% endif %}
{% endif %}
{% if site.author.country %}
<span class="p-country-name" itemprop="addressCountry">{{ site.author.country }}</span>
{% endif %}
{# Fallback to legacy location field if structured fields not set #}
{% if not site.author.locality and not site.author.region and not site.author.country and site.author.location %}
<span class="p-locality">{{ site.author.location }}</span>
{% if site.author.keyUrl %}
<a href="{{ site.author.keyUrl }}" class="u-key text-surface-500 dark:text-surface-400 hover:underline" rel="pgpkey">
PGP Key
</a>
{% endif %}
</div>
{# Email #}
{% if site.author.email %}
<a href="mailto:{{ site.author.email }}" class="u-email text-primary-600 dark:text-primary-400 text-sm hover:underline" itemprop="email">
{{ site.author.email }}
</a>
{% endif %}
{# PGP Key #}
{% if site.author.keyUrl %}
<a href="{{ site.author.keyUrl }}" class="u-key text-surface-500 dark:text-surface-400 text-sm hover:underline" rel="pgpkey">
🔐 PGP Key
</a>
{% endif %}
{# Categories / Skills #}
{% if site.author.categories and site.author.categories.length %}
<div class="mt-2 flex flex-wrap gap-1">
<div class="mt-3 flex flex-wrap gap-1">
{% for category in site.author.categories %}
<span class="p-category text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-800 rounded">{{ category }}</span>
{% endfor %}
</div>
{% endif %}
{# Social links with rel="me" #}
<nav class="flex flex-wrap gap-3 mt-3" aria-label="Social links">
{% for link in site.social %}
<a
href="{{ link.url }}"
rel="{{ link.rel }} noopener"
class="u-url text-surface-500 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
aria-label="{{ link.name }}"
target="_blank">
{% if link.icon == "github" %}
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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" aria-hidden="true">
<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" aria-hidden="true">
<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" aria-hidden="true">
<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 %}
{% else %}
{# ===== COMPACT VARIANT - Blog sidebar ===== #}
<div class="flex items-center gap-3">
<a href="{{ site.author.url }}" class="u-url u-uid" rel="me">
<img
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
class="u-photo w-12 h-12 rounded-full object-cover"
loading="lazy"
>
</a>
{% endfor %}
</nav>
<div>
<a href="{{ site.author.url }}" class="u-url p-name font-medium text-surface-900 dark:text-surface-100 hover:text-primary-600 dark:hover:text-primary-400">
{{ site.author.name }}
</a>
<p class="p-job-title text-xs text-surface-500">{{ site.author.title }}</p>
{% if site.author.locality %}
<p class="text-xs text-surface-500">
<span class="p-locality">{{ site.author.locality }}</span>{% if site.author.country %}, <span class="p-country-name">{{ site.author.country }}</span>{% endif %}
</p>
{% endif %}
</div>
</div>
{# Hidden but present for microformat completeness #}
<p class="p-note hidden">{{ site.author.bio }}</p>
{% if site.author.email %}<data class="u-email hidden" value="{{ site.author.email }}"></data>{% endif %}
{% if site.author.org %}<data class="p-org hidden" value="{{ site.author.org }}"></data>{% endif %}
{% endif %}
</div>

View File

@@ -1,72 +1,10 @@
{# Sidebar Components #}
{# Contains: Author card, Bluesky feed, GitHub repos, RSS feed #}
{# Author Card Widget - Full h-card microformat #}
{# Author Card Widget - includes h-card component #}
<div class="widget">
<div class="h-card p-author" itemscope itemtype="http://schema.org/Person">
<div class="flex items-center gap-4">
<a href="{{ site.author.url }}" class="u-url u-uid" rel="me">
<img
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
class="u-photo w-16 h-16 rounded-full object-cover"
loading="lazy"
itemprop="image"
>
</a>
<div>
<a href="{{ site.author.url }}" class="u-url p-name font-bold text-lg block hover:text-primary-600 dark:hover:text-primary-400" itemprop="name">
{{ site.author.name }}
</a>
{% if site.author.pronoun %}
<span class="p-pronoun text-xs text-surface-500">({{ site.author.pronoun }})</span>
{% endif %}
<p class="p-job-title text-sm text-surface-600 dark:text-surface-400" itemprop="jobTitle">{{ site.author.title }}</p>
{# Structured address #}
<p class="p-adr h-adr text-sm text-surface-500 dark:text-surface-500" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
{% if site.author.locality %}
<span class="p-locality" itemprop="addressLocality">{{ site.author.locality }}</span>{% if site.author.country %}, {% endif %}
{% endif %}
{% if site.author.country %}
<span class="p-country-name" itemprop="addressCountry">{{ site.author.country }}</span>
{% endif %}
{# Fallback to legacy location #}
{% if not site.author.locality and site.author.location %}
<span class="p-locality">{{ site.author.location }}</span>
{% endif %}
</p>
</div>
</div>
{# Bio #}
<p class="p-note mt-3 text-sm text-surface-700 dark:text-surface-300" itemprop="description">{{ site.author.bio }}</p>
{# Organization #}
{% if site.author.org %}
<p class="mt-2 text-sm text-surface-600 dark:text-surface-400">
<span class="p-org" itemprop="worksFor">{{ site.author.org }}</span>
</p>
{% endif %}
{# Email and PGP Key #}
<div class="mt-2 flex flex-wrap gap-3 text-sm">
{% if site.author.email %}
<a href="mailto:{{ site.author.email }}" class="u-email text-primary-600 dark:text-primary-400 hover:underline" itemprop="email">
✉️ {{ site.author.email }}
</a>
{% endif %}
{% if site.author.keyUrl %}
<a href="{{ site.author.keyUrl }}" class="u-key text-surface-500 dark:text-surface-400 hover:underline" rel="pgpkey">
🔐 PGP Key
</a>
{% endif %}
</div>
{# Categories / Skills #}
{% if site.author.categories and site.author.categories.length %}
<div class="mt-3 flex flex-wrap gap-1">
{% for category in site.author.categories %}
<span class="p-category text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-800 rounded">{{ category }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% set hcardVariant = "full" %}
{% include "components/h-card.njk" %}
</div>
{# Social Feed Widget - Tabbed Bluesky/Mastodon #}