Files
indiekit-blog/_includes/layouts/post.njk
rmdes fa7bfb26ea feat: add comment system components and recent comments widget
- Comment area on post pages (IndieAuth sign-in, submit, display)
- Alpine.js client-side component for auth flow and comment CRUD
- Recent comments sidebar widget with build-time data fetching
- Include comments.js in base layout, comments.njk before webmentions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:56:15 +01:00

181 lines
9.5 KiB
Plaintext

---
layout: layouts/base.njk
withBlogSidebar: true
---
<article class="h-entry post">
{% if title %}
<h1 class="p-name text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-3 sm:mb-4">{{ title }}</h1>
{% endif %}
<div class="post-meta mb-4 sm:mb-6">
<time-difference><time class="dt-published" datetime="{{ date.toISOString() }}">
{{ date | dateDisplay }}
</time></time-difference>
{% if category %}
<span class="post-categories">
{# Handle both string and array categories #}
{% if category is string %}
<a href="/categories/{{ category | slugify }}/" class="p-category">{{ category }}</a>
{% else %}
{% for cat in category %}
<a href="/categories/{{ cat | slugify }}/" class="p-category">{{ cat }}</a>
{% endfor %}
{% endif %}
</span>
{% endif %}
</div>
{# Bridgy syndication content - controls what gets posted to social networks #}
{# For interaction types (bookmarks, likes, replies, reposts), include the target URL #}
{# Support both camelCase (Indiekit Eleventy preset) and underscore (legacy) property names #}
{% set bookmarkedUrl = bookmarkOf or bookmark_of %}
{% set likedUrl = likeOf or like_of %}
{% set replyTo = inReplyTo or in_reply_to %}
{% set repostedUrl = repostOf or repost_of %}
{% set bridgySummary = description or summary or (content | ogDescription(280)) %}
{% set interactionUrl = bookmarkedUrl or likedUrl or replyTo or repostedUrl %}
{% if bridgySummary or interactionUrl %}
<p class="p-summary e-bridgy-mastodon-content e-bridgy-bluesky-content hidden">{% if bookmarkedUrl %}🔖 {{ bookmarkedUrl }}{% if bridgySummary %} - {{ bridgySummary }}{% endif %}{% elif likedUrl %}❤️ {{ likedUrl }}{% if bridgySummary %} - {{ bridgySummary }}{% endif %}{% elif replyTo %}↩️ {{ replyTo }}{% if bridgySummary %} - {{ bridgySummary }}{% endif %}{% elif repostedUrl %}🔁 {{ repostedUrl }}{% if bridgySummary %} - {{ bridgySummary }}{% endif %}{% else %}{{ bridgySummary }}{% endif %}</p>
{% endif %}
{# Render photo(s) from frontmatter for photo posts - use eleventy:ignore to skip image transform #}
{% if photo %}
<div class="photo-gallery mb-6">
{% for img in photo %}
{% set photoUrl = img.url %}
{% if photoUrl and photoUrl[0] != '/' and 'http' not in photoUrl %}
{% set photoUrl = '/' + photoUrl %}
{% endif %}
<img src="{{ photoUrl }}" alt="{{ img.alt | default('Photo') }}" class="u-photo rounded-lg max-w-full" loading="lazy" eleventy:ignore>
{% endfor %}
</div>
{% endif %}
<div class="e-content prose prose-surface dark:prose-invert max-w-none">
{{ content | safe }}
</div>
{# Rich reply context with h-cite microformat #}
{% include "components/reply-context.njk" %}
{# Pending syndication targets (for services like IndieNews that require u-syndication before webmention) #}
{% if mpSyndicateTo %}
<div class="hidden">
{% for url in mpSyndicateTo %}
{% if "news.indieweb.org" in url %}
<a href="{{ url }}" class="u-syndication" rel="syndication">IndieNews</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{# Syndication Footer - shows where this post was also published #}
{# Skip syndication URLs that point back to our own site (self-hosted AP) #}
{% set externalSyndication = [] %}
{% if syndication %}
{% for url in syndication %}
{% if url.indexOf(site.url) != 0 %}
{% set externalSyndication = (externalSyndication.push(url), externalSyndication) %}
{% endif %}
{% endfor %}
{% endif %}
{% if externalSyndication.length %}
<footer class="post-footer mt-8 pt-6 border-t border-surface-200 dark:border-surface-700">
<div class="flex flex-wrap items-center gap-4">
<span class="text-sm text-surface-500 dark:text-surface-400">Also on:</span>
<div class="flex flex-wrap gap-3">
{% for url in externalSyndication %}
{% if "bsky.app" in url or "bluesky" in url %}
<a class="u-syndication inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[#0085ff]/10 text-[#0085ff] hover:bg-[#0085ff]/20 transition-colors text-sm font-medium" href="{{ url }}" target="_blank" rel="noopener syndication" title="View on Bluesky">
<svg class="w-4 h-4" viewBox="0 0 568 501" fill="currentColor" aria-hidden="true">
<path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"/>
</svg>
<span>Bluesky</span>
</a>
{% elif "mstdn" in url or "mastodon" in url or "social" in url %}
<a class="u-syndication inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[#6364ff]/10 text-[#6364ff] hover:bg-[#6364ff]/20 transition-colors text-sm font-medium" href="{{ url }}" target="_blank" rel="noopener syndication" title="View on Mastodon">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" 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.12z"/>
</svg>
<span>Mastodon</span>
</a>
{% else %}
<a class="u-syndication inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors text-sm font-medium" href="{{ url }}" target="_blank" rel="noopener syndication">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
<span>{{ url | replace("https://", "") | truncate(20) }}</span>
</a>
{% endif %}
{% endfor %}
</div>
</div>
</footer>
{% endif %}
<a class="u-url" href="{{ page.url }}" hidden>Permalink</a>
{# Author h-card for IndieWeb authorship #}
<span class="p-author h-card hidden">
<a class="p-name u-url" href="{{ site.author.url }}">{{ site.author.name }}</a>
<img class="u-photo" src="{{ site.author.avatar }}" alt="{{ site.author.name }}" hidden>
</span>
{# JSON-LD Structured Data for SEO #}
{# Handle photo as potentially an array #}
{% set postImage = photo %}
{% if postImage %}
{# If photo is an array, use first element (check if first element looks like a URL) #}
{% if postImage[0] and (postImage[0] | length) > 10 %}
{% set postImage = postImage[0] %}
{% endif %}
{% endif %}
{% if not postImage or postImage == "" %}
{% set postImage = image or (content | extractFirstImage) %}
{% endif %}
{% set postDesc = description | default(content | ogDescription(160)) %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": {{ (title or "Untitled") | dump | safe }},
"url": "{{ site.url }}{{ page.url }}",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "{{ site.url }}{{ page.url }}"
},
"datePublished": "{{ date.toISOString() }}",
"dateModified": "{{ date.toISOString() }}",
"author": {
"@type": "Person",
"name": "{{ site.author.name }}",
"url": "{{ site.author.url }}"
},
"publisher": {
"@type": "Organization",
"name": "{{ site.name }}",
"url": "{{ site.url }}",
"logo": {
"@type": "ImageObject",
"url": "{{ site.url }}/images/og-default.png"
}
},
"description": {{ postDesc | dump | safe }}{% if postImage and postImage != "" and (postImage | length) > 10 %},
"image": ["{{ site.url }}{% if '/' in postImage and postImage[0] == '/' %}{{ postImage }}{% else %}/{{ postImage }}{% endif %}"]{% endif %}
}
</script>
</article>
{# Comments section #}
{% include "components/comments.njk" %}
{# Webmentions display - likes, reposts, replies #}
{% include "components/webmentions.njk" %}
{# Post Navigation - Previous/Next #}
{% include "components/post-navigation.njk" %}