mirror of
https://github.com/svemagie/blog-eleventy-indiekit.git
synced 2026-04-02 16:44:56 +02:00
feat: neutralize theme for fresh deployments
Strip personal data from templates so the theme ships clean for any deployer. Collection pages now use generatePageOnEmptyData so empty post types show encouraging placeholders instead of 404s. Navigation is conditional on enabled post types and installed plugins. Sidebar widgets split into individual components with plugin-aware visibility. Slashes page explains required plugins for root-level page creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
_includes/components/empty-collection.njk
Normal file
27
_includes/components/empty-collection.njk
Normal file
@@ -0,0 +1,27 @@
|
||||
{# Empty collection placeholder — encourages creating content #}
|
||||
{# Usage: {% include "components/empty-collection.njk" %} with postType set before include #}
|
||||
{% set typeInfo = null %}
|
||||
{% for pt in enabledPostTypes %}
|
||||
{% if pt.type == postType %}{% set typeInfo = pt %}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="text-center py-12 px-4">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 mb-4">
|
||||
<svg class="w-8 h-8 text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-surface-700 dark:text-surface-300 mb-2">No {{ title | lower }} yet</h2>
|
||||
<p class="text-surface-500 dark:text-surface-400 mb-6 max-w-md mx-auto">
|
||||
This is where your {{ title | lower }} will appear once you start creating content.
|
||||
</p>
|
||||
{% if typeInfo %}
|
||||
<a href="{{ typeInfo.createUrl }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors text-sm font-medium">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
Create your first {{ postType }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,300 +1,26 @@
|
||||
{# Sidebar Components #}
|
||||
{# Contains: Author card (via h-card component), Bluesky feed, GitHub repos, RSS feed #}
|
||||
{# Sidebar — composed from individual widget partials #}
|
||||
{# Each widget handles its own conditional display internally, #}
|
||||
{# except API-only widgets which need a data-source guard here. #}
|
||||
|
||||
{# Author Card Widget - includes the canonical h-card component #}
|
||||
<div class="widget">
|
||||
{% include "components/h-card.njk" %}
|
||||
</div>
|
||||
{# Author Card (h-card) — always shown #}
|
||||
{% include "components/widgets/author-card.njk" %}
|
||||
|
||||
{# Social Feed Widget - Tabbed Bluesky/Mastodon #}
|
||||
{% if (blueskyFeed and blueskyFeed.length) or (mastodonFeed and mastodonFeed.length) %}
|
||||
<div class="widget" x-data="{ activeTab: 'bluesky' }">
|
||||
<h3 class="widget-title">Social Activity</h3>
|
||||
{# Social Activity — Bluesky/Mastodon feeds #}
|
||||
{% include "components/widgets/social-activity.njk" %}
|
||||
|
||||
{# Tab buttons #}
|
||||
<div class="flex gap-1 mb-4 border-b border-surface-200 dark:border-surface-700">
|
||||
{% if blueskyFeed and blueskyFeed.length %}
|
||||
<button
|
||||
@click="activeTab = 'bluesky'"
|
||||
:class="activeTab === 'bluesky' ? 'border-b-2 border-primary-500 text-primary-600 dark:text-primary-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px"
|
||||
>
|
||||
<svg class="w-4 h-4 text-[#0085ff]" 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>
|
||||
Bluesky
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if mastodonFeed and mastodonFeed.length %}
|
||||
<button
|
||||
@click="activeTab = 'mastodon'"
|
||||
:class="activeTab === 'mastodon' ? 'border-b-2 border-primary-500 text-primary-600 dark:text-primary-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px"
|
||||
>
|
||||
<svg class="w-4 h-4 text-[#6364ff]" 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>
|
||||
Mastodon
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# GitHub Repos #}
|
||||
{% include "components/widgets/github-repos.njk" %}
|
||||
|
||||
{# Bluesky Tab Content #}
|
||||
{% if blueskyFeed and blueskyFeed.length %}
|
||||
<div x-show="activeTab === 'bluesky'" x-cloak>
|
||||
<ul class="space-y-3">
|
||||
{% for post in blueskyFeed | head(5) %}
|
||||
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
|
||||
<a href="{{ post.url }}" target="_blank" rel="noopener" class="block group">
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
{{ post.text | truncate(140) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3 mt-2 text-xs text-surface-500">
|
||||
<time datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
|
||||
{% if post.likeCount > 0 %}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3 h-3" 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>
|
||||
{{ post.likeCount }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="https://bsky.app/profile/{{ site.feeds.bluesky }}" target="_blank" rel="noopener" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 inline-flex items-center gap-1">
|
||||
View on Bluesky
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Funkwhale — Now Playing / Listening Stats #}
|
||||
{% include "components/widgets/funkwhale.njk" %}
|
||||
|
||||
{# Mastodon Tab Content #}
|
||||
{% if mastodonFeed and mastodonFeed.length %}
|
||||
<div x-show="activeTab === 'mastodon'" x-cloak>
|
||||
<ul class="space-y-3">
|
||||
{% for post in mastodonFeed | head(5) %}
|
||||
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
|
||||
<a href="{{ post.url }}" target="_blank" rel="noopener" class="block group">
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
{{ post.text | truncate(140) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3 mt-2 text-xs text-surface-500">
|
||||
<time datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
|
||||
{% if post.favouritesCount > 0 %}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
|
||||
{{ post.favouritesCount }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="https://{{ site.feeds.mastodon.instance }}/@{{ site.feeds.mastodon.username }}" target="_blank" rel="noopener" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 inline-flex items-center gap-1">
|
||||
View on Mastodon
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# Recent Posts (for non-blog pages) #}
|
||||
{% include "components/widgets/recent-posts.njk" %}
|
||||
|
||||
{# Blogroll — only when backend is available #}
|
||||
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}
|
||||
{% include "components/widgets/blogroll.njk" %}
|
||||
{% endif %}
|
||||
|
||||
{# GitHub Repos Widget #}
|
||||
{% if githubRepos and githubRepos.length %}
|
||||
<div class="widget">
|
||||
<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">
|
||||
<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>
|
||||
GitHub Projects
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
{% for repo in githubRepos | head(5) %}
|
||||
<li class="repo-card">
|
||||
<a href="{{ repo.html_url }}" class="font-medium text-primary-600 dark:text-primary-400 hover:underline" target="_blank" rel="noopener">
|
||||
{{ repo.name }}
|
||||
</a>
|
||||
{% if repo.description %}
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">{{ repo.description | truncate(80) }}</p>
|
||||
{% endif %}
|
||||
<div class="repo-meta">
|
||||
{% if repo.language %}
|
||||
<span>{{ repo.language }}</span>
|
||||
{% endif %}
|
||||
<span>{{ repo.stargazers_count }} stars</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="https://github.com/{{ site.feeds.github }}" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 block">
|
||||
View all repositories
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Funkwhale Now Playing Widget #}
|
||||
{% if funkwhaleActivity and (funkwhaleActivity.nowPlaying or funkwhaleActivity.stats) %}
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
||||
</svg>
|
||||
Listening
|
||||
</h3>
|
||||
|
||||
{# Now Playing / Recently Played #}
|
||||
{% if funkwhaleActivity.nowPlaying and funkwhaleActivity.nowPlaying.track %}
|
||||
<div class="{% if funkwhaleActivity.nowPlaying.status == 'now-playing' %}bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800{% else %}bg-surface-50 dark:bg-surface-800{% endif %} rounded-lg p-3 mb-3">
|
||||
{% if funkwhaleActivity.nowPlaying.status == 'now-playing' %}
|
||||
<div class="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 mb-2">
|
||||
<span class="flex gap-0.5 items-end h-2.5">
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 30%;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 70%; animation-delay: 0.2s;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 50%; animation-delay: 0.4s;"></span>
|
||||
</span>
|
||||
Now Playing
|
||||
</div>
|
||||
{% elif funkwhaleActivity.nowPlaying.status == 'recently-played' %}
|
||||
<div class="text-xs text-surface-500 mb-2">Recently Played</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
{% if funkwhaleActivity.nowPlaying.coverUrl %}
|
||||
<img src="{{ funkwhaleActivity.nowPlaying.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
|
||||
{% endif %}
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium text-sm text-surface-900 dark:text-surface-100 truncate">
|
||||
{% if funkwhaleActivity.nowPlaying.trackUrl %}
|
||||
<a href="{{ funkwhaleActivity.nowPlaying.trackUrl }}" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank" rel="noopener">
|
||||
{{ funkwhaleActivity.nowPlaying.track }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ funkwhaleActivity.nowPlaying.track }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ funkwhaleActivity.nowPlaying.artist }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Quick Stats #}
|
||||
{% if funkwhaleActivity.stats and funkwhaleActivity.stats.summary %}
|
||||
{% set stats = funkwhaleActivity.stats.summary.all %}
|
||||
<div class="grid grid-cols-3 gap-2 text-center mb-3">
|
||||
<div class="p-2 bg-surface-50 dark:bg-surface-800 rounded">
|
||||
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 block">{{ stats.totalPlays or 0 }}</span>
|
||||
<span class="text-[10px] text-surface-500 uppercase">plays</span>
|
||||
</div>
|
||||
<div class="p-2 bg-surface-50 dark:bg-surface-800 rounded">
|
||||
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 block">{{ stats.uniqueArtists or 0 }}</span>
|
||||
<span class="text-[10px] text-surface-500 uppercase">artists</span>
|
||||
</div>
|
||||
<div class="p-2 bg-surface-50 dark:bg-surface-800 rounded">
|
||||
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 block">{{ stats.totalDurationFormatted or '0m' }}</span>
|
||||
<span class="text-[10px] text-surface-500 uppercase">listened</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="/listening/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline flex items-center gap-1">
|
||||
View full listening history
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
{# Recent Posts Widget (for non-blog pages) #}
|
||||
{% if recentPosts and recentPosts.length %}
|
||||
<div class="widget">
|
||||
<h3 class="widget-title">Recent Posts</h3>
|
||||
<ul class="space-y-2">
|
||||
{% for post in recentPosts | head(5) %}
|
||||
<li>
|
||||
<a href="{{ post.url }}" class="text-sm text-primary-600 dark:text-primary-400 hover:underline">
|
||||
{{ post.data.title or post.data.name or "Untitled" }}
|
||||
</a>
|
||||
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published }}">
|
||||
{{ post.data.published | date("MMM d, yyyy") }}
|
||||
</time>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="/posts/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 block">
|
||||
View all posts
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Blogroll Widget - Dynamic loading from API #}
|
||||
<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">
|
||||
<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>
|
||||
<a href="/blogroll/" class="hover:text-primary-600 dark:hover:text-primary-400">Blogroll</a>
|
||||
</h3>
|
||||
|
||||
<ul x-show="blogs.length > 0" class="space-y-2 mt-3">
|
||||
<template x-for="blog in blogs.slice(0, 8)" :key="blog.id">
|
||||
<li>
|
||||
<a
|
||||
:href="blog.siteUrl || blog.feedUrl"
|
||||
class="flex items-center gap-2 text-sm text-surface-700 dark:text-surface-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span class="w-5 h-5 rounded bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-white text-xs font-bold" x-text="blog.title?.charAt(0)?.toUpperCase()"></span>
|
||||
</span>
|
||||
<span class="truncate" x-text="blog.title"></span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<div x-show="blogs.length === 0 && !loading" class="text-sm text-surface-500 py-2">
|
||||
No blogs loaded yet.
|
||||
</div>
|
||||
|
||||
<a x-show="blogs.length > 0" href="/blogroll/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 inline-flex items-center gap-1">
|
||||
View all <span x-text="blogs.length"></span> blogs
|
||||
<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>
|
||||
|
||||
<script>
|
||||
function blogrollWidget() {
|
||||
return {
|
||||
blogs: [],
|
||||
loading: true,
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const res = await fetch('/blogrollapi/api/blogs?limit=10').then(r => r.json());
|
||||
this.blogs = res.items || [];
|
||||
} catch (err) {
|
||||
console.error('Blogroll widget error:', err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{# Categories/Tags Widget #}
|
||||
{% if categories and categories.length %}
|
||||
<div class="widget">
|
||||
<h3 class="widget-title">Categories</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for category in categories %}
|
||||
<a href="/categories/{{ category | slugify }}/" class="p-category">
|
||||
{{ category }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Categories/Tags #}
|
||||
{% include "components/widgets/categories.njk" %}
|
||||
|
||||
4
_includes/components/widgets/author-card.njk
Normal file
4
_includes/components/widgets/author-card.njk
Normal file
@@ -0,0 +1,4 @@
|
||||
{# Author Card Widget - includes the canonical h-card component #}
|
||||
<div class="widget">
|
||||
{% include "components/h-card.njk" %}
|
||||
</div>
|
||||
56
_includes/components/widgets/blogroll.njk
Normal file
56
_includes/components/widgets/blogroll.njk
Normal file
@@ -0,0 +1,56 @@
|
||||
{# Blogroll Widget - Dynamic loading from API #}
|
||||
<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">
|
||||
<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>
|
||||
<a href="/blogroll/" class="hover:text-primary-600 dark:hover:text-primary-400">Blogroll</a>
|
||||
</h3>
|
||||
|
||||
<ul x-show="blogs.length > 0" class="space-y-2 mt-3">
|
||||
<template x-for="blog in blogs.slice(0, 8)" :key="blog.id">
|
||||
<li>
|
||||
<a
|
||||
:href="blog.siteUrl || blog.feedUrl"
|
||||
class="flex items-center gap-2 text-sm text-surface-700 dark:text-surface-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span class="w-5 h-5 rounded bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-white text-xs font-bold" x-text="blog.title?.charAt(0)?.toUpperCase()"></span>
|
||||
</span>
|
||||
<span class="truncate" x-text="blog.title"></span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<div x-show="blogs.length === 0 && !loading" class="text-sm text-surface-500 py-2">
|
||||
No blogs loaded yet.
|
||||
</div>
|
||||
|
||||
<a x-show="blogs.length > 0" href="/blogroll/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 inline-flex items-center gap-1">
|
||||
View all <span x-text="blogs.length"></span> blogs
|
||||
<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>
|
||||
|
||||
<script>
|
||||
function blogrollWidget() {
|
||||
return {
|
||||
blogs: [],
|
||||
loading: true,
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const res = await fetch('/blogrollapi/api/blogs?limit=10').then(r => r.json());
|
||||
this.blogs = res.items || [];
|
||||
} catch (err) {
|
||||
console.error('Blogroll widget error:', err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
13
_includes/components/widgets/categories.njk
Normal file
13
_includes/components/widgets/categories.njk
Normal file
@@ -0,0 +1,13 @@
|
||||
{# Categories/Tags Widget #}
|
||||
{% if categories and categories.length %}
|
||||
<div class="widget">
|
||||
<h3 class="widget-title">Categories</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for category in categories %}
|
||||
<a href="/categories/{{ category | slugify }}/" class="p-category">
|
||||
{{ category }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
71
_includes/components/widgets/funkwhale.njk
Normal file
71
_includes/components/widgets/funkwhale.njk
Normal file
@@ -0,0 +1,71 @@
|
||||
{# Funkwhale Now Playing Widget #}
|
||||
{% if funkwhaleActivity and (funkwhaleActivity.nowPlaying or funkwhaleActivity.stats) %}
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
||||
</svg>
|
||||
Listening
|
||||
</h3>
|
||||
|
||||
{# Now Playing / Recently Played #}
|
||||
{% if funkwhaleActivity.nowPlaying and funkwhaleActivity.nowPlaying.track %}
|
||||
<div class="{% if funkwhaleActivity.nowPlaying.status == 'now-playing' %}bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800{% else %}bg-surface-50 dark:bg-surface-800{% endif %} rounded-lg p-3 mb-3">
|
||||
{% if funkwhaleActivity.nowPlaying.status == 'now-playing' %}
|
||||
<div class="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 mb-2">
|
||||
<span class="flex gap-0.5 items-end h-2.5">
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 30%;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 70%; animation-delay: 0.2s;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 50%; animation-delay: 0.4s;"></span>
|
||||
</span>
|
||||
Now Playing
|
||||
</div>
|
||||
{% elif funkwhaleActivity.nowPlaying.status == 'recently-played' %}
|
||||
<div class="text-xs text-surface-500 mb-2">Recently Played</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
{% if funkwhaleActivity.nowPlaying.coverUrl %}
|
||||
<img src="{{ funkwhaleActivity.nowPlaying.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
|
||||
{% endif %}
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium text-sm text-surface-900 dark:text-surface-100 truncate">
|
||||
{% if funkwhaleActivity.nowPlaying.trackUrl %}
|
||||
<a href="{{ funkwhaleActivity.nowPlaying.trackUrl }}" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank" rel="noopener">
|
||||
{{ funkwhaleActivity.nowPlaying.track }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ funkwhaleActivity.nowPlaying.track }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ funkwhaleActivity.nowPlaying.artist }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Quick Stats #}
|
||||
{% if funkwhaleActivity.stats and funkwhaleActivity.stats.summary %}
|
||||
{% set stats = funkwhaleActivity.stats.summary.all %}
|
||||
<div class="grid grid-cols-3 gap-2 text-center mb-3">
|
||||
<div class="p-2 bg-surface-50 dark:bg-surface-800 rounded">
|
||||
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 block">{{ stats.totalPlays or 0 }}</span>
|
||||
<span class="text-[10px] text-surface-500 uppercase">plays</span>
|
||||
</div>
|
||||
<div class="p-2 bg-surface-50 dark:bg-surface-800 rounded">
|
||||
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 block">{{ stats.uniqueArtists or 0 }}</span>
|
||||
<span class="text-[10px] text-surface-500 uppercase">artists</span>
|
||||
</div>
|
||||
<div class="p-2 bg-surface-50 dark:bg-surface-800 rounded">
|
||||
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 block">{{ stats.totalDurationFormatted or '0m' }}</span>
|
||||
<span class="text-[10px] text-surface-500 uppercase">listened</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="/listening/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline flex items-center gap-1">
|
||||
View full listening history
|
||||
<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>
|
||||
{% endif %}
|
||||
32
_includes/components/widgets/github-repos.njk
Normal file
32
_includes/components/widgets/github-repos.njk
Normal file
@@ -0,0 +1,32 @@
|
||||
{# GitHub Repos Widget #}
|
||||
{% if githubRepos and githubRepos.length %}
|
||||
<div class="widget">
|
||||
<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">
|
||||
<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>
|
||||
GitHub Projects
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
{% for repo in githubRepos | head(5) %}
|
||||
<li class="repo-card">
|
||||
<a href="{{ repo.html_url }}" class="font-medium text-primary-600 dark:text-primary-400 hover:underline" target="_blank" rel="noopener">
|
||||
{{ repo.name }}
|
||||
</a>
|
||||
{% if repo.description %}
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">{{ repo.description | truncate(80) }}</p>
|
||||
{% endif %}
|
||||
<div class="repo-meta">
|
||||
{% if repo.language %}
|
||||
<span>{{ repo.language }}</span>
|
||||
{% endif %}
|
||||
<span>{{ repo.stargazers_count }} stars</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="https://github.com/{{ site.feeds.github }}" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 block">
|
||||
View all repositories
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
21
_includes/components/widgets/recent-posts.njk
Normal file
21
_includes/components/widgets/recent-posts.njk
Normal file
@@ -0,0 +1,21 @@
|
||||
{# Recent Posts Widget (for non-blog pages) #}
|
||||
{% if recentPosts and recentPosts.length %}
|
||||
<div class="widget">
|
||||
<h3 class="widget-title">Recent Posts</h3>
|
||||
<ul class="space-y-2">
|
||||
{% for post in recentPosts | head(5) %}
|
||||
<li>
|
||||
<a href="{{ post.url }}" class="text-sm text-primary-600 dark:text-primary-400 hover:underline">
|
||||
{{ post.data.title or post.data.name or "Untitled" }}
|
||||
</a>
|
||||
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published }}">
|
||||
{{ post.data.published | date("MMM d, yyyy") }}
|
||||
</time>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="/posts/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 block">
|
||||
View all posts
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
94
_includes/components/widgets/social-activity.njk
Normal file
94
_includes/components/widgets/social-activity.njk
Normal file
@@ -0,0 +1,94 @@
|
||||
{# Social Feed Widget - Tabbed Bluesky/Mastodon #}
|
||||
{% if (blueskyFeed and blueskyFeed.length) or (mastodonFeed and mastodonFeed.length) %}
|
||||
<div class="widget" x-data="{ activeTab: 'bluesky' }">
|
||||
<h3 class="widget-title">Social Activity</h3>
|
||||
|
||||
{# Tab buttons #}
|
||||
<div class="flex gap-1 mb-4 border-b border-surface-200 dark:border-surface-700">
|
||||
{% if blueskyFeed and blueskyFeed.length %}
|
||||
<button
|
||||
@click="activeTab = 'bluesky'"
|
||||
:class="activeTab === 'bluesky' ? 'border-b-2 border-primary-500 text-primary-600 dark:text-primary-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px"
|
||||
>
|
||||
<svg class="w-4 h-4 text-[#0085ff]" 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>
|
||||
Bluesky
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if mastodonFeed and mastodonFeed.length %}
|
||||
<button
|
||||
@click="activeTab = 'mastodon'"
|
||||
:class="activeTab === 'mastodon' ? 'border-b-2 border-primary-500 text-primary-600 dark:text-primary-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px"
|
||||
>
|
||||
<svg class="w-4 h-4 text-[#6364ff]" 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>
|
||||
Mastodon
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Bluesky Tab Content #}
|
||||
{% if blueskyFeed and blueskyFeed.length %}
|
||||
<div x-show="activeTab === 'bluesky'" x-cloak>
|
||||
<ul class="space-y-3">
|
||||
{% for post in blueskyFeed | head(5) %}
|
||||
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
|
||||
<a href="{{ post.url }}" target="_blank" rel="noopener" class="block group">
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
{{ post.text | truncate(140) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3 mt-2 text-xs text-surface-500">
|
||||
<time datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
|
||||
{% if post.likeCount > 0 %}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3 h-3" 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>
|
||||
{{ post.likeCount }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="https://bsky.app/profile/{{ site.feeds.bluesky }}" target="_blank" rel="noopener" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 inline-flex items-center gap-1">
|
||||
View on Bluesky
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Mastodon Tab Content #}
|
||||
{% if mastodonFeed and mastodonFeed.length %}
|
||||
<div x-show="activeTab === 'mastodon'" x-cloak>
|
||||
<ul class="space-y-3">
|
||||
{% for post in mastodonFeed | head(5) %}
|
||||
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
|
||||
<a href="{{ post.url }}" target="_blank" rel="noopener" class="block group">
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
{{ post.text | truncate(140) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3 mt-2 text-xs text-surface-500">
|
||||
<time datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
|
||||
{% if post.favouritesCount > 0 %}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
|
||||
{{ post.favouritesCount }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="https://{{ site.feeds.mastodon.instance }}/@{{ site.feeds.mastodon.username }}" target="_blank" rel="noopener" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-3 inline-flex items-center gap-1">
|
||||
View on Mastodon
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -114,7 +114,9 @@
|
||||
<nav class="site-nav" id="site-nav">
|
||||
<a href="/">Home</a>
|
||||
<a href="/about/">About</a>
|
||||
{% if collections.pages | selectattr("url", "equalto", "/now/") | list | length %}
|
||||
<a href="/now/">Now</a>
|
||||
{% endif %}
|
||||
{# Slash pages dropdown - all root pages in one menu #}
|
||||
<div class="nav-dropdown" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
||||
<a href="/slashes/" class="nav-dropdown-trigger">
|
||||
@@ -128,13 +130,24 @@
|
||||
{% for item in collections.pages %}
|
||||
<a href="{{ item.url }}">/{{ item.fileSlug }}</a>
|
||||
{% endfor %}
|
||||
{# Plugin pages — only show when their data source is configured #}
|
||||
{% set hasPluginPages = (funkwhaleActivity and funkwhaleActivity.source == "indiekit") or
|
||||
(githubActivity and githubActivity.source != "error") or
|
||||
(lastfmActivity and lastfmActivity.source == "indiekit") or
|
||||
(newsActivity and newsActivity.source == "indiekit") or
|
||||
(youtubeChannel and youtubeChannel.source == "indiekit") or
|
||||
(blogrollStatus and blogrollStatus.source == "indiekit") or
|
||||
(podrollStatus and podrollStatus.source == "indiekit") %}
|
||||
{% if hasPluginPages %}
|
||||
<div class="nav-dropdown-divider"></div>
|
||||
<a href="/funkwhale/">/funkwhale</a>
|
||||
<a href="/github/">/github</a>
|
||||
<a href="/listening/">/listening</a>
|
||||
<a href="/news/">/news</a>
|
||||
<a href="/podroll/">/podroll</a>
|
||||
<a href="/youtube/">/youtube</a>
|
||||
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/">/blogroll</a>{% endif %}
|
||||
{% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}<a href="/funkwhale/">/funkwhale</a>{% endif %}
|
||||
{% if githubActivity and githubActivity.source != "error" %}<a href="/github/">/github</a>{% endif %}
|
||||
{% if lastfmActivity and lastfmActivity.source == "indiekit" %}<a href="/listening/">/listening</a>{% endif %}
|
||||
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/">/news</a>{% endif %}
|
||||
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/">/podroll</a>{% endif %}
|
||||
{% if youtubeChannel and youtubeChannel.source == "indiekit" %}<a href="/youtube/">/youtube</a>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{# Blog dropdown #}
|
||||
@@ -147,13 +160,9 @@
|
||||
</a>
|
||||
<div class="nav-dropdown-menu" x-show="open" x-transition x-cloak>
|
||||
<a href="/blog/">All Posts</a>
|
||||
<a href="/articles/">Articles</a>
|
||||
<a href="/notes/">Notes</a>
|
||||
<a href="/photos/">Photos</a>
|
||||
<a href="/bookmarks/">Bookmarks</a>
|
||||
<a href="/likes/">Likes</a>
|
||||
<a href="/replies/">Replies</a>
|
||||
<a href="/reposts/">Reposts</a>
|
||||
{% for pt in enabledPostTypes %}
|
||||
<a href="{{ pt.path }}">{{ pt.label }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="/interactions/">Interactions</a>
|
||||
@@ -191,7 +200,9 @@
|
||||
<nav class="mobile-nav hidden" id="mobile-nav" x-data="{ blogOpen: false, slashOpen: false }">
|
||||
<a href="/">Home</a>
|
||||
<a href="/about/">About</a>
|
||||
{% if collections.pages | selectattr("url", "equalto", "/now/") | list | length %}
|
||||
<a href="/now/">Now</a>
|
||||
{% endif %}
|
||||
{# Slash pages section - all root pages in one menu #}
|
||||
<div class="mobile-nav-section">
|
||||
<button type="button" class="mobile-nav-toggle" @click="slashOpen = !slashOpen">
|
||||
@@ -205,13 +216,17 @@
|
||||
{% for item in collections.pages %}
|
||||
<a href="{{ item.url }}">/{{ item.fileSlug }}</a>
|
||||
{% endfor %}
|
||||
{# Plugin pages — only show when configured #}
|
||||
{% if hasPluginPages %}
|
||||
<div class="mobile-nav-divider"></div>
|
||||
<a href="/funkwhale/">/funkwhale</a>
|
||||
<a href="/github/">/github</a>
|
||||
<a href="/listening/">/listening</a>
|
||||
<a href="/news/">/news</a>
|
||||
<a href="/podroll/">/podroll</a>
|
||||
<a href="/youtube/">/youtube</a>
|
||||
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/">/blogroll</a>{% endif %}
|
||||
{% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}<a href="/funkwhale/">/funkwhale</a>{% endif %}
|
||||
{% if githubActivity and githubActivity.source != "error" %}<a href="/github/">/github</a>{% endif %}
|
||||
{% if lastfmActivity and lastfmActivity.source == "indiekit" %}<a href="/listening/">/listening</a>{% endif %}
|
||||
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/">/news</a>{% endif %}
|
||||
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/">/podroll</a>{% endif %}
|
||||
{% if youtubeChannel and youtubeChannel.source == "indiekit" %}<a href="/youtube/">/youtube</a>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{# Blog section #}
|
||||
@@ -224,13 +239,9 @@
|
||||
</button>
|
||||
<div class="mobile-nav-submenu" x-show="blogOpen" x-collapse>
|
||||
<a href="/blog/">All Posts</a>
|
||||
<a href="/articles/">Articles</a>
|
||||
<a href="/notes/">Notes</a>
|
||||
<a href="/photos/">Photos</a>
|
||||
<a href="/bookmarks/">Bookmarks</a>
|
||||
<a href="/likes/">Likes</a>
|
||||
<a href="/replies/">Replies</a>
|
||||
<a href="/reposts/">Reposts</a>
|
||||
{% for pt in enabledPostTypes %}
|
||||
<a href="{{ pt.path }}">{{ pt.label }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="/interactions/">Interactions</a>
|
||||
|
||||
@@ -22,13 +22,17 @@ withSidebar: true
|
||||
<p class="text-lg sm:text-xl text-primary-600 dark:text-primary-400 mb-3 sm:mb-4">
|
||||
{{ site.author.title }}
|
||||
</p>
|
||||
{% if site.author.bio %}
|
||||
<p class="text-base sm:text-lg text-surface-700 dark:text-surface-300 mb-4">
|
||||
Hi, I geek around tech, information systems, democracy, justice, coercive groups (aka cults), and discernment.
|
||||
{{ site.author.bio }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if site.description %}
|
||||
<p class="text-base sm:text-lg text-surface-700 dark:text-surface-300 mb-4 sm:mb-6">
|
||||
My blog serves as a repository for my thoughts, long-form writings (some still in draft), and a place where I bookmark interesting finds from the web. It's also my central hub for cross-posting to networks like Mastodon, Bluesky.
|
||||
{{ site.description }}
|
||||
<a href="/about/" class="text-primary-600 dark:text-primary-400 hover:underline font-medium">Read more →</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{# Social Links #}
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@@ -56,6 +60,25 @@ withSidebar: true
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# Homepage content — three-tier fallback: #}
|
||||
{# 1. Plugin config (homepageConfig) — Phase 3, future #}
|
||||
{# 2. CV data — show experience/projects/skills #}
|
||||
{# 3. Default — show recent posts and activity #}
|
||||
|
||||
{% set hasCvData = (cv.experience and cv.experience.length) or
|
||||
(cv.projects and cv.projects.length) or
|
||||
(cv.skills and (cv.skills | dictsort | length)) %}
|
||||
|
||||
{# --- Tier 1: Plugin-driven layout (future) --- #}
|
||||
{% if homepageConfig and homepageConfig.sections %}
|
||||
{# Reserved for indiekit-endpoint-homepage plugin — will render configured sections here #}
|
||||
<section class="mb-8 sm:mb-12">
|
||||
<p class="text-surface-500 text-center">Homepage plugin layout will render here.</p>
|
||||
</section>
|
||||
|
||||
{# --- Tier 2: CV-based layout --- #}
|
||||
{% elif hasCvData %}
|
||||
|
||||
{# Work Experience Timeline - only show if data exists #}
|
||||
{% if cv.experience and cv.experience.length %}
|
||||
<section class="mb-8 sm:mb-12">
|
||||
@@ -210,3 +233,83 @@ withSidebar: true
|
||||
Last updated: {{ cv.lastUpdated }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{# --- Tier 3: Default — recent activity when no CV and no plugin --- #}
|
||||
{% else %}
|
||||
|
||||
{# Recent Posts #}
|
||||
{% if collections.posts and collections.posts.length %}
|
||||
<section class="mb-8 sm:mb-12">
|
||||
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Recent Posts</h2>
|
||||
<div class="space-y-4">
|
||||
{% for post in collections.posts | head(10) %}
|
||||
<article class="p-4 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-primary-400 dark:hover:border-primary-600 transition-colors">
|
||||
<h3 class="font-semibold text-surface-900 dark:text-surface-100 mb-1">
|
||||
<a href="{{ post.url }}" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||
{{ post.data.title or post.data.name or "Untitled" }}
|
||||
</a>
|
||||
</h3>
|
||||
{% if post.data.summary %}
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2 line-clamp-2">{{ post.data.summary }}</p>
|
||||
{% endif %}
|
||||
<div class="flex items-center gap-3 text-xs text-surface-500">
|
||||
<time datetime="{{ post.data.published or post.date }}">
|
||||
{{ (post.data.published or post.date) | date("MMM d, yyyy") }}
|
||||
</time>
|
||||
{% if post.data.postType %}
|
||||
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded">{{ post.data.postType }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href="/blog/" class="text-sm text-primary-600 dark:text-primary-400 hover:underline mt-4 inline-flex items-center gap-1">
|
||||
View all posts
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# Getting Started — onboarding guide for new deployments #}
|
||||
<section class="mb-8 sm:mb-12 p-6 bg-primary-50 dark:bg-primary-900/20 rounded-lg border border-primary-200 dark:border-primary-800">
|
||||
<h2 class="text-xl font-bold text-surface-900 dark:text-surface-100 mb-4">Getting Started</h2>
|
||||
|
||||
<div class="space-y-4 text-surface-700 dark:text-surface-300">
|
||||
<div class="flex gap-3">
|
||||
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-primary-600 text-white text-sm flex items-center justify-center font-bold">1</span>
|
||||
<div>
|
||||
<strong class="text-surface-900 dark:text-surface-100">Create your first post</strong>
|
||||
<p class="text-sm mt-1">
|
||||
<a href="/session/login" class="text-primary-600 dark:text-primary-400 hover:underline">Sign in</a>,
|
||||
then visit <a href="/create" class="text-primary-600 dark:text-primary-400 hover:underline">/create</a>
|
||||
to publish notes, articles, bookmarks, and photos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-primary-600 text-white text-sm flex items-center justify-center font-bold">2</span>
|
||||
<div>
|
||||
<strong class="text-surface-900 dark:text-surface-100">Set up syndication</strong>
|
||||
<p class="text-sm mt-1">
|
||||
Cross-post to Mastodon, Bluesky, and LinkedIn automatically.
|
||||
Add your credentials to the <code class="text-xs bg-surface-200 dark:bg-surface-700 px-1 py-0.5 rounded">.env</code> file and restart.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-primary-600 text-white text-sm flex items-center justify-center font-bold">3</span>
|
||||
<div>
|
||||
<strong class="text-surface-900 dark:text-surface-100">Enable interactions</strong>
|
||||
<p class="text-sm mt-1">
|
||||
Receive likes, replies, and reposts from across the web.
|
||||
Register at <a href="https://webmention.io" class="text-primary-600 dark:text-primary-400 hover:underline" target="_blank" rel="noopener">webmention.io</a>
|
||||
and add the token to <code class="text-xs bg-surface-200 dark:bg-surface-700 px-1 py-0.5 rounded">.env</code> as <code class="text-xs bg-surface-200 dark:bg-surface-700 px-1 py-0.5 rounded">WEBMENTION_IO_TOKEN</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endif %} {# end three-tier fallback #}
|
||||
|
||||
Reference in New Issue
Block a user