Files
indiekit-blog/categories.njk
svemagie e8ba3b9ae6 feat: nested tags (Obsidian-style) for categories system
Adds hierarchical tag support using "/" separator (e.g. "tech/programming/js").
- New filters: nestedSlugify, categoryMatches, categoryBreadcrumb,
  categoryGroupByRoot, categoryDirectChildren
- categories collection auto-generates ancestor pages for nested tags
- categories.njk: breadcrumb nav, sub-tags section, ancestor-aware post matching
- categories-index.njk: grouped tree view (root + indented children)
- categories widget: shows root tags only with child count badge
- All category links updated from slugify → nestedSlugify (backward-compatible)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:56:34 +01:00

111 lines
4.6 KiB
Plaintext

---
layout: layouts/base.njk
withSidebar: true
pagefindIgnore: true
pagination:
data: collections.categories
size: 1
alias: category
permalink: "categories/{{ category | nestedSlugify }}/"
eleventyComputed:
title: "{{ category }}"
---
<div class="h-feed">
{# Breadcrumb — only shown for nested paths like "tech/programming" #}
{% set breadcrumb = category | categoryBreadcrumb %}
{% if breadcrumb.length > 1 %}
<nav class="text-sm text-surface-500 dark:text-surface-400 mb-4 flex flex-wrap gap-1 items-center" aria-label="Category breadcrumb">
<a href="/categories/" class="hover:text-accent-700 dark:hover:text-accent-300 transition-colors">Categories</a>
{% for crumb in breadcrumb %}
<span aria-hidden="true">/</span>
{% if crumb.isLast %}
<span class="text-surface-900 dark:text-surface-100 font-medium">{{ crumb.label }}</span>
{% else %}
<a href="/categories/{{ crumb.path | nestedSlugify }}/" class="hover:text-accent-700 dark:hover:text-accent-300 transition-colors">{{ crumb.label }}</a>
{% endif %}
{% endfor %}
</nav>
{% endif %}
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-2">{{ category }}</h1>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
Posts tagged with "{{ category }}".
</p>
{# Direct child tags #}
{% set childCats = collections.categories | categoryDirectChildren(category) %}
{% if childCats.length > 0 %}
<div class="mb-8">
<h2 class="text-sm font-semibold text-surface-500 dark:text-surface-400 uppercase tracking-wide mb-3">Sub-tags</h2>
<ul class="flex flex-wrap gap-2">
{% for child in childCats %}
<li>
<a href="/categories/{{ child | nestedSlugify }}/" class="inline-block px-3 py-1 bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 rounded-lg hover:bg-accent-100 dark:hover:bg-accent-900 hover:text-accent-700 dark:hover:text-accent-300 transition-colors text-sm">
{{ child }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% set categoryPosts = [] %}
{% for post in collections.posts %}
{% if post.data.category | categoryMatches(category) %}
{% set categoryPosts = (categoryPosts.push(post), categoryPosts) %}
{% endif %}
{% endfor %}
{% if categoryPosts.length > 0 %}
<p class="text-sm text-surface-600 dark:text-surface-400 mb-4">{{ categoryPosts.length }} post{% if categoryPosts.length != 1 %}s{% endif %}</p>
<ul class="post-list">
{% for post in categoryPosts %}
{% 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-accent-400 dark:border-l-accent-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-accent-700 dark:hover:text-accent-300" href="{{ post.url }}">
{{ post.data.title or post.templateContent | striptags | truncate(60) or "Untitled" }}
</a>
</h2>
</div>
<div class="post-meta mt-2">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
<span class="post-type">{{ postType }}</span>
</div>
<p class="p-summary text-surface-700 dark:text-surface-300 mt-3">
{{ post.templateContent | striptags | truncate(250) }}
</p>
<a href="{{ post.url }}" class="text-sm text-accent-700 dark:text-accent-300 hover:underline mt-3 inline-block">
View &rarr;
</a>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-surface-600 dark:text-surface-400">No posts found with this category.</p>
{% endif %}
<div class="mt-8">
<a href="/categories/" class="text-accent-700 dark:text-accent-300 hover:underline">&larr; All categories</a>
</div>
</div>