feat: harmonize blog views with homepage UI/UX

Add color-coded left borders to post cards on all blog listing and
category pages, and make sidebar widgets collapsible with localStorage
persistence on both listing and single-post sidebars.
This commit is contained in:
Ricardo
2026-02-24 16:58:46 +01:00
parent b8c76e8f22
commit 91ed859a35
11 changed files with 486 additions and 137 deletions

View File

@@ -1,9 +1,68 @@
{# Blog Sidebar - Shown on individual post pages #}
{# Data-driven when homepageConfig.blogPostSidebar is configured, otherwise falls back to default widgets #}
{# Each widget is wrapped in a collapsible container with localStorage persistence #}
{% if homepageConfig and homepageConfig.blogPostSidebar and homepageConfig.blogPostSidebar.length %}
{# === Data-driven mode: render configured widgets === #}
{% for widget in homepageConfig.blogPostSidebar %}
{# Resolve widget title #}
{% if widget.type == "search" %}{% set widgetTitle = "Search" %}
{% elif widget.type == "social-activity" %}{% set widgetTitle = "Social Activity" %}
{% elif widget.type == "github-repos" %}{% set widgetTitle = "GitHub" %}
{% elif widget.type == "funkwhale" %}{% set widgetTitle = "Listening" %}
{% elif widget.type == "recent-posts" %}{% set widgetTitle = "Recent Posts" %}
{% elif widget.type == "blogroll" %}{% set widgetTitle = "Blogroll" %}
{% elif widget.type == "feedland" %}{% set widgetTitle = "FeedLand" %}
{% elif widget.type == "categories" %}{% set widgetTitle = "Categories" %}
{% elif widget.type == "webmentions" %}{% set widgetTitle = "Webmentions" %}
{% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %}
{% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %}
{% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %}
{% elif widget.type == "author-card-compact" %}{% set widgetTitle = "Author" %}
{% elif widget.type == "subscribe" %}{% set widgetTitle = "Subscribe" %}
{% elif widget.type == "toc" %}{% set widgetTitle = "Table of Contents" %}
{% elif widget.type == "post-categories" %}{% set widgetTitle = "Categories" %}
{% elif widget.type == "share" %}{% set widgetTitle = "Share" %}
{% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %}
{% else %}{% set widgetTitle = widget.type %}
{% endif %}
{% set widgetKey = "post-widget-" + widget.type + "-" + loop.index0 %}
{% set defaultOpen = "true" if loop.index0 < 3 else "false" %}
{# Collapsible wrapper — Alpine.js handles toggle, localStorage persists state #}
<div
class="widget-collapsible mb-4"
x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : {{ defaultOpen }} }"
>
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button
class="widget-header w-full p-4"
@click="open = !open; localStorage.setItem('{{ widgetKey }}', open)"
:aria-expanded="open ? 'true' : 'false'"
>
<h3 class="widget-title font-bold text-lg">{{ widgetTitle }}</h3>
<svg
class="widget-chevron"
:class="open && 'rotate-180'"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div
x-show="open"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak
>
{# Widget content — inner .widget provides padding, inner title hidden by CSS #}
{% if widget.type == "author-card-compact" %}
{% include "components/widgets/author-card-compact.njk" %}
{% elif widget.type == "author-card" %}
@@ -37,7 +96,6 @@
{% elif widget.type == "search" %}
<is-land on:visible>
<div class="widget">
<h3 class="widget-title">Search</h3>
<div id="blog-sidebar-search"></div>
<script>initPagefind("#blog-sidebar-search");</script>
</div>
@@ -48,9 +106,6 @@
{% set wConfig = widget.config or {} %}
<is-land on:visible>
<div class="widget">
{% if wConfig.title %}
<h3 class="widget-title">{{ wConfig.title }}</h3>
{% endif %}
{% if wConfig.content %}
<div class="prose dark:prose-invert prose-sm max-w-none">
{{ wConfig.content | safe }}
@@ -61,15 +116,124 @@
{% else %}
<!-- Unknown widget type: {{ widget.type }} -->
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
{# === Fallback: default blog post sidebar (backward compatibility) === #}
{# Each widget wrapped in collapsible container #}
{# Author Card Compact #}
{% set widgetKey = "post-fb-author-card-compact" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Author</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/author-card-compact.njk" %}
</div>
</div>
</div>
{# Table of Contents #}
{% set widgetKey = "post-fb-toc" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Table of Contents</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/toc.njk" %}
</div>
</div>
</div>
{# Post Categories #}
{% set widgetKey = "post-fb-post-categories" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Categories</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/post-categories.njk" %}
</div>
</div>
</div>
{# Recent Posts #}
{% set widgetKey = "post-fb-recent-posts" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Recent Posts</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-posts-blog.njk" %}
</div>
</div>
</div>
{# Webmentions #}
{% set widgetKey = "post-fb-webmentions" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Webmentions</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/webmentions.njk" %}
</div>
</div>
</div>
{# Share #}
{% set widgetKey = "post-fb-share" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Share</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/share.njk" %}
</div>
</div>
</div>
{# Subscribe #}
{% set widgetKey = "post-fb-subscribe" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Subscribe</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/subscribe.njk" %}
</div>
</div>
</div>
{# Recent Comments #}
{% set widgetKey = "post-fb-recent-comments" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Recent Comments</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-comments.njk" %}
</div>
</div>
</div>
{% endif %}

View File

@@ -1,9 +1,65 @@
{# Sidebar — for blog listing pages (/blog/, /notes/, /articles/...) #}
{# Data-driven when homepageConfig.blogListingSidebar is configured, otherwise falls back to default widgets #}
{# Each widget is wrapped in a collapsible container with localStorage persistence #}
{% if homepageConfig and homepageConfig.blogListingSidebar and homepageConfig.blogListingSidebar.length %}
{# === Data-driven mode: render configured widgets === #}
{% for widget in homepageConfig.blogListingSidebar %}
{# Resolve widget title #}
{% if widget.type == "search" %}{% set widgetTitle = "Search" %}
{% elif widget.type == "social-activity" %}{% set widgetTitle = "Social Activity" %}
{% elif widget.type == "github-repos" %}{% set widgetTitle = "GitHub" %}
{% elif widget.type == "funkwhale" %}{% set widgetTitle = "Listening" %}
{% elif widget.type == "recent-posts" %}{% set widgetTitle = "Recent Posts" %}
{% elif widget.type == "blogroll" %}{% set widgetTitle = "Blogroll" %}
{% elif widget.type == "feedland" %}{% set widgetTitle = "FeedLand" %}
{% elif widget.type == "categories" %}{% set widgetTitle = "Categories" %}
{% elif widget.type == "webmentions" %}{% set widgetTitle = "Webmentions" %}
{% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %}
{% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %}
{% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %}
{% elif widget.type == "author-card-compact" %}{% set widgetTitle = "Author" %}
{% elif widget.type == "subscribe" %}{% set widgetTitle = "Subscribe" %}
{% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %}
{% else %}{% set widgetTitle = widget.type %}
{% endif %}
{% set widgetKey = "listing-widget-" + widget.type + "-" + loop.index0 %}
{% set defaultOpen = "true" if loop.index0 < 3 else "false" %}
{# Collapsible wrapper — Alpine.js handles toggle, localStorage persists state #}
<div
class="widget-collapsible mb-4"
x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : {{ defaultOpen }} }"
>
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button
class="widget-header w-full p-4"
@click="open = !open; localStorage.setItem('{{ widgetKey }}', open)"
:aria-expanded="open ? 'true' : 'false'"
>
<h3 class="widget-title font-bold text-lg">{{ widgetTitle }}</h3>
<svg
class="widget-chevron"
:class="open && 'rotate-180'"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div
x-show="open"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak
>
{# Widget content — inner .widget provides padding, inner title hidden by CSS #}
{% if widget.type == "author-card" %}
{% include "components/widgets/author-card.njk" %}
{% elif widget.type == "author-card-compact" %}
@@ -31,7 +87,6 @@
{% elif widget.type == "search" %}
<is-land on:visible>
<div class="widget">
<h3 class="widget-title">Search</h3>
<div id="listing-sidebar-search"></div>
<script>initPagefind("#listing-sidebar-search");</script>
</div>
@@ -44,9 +99,6 @@
{% set wConfig = widget.config or {} %}
<is-land on:visible>
<div class="widget">
{% if wConfig.title %}
<h3 class="widget-title">{{ wConfig.title }}</h3>
{% endif %}
{% if wConfig.content %}
<div class="prose dark:prose-invert prose-sm max-w-none">
{{ wConfig.content | safe }}
@@ -57,37 +109,142 @@
{% else %}
<!-- Unknown widget type: {{ widget.type }} -->
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
{# === Fallback: current hardcoded sidebar (backward compatibility) === #}
{# Each widget wrapped in collapsible container #}
{# Author Card (h-card) — always shown #}
{% set widgetKey = "listing-fb-author-card" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Author</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/author-card.njk" %}
</div>
</div>
</div>
{# Social Activity — Bluesky/Mastodon feeds #}
{% set widgetKey = "listing-fb-social-activity" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Social Activity</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/social-activity.njk" %}
</div>
</div>
</div>
{# GitHub Repos #}
{% set widgetKey = "listing-fb-github-repos" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">GitHub</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/github-repos.njk" %}
</div>
</div>
</div>
{# Funkwhale — Now Playing / Listening Stats #}
{% set widgetKey = "listing-fb-funkwhale" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Listening</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/funkwhale.njk" %}
</div>
</div>
</div>
{# Recent Posts (for non-blog pages) #}
{# Recent Posts #}
{% set widgetKey = "listing-fb-recent-posts" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Recent Posts</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-posts.njk" %}
</div>
</div>
</div>
{# Blogroll — only when backend is available #}
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}
{% set widgetKey = "listing-fb-blogroll" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Blogroll</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/blogroll.njk" %}
</div>
</div>
</div>
{% endif %}
{# FeedLand — only when backend is available #}
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}
{% set widgetKey = "listing-fb-feedland" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">FeedLand</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/feedland.njk" %}
</div>
</div>
</div>
{% endif %}
{# Recent Comments #}
{% set widgetKey = "listing-fb-recent-comments" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Recent Comments</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-comments.njk" %}
</div>
</div>
</div>
{# Categories/Tags #}
{% set widgetKey = "listing-fb-categories" %}
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg">Categories</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/categories.njk" %}
</div>
</div>
</div>
{% endif %}

View File

@@ -25,7 +25,7 @@ permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{% if paginatedArticles.length > 0 %}
<ul class="post-list">
{% for post in paginatedArticles %}
<li class="h-entry post-card">
<li class="h-entry post-card border-l-[3px] border-l-surface-300 dark:border-l-surface-600">
<div class="post-header">
<h2 class="text-xl font-semibold mb-1 flex-1">
<a class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-primary-600 dark:hover:text-primary-400" href="{{ post.url }}">

View File

@@ -45,7 +45,21 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% 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 }}">
{% set borderClass = "" %}
{% if likedUrl %}
{% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %}
{% elif bookmarkedUrl %}
{% set borderClass = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %}
{% elif repostedUrl %}
{% set borderClass = "border-l-[3px] border-l-green-400 dark:border-l-green-500" %}
{% elif replyToUrl %}
{% set borderClass = "border-l-[3px] border-l-primary-400 dark:border-l-primary-500" %}
{% elif hasPhotos %}
{% set borderClass = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %}
{% else %}
{% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %}
{% endif %}
<li class="h-entry post-card {{ borderClass }}" data-filter-type="{{ _postType | trim }}">
{% if likedUrl %}
{# ── Like card ── #}

View File

@@ -25,7 +25,7 @@ permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageN
{% if paginatedBookmarks.length > 0 %}
<ul class="post-list">
{% for post in paginatedBookmarks %}
<li class="h-entry post-card">
<li class="h-entry post-card border-l-[3px] border-l-amber-400 dark:border-l-amber-500">
<div class="post-header">
{% if post.data.title %}
<h2 class="text-xl font-semibold mb-1 flex-1">

View File

@@ -34,7 +34,23 @@ eleventyComputed:
<p class="text-sm text-surface-500 dark:text-surface-400 mb-4">{{ categoryPosts.length }} post{% if categoryPosts.length != 1 %}s{% endif %}</p>
<ul class="post-list">
{% for post in categoryPosts %}
<li class="h-entry post-card">
{% set postType = post.inputPath | replace("./content/", "") %}
{% set postType = postType.split("/")[0] %}
{% set borderClass = "" %}
{% if postType == "likes" %}
{% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %}
{% elif postType == "bookmarks" %}
{% set borderClass = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %}
{% elif postType == "reposts" %}
{% set borderClass = "border-l-[3px] border-l-green-400 dark:border-l-green-500" %}
{% elif postType == "replies" %}
{% set borderClass = "border-l-[3px] border-l-primary-400 dark:border-l-primary-500" %}
{% elif postType == "photos" %}
{% set borderClass = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %}
{% else %}
{% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %}
{% endif %}
<li class="h-entry post-card {{ borderClass }}">
<div class="post-header">
<h2 class="text-xl font-semibold mb-1 flex-1">
<a class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-primary-600 dark:hover:text-primary-400" href="{{ post.url }}">
@@ -46,8 +62,6 @@ eleventyComputed:
<time class="dt-published" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% set postType = post.inputPath | replace("./content/", "") %}
{% set postType = postType.split("/")[0] %}
<span class="post-type">{{ postType }}</span>
</div>
<p class="p-summary text-surface-700 dark:text-surface-300 mt-3">

View File

@@ -25,7 +25,7 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% if paginatedLikes.length > 0 %}
<ul class="post-list">
{% for post in paginatedLikes %}
<li class="h-entry post-card">
<li class="h-entry post-card border-l-[3px] border-l-red-400 dark:border-l-red-500">
<div class="post-header flex items-start gap-3">
<div class="flex-shrink-0 mt-1">
<svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">

View File

@@ -25,7 +25,7 @@ permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% if paginatedNotes.length > 0 %}
<ul class="post-list">
{% for post in paginatedNotes %}
<li class="h-entry post-card">
<li class="h-entry post-card border-l-[3px] border-l-surface-300 dark:border-l-surface-600">
<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 }}">

View File

@@ -25,7 +25,7 @@ permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
{% if paginatedPhotos.length > 0 %}
<ul class="post-list photo-list">
{% for post in paginatedPhotos %}
<li class="h-entry post-card">
<li class="h-entry post-card border-l-[3px] border-l-purple-400 dark:border-l-purple-500">
<div class="post-meta">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}

View File

@@ -25,7 +25,7 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% if paginatedReplies.length > 0 %}
<ul class="post-list">
{% for post in paginatedReplies %}
<li class="h-entry post-card">
<li class="h-entry post-card border-l-[3px] border-l-primary-400 dark:border-l-primary-500">
<div class="post-header flex items-start gap-3">
<div class="flex-shrink-0 mt-1">
<svg class="w-5 h-5 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">

View File

@@ -25,7 +25,7 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% if paginatedReposts.length > 0 %}
<ul class="post-list">
{% for post in paginatedReposts %}
<li class="h-entry post-card">
<li class="h-entry post-card border-l-[3px] border-l-green-400 dark:border-l-green-500">
<div class="post-header flex items-start gap-3">
<div class="flex-shrink-0 mt-1">
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">