· astro / rss / tutorial

Cách thêm RSS feed vào trang Astro bằng @astrojs/rss

Thêm RSS feed vào trang Astro trong 20 phút với @astrojs/rss — bao gồm content collections, rssSchema, auto-discovery, nội dung đầy đủ và kiểm tra W3C.

Bởi

1.749 từ · 9 phút đọc

RSS không phải là công nghệ lỗi thời. Substack đã vượt mốc 5 triệu lượt đăng ký có trả phí — hệ sinh thái newsletter vẫn vận hành trên nền tảng đăng ký mà RSS đã góp phần xây dựng. Những người dùng Feedly, NetNewsWire, Feedbin hay Inoreader đang chủ động từ chối các feed thuật toán. Một RSS feed trên trang Astro của bạn cho phép họ đăng ký theo dõi trực tiếp mà không cần qua bên trung gian nào.

Thêm feed mất khoảng 20 phút. Package @astrojs/rss xử lý việc tạo RSS 2.0 XML. Bạn chỉ cần viết một file endpoint là xong.

Đây là dành cho ai

Dành cho các developer đang dùng Astro 5 với content collections và muốn có endpoint /rss.xml. Code hoạt động tốt với cả Astro 4, nhưng sự khác biệt giữa post.idpost.slug chỉ áp dụng cho content layer của Astro 5. Nếu bạn đang dùng Astro 4, hãy dùng post.slug.

Cài đặt

Thêm package chính thức:

pnpm add @astrojs/rss

Phiên bản ổn định hiện tại là v4.0.18, yêu cầu Node.js v22.12.0 trở lên. Các phiên bản cũ dùng pagesGlobToRssItems() mà không có content collections API vẫn hoạt động, nhưng cách dùng content collections cho bạn khả năng kiểm tra frontmatter lúc compile — đây là lựa chọn tốt hơn.

Cấu hình: đặt site trong astro.config.mjs

@astrojs/rss đọc URL trang của bạn từ astro.config.mjs. Đây là bắt buộc. Nếu thiếu, package không thể tạo URL tuyệt đối cho các item trong feed và hầu hết feed reader sẽ từ chối feed đó.

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://yourdomain.com',
});

Bên trong endpoint, truy cập giá trị này qua context.site. Đừng hard-code domain trực tiếp vào lời gọi rss() và đừng dùng import.meta.env.SITE — biến đó chỉ hoạt động ở phía client. Trong một server-side route handler, nó sẽ là undefined. context.site luôn là lựa chọn đúng.

Tạo RSS feed endpoint

Tạo file src/pages/rss.xml.js. Tên file ánh xạ trực tiếp thành URL: file này sẽ trở thành /rss.xml.

// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const blog = await getCollection('blog');
  const posts = blog.sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  );
  return rss({
    title: 'Your Site — Tagline',
    description: 'A short description of your site.',
    site: context.site,
    customData: `<language>en-us</language>`,
    items: posts.map((post) => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      description: post.data.description,
      link: `/blog/${post.id}/`,
    })),
  });
}

Ba điểm cần chú ý trong Astro 5:

Dùng post.id, không phải post.slug. Content layer API trong Astro 5 đã bỏ .slug. Nếu bạn đang chuyển code từ Astro 4, đây là điểm dễ gặp lỗi nhất. post.id trả về cùng giá trị — tên file không có phần mở rộng — nên URL của bạn trông giống hệt nhau.

Sắp xếp trước khi map. Content loader của Astro 5 không đảm bảo thứ tự. Bỏ qua bước sắp xếp và các item trong feed sẽ xuất hiện theo thứ tự không thể đoán trước. Feed reader thường hiển thị các item theo thứ tự chúng xuất hiện trong XML, vì vậy hãy sắp xếp giảm dần theo pubDate trước khi truyền vào rss().

customData để khai báo ngôn ngữ. Trường này nhận chuỗi XML thô. Dùng nó để đặt phần tử <language> theo đặc tả RSS 2.0. Với trang đa ngôn ngữ, feed tiếng Anh dùng en-us và feed tiếng Việt dùng vi. Các feed aggregator dùng thông tin này để định tuyến và phân loại theo ngôn ngữ. Nếu bạn đang xây dựng trang Astro đa ngôn ngữ, xem Cách xây dựng một site Astro đa ngôn ngữ (EN + VI) để biết cách thiết lập i18n routing đầy đủ.

Đảm bảo cấu trúc frontmatter với rssSchema

Package @astrojs/rss đi kèm một Zod schema để kiểm tra frontmatter của content collection lúc build. Việc cài đặt thêm phần này rất đáng làm.

// src/content/config.ts
import { defineCollection } from 'astro:content';
import { rssSchema } from '@astrojs/rss';

const blog = defineCollection({
  schema: rssSchema,
});

export const collections = { blog };

rssSchema đảm bảo mỗi bài viết đều có title, pubDatedescription. Nếu không có nó, các trường bị thiếu sẽ tạo ra những item bị lỗi trong feed — và bạn chỉ phát hiện ra vấn đề khi xem trong feed reader, không phải lúc build. Helper pagesGlobToRssItems() cũ không cung cấp sự đảm bảo này: docs của nó nói rõ rằng helper này giả định các thuộc tính cần thiết đã có mặt nhưng không kiểm tra thực tế.

Thêm auto-discovery

Feed reader và trình duyệt phát hiện RSS qua thẻ <link> trong <head> của trang. Nếu thiếu thẻ này, người đăng ký phải biết URL feed của bạn từ trước.

<!-- src/layouts/BaseLayout.astro -->
<link
  rel="alternate"
  type="application/rss+xml"
  title="Your Site RSS Feed"
  href={new URL("rss.xml", Astro.site)}
/>

Astro.site lấy giá trị từ astro.config.mjs. Lời gọi new URL() tạo ra URL tuyệt đối — bắt buộc theo đặc tả RSS. Thêm đoạn này vào <head> của base layout và mọi trang trên site của bạn sẽ tự động thông báo về feed.

Khi thẻ <link> có mặt, thanh URL của các trình duyệt như Safari sẽ hiển thị chỉ báo feed. Thực tế hơn, các feed reader có khả năng tự phát hiện feed từ URL trang sẽ tìm thấy nó mà người dùng không cần phải tự tìm URL feed theo cách thủ công.

Kiểm tra trên máy local

Khởi động dev server và mở http://localhost:4321/rss.xml.

Bạn sẽ thấy raw XML. Nếu thấy một trang được render thay vào đó, file chưa được xử lý như một endpoint. Kiểm tra hai điều: file export một hàm GET (không phải default export), và tên file kết thúc bằng .js chứ không phải .astro.

Các lỗi thường gặp ban đầu:

  • Trang trắng hoặc 404: site bị thiếu trong astro.config.mjs.
  • URL tương đối trong các trường link: bạn đã hard-code một đường dẫn mà không có context.site. Feed reader yêu cầu URL tuyệt đối.
  • Tất cả bài viết cùng ngày hoặc sai thứ tự: thiếu bước sắp xếp hoặc đang dùng pubDate như chuỗi thay vì gọi .valueOf().

Kiểm tra với W3C

Sau khi deploy, hãy chạy URL feed của bạn qua W3C Feed Validation Service. Dịch vụ này kiểm tra sự tuân thủ theo RSS 2.0 và báo cáo các vấn đề cụ thể.

Các lỗi thường được phát hiện:

  • Thiếu <link> trong phần tử channel
  • Định dạng ngày không hợp lệ — RSS 2.0 yêu cầu RFC 822 (Mon, 25 May 2026 00:00:00 GMT), không phải ISO 8601
  • Các item không có cả <title> lẫn <description>

Để kiểm tra trên feed reader thực tế, dán URL vào NetNewsWire (miễn phí, macOS và iOS) qua File → New Feed. NetNewsWire hiển thị các item trong feed với ảnh và mô tả được định dạng. Nếu các item trông đúng ở đó, chúng sẽ hoạt động trên mọi feed reader phổ biến.

Thêm nội dung bài viết đầy đủ (tùy chọn)

Feed mặc định bao gồm tiêu đề, mô tả và link. Nếu bạn muốn nội dung bài viết đầy đủ trong feed — để người dùng có thể đọc mà không cần rời khỏi app feed reader — hãy cài thêm hai package:

pnpm add markdown-it sanitize-html
pnpm add -D @types/sanitize-html

Sau đó mở rộng endpoint để render và làm sạch nội dung của mỗi bài viết:

// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it';

const parser = new MarkdownIt();

export async function GET(context) {
  const blog = await getCollection('blog');
  const posts = blog.sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  );
  return rss({
    title: 'Your Site — Tagline',
    description: 'A short description of your site.',
    site: context.site,
    customData: `<language>en-us</language>`,
    items: posts.map((post) => ({
      link: `/blog/${post.id}/`,
      content: sanitizeHtml(parser.render(post.body), {
        allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
      }),
      ...post.data,
    })),
  });
}

sanitizeHtml loại bỏ mọi HTML có khả năng gây hại trước khi đưa vào feed. Dòng concat(['img']) thêm thẻ image trở lại vào danh sách cho phép — mặc định khá thận trọng và loại trừ chúng.

Một giới hạn quan trọng: markdown-it chỉ xử lý Markdown thuần. Nó không xử lý Astro component hay JSX bên trong file .mdx. Nếu các bài viết của bạn là file .md thuần, cách này hoạt động tốt. Với các bài viết .mdx có component, bạn cần một pipeline render phức tạp hơn dùng hệ sinh thái unified — cách tiếp cận của George Song đề cập điều này chi tiết.

Tạo giao diện cho feed trên trình duyệt (tùy chọn)

Raw XML trong trình duyệt không thân thiện với người dùng. Một XSL stylesheet sẽ hiển thị nó như một trang HTML có định dạng. Thêm trường stylesheet:

return rss({
  stylesheet: '/rss/styles.xsl',
  // ... rest of config
});

Đặt stylesheet tại public/rss/styles.xsl. Điểm khởi đầu được dùng nhiều là pretty-feed-v3.xsl — tìm kiếm trên GitHub và nó sẽ là kết quả đầu tiên. Hầu hết các Astro blog starter theme đã bao gồm một phiên bản của nó.

Kết quả đạt được

Một endpoint /rss.xml với các tính năng:

  • Hiển thị bài viết theo thứ tự mới nhất lên đầu
  • Bao gồm <language> để tương thích với trang đa ngôn ngữ
  • Kiểm tra frontmatter lúc build thông qua rssSchema
  • Thông báo URL feed qua thẻ <link> auto-discovery trên mọi trang

Mỗi bài viết bạn xuất bản sẽ xuất hiện trong feed tự động. Các feed reader đang theo dõi URL của bạn sẽ cập nhật vào lần sync tiếp theo — thường trong vòng một giờ với người đăng ký đang hoạt động.

Việc kiểm tra đặc tả sẽ bắt được hầu hết vấn đề trong vài phút. Chạy W3C validator sau lần deploy đầu tiên và sửa mọi lỗi được phát hiện trước khi người dùng thêm feed. Sửa XML bị lỗi sau khi đã có người đăng ký sẽ khó hơn, vì feed reader cache trạng thái feed và hành vi sync lại sẽ khác nhau giữa các ứng dụng.

Khi feed đã vượt qua kiểm tra W3C, triển khai lên Cloudflare Pages là cách nhanh nhất để đưa trang lên production — miễn phí, không giới hạn băng thông và phân phối từ edge.