feat: add zachleat.com-inspired theme enhancements
- Add time-difference web component for relative dates - Add @zachleat/table-saw for responsive tables - Add webmention facepile styling with bookmarks support - Add OG image thumbnails to post navigation - Add @11ty/is-land for lazy widget hydration - Wrap sidebar widgets in is-land for deferred loading - Lazy-load webmention avatars with is-land - Add @zachleat/filter-container for blog archive filtering - Add posting frequency sparkline to blog header - Inline critical CSS and defer full stylesheet loading
This commit is contained in:
@@ -12,22 +12,28 @@
|
||||
{% set _bookmarkedUrl = _prevPost.data.bookmarkOf or _prevPost.data.bookmark_of %}
|
||||
{% set _repostedUrl = _prevPost.data.repostOf or _prevPost.data.repost_of %}
|
||||
{% set _replyToUrl = _prevPost.data.inReplyTo or _prevPost.data.in_reply_to %}
|
||||
<a href="{{ _prevPost.url }}" class="text-sm text-primary-600 dark:text-primary-400 hover:underline line-clamp-2 flex items-center gap-1.5">
|
||||
{% if _likedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
||||
Liked {{ _likedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _bookmarkedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-amber-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/></svg>
|
||||
{{ _prevPost.data.title or ("Bookmarked " + (_bookmarkedUrl | replace("https://", "") | truncate(30))) }}
|
||||
{% elif _repostedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Reposted {{ _repostedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _replyToUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-primary-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
|
||||
Reply to {{ _replyToUrl | replace("https://", "") | truncate(35) }}
|
||||
{% else %}
|
||||
{{ _prevPost.data.title or _prevPost.data.name or (_prevPost.templateContent | striptags | truncate(50)) or "Note" }}
|
||||
{% set _prevHasOg = _prevPost.fileSlug | hasOgImage %}
|
||||
<a href="{{ _prevPost.url }}" class="group flex items-start gap-3 text-sm text-primary-600 dark:text-primary-400 hover:underline">
|
||||
{% if _prevHasOg %}
|
||||
<img src="/og/{{ _prevPost.fileSlug }}.png" alt="" class="w-20 h-[42px] object-cover rounded flex-shrink-0 opacity-80 group-hover:opacity-100 transition-opacity" loading="lazy" eleventy:ignore>
|
||||
{% endif %}
|
||||
<span class="line-clamp-2 flex items-center gap-1.5">
|
||||
{% if _likedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
||||
Liked {{ _likedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _bookmarkedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-amber-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/></svg>
|
||||
{{ _prevPost.data.title or ("Bookmarked " + (_bookmarkedUrl | replace("https://", "") | truncate(30))) }}
|
||||
{% elif _repostedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Reposted {{ _repostedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _replyToUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-primary-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
|
||||
Reply to {{ _replyToUrl | replace("https://", "") | truncate(35) }}
|
||||
{% else %}
|
||||
{{ _prevPost.data.title or _prevPost.data.name or (_prevPost.templateContent | striptags | truncate(50)) or "Note" }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -40,21 +46,27 @@
|
||||
{% 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 %}
|
||||
<a href="{{ _nextPost.url }}" class="text-sm text-primary-600 dark:text-primary-400 hover:underline line-clamp-2 flex items-center gap-1.5 {% if _prevPost %}justify-end{% endif %}">
|
||||
{% if _likedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
||||
Liked {{ _likedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _bookmarkedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-amber-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/></svg>
|
||||
{{ _nextPost.data.title or ("Bookmarked " + (_bookmarkedUrl | replace("https://", "") | truncate(30))) }}
|
||||
{% elif _repostedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Reposted {{ _repostedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _replyToUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-primary-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
|
||||
Reply to {{ _replyToUrl | replace("https://", "") | truncate(35) }}
|
||||
{% else %}
|
||||
{{ _nextPost.data.title or _nextPost.data.name or (_nextPost.templateContent | striptags | truncate(50)) or "Note" }}
|
||||
{% set _nextHasOg = _nextPost.fileSlug | hasOgImage %}
|
||||
<a href="{{ _nextPost.url }}" class="group flex items-start gap-3 text-sm text-primary-600 dark:text-primary-400 hover:underline {% if _prevPost %}justify-end{% endif %}">
|
||||
<span class="line-clamp-2 flex items-center gap-1.5 {% if _prevPost %}justify-end{% endif %}">
|
||||
{% if _likedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
||||
Liked {{ _likedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _bookmarkedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-amber-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/></svg>
|
||||
{{ _nextPost.data.title or ("Bookmarked " + (_bookmarkedUrl | replace("https://", "") | truncate(30))) }}
|
||||
{% elif _repostedUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Reposted {{ _repostedUrl | replace("https://", "") | truncate(35) }}
|
||||
{% elif _replyToUrl %}
|
||||
<svg class="w-3.5 h-3.5 text-primary-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
|
||||
Reply to {{ _replyToUrl | replace("https://", "") | truncate(35) }}
|
||||
{% else %}
|
||||
{{ _nextPost.data.title or _nextPost.data.name or (_nextPost.templateContent | striptags | truncate(50)) or "Note" }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if _nextHasOg %}
|
||||
<img src="/og/{{ _nextPost.fileSlug }}.png" alt="" class="w-20 h-[42px] object-cover rounded flex-shrink-0 opacity-80 group-hover:opacity-100 transition-opacity" loading="lazy" eleventy:ignore>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -27,22 +27,24 @@
|
||||
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
|
||||
{{ likes.length }} Like{% if likes.length != 1 %}s{% endif %}
|
||||
</h3>
|
||||
<div class="avatar-row">
|
||||
<is-land on:visible>
|
||||
<div class="facepile">
|
||||
{% for like in likes %}
|
||||
<a href="{{ like.author.url }}"
|
||||
class="inline-block"
|
||||
class="facepile-avatar"
|
||||
title="{{ like.author.name }}"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<img
|
||||
src="{{ like.author.photo or '/images/default-avatar.svg' }}"
|
||||
alt="{{ like.author.name }}"
|
||||
class="w-8 h-8 rounded-full"
|
||||
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
|
||||
loading="lazy"
|
||||
>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</is-land>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -53,22 +55,52 @@
|
||||
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
|
||||
{{ reposts.length }} Repost{% if reposts.length != 1 %}s{% endif %}
|
||||
</h3>
|
||||
<div class="avatar-row">
|
||||
<is-land on:visible>
|
||||
<div class="facepile">
|
||||
{% for repost in reposts %}
|
||||
<a href="{{ repost.author.url }}"
|
||||
class="inline-block"
|
||||
class="facepile-avatar"
|
||||
title="{{ repost.author.name }}"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<img
|
||||
src="{{ repost.author.photo or '/images/default-avatar.svg' }}"
|
||||
alt="{{ repost.author.name }}"
|
||||
class="w-8 h-8 rounded-full"
|
||||
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
|
||||
loading="lazy"
|
||||
>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</is-land>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Bookmarks #}
|
||||
{% set bookmarks = mentions | webmentionsByType('bookmarks') %}
|
||||
{% if bookmarks.length %}
|
||||
<div class="webmention-bookmarks mb-6">
|
||||
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
|
||||
{{ bookmarks.length }} Bookmark{% if bookmarks.length != 1 %}s{% endif %}
|
||||
</h3>
|
||||
<is-land on:visible>
|
||||
<div class="facepile">
|
||||
{% for bookmark in bookmarks %}
|
||||
<a href="{{ bookmark.author.url }}"
|
||||
class="facepile-avatar"
|
||||
title="{{ bookmark.author.name }}"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<img
|
||||
src="{{ bookmark.author.photo or '/images/default-avatar.svg' }}"
|
||||
alt="{{ bookmark.author.name }}"
|
||||
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
|
||||
loading="lazy"
|
||||
>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</is-land>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{# Blogroll Widget - Dynamic loading from API with source tabs #}
|
||||
<is-land on:visible>
|
||||
<div class="widget" x-data="blogrollWidget()" x-init="init()">
|
||||
<h3 class="widget-title flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -106,3 +107,4 @@ function blogrollWidget() {
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</is-land>
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<is-land on:visible>
|
||||
<div class="widget" x-data="feedlandWidget()" x-init="init()">
|
||||
<div class="fl-wrap" tabindex="0">
|
||||
{# Title + menu #}
|
||||
@@ -379,3 +380,4 @@ function feedlandWidget() {
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</is-land>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{# Funkwhale Now Playing Widget #}
|
||||
{% if funkwhaleActivity and (funkwhaleActivity.nowPlaying or funkwhaleActivity.stats) %}
|
||||
<is-land on:visible>
|
||||
<div class="widget">
|
||||
<h3 class="widget-title flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -68,4 +69,5 @@
|
||||
<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>
|
||||
</div>
|
||||
</is-land>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{# GitHub Activity Widget - Tabbed Commits/Repos/Featured/PRs with live API data #}
|
||||
<is-land on:visible>
|
||||
<div class="widget" x-data="githubWidget('{{ site.feeds.github }}')" x-init="init()">
|
||||
<h3 class="widget-title flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
@@ -209,3 +210,4 @@ function githubWidget(username) {
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</is-land>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{# Social Feed Widget - Tabbed Bluesky/Mastodon #}
|
||||
{% if (blueskyFeed and blueskyFeed.length) or (mastodonFeed and mastodonFeed.length) %}
|
||||
<is-land on:visible>
|
||||
<div class="widget" x-data="{ activeTab: 'bluesky' }">
|
||||
<h3 class="widget-title">Social Activity</h3>
|
||||
|
||||
@@ -91,4 +92,5 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</is-land>
|
||||
{% endif %}
|
||||
|
||||
@@ -58,13 +58,19 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="/pagefind/pagefind-ui.css">
|
||||
{# Critical CSS — inlined for fast first paint #}
|
||||
<style>{{ "css/critical.css" | inlineFile | safe }}</style>
|
||||
{# Defer full stylesheet — loads after first paint #}
|
||||
<link rel="stylesheet" href="/css/style.css?v={{ '/css/style.css' | hash }}" media="print" onload="this.media='all'">
|
||||
<noscript><link rel="stylesheet" href="/css/style.css?v={{ '/css/style.css' | hash }}"></noscript>
|
||||
<link rel="stylesheet" href="/css/prism-theme.css?v={{ '/css/prism-theme.css' | hash }}" media="print" onload="this.media='all'">
|
||||
<noscript><link rel="stylesheet" href="/css/prism-theme.css?v={{ '/css/prism-theme.css' | hash }}"></noscript>
|
||||
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" media="print" onload="this.media='all'">
|
||||
<noscript><link rel="stylesheet" href="/pagefind/pagefind-ui.css"></noscript>
|
||||
<script>
|
||||
var _pfQueue = [];
|
||||
function initPagefind(sel, opts) { _pfQueue.push([sel, opts]); }
|
||||
</script>
|
||||
<link rel="stylesheet" href="/css/style.css?v={{ '/css/style.css' | hash }}">
|
||||
<link rel="stylesheet" href="/css/prism-theme.css?v={{ '/css/prism-theme.css' | hash }}">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lite-youtube-embed@0.3.2/src/lite-yt-embed.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/lite-youtube-embed@0.3.2/src/lite-yt-embed.min.js" defer></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||
@@ -409,6 +415,14 @@
|
||||
document.documentElement.addEventListener('mouseover', prefetch, { capture: true, passive: true });
|
||||
document.documentElement.addEventListener('touchstart', prefetch, { capture: true, passive: true });
|
||||
</script>
|
||||
{# Island architecture - lazy hydration for widgets #}
|
||||
<script type="module" src="/js/is-land.js"></script>
|
||||
{# Relative date display - progressively enhances <time> elements #}
|
||||
<script src="/js/time-difference.js?v={{ '/js/time-difference.js' | hash }}" defer></script>
|
||||
{# Responsive tables - auto-enhances <table> on narrow screens #}
|
||||
<script type="module" src="/js/table-saw.js"></script>
|
||||
{# Client-side filtering for archive pages #}
|
||||
<script type="module" src="/js/filter-container.js"></script>
|
||||
{# Client-side webmention fetcher - supplements build-time cache with real-time data #}
|
||||
<script src="/js/webmentions.js?v={{ '/js/webmentions.js' | hash }}" defer></script>
|
||||
{# Admin auth detection - shows dashboard link + FAB when logged in #}
|
||||
|
||||
@@ -8,9 +8,9 @@ withBlogSidebar: true
|
||||
{% endif %}
|
||||
|
||||
<div class="post-meta mb-4 sm:mb-6">
|
||||
<time class="dt-published" datetime="{{ date.toISOString() }}">
|
||||
<time-difference><time class="dt-published" datetime="{{ date.toISOString() }}">
|
||||
{{ date | dateDisplay }}
|
||||
</time>
|
||||
</time></time-difference>
|
||||
{% if category %}
|
||||
<span class="post-categories">
|
||||
{# Handle both string and array categories #}
|
||||
|
||||
62
blog.njk
62
blog.njk
@@ -9,22 +9,43 @@ pagination:
|
||||
permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
|
||||
---
|
||||
<div class="h-feed">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-2">Blog</h1>
|
||||
<div class="flex flex-wrap items-center gap-4 mb-2">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Blog</h1>
|
||||
{% set sparklineData = collections.posts | postingFrequency %}
|
||||
{% if sparklineData %}
|
||||
<img src="https://v1.sparkline.11ty.dev/{{ sparklineData }}/" alt="Posting frequency over the last 12 months" width="200" height="30" class="opacity-60 dark:invert dark:opacity-40" loading="lazy">
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
|
||||
All posts including articles and notes.
|
||||
<span class="text-sm">({{ collections.posts.length }} total)</span>
|
||||
</p>
|
||||
|
||||
{% if paginatedPosts.length > 0 %}
|
||||
<filter-container oninit leave-url-alone>
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<select data-filter-key="type" class="px-3 py-1.5 text-sm bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg">
|
||||
<option value="">All Types</option>
|
||||
<option value="article">Articles</option>
|
||||
<option value="note">Notes</option>
|
||||
<option value="photo">Photos</option>
|
||||
<option value="bookmark">Bookmarks</option>
|
||||
<option value="like">Likes</option>
|
||||
<option value="reply">Replies</option>
|
||||
<option value="repost">Reposts</option>
|
||||
</select>
|
||||
<span data-filter-results class="text-sm text-surface-500 dark:text-surface-400 self-center"></span>
|
||||
</div>
|
||||
<ul class="post-list">
|
||||
{% for post in paginatedPosts %}
|
||||
<li class="h-entry post-card">
|
||||
{# 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 %}
|
||||
{# 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 _postType %}{% if likedUrl %}like{% elif bookmarkedUrl %}bookmark{% elif repostedUrl %}repost{% elif replyToUrl %}reply{% elif hasPhotos %}photo{% elif post.data.title %}article{% else %}note{% endif %}{% endset %}
|
||||
<li class="h-entry post-card" data-filter-type="{{ _postType | trim }}">
|
||||
|
||||
{% if likedUrl %}
|
||||
{# ── Like card ── #}
|
||||
@@ -37,9 +58,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="post-meta">
|
||||
<span class="font-medium text-red-600 dark:text-red-400">Liked</span>
|
||||
<time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
{{ post.date | dateDisplay }}
|
||||
</time>
|
||||
</time></time-difference>
|
||||
{% if post.data.category %}
|
||||
<span class="post-categories">
|
||||
{% if post.data.category is string %}
|
||||
@@ -77,9 +98,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="post-meta">
|
||||
<span class="font-medium text-amber-600 dark:text-amber-400">Bookmarked</span>
|
||||
<time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
{{ post.date | dateDisplay }}
|
||||
</time>
|
||||
</time></time-difference>
|
||||
{% if post.data.category %}
|
||||
<span class="post-categories">
|
||||
{% if post.data.category is string %}
|
||||
@@ -122,9 +143,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="post-meta">
|
||||
<span class="font-medium text-green-600 dark:text-green-400">Reposted</span>
|
||||
<time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
{{ post.date | dateDisplay }}
|
||||
</time>
|
||||
</time></time-difference>
|
||||
{% if post.data.category %}
|
||||
<span class="post-categories">
|
||||
{% if post.data.category is string %}
|
||||
@@ -162,9 +183,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="post-meta">
|
||||
<span class="font-medium text-primary-600 dark:text-primary-400">In reply to</span>
|
||||
<time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
{{ post.date | dateDisplay }}
|
||||
</time>
|
||||
</time></time-difference>
|
||||
{% if post.data.category %}
|
||||
<span class="post-categories">
|
||||
{% if post.data.category is string %}
|
||||
@@ -201,9 +222,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="post-meta">
|
||||
<span class="font-medium text-purple-600 dark:text-purple-400">Photo</span>
|
||||
<time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
|
||||
{{ post.date | dateDisplay }}
|
||||
</time>
|
||||
</time></time-difference>
|
||||
{% if post.data.category %}
|
||||
<span class="post-categories">
|
||||
{% if post.data.category is string %}
|
||||
@@ -272,9 +293,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
||||
{# ── Note card (unchanged) ── #}
|
||||
<div class="post-header">
|
||||
<a class="u-url" href="{{ post.url }}">
|
||||
<time class="dt-published text-sm text-primary-600 dark:text-primary-400 font-medium" datetime="{{ post.date | isoDate }}">
|
||||
<time-difference><time class="dt-published text-sm text-primary-600 dark:text-primary-400 font-medium" datetime="{{ post.date | isoDate }}">
|
||||
{{ post.date | dateDisplay }}
|
||||
</time>
|
||||
</time></time-difference>
|
||||
</a>
|
||||
{% if post.data.category %}
|
||||
<span class="post-categories ml-2">
|
||||
@@ -300,6 +321,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</filter-container>
|
||||
|
||||
{# Pagination controls #}
|
||||
{% if pagination.pages.length > 1 %}
|
||||
|
||||
57
css/critical.css
Normal file
57
css/critical.css
Normal file
@@ -0,0 +1,57 @@
|
||||
/* Critical CSS — inlined in <head> for first paint */
|
||||
/* Covers: layout shell, header, dark mode toggle, font display, basic typography */
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
body{margin:0;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||
|
||||
/* Dark mode base */
|
||||
body{background-color:#fff;color:#18181b}
|
||||
.dark body{background-color:#09090b;color:#f4f4f5}
|
||||
|
||||
/* Container */
|
||||
.container{max-width:64rem;margin-left:auto;margin-right:auto;padding-left:1rem;padding-right:1rem}
|
||||
|
||||
/* Header — sticky, visible immediately */
|
||||
.site-header{background-color:#fff;border-bottom:1px solid #e4e4e7;padding-top:1rem;padding-bottom:1rem;position:sticky;top:0;z-index:50}
|
||||
.dark .site-header{background-color:#18181b;border-bottom-color:#3f3f46}
|
||||
.header-container{display:flex;align-items:center;justify-content:space-between}
|
||||
.site-title{font-size:1.25rem;font-weight:700;color:#18181b;text-decoration:none}
|
||||
.dark .site-title{color:#fff}
|
||||
|
||||
/* Header actions — hidden on mobile */
|
||||
.header-actions{display:none}
|
||||
@media(min-width:768px){.header-actions{display:flex;align-items:center;gap:1rem}}
|
||||
|
||||
/* Mobile menu toggle */
|
||||
.menu-toggle{display:block;padding:0.5rem;border-radius:0.5rem;background:none;border:none;color:#52525b;cursor:pointer}
|
||||
@media(min-width:768px){.menu-toggle{display:none}}
|
||||
.dark .menu-toggle{color:#a1a1aa}
|
||||
|
||||
/* Hidden utility */
|
||||
.hidden{display:none!important}
|
||||
[x-cloak]{display:none!important}
|
||||
|
||||
/* Dark mode theme toggle icons */
|
||||
.theme-toggle .sun-icon{display:none}
|
||||
.theme-toggle .moon-icon{display:block}
|
||||
.dark .theme-toggle .sun-icon{display:block}
|
||||
.dark .theme-toggle .moon-icon{display:none}
|
||||
|
||||
/* Main content padding */
|
||||
main.container{padding-top:1.5rem;padding-bottom:1.5rem}
|
||||
@media(min-width:768px){main.container{padding-top:2rem;padding-bottom:2rem}}
|
||||
|
||||
/* Layout with sidebar */
|
||||
.layout-with-sidebar{display:grid;grid-template-columns:1fr;gap:1.5rem}
|
||||
@media(min-width:1024px){.layout-with-sidebar{grid-template-columns:2fr 1fr;gap:2rem}}
|
||||
.main-content{min-width:0;overflow-x:hidden}
|
||||
|
||||
/* Basic typography — prevent FOUT */
|
||||
h1,h2,h3,h4{margin:0;line-height:1.25}
|
||||
a{color:#2563eb}
|
||||
.dark a{color:#60a5fa}
|
||||
|
||||
/* Prevent flash of unstyled content for nav */
|
||||
.site-nav{display:flex;align-items:center;gap:1rem}
|
||||
.site-nav>a,.site-nav .nav-dropdown-trigger{color:#52525b;text-decoration:none;padding-top:0.5rem;padding-bottom:0.5rem}
|
||||
.dark .site-nav>a,.dark .site-nav .nav-dropdown-trigger{color:#a1a1aa}
|
||||
@@ -237,12 +237,16 @@
|
||||
@apply inline-block px-2 py-0.5 text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 rounded;
|
||||
}
|
||||
|
||||
/* Webmention styles */
|
||||
.webmention-likes .avatar-row {
|
||||
@apply flex flex-wrap gap-1;
|
||||
/* Webmention facepile - overlapping avatar display */
|
||||
.facepile {
|
||||
@apply flex flex-wrap items-center;
|
||||
}
|
||||
|
||||
.webmention-likes img {
|
||||
.facepile-avatar {
|
||||
@apply inline-block -ml-2 first:ml-0 transition-transform hover:z-10 hover:scale-110;
|
||||
}
|
||||
|
||||
.facepile-avatar img {
|
||||
@apply w-8 h-8 rounded-full;
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,13 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPassthroughCopy("favicon.ico");
|
||||
eleventyConfig.addPassthroughCopy({ ".cache/og": "og" });
|
||||
|
||||
// Copy vendor web components from node_modules
|
||||
eleventyConfig.addPassthroughCopy({
|
||||
"node_modules/@zachleat/table-saw/table-saw.js": "js/table-saw.js",
|
||||
"node_modules/@11ty/is-land/is-land.js": "js/is-land.js",
|
||||
"node_modules/@zachleat/filter-container/filter-container.js": "js/filter-container.js",
|
||||
});
|
||||
|
||||
// Watch for content changes
|
||||
eleventyConfig.addWatchTarget("./content/");
|
||||
eleventyConfig.addWatchTarget("./css/");
|
||||
@@ -352,6 +359,16 @@ export default function (eleventyConfig) {
|
||||
return existsSync(ogPath);
|
||||
});
|
||||
|
||||
// Inline file contents (for critical CSS inlining)
|
||||
eleventyConfig.addFilter("inlineFile", (filePath) => {
|
||||
try {
|
||||
const fullPath = resolve(__dirname, filePath.startsWith("/") ? `.${filePath}` : filePath);
|
||||
return readFileSync(fullPath, "utf-8");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
// Current timestamp filter (for client-side JS buildtime)
|
||||
eleventyConfig.addFilter("timestamp", () => Date.now());
|
||||
|
||||
@@ -426,6 +443,22 @@ export default function (eleventyConfig) {
|
||||
: null;
|
||||
});
|
||||
|
||||
// Posting frequency — compute posts-per-month for last 12 months (for sparkline)
|
||||
eleventyConfig.addFilter("postingFrequency", (posts) => {
|
||||
if (!Array.isArray(posts) || posts.length === 0) return "";
|
||||
const now = new Date();
|
||||
const counts = new Array(12).fill(0);
|
||||
for (const post of posts) {
|
||||
const postDate = new Date(post.date || post.data?.date);
|
||||
if (isNaN(postDate.getTime())) continue;
|
||||
const monthsAgo = (now.getFullYear() - postDate.getFullYear()) * 12 + (now.getMonth() - postDate.getMonth());
|
||||
if (monthsAgo >= 0 && monthsAgo < 12) {
|
||||
counts[11 - monthsAgo]++;
|
||||
}
|
||||
}
|
||||
return counts.join(",");
|
||||
});
|
||||
|
||||
// Collections for different post types
|
||||
// Note: content path is content/ due to symlink structure
|
||||
// "posts" shows ALL content types combined
|
||||
|
||||
91
js/time-difference.js
Normal file
91
js/time-difference.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* <time-difference> Web Component
|
||||
* Progressively enhances <time> elements with relative date display.
|
||||
* Falls back to static date text when JS is unavailable.
|
||||
*
|
||||
* Usage: <time-difference><time datetime="2026-02-15T...">February 15, 2026</time></time-difference>
|
||||
*
|
||||
* Inspired by zachleat.com's time-difference component.
|
||||
*/
|
||||
class TimeDifference extends HTMLElement {
|
||||
static register(tagName = "time-difference") {
|
||||
if ("customElements" in window) {
|
||||
customElements.define(tagName, TimeDifference);
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.update();
|
||||
// Auto-update every 60 seconds
|
||||
this._interval = setInterval(() => this.update(), 60000);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
clearInterval(this._interval);
|
||||
}
|
||||
|
||||
update() {
|
||||
const time = this.querySelector("time[datetime]");
|
||||
if (!time) return;
|
||||
|
||||
const datetime = time.getAttribute("datetime");
|
||||
if (!datetime) return;
|
||||
|
||||
const date = new Date(datetime);
|
||||
if (isNaN(date.getTime())) return;
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
|
||||
// Don't show relative time for future dates
|
||||
if (diffMs < 0) return;
|
||||
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHour / 24);
|
||||
const diffWeek = Math.floor(diffDay / 7);
|
||||
const diffMonth = Math.floor(diffDay / 30.44);
|
||||
const diffYear = Math.floor(diffDay / 365.25);
|
||||
|
||||
let value, unit;
|
||||
|
||||
if (diffSec < 60) {
|
||||
value = -diffSec;
|
||||
unit = "second";
|
||||
} else if (diffMin < 60) {
|
||||
value = -diffMin;
|
||||
unit = "minute";
|
||||
} else if (diffHour < 24) {
|
||||
value = -diffHour;
|
||||
unit = "hour";
|
||||
} else if (diffDay < 7) {
|
||||
value = -diffDay;
|
||||
unit = "day";
|
||||
} else if (diffWeek < 4) {
|
||||
value = -diffWeek;
|
||||
unit = "week";
|
||||
} else if (diffMonth < 12) {
|
||||
value = -diffMonth;
|
||||
unit = "month";
|
||||
} else {
|
||||
value = -diffYear;
|
||||
unit = "year";
|
||||
}
|
||||
|
||||
try {
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
const relative = rtf.format(value, unit);
|
||||
|
||||
// Store original text as title for hover tooltip
|
||||
if (!time.hasAttribute("title")) {
|
||||
time.setAttribute("title", time.textContent.trim());
|
||||
}
|
||||
time.textContent = relative;
|
||||
} catch {
|
||||
// Intl.RelativeTimeFormat not supported, keep static text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimeDifference.register();
|
||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -13,11 +13,14 @@
|
||||
"@11ty/eleventy-img": "^6.0.0",
|
||||
"@11ty/eleventy-plugin-rss": "^2.0.2",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||
"@11ty/is-land": "^5.0.1",
|
||||
"@atproto/api": "^0.12.0",
|
||||
"@chrisburnell/eleventy-cache-webmentions": "^2.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@quasibit/eleventy-plugin-sitemap": "^2.2.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@zachleat/filter-container": "^4.0.0",
|
||||
"@zachleat/table-saw": "^1.0.7",
|
||||
"eleventy-plugin-embed-everything": "^1.21.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-minifier-terser": "^7.0.0",
|
||||
@@ -259,6 +262,16 @@
|
||||
"url": "https://opencollective.com/11ty"
|
||||
}
|
||||
},
|
||||
"node_modules/@11ty/is-land": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@11ty/is-land/-/is-land-5.0.1.tgz",
|
||||
"integrity": "sha512-Rh/sLhE4vrc2JaSjeY385v2UxnDY9BhnQtitETb3SKyr0A48Q5Vn06q2AvDBHObtk9+dcFWsoZX4jhT+O9g+xQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/11ty"
|
||||
}
|
||||
},
|
||||
"node_modules/@11ty/lodash-custom": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz",
|
||||
@@ -1338,6 +1351,18 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@zachleat/filter-container": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@zachleat/filter-container/-/filter-container-4.0.0.tgz",
|
||||
"integrity": "sha512-4etUifhHciQTMtlDymb4cus03qYZrKiuK0gY0cOU4Jrt0LA3ZvPVTwO6esqUdvbpte5ypodmpx84wwbT3Fy5Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zachleat/table-saw": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@zachleat/table-saw/-/table-saw-1.0.7.tgz",
|
||||
"integrity": "sha512-AxYShldkUoofdzqyH/pgbiXBgoRbFtoQYKbCjTK7diViqepM2SqZoMZm7ofc9UeGaDWgktAOmoJg3T+yN0SAxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/a-sync-waterfall": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
"@11ty/eleventy-img": "^6.0.0",
|
||||
"@11ty/eleventy-plugin-rss": "^2.0.2",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||
"@11ty/is-land": "^5.0.1",
|
||||
"@atproto/api": "^0.12.0",
|
||||
"@chrisburnell/eleventy-cache-webmentions": "^2.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@quasibit/eleventy-plugin-sitemap": "^2.2.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@zachleat/filter-container": "^4.0.0",
|
||||
"@zachleat/table-saw": "^1.0.7",
|
||||
"eleventy-plugin-embed-everything": "^1.21.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-minifier-terser": "^7.0.0",
|
||||
|
||||
Reference in New Issue
Block a user