feat: add soft-delete filter and content-warning support

Filter posts with `deleted: true` from all collections so soft-deleted
posts no longer appear on the blog. Add content-warning support: on
listing pages, CW posts show a warning label instead of content; on
single post pages, content is wrapped in a collapsible <details>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-19 00:59:19 +01:00
parent a85a67c0d0
commit d9ac9bffc5
4 changed files with 45 additions and 5 deletions

View File

@@ -43,6 +43,9 @@ Posts declare AI involvement level in front matter (e.g. `aiCode: T1/C2`). Rende
### Nested tags
Categories use Obsidian-style path notation (`lang/de`, `tech/programming`). The `nestedSlugify()` function in `eleventy.config.js` preserves `/` separators during slug generation. Slugification is applied per segment.
### Changelog
`changelog.njk` — public page at `/changelog/` showing development activity. Uses Alpine.js to fetch commits from the IndieKit server's GitHub endpoint (`/github/api/changelog`). Commits are categorised by commit-message prefix (`feat:` → Features, `fix:` → Fixes, `perf:` → Performance, `a11y:` → Accessibility, `docs:` → Docs, everything else → Other). The server-side categorisation is applied by the postinstall patch `patch-endpoint-github-changelog-categories.mjs` in `indiekit-blog`. Tabs, labels, and colours in `changelog.njk` must stay in sync with that patch.
### Unfurl shortcode
`{% unfurl url %}` generates a rich link preview card with caching. Cache lives in `.cache/unfurl/`. The shortcode is registered from `lib/unfurl-shortcode.js`.
@@ -131,4 +134,5 @@ BLUESKY_HANDLE svemagie
- **Webmention self-filter** — own Bluesky account filtered from interactions
- **Markdown Agents** — clean Markdown served to AI crawlers
- **Mermaid diagrams** — `eleventy-plugin-mermaid` integrated
- **Changelog page** — commit-type tabs (feat/fix/perf/a11y/docs) via IndieKit GitHub endpoint
- **Upstream drift check script** — `scripts/check-upstream-widget-drift.mjs`

View File

@@ -80,9 +80,22 @@ withBlogSidebar: true
{% set isInteraction = replyTo or likedUrl or repostedUrl or bookmarkedUrl %}
{% set hasContent = content and content | striptags | trim %}
{% set contentWarning = contentWarning or content_warning %}
{% if contentWarning %}
<details class="content-warning mb-4">
<summary class="cursor-pointer inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 text-sm font-medium">
<span>&#9888; {{ contentWarning }}</span>
<span class="text-xs text-amber-600 dark:text-amber-400">(click to show)</span>
</summary>
<div class="e-content prose prose-surface dark:prose-invert max-w-none mt-4{% if isInteraction and hasContent %} border-l-[3px] border-l-accent-500 dark:border-l-accent-400 pl-4{% endif %}">
{{ content | safe }}
</div>
</details>
{% else %}
<div class="e-content prose prose-surface dark:prose-invert max-w-none{% if isInteraction and hasContent %} border-l-[3px] border-l-accent-500 dark:border-l-accent-400 pl-4{% endif %}">
{{ content | safe }}
</div>
{% endif %}
{# Rich reply context with h-cite microformat #}
{% include "components/reply-context.njk" %}

View File

@@ -37,6 +37,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% set repostedUrl = post.data.repostOf or post.data.repost_of %}
{% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %}
{% set hasPhotos = post.data.photo and post.data.photo.length %}
{% set postCW = post.data.contentWarning or post.data.content_warning %}
{% set borderClass = "" %}
{% if likedUrl %}
{% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %}
@@ -86,7 +87,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<a class="u-like-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
{% if post.templateContent %}
{% if postCW %}
<p class="mt-3 text-sm text-amber-700 dark:text-amber-300">&#9888; {{ postCW }} — <a href="{{ post.url }}" class="underline">View post</a></p>
{% elif post.templateContent %}
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
@@ -131,7 +134,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<a class="u-bookmark-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
{% if post.templateContent %}
{% if postCW %}
<p class="mt-3 text-sm text-amber-700 dark:text-amber-300">&#9888; {{ postCW }} — <a href="{{ post.url }}" class="underline">View post</a></p>
{% elif post.templateContent %}
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
@@ -171,7 +176,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<a class="u-repost-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
{% if post.templateContent %}
{% if postCW %}
<p class="mt-3 text-sm text-amber-700 dark:text-amber-300">&#9888; {{ postCW }} — <a href="{{ post.url }}" class="underline">View post</a></p>
{% elif post.templateContent %}
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
@@ -211,9 +218,13 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<a class="u-in-reply-to text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
{{ replyToUrl }}
</a>
{% if postCW %}
<p class="mt-3 text-sm text-amber-700 dark:text-amber-300">&#9888; {{ postCW }} — <a href="{{ post.url }}" class="underline">View post</a></p>
{% else %}
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-sm text-sky-700 dark:text-sky-300 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Reply from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
@@ -246,6 +257,9 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% endif %}
{% include "components/garden-badge.njk" %}
</div>
{% if postCW %}
<p class="mt-3 text-sm text-amber-700 dark:text-amber-300">&#9888; {{ postCW }} — <a href="{{ post.url }}" class="underline">View post</a></p>
{% else %}
<div class="photo-gallery mt-3">
{% for img in post.data.photo %}
{% set photoUrl = img.url %}
@@ -257,7 +271,8 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
</a>
{% endfor %}
</div>
{% if post.templateContent %}
{% endif %}
{% if not postCW and post.templateContent %}
<div class="e-content photo-caption prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
@@ -292,12 +307,16 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% include "components/garden-badge.njk" %}
</div>
</div>
{% if postCW %}
<p class="mt-3 text-sm text-amber-700 dark:text-amber-300">&#9888; {{ postCW }} — <a href="{{ post.url }}" class="underline">View post</a></p>
{% else %}
<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-indigo-700 dark:text-indigo-300 hover:underline mt-3 inline-block">
Read more &rarr;
</a>
{% endif %}
{% else %}
{# ── Note card (unchanged) ── #}
@@ -320,9 +339,13 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% endif %}
{% include "components/garden-badge.njk" %}
</div>
{% if postCW %}
<p class="mt-3 text-sm text-amber-700 dark:text-amber-300">&#9888; {{ postCW }} — <a href="{{ post.url }}" class="underline">View post</a></p>
{% else %}
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
{% endif %}
<div class="post-footer mt-3">
<a href="{{ post.url }}" class="text-sm text-teal-700 dark:text-teal-300 hover:underline" aria-label="Permalink: {{ post.data.title or ('Note from ' + (post.date | dateDisplay)) }}">
Permalink

View File

@@ -1028,7 +1028,7 @@ export default function (eleventyConfig) {
});
// Helper: exclude drafts from collections
const isPublished = (item) => !item.data.draft;
const isPublished = (item) => !item.data.draft && !item.data.deleted;
// Helper: exclude unlisted/private visibility from public listing surfaces
const isListed = (item) => {