Update Webmentions.astro

This commit is contained in:
2026-08-27 22:08:37 +08:00
parent 1207e862d4
commit b2982d16ca
+110 -10
View File
@@ -175,6 +175,42 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
text-decoration: none; text-decoration: none;
} }
:global(.webmention-site-icon) {
position: relative;
display: grid;
width: 100%;
height: 100%;
place-items: center;
overflow: hidden;
border-radius: inherit;
}
:global(.webmention-site-icon img),
:global(.webmention-icon-fallback) {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
:global(.webmention-site-icon img) {
z-index: 1;
padding: .3rem;
border-radius: inherit;
background: var(--color-surface);
object-fit: contain;
opacity: 0;
}
:global(.webmention-site-icon[data-icon-loaded='true'] img) {
opacity: 1;
}
:global(.webmention-icon-fallback) {
display: grid;
place-items: center;
}
:global(.webmention-reaction:hover) { :global(.webmention-reaction:hover) {
border-color: var(--color-accent); border-color: var(--color-accent);
color: var(--color-accent-strong); color: var(--color-accent-strong);
@@ -222,6 +258,12 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
:global(.webmention-host) {
color: var(--color-muted);
font-size: .75rem;
overflow-wrap: anywhere;
}
:global(.webmention-date), :global(.webmention-date),
:global(.webmention-kind) { :global(.webmention-kind) {
color: var(--color-muted); color: var(--color-muted);
@@ -325,10 +367,13 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
type WebmentionRecord = { type WebmentionRecord = {
id: string; id: string;
source: string; source: string;
hostname: string;
createdAt: string; createdAt: string;
title: string; title: string;
content: string; content: string;
author: string; author: string;
siteName: string;
favicon: string;
type: string; type: string;
}; };
@@ -360,6 +405,16 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
} }
}; };
const safeAsset = (value: string, base: URL) => {
if (!value) return '';
try {
const url = new URL(value, base);
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : '';
} catch {
return '';
}
};
const plainText = (value: string) => { const plainText = (value: string) => {
if (!value) return ''; if (!value) return '';
const parsed = new DOMParser().parseFromString(value, 'text/html'); const parsed = new DOMParser().parseFromString(value, 'text/html');
@@ -371,13 +426,25 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
const record = value as Record<string, unknown>; const record = value as Record<string, unknown>;
const source = safeSource(getString(record, 'source')); const source = safeSource(getString(record, 'source'));
if (!source) return null; if (!source) return null;
const author = plainText(getString(record, 'author_name'));
const siteName = plainText(getString(record, 'site_name'))
|| plainText(getString(record, 'site_title'))
|| author
|| source.hostname;
const favicon = safeAsset(
getString(record, 'favicon') || getString(record, 'site_icon') || getString(record, 'icon'),
source
) || new URL('/favicon.ico', source).href;
return { return {
id: getString(record, 'id'), id: getString(record, 'id'),
source: source.href, source: source.href,
hostname: source.hostname,
createdAt: getString(record, 'created_at'), createdAt: getString(record, 'created_at'),
title: plainText(getString(record, 'title')), title: plainText(getString(record, 'title')),
content: plainText(getString(record, 'content')), content: plainText(getString(record, 'content')),
author: plainText(getString(record, 'author_name')) || source.hostname, author: author || source.hostname,
siteName,
favicon,
type: getString(record, 'type').toLowerCase() type: getString(record, 'type').toLowerCase()
}; };
}; };
@@ -398,6 +465,8 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
const initialFor = (name: string) => Array.from(name.trim())[0]?.toLocaleUpperCase() ?? '@'; const initialFor = (name: string) => Array.from(name.trim())[0]?.toLocaleUpperCase() ?? '@';
const displayTitle = (mention: WebmentionRecord) => mention.title || mention.siteName || mention.hostname;
const sourceLink = (mention: WebmentionRecord, className: string) => { const sourceLink = (mention: WebmentionRecord, className: string) => {
const link = document.createElement('a'); const link = document.createElement('a');
link.className = className; link.className = className;
@@ -407,11 +476,38 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
return link; return link;
}; };
const createSiteIcon = (mention: WebmentionRecord, className: string) => {
const wrapper = document.createElement('span');
wrapper.className = className;
wrapper.setAttribute('aria-hidden', 'true');
const fallback = document.createElement('span');
fallback.className = 'webmention-icon-fallback';
fallback.textContent = initialFor(mention.siteName || mention.hostname);
const image = document.createElement('img');
image.src = mention.favicon;
image.alt = '';
image.loading = 'lazy';
image.decoding = 'async';
image.referrerPolicy = 'no-referrer';
image.addEventListener('load', () => {
wrapper.dataset.iconLoaded = 'true';
});
image.addEventListener('error', () => {
image.remove();
});
wrapper.append(fallback, image);
return wrapper;
};
const renderReaction = (mention: WebmentionRecord) => { const renderReaction = (mention: WebmentionRecord) => {
const link = sourceLink(mention, 'webmention-reaction'); const link = sourceLink(mention, 'webmention-reaction');
link.textContent = initialFor(mention.author); const title = displayTitle(mention);
link.title = mention.author; link.append(createSiteIcon(mention, 'webmention-site-icon'));
link.setAttribute('aria-label', `${mention.author}:${kindLabel(kindOf(mention))}`); link.title = `${title} (${mention.hostname})`;
link.setAttribute('aria-label', `${title}:${kindLabel(kindOf(mention))}`);
return link; return link;
}; };
@@ -420,19 +516,23 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
const item = document.createElement('li'); const item = document.createElement('li');
item.className = 'webmention-item'; item.className = 'webmention-item';
const avatar = document.createElement('span'); const avatar = createSiteIcon(mention, 'webmention-avatar webmention-site-icon');
avatar.className = 'webmention-avatar';
avatar.textContent = initialFor(mention.author);
avatar.setAttribute('aria-hidden', 'true');
const body = document.createElement('div'); const body = document.createElement('div');
const meta = document.createElement('div'); const meta = document.createElement('div');
meta.className = 'webmention-meta'; meta.className = 'webmention-meta';
const author = sourceLink(mention, 'webmention-author'); const author = sourceLink(mention, 'webmention-author');
author.textContent = mention.author; author.textContent = displayTitle(mention);
meta.append(author); meta.append(author);
if (displayTitle(mention) !== mention.hostname) {
const host = document.createElement('span');
host.className = 'webmention-host';
host.textContent = mention.hostname;
meta.append(host);
}
if (mention.createdAt) { if (mention.createdAt) {
const date = new Date(mention.createdAt); const date = new Date(mention.createdAt);
if (!Number.isNaN(date.getTime())) { if (!Number.isNaN(date.getTime())) {
@@ -450,7 +550,7 @@ const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : '';
meta.append(type); meta.append(type);
body.append(meta); body.append(meta);
const text = mention.content || mention.title; const text = mention.content;
if (text) { if (text) {
const content = document.createElement('p'); const content = document.createElement('p');
content.className = 'webmention-content'; content.className = 'webmention-content';