Files
blog-eleventy-indiekit/_includes/components/h-card.njk
Ricardo 1026d728af a11y: fix all remaining WCAG 2.1 AA issues from audit round 2
- Focus traps for fediverse modal and lightbox dialogs (C3, C4)
- Search widget input label (C5)
- Blogroll widget tab ARIA semantics (C6)
- Footer social links "opens in new tab" warning (S5)
- Reply context aria-label on aside (S8)
- Photo alt text fallback includes post title (S10)
- Post categories use list markup (M3)
- Funkwhale now-playing bars aria-hidden (M7)
- TOC uses static Tailwind classes instead of dynamic (M9)
- Footer headings use proper aria heading roles (M15)
- Header anchor opacity increased to 1 for contrast (M18)
- Custom HTML widgets labeled as regions (M19)
- Empty collection placeholder role=status (M22)
- GitHub widget loading state announced (N5)
- Subscribe icon contrast improved (m1)
- All Permalink links have aria-label with post context (m3)
- Podroll audio element aria-label (m4)
- Obfuscated email link aria-label (m6)
- Fediverse follow button uses aria-label (M10)

Score: 53.6% → 92.9% (26/28 WCAG criteria passing)

Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
2026-03-07 19:34:25 +01:00

114 lines
4.8 KiB
Plaintext

{# 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 or site.author.name %}
{% set authorAvatar = id.avatar or site.author.avatar %}
{% set authorTitle = id.title or site.author.title %}
{% set authorBio = id.bio or site.author.bio %}
{% set authorUrl = id.url or site.author.url %}
{% set authorPronoun = id.pronoun or site.author.pronoun %}
{% set authorLocality = id.locality or site.author.locality %}
{% set authorCountry = id.country or site.author.country %}
{% set authorLocation = site.author.location %}
{% set authorOrg = id.org or site.author.org %}
{% set authorEmail = id.email or site.author.email %}
{% set authorKeyUrl = id.keyUrl or site.author.keyUrl %}
{% set authorCategories = id.categories if (id.categories and id.categories.length) else site.author.categories %}
{% set socialLinks = id.social if (id.social and id.social.length) else site.social %}
<div class="h-card p-author" itemscope itemtype="http://schema.org/Person">
{# Hidden u-photo for reliable microformat parsing (some parsers struggle with img inside links) #}
<data class="u-photo hidden" value="{{ authorAvatar }}"></data>
<div class="flex items-center gap-4">
<a href="{{ authorUrl }}" class="u-url u-uid" rel="me" itemprop="url">
<img
src="{{ authorAvatar }}"
alt="{{ authorName }}"
width="64"
height="64"
class="w-16 h-16 rounded-full object-cover shadow-lg"
loading="lazy"
itemprop="image"
>
</a>
<div>
<a href="{{ authorUrl }}" class="u-url p-name font-bold text-lg block hover:text-accent-600 dark:hover:text-accent-400" itemprop="name">
{{ authorName }}
</a>
{% if authorPronoun %}
<span class="p-pronoun text-xs text-surface-600 dark:text-surface-400">({{ authorPronoun }})</span>
{% endif %}
<p class="p-job-title text-sm text-surface-600 dark:text-surface-400" itemprop="jobTitle">{{ authorTitle }}</p>
{# Structured address #}
<p class="p-adr h-adr text-sm text-surface-600 dark:text-surface-400" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
{% if authorLocality %}
<span class="p-locality" itemprop="addressLocality">{{ authorLocality }}</span>{% if authorCountry %}, {% endif %}
{% endif %}
{% if authorCountry %}
<span class="p-country-name" itemprop="addressCountry">{{ authorCountry }}</span>
{% endif %}
{# Fallback to legacy location field #}
{% if not authorLocality and authorLocation %}
<span class="p-locality">{{ authorLocation }}</span>
{% endif %}
</p>
</div>
</div>
{# Bio #}
<p class="p-note mt-3 text-sm text-surface-700 dark:text-surface-300" itemprop="description">{{ authorBio }}</p>
{# Organization #}
{% if authorOrg %}
<p class="mt-2 text-sm text-surface-600 dark:text-surface-400">
<span class="p-org" itemprop="worksFor">{{ authorOrg }}</span>
</p>
{% endif %}
{# Email and PGP Key #}
<div class="mt-2 flex flex-wrap gap-3 text-sm">
{% if authorEmail %}
{# Display text obfuscated to deter spam harvesters; href kept plain for browser compatibility #}
<a href="mailto:{{ authorEmail }}" class="u-email text-accent-600 dark:text-accent-400 hover:underline" itemprop="email" aria-label="Email {{ authorEmail }}">
✉️ {{ authorEmail | obfuscateEmail | safe }}
</a>
{% endif %}
{% if authorKeyUrl %}
<a href="{{ authorKeyUrl }}" class="u-key text-surface-600 dark:text-surface-400 hover:underline" rel="pgpkey">
🔐 PGP Key
</a>
{% endif %}
</div>
{# Categories / Skills #}
{% if authorCategories and authorCategories.length %}
<ul class="mt-3 flex flex-wrap gap-1 list-none p-0 m-0" role="list" aria-label="Skills and interests">
{% for category in authorCategories %}
<li class="p-category text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-800 rounded-full">{{ category }}</li>
{% endfor %}
</ul>
{% endif %}
{# Social links with rel="me" - critical for IndieWeb identity verification #}
{% from "components/social-icon.njk" import socialIcon %}
{% if socialLinks and socialLinks.length %}
<nav class="flex flex-wrap gap-3 mt-3" aria-label="Social links">
{% for link in socialLinks %}
<a
href="{{ link.url }}"
rel="{{ link.rel }} noopener"
class="u-url text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400 transition-colors"
aria-label="{{ link.name }} (opens in new tab)"
target="_blank">
{{ socialIcon(link.icon, "w-5 h-5") }}
</a>
{% endfor %}
</nav>
{% endif %}
</div>