为博客添加 Mastodon 嘟文页面
预计 8 min read
为博客添加 Mastodon 嘟文页面
问题背景
Mastodon 是一个开源的去中心化社交网络,我在 m.cmx.im 实例上发布了一些内容。想把这些嘟文展示在博客里,但遇到了两个问题:
- 国内网络限制:Mastodon API 在国内无法直接访问
- 技术实现:如何在 Astro 项目中集成 Mastodon 数据
解决方案概览
最终方案:Vercel Edge Function 代理 + Astro React 组件
- 生产环境:用户访问 → Vercel 代理函数(海外节点) → Mastodon API → 返回数据
- 开发环境:直连 Mastodon API(需本地 VPN)
整个实现分为三个部分:
| 部分 | 文件 | 作用 |
|---|---|---|
| API 代理 | api/mastodon.ts | Vercel Serverless Function,转发 Mastodon API |
| React 组件 | src/components/MastodonFeed.tsx | 客户端组件,加载并渲染嘟文 |
| Astro 页面 | src/pages/mastodon.astro | 页面容器,复用博客 Layout |
实现步骤
1. 创建 Vercel API 代理
在项目根目录创建 api/mastodon.ts:
1import type { VercelRequest, VercelResponse } from '@vercel/node';2
3export default async function handler(req: VercelRequest, res: VercelResponse) {4 // CORS headers5 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 自动切换
1const 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.article2 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---2import Layout from "../layouts/Layout.astro";3import 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.astro 和 MobileHeader.astro 添加导航链接:
1const 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 参数
1https://m.cmx.im/api/v1/accounts/{account_id}/statuses2 ?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,加载流畅
觉得这篇文章怎么样?给个反应吧!