Asia/Shanghai
March 5, 2026

Adding Bilingual RSS Feeds + Folo Discovery to a Next.js Blog

给 Next.js 博客加上双语 RSS 和 Folo 订阅

Mingjian Shao
Adding Bilingual RSS Feeds + Folo Discovery to a Next.js Blog
RSS isn't dead — it just moved to better readers. Here's how I added bilingual feeds to a Next.js blog and made them discoverable on Folo.
I keep coming back to RSS for one reason: I choose what I read. No algorithm, no engagement optimization, no "you might also like." Just chronological updates from sources I picked.Folo (formerly Follow) is the RSS reader that made me care about feeds again. It feels modern — not like a Google Reader relic — and it has a discovery system where feeds can be claimed and verified by their authors.So when I rebuilt my portfolio on Next.js, adding proper RSS was non-negotiable. And since my blog posts are bilingual (English + Chinese), I wanted two feeds, not one.My blog uses MDX with <En> and <Cn> wrapper components for bilingual content. Each post has both languages inline:
Mdx
---
title: "English Title"
titleCn: "中文标题"
---

<En>
English content here...
</En>

<Cn>
中文内容...
</Cn>
For RSS, I needed to:
  • Extract only the relevant language block from each post
  • Convert raw MDX to clean HTML (not markdown — RSS readers expect HTML)
  • Serve two separate feeds at /rss.xml and /rss-cn.xml
  • Make them discoverable via autodiscovery, UI entry points, and Folo
My first attempt was a classic API route at /api/rss. It worked locally but returned 404 on Vercel.The problem: getPosts() uses fs.readFileSync to read MDX files. In a serverless function, those source files aren't bundled — they only exist at build time.The fix: make the route statically generated:
Typescript
// src/app/rss.xml/route.ts
export const dynamic = "force-static";
This tells Next.js to run the route handler at build time and serve the output as a static file. Same freshness as the blog itself — both update on deploy.RSS readers expect <content:encoded> with HTML, not raw markdown. I built a mdToHtml() function that:
  • Extracts the <En> or <Cn> block based on a lang parameter
  • Strips MDX imports and custom React components
  • Converts markdown to HTML: headings, bold, italic, code blocks, links, lists, blockquotes
  • Converts markdown tables to proper <table><tr><td> HTML
  • Converts ![alt](url) to <img> tags
  • Escapes XML entities
The key insight: I didn't need a full markdown parser. A focused set of regex transformations handles 95% of my content. The remaining 5% (mermaid diagrams) renders as <pre><code> blocks — acceptable for RSS.Both feeds share a single generateRssFeed() function:
Typescript
// src/utils/rss.ts
export function generateRssFeed({
  lang,
  feedPath,
}: {
  lang: "en" | "cn";
  feedPath: string;
}): string {
  const posts = getPosts(["src", "app", "blog", "posts"]);
  // ... generate RSS XML with language-specific content
}
The EN feed uses metadata.title and extracts <En> blocks. The CN feed uses metadata.titleCn, extracts <Cn> blocks, and sets <language>zh-cn</language>.Each route file is just a thin wrapper:
Typescript
// src/app/rss.xml/route.ts
import { generateRssFeed } from "@/utils/rss";
export const dynamic = "force-static";

export async function GET() {
  const feed = generateRssFeed({ lang: "en", feedPath: "/rss.xml" });
  return new Response(feed, {
    headers: { "Content-Type": "application/xml; charset=utf-8" },
  });
}
Users shouldn't have to guess the feed URL. I added three discovery mechanisms:1. Autodiscovery <link> tags — RSS readers auto-detect these:
Html
<link rel="alternate" type="application/rss+xml"
  href="/rss.xml" title="Blog (English)" />
<link rel="alternate" type="application/rss+xml"
  href="/rss-cn.xml" title="Blog (中文)" />
2. Blog page pills — Two visible buttons next to the blog title: "RSS" and "中文 RSS"3. Per-post CTA — At the bottom of every post: "Enjoy this post? Subscribe via RSS: English | 中文"Folo lets feed authors claim their feeds for verification. Add this to your RSS channel:
Xml
<follow_challenge>
  <feedId>YOUR_FEED_ID</feedId>
  <userId>YOUR_USER_ID</userId>
</follow_challenge>
You get these IDs from Folo's feed claim page after adding your feed URL. Once the claim is verified, your feed shows as "claimed" in Folo's directory — a small trust signal for potential subscribers.Two bilingual RSS feeds serving full HTML articles:Both render with proper formatting in Folo and other RSS readers — tables, code blocks, images, the works. Not just a summary teaser, but the complete post.If your blog is bilingual, separate feeds per language is the way to go. Readers subscribe to what they can actually read.
RSS 没死——只是换了更好的阅读器。这篇讲讲怎么给 Next.js 博客加双语 RSS,并在 Folo 上被发现。
一个原因:我自己选读什么。没有算法推荐,没有"猜你喜欢",就是按时间排序看我关注的来源。Folo(原名 Follow)是让我重新关注 RSS 的阅读器。UI 很现代,不像 Google Reader 遗老,而且有个 feed 认领系统,作者可以验证自己的订阅源。所以用 Next.js 重建个人网站时,加 RSS 是必须的。而且我的博客是双语的(英文 + 中文),所以需要两个 feed,不是一个。我的博客用 MDX,通过 <En><Cn> 组件来区分双语内容。每篇文章两种语言都在同一个文件里:
Mdx
---
title: "English Title"
titleCn: "中文标题"
---

<En>
English content here...
</En>

<Cn>
中文内容...
</Cn>
RSS 需要做的事:
  • 从每篇文章中提取对应语言的内容块
  • 把 MDX 转成干净的 HTML(RSS 阅读器要 HTML,不是 markdown)
  • 分别提供 /rss.xml(英文)和 /rss-cn.xml(中文)
  • 让用户能发现这些 feed——自动发现、UI 入口、Folo 认领
第一版用的 API route /api/rss,本地没问题,Vercel 上 404。原因:getPosts()fs.readFileSync 读 MDX 文件,但 serverless function 不会打包源文件——它们只在构建时存在。解决:强制静态生成:
Typescript
// src/app/rss.xml/route.ts
export const dynamic = "force-static";
这样 Next.js 在构建时执行 route handler,输出静态文件。和博客本身一样的新鲜度——都在部署时更新。RSS 阅读器需要 <content:encoded> 里是 HTML。我写了个 mdToHtml() 函数:
  • 根据 lang 参数提取 <En><Cn> 内容块
  • 去掉 MDX import 和自定义 React 组件
  • 把 markdown 转成 HTML:标题、加粗、斜体、代码块、链接、列表、引用
  • 把 markdown 表格转成 <table><tr><td> HTML
  • ![alt](url) 转成 <img> 标签
  • 转义 XML 实体
关键发现:不需要完整的 markdown 解析器。一组有针对性的正则表达式能处理 95% 的内容。剩下的 5%(mermaid 图)渲染成 <pre><code> 代码块——RSS 里可以接受。两个 feed 共用一个 generateRssFeed() 函数:
Typescript
// src/utils/rss.ts
export function generateRssFeed({
  lang,
  feedPath,
}: {
  lang: "en" | "cn";
  feedPath: string;
}): string {
  const posts = getPosts(["src", "app", "blog", "posts"]);
  // ... 根据语言生成 RSS XML
}
英文 feed 用 metadata.title,提取 <En> 块。中文 feed 用 metadata.titleCn,提取 <Cn> 块,设置 <language>zh-cn</language>每个 route 文件就是个薄封装:
Typescript
// src/app/rss.xml/route.ts
import { generateRssFeed } from "@/utils/rss";
export const dynamic = "force-static";

export async function GET() {
  const feed = generateRssFeed({ lang: "en", feedPath: "/rss.xml" });
  return new Response(feed, {
    headers: { "Content-Type": "application/xml; charset=utf-8" },
  });
}
用户不该去猜 feed URL。我加了三种发现方式:1. 自动发现 <link> 标签 — RSS 阅读器会自动检测:
Html
<link rel="alternate" type="application/rss+xml"
  href="/rss.xml" title="Blog (English)" />
<link rel="alternate" type="application/rss+xml"
  href="/rss-cn.xml" title="Blog (中文)" />
2. 博客页面按钮 — 标题旁边两个按钮:"RSS" 和 "中文 RSS"3. 文章底部 CTA — 每篇文章下方:"Enjoy this post? Subscribe via RSS: English | 中文"Folo 允许作者认领自己的 feed 做验证。在 RSS channel 里加上:
Xml
<follow_challenge>
  <feedId>你的FEED_ID</feedId>
  <userId>你的USER_ID</userId>
</follow_challenge>
这些 ID 从 Folo 的 feed 认领页面获取。认领通过后,你的 feed 在 Folo 目录里显示为"已认领"——对潜在订阅者来说是个信任信号。两个双语 RSS feed,提供完整的 HTML 文章:在 Folo 和其他 RSS 阅读器里都能正确渲染——表格、代码块、图片,全都有。不是只有摘要,是完整文章。如果你的博客也是双语的,按语言分开 feed 是正确做法。读者订阅他们能读的语言就好。
Share this post:
Enjoy this post? Subscribe via RSS: English | 中文