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 }) => ( + + {author.name} + +) + +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

+ +
+ ) + : 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 ( +
+

+ powered by WebMention.io +

+
+ You can{' '} + tweet this post{' '} + or{' '} + discuss it on Twitter + , 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;