/**
* Webmention 动态加载脚本 - ES6版本
* 支持三种模式:static, dynamic, hybrid
* 按类型分组展示:replies, likes, reposts, bookmarks, mentions
*/
class WebmentionLoader {
constructor() {
// i18n labels (fallback to Chinese)
this.labels = {
title: 'Webmentions',
replies: document.documentElement.lang === 'en' ? 'Replies' : '回复',
likes: document.documentElement.lang === 'en' ? 'Likes' : '喜欢',
reposts: document.documentElement.lang === 'en' ? 'Reposts' : '转发',
bookmarks: document.documentElement.lang === 'en' ? 'Bookmarks' : '收藏',
mentions: document.documentElement.lang === 'en' ? 'Mentions' : '提及',
view_source: document.documentElement.lang === 'en' ? 'View source' : '查看原文',
empty: document.documentElement.lang === 'en' ? 'No Webmentions yet' : '暂无 Webmentions',
loading: document.documentElement.lang === 'en' ? 'Loading...' : '正在加载...',
error: document.documentElement.lang === 'en' ? 'Failed to load' : '加载失败'
};
this.init();
}
// 内容截断函数
truncateContent(content, maxLength = 200) {
if (!content) return '';
const textContent = content.replace(/<[^>]*>/g, '');
if (textContent.length <= maxLength) {
return content;
}
const truncatedText = textContent.substring(0, maxLength).trim();
return content.includes('<') ? `
${truncatedText}…
` : `${truncatedText}…`;
}
// 按类型分组 mentions
groupMentions(mentions) {
const groups = {
replies: [],
likes: [],
reposts: [],
bookmarks: [],
mentions: []
};
mentions.forEach(mention => {
const prop = mention['wm-property'] || '';
if (prop === 'in-reply-to') {
groups.replies.push(mention);
} else if (prop === 'like-of') {
groups.likes.push(mention);
} else if (prop === 'repost-of') {
groups.reposts.push(mention);
} else if (prop === 'bookmark-of') {
groups.bookmarks.push(mention);
} else {
groups.mentions.push(mention);
}
});
return groups;
}
// 创建卡片式 webmention HTML元素(用于 replies 和 mentions)
createCardElement(mention) {
const item = document.createElement('div');
item.className = 'webmention-item webmention-dynamic';
item.id = `webmention-${mention['wm-id']}`;
item.setAttribute('data-webmention-id', mention['wm-id']);
const authorName = mention.author ? mention.author.name || 'Anonymous' : 'Anonymous';
const authorUrl = mention.author ? mention.author.url : '';
const authorPhoto = mention.author ? mention.author.photo : '';
const authorHtml = authorUrl
? `${authorName}`
: `${authorName}`;
const photoHtml = authorPhoto
? `
`
: '';
const publishedDate = new Date(mention.published || mention['wm-received']);
const dateStr = publishedDate.toLocaleDateString('zh-CN');
let content = mention.content;
if (Array.isArray(content) && content.length > 0) {
content = content[0];
}
const contentHtml = content ? content.html || content.text : '';
item.innerHTML = `
${photoHtml}
${authorHtml}
${dateStr}
${DOMPurify.sanitize(this.truncateContent(contentHtml))}
`;
return item;
}
// 创建紧凑式 webmention HTML元素(用于 likes, reposts, bookmarks)
createCompactElement(mention) {
const item = document.createElement('div');
item.className = 'webmention-compact-item';
const authorName = mention.author ? mention.author.name || 'Anonymous' : 'Anonymous';
const authorUrl = mention.author ? mention.author.url : '';
const authorPhoto = mention.author ? mention.author.photo : '';
const photoHtml = authorPhoto
? `
`
: '';
const authorHtml = authorUrl
? `${authorName}`
: `${authorName}`;
item.innerHTML = `${photoHtml}${authorHtml}`;
return item;
}
// 获取已存在的webmention IDs
getExistingWebmentionIds(container) {
const existingItems = container.querySelectorAll('[data-webmention-id]');
const ids = new Set();
existingItems.forEach(item => {
const id = item.getAttribute('data-webmention-id');
if (id) {
ids.add(parseInt(id, 10));
}
});
return ids;
}
// 更新webmention计数
updateWebmentionCount(container, newCount) {
const countEl = container.querySelector('.webmention-count');
if (countEl) {
countEl.textContent = newCount;
}
}
// 添加加载动画
addLoadingAnimation(element) {
element.style.opacity = '0';
element.style.transform = 'translateY(10px)';
element.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
element.style.opacity = '1';
element.style.transform = 'translateY(0)';
});
});
}
// 渲染分组内容到容器
renderGroups(container, groups, totalCount) {
let html = `
${this.labels.title} (${totalCount})
`;
container.innerHTML = html;
// Card groups (replies, mentions)
const cardGroupDefs = [
{ key: 'replies', items: groups.replies },
{ key: 'mentions', items: groups.mentions }
];
cardGroupDefs.forEach(({ key, items }) => {
if (items.length === 0) return;
const group = document.createElement('div');
group.className = `webmention-group webmention-group-${key}`;
group.innerHTML = `${this.labels[key]} (${items.length})
`;
const list = document.createElement('div');
list.className = 'webmention-list';
items
.sort((a, b) => new Date(a['wm-received']) - new Date(b['wm-received']))
.forEach(mention => {
const element = this.createCardElement(mention);
list.appendChild(element);
this.addLoadingAnimation(element);
});
group.appendChild(list);
container.appendChild(group);
});
// Compact groups (likes, reposts, bookmarks)
const compactGroupDefs = [
{ key: 'likes', items: groups.likes },
{ key: 'reposts', items: groups.reposts },
{ key: 'bookmarks', items: groups.bookmarks }
];
compactGroupDefs.forEach(({ key, items }) => {
if (items.length === 0) return;
const group = document.createElement('div');
group.className = `webmention-group webmention-group-${key}`;
group.innerHTML = `${this.labels[key]} (${items.length})
`;
const list = document.createElement('div');
list.className = 'webmention-compact-list';
items.forEach(mention => {
const element = this.createCompactElement(mention);
list.appendChild(element);
this.addLoadingAnimation(element);
});
group.appendChild(list);
container.appendChild(group);
});
}
// 获取webmention数据(使用target API,无需客户端过滤)
async fetchWebmentions(fullUrl) {
const apiUrl = `https://webmention.io/api/mentions.jf2?target=${encodeURIComponent(fullUrl)}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// 直接返回API结果,无需客户端处理
return data.children || [];
} catch (error) {
console.error('Failed to fetch webmentions:', error);
throw error;
}
}
// 静态模式:什么都不做
handleStaticMode(container) {
console.log('Webmention static mode - no dynamic loading');
}
// 动态模式:完全由客户端加载
async handleDynamicMode(container) {
const fullUrl = container.getAttribute('data-full-url');
try {
const webmentions = await this.fetchWebmentions(fullUrl);
if (webmentions.length > 0) {
const groups = this.groupMentions(webmentions);
this.renderGroups(container, groups, webmentions.length);
console.log(`Loaded ${webmentions.length} webmentions in dynamic mode`);
} else {
// 显示空状态
container.innerHTML = `
${this.labels.title} (0)
${this.labels.empty}
`;
}
// 移除loading类
if (container.classList.contains('webmention-empty')) {
container.classList.remove('webmention-empty');
}
} catch (error) {
console.error('Dynamic webmention loading failed:', error);
container.innerHTML = `
${this.labels.title}
${this.labels.error}
`;
}
}
// 混合模式:与静态内容合并
async handleHybridMode(container) {
const fullUrl = container.getAttribute('data-full-url');
try {
const webmentions = await this.fetchWebmentions(fullUrl);
// 获取已存在的webmention IDs
const existingIds = this.getExistingWebmentionIds(container);
// 过滤出新的webmentions
const newWebmentions = webmentions.filter(mention =>
!existingIds.has(mention['wm-id'])
);
if (newWebmentions.length > 0) {
// Re-render with all webmentions grouped
const allGroups = this.groupMentions(webmentions);
const totalCount = existingIds.size + newWebmentions.length;
this.renderGroups(container, allGroups, totalCount);
if (container.classList.contains('webmention-empty')) {
container.classList.remove('webmention-empty');
}
console.log(`Added ${newWebmentions.length} new webmentions in hybrid mode`);
} else {
console.log('No new webmentions found in hybrid mode');
}
} catch (error) {
console.error('Hybrid webmention loading failed:', error);
// 静默失败,不影响已有的静态内容
}
}
// 处理单个webmention容器
async processContainer(container) {
const mode = container.getAttribute('data-mode') || 'static';
const fullUrl = container.getAttribute('data-full-url');
if (!fullUrl) {
console.warn('Webmention container missing required data-full-url attribute');
return;
}
console.log(`Processing webmention container in ${mode} mode`);
switch (mode) {
case 'static':
this.handleStaticMode(container);
break;
case 'dynamic':
await this.handleDynamicMode(container);
break;
case 'hybrid':
await this.handleHybridMode(container);
break;
default:
console.warn(`Unknown webmention mode: ${mode}`);
}
}
// 初始化
init() {
const containers = document.querySelectorAll('.webmention-section[data-full-url]');
if (containers.length === 0) {
return;
}
// 处理所有容器
containers.forEach(container => this.processContainer(container));
}
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new WebmentionLoader());
} else {
new WebmentionLoader();
}