diff --git a/blog.config.js b/blog.config.js
index 23835465..e9c2f96a 100644
--- a/blog.config.js
+++ b/blog.config.js
@@ -177,6 +177,21 @@ const BLOG = {
COMMENT_WALINE_SERVER_URL: process.env.NEXT_PUBLIC_WALINE_SERVER_URL || '', // 请配置完整的Waline评论地址 例如 hhttps://preview-waline.tangly1024.com @see https://waline.js.org/guide/get-started.html
COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
+ // 此评论系统基于WebMention,细节可参考https://webmention.io
+ // 它是一个基于IndieWeb理念的开放式评论系统,下方COMMENT_WEBMENTION包含的属性皆需配置:
+ // ENABLE: 是否开启
+ // AUTH: Webmention使用的IndieLogin,可使用Twitter或Github个人页面连结
+ // HOSTNAME: Webmention绑定之网域,通常即为本站网址
+ // TWITTER_USERNAME: 评论显示区域需要的资讯
+ // TOKEN: Webmention的API token
+ COMMENT_WEBMENTION: {
+ ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
+ AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
+ HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
+ TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
+ TOKEN: process.env.NEXT_PUBLIC_WEBMENTION_TOKEN || ''
+ },
+
// <---- 评论插件
// ----> 站点统计
diff --git a/components/Comment.js b/components/Comment.js
index 570d122d..e1b1f6bb 100644
--- a/components/Comment.js
+++ b/components/Comment.js
@@ -43,6 +43,12 @@ const GiscusComponent = dynamic(
},
{ ssr: false }
)
+const WebMentionComponent = dynamic(
+ () => {
+ return import('@/components/WebMention')
+ },
+ { ssr: false }
+)
const ValineComponent = dynamic(() => import('@/components/ValineComponent'), {
ssr: false
@@ -100,6 +106,10 @@ const Comment = ({ frontMatter }) => {
{BLOG.COMMENT_GITALK_CLIENT_ID && (
)}
+
+ {BLOG.COMMENT_WEBMENTION.ENABLE && (
+
+
)}
)
diff --git a/components/CommonHead.js b/components/CommonHead.js
index 0f4e0555..8104ea80 100644
--- a/components/CommonHead.js
+++ b/components/CommonHead.js
@@ -44,6 +44,16 @@ const CommonHead = ({ meta, children }) => {
+ {BLOG.COMMENT_WEBMENTION.ENABLE && (
+ <>
+
+
+ >
+ )}
+ {BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
+
+ )}
+
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && }
{meta?.type === 'Post' && (
<>
diff --git a/components/WebMention.js b/components/WebMention.js
new file mode 100644
index 00000000..375a0141
--- /dev/null
+++ b/components/WebMention.js
@@ -0,0 +1,173 @@
+import BLOG from '@/blog.config'
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import Image from 'next/image'
+
+/**
+ * 评论插件
+ * @param issueTerm
+ * @param layout
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const WebmentionCount = ({ target }) => {
+ const initialCounts = {
+ count: 0,
+ type: {
+ like: 0,
+ mention: 0,
+ reply: 0,
+ repost: 0
+ }
+ }
+ const [counts, setCounts] = useState(initialCounts)
+ const fetchCounts = async (target) => {
+ const responseData = await fetch(`https://webmention.io/api/count.json?target=${encodeURIComponent(target)}`)
+ return (responseData.json) ? await responseData.json() : responseData
+ }
+ useEffect(() => {
+ async function getCounts() {
+ const responseCounts = await fetchCounts(target)
+ setCounts(responseCounts)
+ }
+ getCounts()
+ }, [target])
+
+ return (
+
+ {counts
+ ? (
+
+
+ {counts.type.like || 0} Likes
+
+
+ {counts.type.reply || 0} Replies
+
+
+
+ {(counts.type.repost || 0) + (counts.type.mention || 0)}
+
+ Mentions
+
+
+ )
+ : (
+
Failed to fetch Webmention counts
+ )
+ }
+
+ )
+}
+
+const Avatar = ({ author }) => (
+
+
+
+)
+
+const WebmentionReplies = ({ target }) => {
+ const [mentions, setMentions] = useState([])
+ const fetchMentions = async (target) =>
+ fetch(
+ `https://webmention.io/api/mentions.jf2?per-page=500&target=${encodeURIComponent(target)}&token=${BLOG.COMMENT_WEBMENTION.TOKEN}`
+ ).then((response) => (response.json ? response.json() : response))
+ useEffect(() => {
+ async function getMentions() {
+ const responseMentions = await fetchMentions(target)
+ if (responseMentions.children) {
+ setMentions(responseMentions.children)
+ }
+ }
+
+ getMentions()
+ }, [target])
+
+ const distinctMentions = [
+ ...new Map(mentions.map((item) => [item.author.url, item])).values()
+ ].sort((a, b) => new Date(a['wm-received']) - new Date(b['wm-received']))
+
+ const replies = mentions.filter(
+ (mention) => 'in-reply-to' in mention && 'content' in mention
+ )
+
+ return (
+
+
+ {distinctMentions.length > 0
+ ? `Already ${distinctMentions.length} people liked, shared or talked about this article:`
+ : 'Be the first one to share this article!'}
+
+
+ {distinctMentions.map((reply) => (
+
+ ))}
+
+ {replies && replies.length
+ ? (
+
+
Replies
+
+ {replies.map((reply) => (
+
+
+
+
{reply.author.name}
+
{reply.content.text}
+
+
+ ))}
+
+
+ )
+ : null}
+
+ )
+}
+
+const WebMentionBlock = ({ frontMatter }) => {
+ const router = useRouter()
+ const url = `https://${BLOG.COMMENT_WEBMENTION.HOSTNAME}${router.asPath}`
+ const tweet = `${frontMatter.title} by @${BLOG.COMMENT_WEBMENTION.TWITTER_USERNAME} ${url}`
+
+ return (
+
+
+
+ You can{' '}
+ {' '}
+ or{' '}
+
+ , the comments will show up here.
+
+
+
+
+
+
+ )
+}
+
+export default WebMentionBlock
diff --git a/next.config.js b/next.config.js
index 65fde6e7..c789c2d0 100644
--- a/next.config.js
+++ b/next.config.js
@@ -13,7 +13,8 @@ module.exports = withBundleAnalyzer({
'avatars.githubusercontent.com',
'images.unsplash.com',
'source.unsplash.com',
- 'p1.qhimg.com'
+ 'p1.qhimg.com',
+ 'webmention.io'
]
},
// 默认将feed重定向至 /public/rss/feed.xml
diff --git a/styles/globals.css b/styles/globals.css
index 7af1cb0e..ac8bc9d4 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -224,6 +224,90 @@ nav {
@apply text-blue-700
}
+/* Webmention style */
+.webmention-block {
+ background: rgba(0, 116, 222, .2);
+ padding: 1rem 2rem;
+ border-radius: 5px;
+}
+
+.webmention-header {
+ font-style: italic;
+ font-weight: 700;
+ font-size: 16px;
+ margin-bottom: .5rem;
+}
+
+.webmention-block-intro a {
+ color: #0000EE;
+ text-decoration: underline;
+}
+
+.webmention {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+.webmention-counts {
+ padding: 16px 0;
+ font-weight: bold;
+}
+
+.webmention-counts .count {
+ font-weight: bold;
+ margin-right: .2rem;
+}
+
+/* .webmention-counts .counts > span {
+ margin-right: .8rem;
+} */
+.webmention-counts .counts>span:not(:last-child):after {
+ content: " • ";
+}
+
+a.avatar-wrapper {
+ display: inline-block;
+ width: 50px;
+ height: 50px;
+ position: relative;
+}
+
+.webmention-avatars .avatar-wrapper {
+ margin-right: -8px;
+}
+
+.avatar {
+ border-radius: 50%;
+ margin: 0;
+ border: 3px solid rgba(0, 116, 222, .5);
+}
+
+.replies {
+ margin: 0;
+ padding: 0;
+}
+
+.reply {
+ list-style: none;
+ display: flex;
+ position: relative;
+ padding: 0;
+ align-items: flex-start;
+ margin-top: .6rem;
+}
+
+.reply p {
+ margin: 0;
+}
+
+.reply .text {
+ margin-left: 1rem;
+ font-size: 14px;
+}
+
+.reply-author-name {
+ font-weight: 500;
/* 最多显示4行文字 */
.p-4-lines {
display: -webkit-box;