为博客添加 Mastodon 嘟文页面


预计 8 min read


为博客添加 Mastodon 嘟文页面


问题背景

Mastodon 是一个开源的去中心化社交网络,我在 m.cmx.im 实例上发布了一些内容。想把这些嘟文展示在博客里,但遇到了两个问题:

  1. 国内网络限制:Mastodon API 在国内无法直接访问
  2. 技术实现:如何在 Astro 项目中集成 Mastodon 数据

解决方案概览

最终方案:Vercel Edge Function 代理 + Astro React 组件

  • 生产环境:用户访问 → Vercel 代理函数(海外节点) → Mastodon API → 返回数据
  • 开发环境:直连 Mastodon API(需本地 VPN)

整个实现分为三个部分:

部分文件作用
API 代理api/mastodon.tsVercel Serverless Function,转发 Mastodon API
React 组件src/components/MastodonFeed.tsx客户端组件,加载并渲染嘟文
Astro 页面src/pages/mastodon.astro页面容器,复用博客 Layout

实现步骤

1. 创建 Vercel API 代理

在项目根目录创建 api/mastodon.ts

1
import type { VercelRequest, VercelResponse } from '@vercel/node';
2
3
export default async function handler(req: VercelRequest, res: VercelResponse) {
4
// CORS headers
5
res.setHeader('Access-Control-Allow-Origin', '*');
6
res.setHeader('Cache-Control', 's-maxage=300, stale-while-revalidate=600'); // 5分钟缓存
7
8
if (req.method === 'OPTIONS') {
9
return res.status(200).end();
10
}
11
12
const API_URL = 'https://m.cmx.im/api/v1/accounts/116669312102420954/statuses?exclude_replies=true&exclude_reblogs=true&limit=20';
13
14
try {
15
const response = await fetch(API_URL);
16
if (!response.ok) throw new Error(`HTTP ${response.status}`);
17
const data = await response.json();
18
return res.status(200).json(data);
19
} catch (error) {
20
return res.status(500).json({ error: 'Failed to fetch Mastodon data' });
21
}
22
}

关键点

  • CORS 设置允许跨域访问
  • 缓存策略:5分钟缓存,6分钟 stale-while-revalidate(减少 API 调用)
  • 账号 ID 硬编码(个人博客固定账号,省一次 lookup)

2. React 客户端组件

src/components/MastodonFeed.tsx 的核心逻辑:

API URL 自动切换

1
const API_URL =
2
typeof window !== "undefined" && window.location.hostname !== "localhost"
3
? "/api/mastodon" // 生产:走 Vercel 代理
4
: "https://m.cmx.im/api/v1/accounts/..."; // 开发:直连 Mastodon

时间轴式布局

采用左侧时间线 + 右侧内容卡片的形式,更符合”日志”的阅读习惯:

1
<div className="flex gap-5 sm:gap-6">
2
{/* timeline line */}
3
<div className="relative flex shrink-0 flex-col items-center">
4
<div className="h-2 w-2 rounded-full bg-[var(--hc)]/60 ring-4 ring-[hsl(var(--primary)/0.08)]" />
5
<div className="mt-3 w-px flex-1 bg-[var(--current-line)]" />
6
</div>
7
8
<div className="flex-1 pb-10">
9
{/* 时间链接 */}
10
<a className="mb-2 inline-block font-mono text-xs text-[var(--gray)]">
11
{formatTime(status.created_at)}
12
</a>
13
14
{/* 内容卡片 */}
15
<div className="rounded-2xl border border-[hsl(var(--primary)/0.1)] bg-[hsl(var(--card)/0.4)] p-4">
16
<div className="prose-masto text-[15px]" dangerouslySetInnerHTML={{ __html: status.content }} />
17
{/* 媒体附件 + 互动数据 */}
18
</div>
19
</div>
20
</div>

媒体附件处理

支持图片、视频、音频、GIFV 四种类型:

  • 图片:缩略图网格(1 张大图、2 张并排、3+ 张 2×2)
  • 视频/音频:原生 <video>/<audio> 元素
  • 敏感内容:需点击揭示

入场动画

使用 Framer Motion 的 stagger 淡入效果,并尊重 prefers-reduced-motion

1
<motion.article
2
initial={shouldReduceMotion ? false : { opacity: 0, y: 18 }}
3
animate={{ opacity: 1, y: 0 }}
4
transition={{
5
duration: 0.5,
6
delay: shouldReduceMotion ? 0 : index * 0.07,
7
ease: [0.16, 1, 0.3, 1],
8
}}
9
>

3. Astro 页面容器

src/pages/mastodon.astro 复用博客的 Layout,保持风格一致:

1
---
2
import Layout from "../layouts/Layout.astro";
3
import MastodonFeed from "../components/MastodonFeed";
4
---
5
6
<Layout title="Mastodon" description="SanXiaoXing 的长毛象嘟文">
7
<header class="masto-header">
8
<div class="header-icon">
9
<!-- Mastodon Logo SVG -->
10
</div>
11
<h1 class="masto-title">Mastodon 嘟文</h1>
12
<p class="masto-subtitle">来自 @SanXiaoXing@m.cmx.im 的原创发布</p>
13
</header>
14
15
<section class="masto-feed">
16
<MastodonFeed client:load />
17
</section>
18
19
<footer class="masto-footer">
20
<a href="/" class="back-link">返回首页</a>
21
</footer>
22
</Layout>
23
24
<style>
25
/* prose-masto: Mastodon HTML 样式 */
26
:global(.prose-masto p) {
27
margin: 0.6em 0;
28
line-height: 1.8;
29
}
30
:global(.prose-masto a) {
31
color: var(--hc);
32
border-bottom: 1px solid hsl(var(--primary) / 0.2);
33
}
34
</style>

4. 添加导航入口

Header.astroMobileHeader.astro 添加导航链接:

1
const mastodonSlug = "mastodon";
2
3
<!-- 桌面端 -->
4
<a href={`/${mastodonSlug}`} class={currentPath === `/${mastodonSlug}` ? "active" : ""}>
5
嘟文
6
</a>
7
8
<!-- 移动端 -->
9
<a href={`/${mastodonSlug}`} class="...">
10
嘟文
11
</a>

同时在首页 index.astro 的网格卡片中添加 Mastodon 入口。


部署说明

部署完成后:

  • 生产环境:国内用户访问 https://你的域名/mastodon 无需 VPN
  • 开发环境:本地 localhost:4321/mastodon 需要开启 VPN

技术细节补充

Mastodon API 参数

1
https://m.cmx.im/api/v1/accounts/{account_id}/statuses
2
?exclude_replies=true // 排除回复
3
&exclude_reblogs=true // 排除转发
4
&limit=20 // 最多 20 条

CSS 变量复用

整个页面复用博客的 CSS 变量,确保风格一致:

变量用途
--hc高亮色(链接、时间轴节点)
--gray辅助文字(时间、统计数字)
--fontc正文颜色
--card卡片背景
--current-line分割线、骨架屏
--primary主色调(hover 边框)

性能优化

  • API 缓存:Vercel Edge Function 5分钟缓存
  • 图片懒加载loading="lazy"
  • 骨架屏:加载时显示 3 个骨架卡片
  • 动画降级prefers-reduced-motion 时禁用入场动画

最终效果

访问 /mastodon 页面可以看到:

  • 左侧时间线连接各条嘟文
  • 每条嘟文显示发布时间、正文、媒体附件、互动数据
  • 点击时间链接跳转到 Mastodon 原文
  • 整体风格与博客一致,适配深浅主题

总结

通过 Vercel Edge Function 代理 Mastodon API,成功解决了国内访问限制问题。整个实现:

  • 性能友好:缓存 + 懒加载 + 骨架屏
  • 用户体验好:国内无需 VPN,加载流畅

觉得这篇文章怎么样?给个反应吧!