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:
@@ -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" %}
|
||||
|
||||
Reference in New Issue
Block a user