cf917a9010
Build & Deploy / build-and-deploy (push) Successful in 2m1s
webmentions.js was adding NEW cards for Mastodon replies even when Alpine had already rendered them, because the client-side filter didn't mirror the Nunjucks build-time fed.brid.gy exclusion rule. Pass mastodon-active + bsky-url as data attributes on the webmentions container div, then filter out fed.brid.gy sourced Mastodon replies and bsky.app/brid.gy/bluesky Bluesky replies from regularItems when the corresponding Alpine component is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
335 lines
18 KiB
Plaintext
335 lines
18 KiB
Plaintext
{# Webmentions Component #}
|
|
{# Displays likes, reposts, and replies for a post #}
|
|
{# Also checks legacy URLs from micro.blog and old blog for historical webmentions #}
|
|
{# Client-side JS supplements build-time data with real-time fetches #}
|
|
|
|
{% set mentions = webmentions | webmentionsForUrl(page.url, urlAliases, conversationMentions) %}
|
|
{% set absoluteUrl = site.url + page.url %}
|
|
{% set buildTimestamp = "" | timestamp %}
|
|
|
|
{# Data container for client-side JS to fetch new webmentions #}
|
|
<div data-webmentions
|
|
data-target="{{ absoluteUrl }}"
|
|
data-domain="{{ site.webmentions.domain }}"
|
|
data-buildtime="{{ buildTimestamp }}"
|
|
data-mastodon-active="{{ 'true' if mastodonInstance else '' }}"
|
|
data-bsky-url="{{ bskySyndUrl }}"
|
|
class="hidden"></div>
|
|
|
|
{# Detect Bluesky syndication URL here — needed to show section even without webmentions #}
|
|
{% set bskySyndUrl = "" %}
|
|
{% if syndication %}
|
|
{% for url in syndication %}
|
|
{% if "bsky.app" in url %}{% set bskySyndUrl = url %}{% endif %}
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
{% set mastodonInstance = site.feeds.mastodon.instance %}
|
|
{% if mentions.length or bskySyndUrl or mastodonInstance %}
|
|
<section class="webmentions mt-8 pt-8 border-t border-surface-200 dark:border-surface-700" id="webmentions">
|
|
<h2 class="text-lg font-semibold text-surface-700 dark:text-surface-300 mb-6">
|
|
Interactions ({{ mentions.length }})
|
|
</h2>
|
|
|
|
{# Likes #}
|
|
{% set likes = mentions | webmentionsByType('likes') %}
|
|
{% if likes.length %}
|
|
<div class="webmention-likes mb-6">
|
|
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
|
|
{{ likes.length }} Like{% if likes.length != 1 %}s{% endif %}
|
|
</h3>
|
|
<is-land on:visible>
|
|
<ul class="facepile" role="list">
|
|
{% for like in likes %}
|
|
<li class="inline" data-wm-source="{{ like['wm-source'] if like['wm-source'] else '' }}" data-author-url="{{ like.author.url }}">
|
|
<a href="{{ like.author.url }}"
|
|
class="facepile-avatar"
|
|
aria-label="{{ like.author.name }} (opens in new tab)"
|
|
target="_blank"
|
|
rel="noopener">
|
|
<img
|
|
src="{{ like.author.photo or '/images/default-avatar.svg' }}"
|
|
alt=""
|
|
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
|
|
loading="lazy"
|
|
>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</is-land>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Reposts #}
|
|
{% set reposts = mentions | webmentionsByType('reposts') %}
|
|
{% if reposts.length %}
|
|
<div class="webmention-reposts mb-6">
|
|
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
|
|
{{ reposts.length }} Repost{% if reposts.length != 1 %}s{% endif %}
|
|
</h3>
|
|
<is-land on:visible>
|
|
<ul class="facepile" role="list">
|
|
{% for repost in reposts %}
|
|
<li class="inline" data-wm-source="{{ repost['wm-source'] if repost['wm-source'] else '' }}" data-author-url="{{ repost.author.url }}">
|
|
<a href="{{ repost.author.url }}"
|
|
class="facepile-avatar"
|
|
aria-label="{{ repost.author.name }} (opens in new tab)"
|
|
target="_blank"
|
|
rel="noopener">
|
|
<img
|
|
src="{{ repost.author.photo or '/images/default-avatar.svg' }}"
|
|
alt=""
|
|
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
|
|
loading="lazy"
|
|
>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</is-land>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Bookmarks #}
|
|
{% set bookmarks = mentions | webmentionsByType('bookmarks') %}
|
|
{% if bookmarks.length %}
|
|
<div class="webmention-bookmarks mb-6">
|
|
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
|
|
{{ bookmarks.length }} Bookmark{% if bookmarks.length != 1 %}s{% endif %}
|
|
</h3>
|
|
<is-land on:visible>
|
|
<ul class="facepile" role="list">
|
|
{% for bookmark in bookmarks %}
|
|
<li class="inline" data-wm-source="{{ bookmark['wm-source'] if bookmark['wm-source'] else '' }}" data-author-url="{{ bookmark.author.url }}">
|
|
<a href="{{ bookmark.author.url }}"
|
|
class="facepile-avatar"
|
|
aria-label="{{ bookmark.author.name }} (opens in new tab)"
|
|
target="_blank"
|
|
rel="noopener">
|
|
<img
|
|
src="{{ bookmark.author.photo or '/images/default-avatar.svg' }}"
|
|
alt=""
|
|
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
|
|
loading="lazy"
|
|
>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</is-land>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Replies — webmention replies merged with Bluesky + Mastodon/AP threads (deduplicated) #}
|
|
{% set wm_replies = mentions | webmentionsByType('replies') %}
|
|
|
|
{# Activate Mastodon/AP thread fetching when instance is configured #}
|
|
{% set apIdentity = site.fediverseCreator %}
|
|
{% set mastodonActive = mastodonInstance %}
|
|
{% set replyComponentActive = bskySyndUrl or mastodonActive %}
|
|
|
|
{# Filter: remove Bluesky-sourced webmentions if bskySyndUrl; remove fed.brid.gy webmentions if mastodonActive #}
|
|
{% set filtered_replies = [] %}
|
|
{% for reply in wm_replies %}
|
|
{% set src = reply['wm-source'] | default('') %}
|
|
{% set includeReply = true %}
|
|
{% if bskySyndUrl and ('bsky.app' in src or 'brid.gy' in src or 'bridgy' in src or 'bsky.app' in (reply.author.url | default(''))) %}{% set includeReply = false %}{% endif %}
|
|
{% if mastodonActive and 'fed.brid.gy' in src %}{% set includeReply = false %}{% endif %}
|
|
{% if includeReply %}
|
|
{% set filtered_replies = (filtered_replies.push(reply), filtered_replies) %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if filtered_replies.length or replyComponentActive %}
|
|
{% set wmRepliesCount = filtered_replies.length %}
|
|
<div class="webmention-replies" x-cloak {% if replyComponentActive %}x-show="{{ wmRepliesCount }} + bskyReplies.length + mastodonReplies.length > 0" x-data="socialReplies('{{ bskySyndUrl }}', '{{ absoluteUrl }}', '{{ mastodonInstance if mastodonActive else "" }}', '{{ apIdentity if mastodonActive else "" }}')"{% endif %}>
|
|
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-4"
|
|
{% if replyComponentActive %}x-text="({{ wmRepliesCount }} + bskyReplies.length + mastodonReplies.length) + ' Repl' + (({{ wmRepliesCount }} + bskyReplies.length + mastodonReplies.length !== 1) ? 'ies' : 'y')"{% endif %}>
|
|
{% if not replyComponentActive %}{{ wmRepliesCount }} Repl{% if wmRepliesCount != 1 %}ies{% else %}y{% endif %}{% endif %}
|
|
</h3>
|
|
<ul class="space-y-4">
|
|
{% for reply in filtered_replies %}
|
|
<li class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm"
|
|
data-wm-url="{{ reply.url }}"
|
|
data-wm-source="{{ reply['wm-source'] if reply['wm-source'] else '' }}"
|
|
data-author-url="{{ reply.author.url }}">
|
|
<div class="flex gap-3">
|
|
<a href="{{ reply.author.url }}" target="_blank" rel="noopener">
|
|
<img
|
|
src="{{ reply.author.photo or '/images/default-avatar.svg' }}"
|
|
alt="{{ reply.author.name }}"
|
|
class="w-10 h-10 rounded-full"
|
|
loading="lazy"
|
|
>
|
|
</a>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
|
<a href="{{ reply.author.url }}"
|
|
class="font-semibold text-surface-900 dark:text-surface-100 hover:underline"
|
|
target="_blank"
|
|
rel="noopener">
|
|
{{ reply.author.name }}
|
|
</a>
|
|
<span class="wm-provenance-badge" data-detect="true"></span>
|
|
<a href="{{ reply.url }}"
|
|
class="text-xs text-surface-600 dark:text-surface-400 hover:underline"
|
|
target="_blank"
|
|
rel="noopener">
|
|
<time class="font-mono" datetime="{{ reply.published }}">
|
|
{{ reply.published | date("MMM d, yyyy") }}
|
|
</time>
|
|
</a>
|
|
</div>
|
|
<div class="text-surface-700 dark:text-surface-300 prose dark:prose-invert prose-sm max-w-none">
|
|
{{ reply.content.html | safe if reply.content.html else reply.content.text }}
|
|
</div>
|
|
{% set replySource = reply['wm-source'] | default('', true) %}
|
|
{% set replyAuthorUrl = reply.author.url | default('', true) %}
|
|
{% set buildPlatform = 'webmention' %}
|
|
{% if 'bsky.app' in replyAuthorUrl or ('brid.gy/' in replySource and '/bluesky/' in replySource) %}
|
|
{% set buildPlatform = 'bluesky' %}
|
|
{% elif 'brid.gy/' in replySource and '/mastodon/' in replySource %}
|
|
{% set buildPlatform = 'mastodon' %}
|
|
{% elif 'fed.brid.gy' in replySource %}
|
|
{% set buildPlatform = 'activitypub' %}
|
|
{% endif %}
|
|
<button class="wm-reply-btn hidden text-xs text-primary-600 dark:text-primary-400 hover:underline mt-2"
|
|
data-reply-url="{{ reply.url }}"
|
|
data-platform="{{ buildPlatform }}">
|
|
Reply
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="wm-owner-reply-slot ml-13 mt-2"></div>
|
|
</li>
|
|
{% endfor %}
|
|
{% if bskySyndUrl %}
|
|
<template x-for="reply in bskyReplies" :key="reply.uri">
|
|
<li class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm"
|
|
:class="reply.depth > 0 ? 'ml-4 sm:ml-8' : ''">
|
|
<div class="flex gap-3">
|
|
<a :href="reply.author.url" target="_blank" rel="noopener" class="flex-shrink-0">
|
|
<img :src="reply.author.avatar || '/images/default-avatar.svg'"
|
|
:alt="reply.author.name"
|
|
class="w-10 h-10 rounded-full"
|
|
loading="lazy">
|
|
</a>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
|
<a :href="reply.author.url"
|
|
class="font-semibold text-surface-900 dark:text-surface-100 hover:underline"
|
|
target="_blank" rel="noopener"
|
|
x-text="reply.author.name"></a>
|
|
<span class="wm-provenance-badge inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-sky-100 dark:bg-sky-900/30 text-sky-600 dark:text-sky-400 rounded-full" title="Bluesky"><svg class="w-2 h-2 inline-block" viewBox="0 0 568 501" fill="currentColor"><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>
|
|
<a :href="reply.url"
|
|
class="text-xs text-surface-600 dark:text-surface-400 hover:underline"
|
|
target="_blank" rel="noopener">
|
|
<time :datetime="reply.published" x-text="formatDate(reply.published)"></time>
|
|
</a>
|
|
</div>
|
|
<div class="text-surface-700 dark:text-surface-300 prose dark:prose-invert prose-sm max-w-none"
|
|
x-text="reply.text"></div>
|
|
<a :href="reply.url"
|
|
class="wm-reply-btn hidden text-xs text-primary-600 dark:text-primary-400 hover:underline mt-2"
|
|
target="_blank" rel="noopener">Reply on Bluesky</a>
|
|
</div>
|
|
</div>
|
|
<div class="wm-owner-reply-slot ml-13 mt-2"></div>
|
|
</li>
|
|
</template>
|
|
{% endif %}
|
|
{% if mastodonActive %}
|
|
<template x-for="reply in mastodonReplies" :key="reply.id">
|
|
<li class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
|
|
<div class="flex gap-3">
|
|
<a :href="reply.author.url" target="_blank" rel="noopener" class="flex-shrink-0">
|
|
<img :src="reply.author.avatar || '/images/default-avatar.svg'"
|
|
:alt="reply.author.name"
|
|
class="w-10 h-10 rounded-full"
|
|
loading="lazy">
|
|
</a>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
|
<a :href="reply.author.url"
|
|
class="font-semibold text-surface-900 dark:text-surface-100 hover:underline"
|
|
target="_blank" rel="noopener"
|
|
x-text="reply.author.name"></a>
|
|
<span class="wm-provenance-badge inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 rounded-full" title="Mastodon"><svg class="w-2 h-2 inline-block" viewBox="0 0 24 24" fill="currentColor"><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>
|
|
<a :href="reply.url"
|
|
class="text-xs text-surface-600 dark:text-surface-400 hover:underline"
|
|
target="_blank" rel="noopener">
|
|
<time :datetime="reply.published" x-text="formatDate(reply.published)"></time>
|
|
</a>
|
|
</div>
|
|
<div class="text-surface-700 dark:text-surface-300 prose dark:prose-invert prose-sm max-w-none"
|
|
x-html="reply.content"></div>
|
|
<a :href="reply.url"
|
|
class="wm-reply-btn hidden text-xs text-primary-600 dark:text-primary-400 hover:underline mt-2"
|
|
target="_blank" rel="noopener">Reply on Mastodon</a>
|
|
</div>
|
|
</div>
|
|
<div class="wm-owner-reply-slot ml-13 mt-2"></div>
|
|
</li>
|
|
</template>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Other mentions #}
|
|
{% set otherMentions = mentions | webmentionsByType('mentions') %}
|
|
{% if otherMentions.length %}
|
|
<div class="webmention-mentions mt-6">
|
|
<h3 class="text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3">
|
|
{{ otherMentions.length }} Mention{% if otherMentions.length != 1 %}s{% endif %}
|
|
</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
{% for mention in otherMentions %}
|
|
<li>
|
|
<a href="{{ mention.url }}"
|
|
class="text-accent-600 dark:text-accent-400 hover:underline"
|
|
target="_blank"
|
|
rel="noopener">
|
|
{{ mention.author.name }} mentioned this on <time class="font-mono" datetime="{{ mention.published }}">{{ mention.published | date("MMM d, yyyy") }}</time>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# Webmention send form — collapsed by default #}
|
|
<details class="webmention-form mt-8">
|
|
<summary class="text-sm font-semibold text-surface-600 dark:text-surface-400 cursor-pointer hover:text-surface-700 dark:hover:text-surface-300 transition-colors list-none [&::-webkit-details-marker]:hidden flex items-center gap-1.5">
|
|
<svg class="w-3.5 h-3.5 transition-transform [[open]>&]:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
</svg>
|
|
Send a Webmention
|
|
</summary>
|
|
<div class="mt-3 p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
|
|
<p class="text-xs text-surface-600 dark:text-surface-400 mb-3">
|
|
Have you written a response to this post? Send a webmention by entering your post URL below.
|
|
</p>
|
|
<form action="https://webmention.io/{{ site.webmentions.domain }}/webmention" method="post" class="flex gap-2">
|
|
<input type="hidden" name="target" value="{{ site.url }}{{ page.url }}">
|
|
<label for="webmention-source" class="sr-only">Your post URL</label>
|
|
<input
|
|
id="webmention-source"
|
|
type="url"
|
|
name="source"
|
|
placeholder="https://your-site.com/response"
|
|
required
|
|
class="flex-1 px-3 py-2 text-sm bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded"
|
|
>
|
|
<button
|
|
type="submit"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-accent-600 hover:bg-accent-700 rounded transition-colors">
|
|
Send
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</details>
|