From 46e7e5339d9ceb0860cdee5c522bd9478434027e Mon Sep 17 00:00:00 2001 From: RiseForever Date: Thu, 27 Aug 2026 21:15:24 +0800 Subject: [PATCH] Apply Webmention Function --- NOTICE.md | 18 +- deploy/webmention/.env.example | 15 + deploy/webmention/Caddyfile.example | 4 + deploy/webmention/README.md | 79 ++++ deploy/webmention/compose.yaml | 46 +++ deploy/webmention/nginx.conf.example | 13 + src/components/SeoHead.astro | 5 + src/components/Webmentions.astro | 518 +++++++++++++++++++++++++++ src/lib/webmentions.ts | 4 + src/pages/posts/[...slug].astro | 2 + src/site.config.ts | 5 + src/types/config.ts | 7 + 12 files changed, 715 insertions(+), 1 deletion(-) create mode 100644 deploy/webmention/.env.example create mode 100644 deploy/webmention/Caddyfile.example create mode 100644 deploy/webmention/README.md create mode 100644 deploy/webmention/compose.yaml create mode 100644 deploy/webmention/nginx.conf.example create mode 100644 src/components/Webmentions.astro create mode 100644 src/lib/webmentions.ts diff --git a/NOTICE.md b/NOTICE.md index 62d4da4..16178ad 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -2,7 +2,7 @@ Mirages 是一个基于 Astro 的静态博客主题,用于发布文章和独立页面。旧版 PHP 文件位于本项目之外,保持不变。 -本文档说明 Astro 主题的常用配置、Twikoo 评论和文章 Frontmatter。 +本文档说明 Astro 主题的常用配置、Twikoo 评论、WebMention 和文章 Frontmatter。 ## 运行项目 @@ -117,6 +117,22 @@ pnpm build 如果页面显示“评论暂时不可用”,依次检查云函数地址是否能从浏览器直接访问、CORS 配置、Twikoo 环境变量和 `envId` 是否包含多余路径或空格。 +## WebMention 配置 + +WebMention 与 Twikoo 并行工作:Twikoo 接收本站评论,WebMention 接收其他网站对文章的回复、提及、点赞和转发。配置入口位于 `src/site.config.ts`: + +```ts +webmentions: { + enabled: true, + endpoint: 'https://webmention.example.com', + form: true +}, +``` + +启用后,全站 `` 会声明 `${endpoint}/receive`,文章页从 `${endpoint}/get` 读取当前 canonical URL 的已批准 WebMention。`form` 控制是否在文章底部显示手动发送表单。 + +仓库在 `deploy/webmention/` 提供了基于 webmentiond、SQLite 和 Docker Compose 的自托管配置。服务器部署、SMTP 登录、反向代理及验证步骤见该目录的 `README.md`。 + ## 文章 Frontmatter 文章位于 `src/content/posts`,独立页面位于 `src/content/pages`。两者使用相同的字段结构: diff --git a/deploy/webmention/.env.example b/deploy/webmention/.env.example new file mode 100644 index 0000000..ddcb89a --- /dev/null +++ b/deploy/webmention/.env.example @@ -0,0 +1,15 @@ +WEBMENTION_PORT=8080 +WEBMENTION_PUBLIC_URL=https://webmention.luming.cool +WEBMENTION_ALLOWED_TARGET_DOMAINS=www.luming.cool,luming.cool +WEBMENTION_ALLOWED_ORIGIN=https://www.luming.cool +WEBMENTION_ADMIN_EMAILS=you@example.com + +SERVER_AUTH_JWT_SECRET=replace-with-a-long-random-secret + +MAIL_HOST=smtp.example.com +MAIL_PORT=587 +MAIL_USER=you@example.com +MAIL_PASSWORD=replace-with-your-smtp-password +MAIL_FROM=you@example.com +MAIL_USE_STARTTLS=true +MAIL_NO_TLS=false diff --git a/deploy/webmention/Caddyfile.example b/deploy/webmention/Caddyfile.example new file mode 100644 index 0000000..c7fd02c --- /dev/null +++ b/deploy/webmention/Caddyfile.example @@ -0,0 +1,4 @@ +webmention.luming.cool { + encode zstd gzip + reverse_proxy 127.0.0.1:8080 +} diff --git a/deploy/webmention/README.md b/deploy/webmention/README.md new file mode 100644 index 0000000..dee42ea --- /dev/null +++ b/deploy/webmention/README.md @@ -0,0 +1,79 @@ +# WebMention 接收端 + +本站使用 [webmentiond](https://github.com/zerok/webmentiond) 作为自托管接收端。它负责异步验证来源页面、将结果存入 SQLite,并提供审核后台与公开查询 API。Twikoo 继续作为站内评论系统,两者互不替代。 + +## 1. 准备域名 + +将 `webmention.luming.cool` 解析到运行 Docker 的服务器。如果要换成其他域名,同时修改: + +- `src/site.config.ts` 中的 `webmentions.endpoint` +- `.env` 中的 `WEBMENTION_PUBLIC_URL` +- Caddy 或 Nginx 的域名 + +接收端必须使用公网可访问的 HTTPS 地址。 + +## 2. 配置并启动 + +在服务器上进入本目录,复制示例环境变量并填写真实值: + +```sh +cp .env.example .env +docker compose pull +docker compose up -d +``` + +Compose 会先用一次性 `webmention-init` 容器将数据卷所有者设为 webmentiond 使用的 UID 1500,然后再启动接收端。若曾使用旧版 Compose,并在日志中看到 `unable to open database file`,可在部署目录执行: + +```sh +docker compose stop webmentiond +docker compose run --rm --no-deps --user 0 --entrypoint /bin/sh webmentiond -c 'chown -R 1500:1500 /data' +docker compose up -d webmentiond +``` + +必须修改以下值: + +- `WEBMENTION_ADMIN_EMAILS`:允许登录审核后台的邮箱,可用逗号分隔多个地址。 +- `SERVER_AUTH_JWT_SECRET`:长随机字符串,可用 `openssl rand -hex 32` 生成。 +- `MAIL_*`:SMTP 地址、账号、密码和发件人。后台使用邮件链接登录,因此 SMTP 不能省略。 + +SQLite 数据保存在 Docker 卷 `webmention-data`。更新容器不会删除它,仍建议定期备份该卷。 + +## 3. 配置反向代理 + +任选 `Caddyfile.example` 或 `nginx.conf.example` 作为反向代理参考。容器只监听服务器本机的 `127.0.0.1:8080`,TLS 由反向代理处理。 + +部署完成后访问: + +```text +https://webmention.luming.cool/ui/ +``` + +输入 `WEBMENTION_ADMIN_EMAILS` 中的邮箱,通过邮件链接登录。收到的 WebMention 验证成功后仍需在这里批准,批准后才会出现在博客文章底部。 + +## 4. 接口与站点集成 + +博客构建结果会在 `` 中自动声明: + +```html + +``` + +文章页通过以下接口读取当前 canonical URL 已批准的项目: + +```text +GET https://webmention.luming.cool/get?target=https://www.luming.cool/posts/example/ +``` + +文章底部也提供发送表单,POST `source` 和 `target` 到 `/receive`。`WEBMENTION_ALLOWED_ORIGIN` 必须与博客的浏览器 Origin 完全一致,否则读取与发送都会被 CORS 拦截。 + +## 5. 验证 + +完成接收端和博客部署后: + +1. 查看任一文章源码,确认存在 `rel="webmention"`。 +2. 打开 `https://webmention.luming.cool/ui/`,确认可以收到登录邮件。 +3. 使用 [webmention.rocks](https://webmention.rocks/) 检查接收端标准兼容性。 +4. 从一个确实包含本站文章链接的公开页面发送 WebMention。 +5. 在管理后台批准后,刷新对应文章,确认它出现在“站外回应”。 + +`webmentiond` 按完整 target URL 精确查询。发送方若将带 `#fragment` 的链接作为 target,它会被视为与文章 canonical URL 不同的目标;建议发送时使用不带锚点的文章永久链接。 diff --git a/deploy/webmention/compose.yaml b/deploy/webmention/compose.yaml new file mode 100644 index 0000000..92a8566 --- /dev/null +++ b/deploy/webmention/compose.yaml @@ -0,0 +1,46 @@ +services: + webmention-init: + image: zerok/webmentiond:latest + user: "0:0" + entrypoint: + - /bin/sh + - -c + command: + - chown -R 1500:1500 /data + volumes: + - webmention-data:/data + restart: "no" + + webmentiond: + image: zerok/webmentiond:latest + restart: unless-stopped + depends_on: + webmention-init: + condition: service_completed_successfully + ports: + - "127.0.0.1:${WEBMENTION_PORT:-8080}:8080" + environment: + MAIL_HOST: ${MAIL_HOST} + MAIL_PORT: ${MAIL_PORT} + MAIL_USER: ${MAIL_USER} + MAIL_PASSWORD: ${MAIL_PASSWORD} + MAIL_FROM: ${MAIL_FROM} + MAIL_USE_STARTTLS: ${MAIL_USE_STARTTLS:-true} + MAIL_NO_TLS: ${MAIL_NO_TLS:-false} + SERVER_AUTH_JWT_SECRET: ${SERVER_AUTH_JWT_SECRET} + volumes: + - webmention-data:/data + command: + - --addr + - 0.0.0.0:8080 + - --public-url + - ${WEBMENTION_PUBLIC_URL} + - --allowed-target-domains + - ${WEBMENTION_ALLOWED_TARGET_DOMAINS} + - --allowed-origin + - ${WEBMENTION_ALLOWED_ORIGIN} + - --auth-admin-emails + - ${WEBMENTION_ADMIN_EMAILS} + +volumes: + webmention-data: diff --git a/deploy/webmention/nginx.conf.example b/deploy/webmention/nginx.conf.example new file mode 100644 index 0000000..9491c3c --- /dev/null +++ b/deploy/webmention/nginx.conf.example @@ -0,0 +1,13 @@ +server { + listen 443 ssl http2; + server_name webmention.luming.cool; + + # Keep the existing certificate directives from your server configuration. + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/src/components/SeoHead.astro b/src/components/SeoHead.astro index bbba082..af33eb9 100644 --- a/src/components/SeoHead.astro +++ b/src/components/SeoHead.astro @@ -1,5 +1,6 @@ --- import { siteConfig } from '@/site.config'; +import { getWebmentionEndpoint } from '@/lib/webmentions'; interface Props { title?: string; @@ -19,11 +20,15 @@ const { const pageTitle = title ? `${title} - ${siteConfig.site.title}` : siteConfig.site.title; const canonical = new URL(Astro.url.pathname, siteConfig.site.url); const imageUrl = image ? new URL(image, siteConfig.site.url) : undefined; +const webmentionEndpoint = siteConfig.webmentions.enabled && siteConfig.webmentions.endpoint.trim() + ? getWebmentionEndpoint(siteConfig.webmentions.endpoint, 'receive') + : undefined; --- {pageTitle} +{webmentionEndpoint && } diff --git a/src/components/Webmentions.astro b/src/components/Webmentions.astro new file mode 100644 index 0000000..a4ee8a8 --- /dev/null +++ b/src/components/Webmentions.astro @@ -0,0 +1,518 @@ +--- +import { Send } from '@lucide/astro'; +import { getWebmentionEndpoint } from '@/lib/webmentions'; +import { siteConfig } from '@/site.config'; + +interface Props { + target?: string; +} + +const { target = new URL(Astro.url.pathname, siteConfig.site.url).href } = Astro.props; +const config = siteConfig.webmentions; +const endpoint = config.endpoint.trim(); +const receiveEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'receive') : ''; +const getEndpoint = endpoint ? getWebmentionEndpoint(endpoint, 'get') : ''; +--- + +{config.enabled && endpoint &&
+

WebMention

+ +

正在读取站外回应...

+ + + + + + {config.form &&
+ +
+ + + +
+

+
} +
} + + + + diff --git a/src/lib/webmentions.ts b/src/lib/webmentions.ts new file mode 100644 index 0000000..f5d67fb --- /dev/null +++ b/src/lib/webmentions.ts @@ -0,0 +1,4 @@ +export function getWebmentionEndpoint(base: string, route: 'get' | 'receive'): string { + const endpoint = new URL(base.endsWith('/') ? base : `${base}/`); + return new URL(route, endpoint).href; +} diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index d7114ae..07cb800 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -8,6 +8,7 @@ import TwikooComments from '@/components/TwikooComments.astro'; import TableOfContents from '@/components/TableOfContents.astro'; import Mermaid from '@/components/Mermaid.astro'; import ImageLightbox from '@/components/ImageLightbox.astro'; +import Webmentions from '@/components/Webmentions.astro'; export async function getStaticPaths() { const posts = await getPublicPosts(); return posts.map((post, index) => ({ @@ -48,6 +49,7 @@ const dateText = `${date.year} 年 ${date.month} 月 ${date.day} 日`; {older ? 上一篇{older.data.title} : 上一篇没有更多了} {newer ? 下一篇{newer.data.title} : 下一篇没有更多了} + diff --git a/src/site.config.ts b/src/site.config.ts index 4b4625d..8963357 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -62,6 +62,11 @@ export const siteConfig: SiteConfig = { region: 'cn', lang: 'zh-CN' }, + webmentions: { + enabled: true, + endpoint: 'https://webmention.luming.cool', + form: true + }, search: { provider: 'pagefind', placeholder: '搜索文章' }, toolbarItems: [ { type: 'search', icon: 'search', name: '搜索文章' }, diff --git a/src/types/config.ts b/src/types/config.ts index 9d767c7..3e26160 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -15,6 +15,12 @@ type SearchConfig = | { provider: 'none' } | { provider: 'pagefind'; placeholder: string }; +export interface WebmentionConfig { + enabled: boolean; + endpoint: string; + form?: boolean; +} + export type ToolbarIcon = 'search' | 'rss' | 'settings' | 'link' | 'tram-front'; export type ToolbarItem = @@ -66,6 +72,7 @@ export interface SiteConfig { links: LinkItem[]; }; comments: CommentsConfig; + webmentions: WebmentionConfig; search: SearchConfig; pjax: { enabled: boolean;