diff --git a/nginx.conf b/nginx.conf
index 537040e..82889f1 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -25,8 +25,8 @@ server {
try_files $uri $uri/ =404;
}
- # Crawling instructions and sitemaps should refresh promptly after deployment.
- location ~* \.(xml|txt)$ {
+ # Crawling instructions, sitemaps and the blog feed should refresh promptly after deployment.
+ location ~* \.(xml|txt|json)$ {
try_files $uri =404;
expires 1h;
add_header Cache-Control "public, no-transform";
diff --git a/src/content/privacy.md b/src/content/privacy.md
index 4d022a6..e5294c1 100644
--- a/src/content/privacy.md
+++ b/src/content/privacy.md
@@ -1,7 +1,7 @@
---
title: Chính sách quyền riêng tư
description: Chính sách quyền riêng tư của Funput — bộ gõ tiếng Việt mã nguồn mở.
-lastUpdated: '19 tháng 7 năm 2026'
+lastUpdated: '15 tháng 10 năm 2026'
---
Chính sách này giải thích cách Funput xử lý thông tin khi bạn sử dụng ứng dụng bộ gõ Funput và website [funput.app](https://funput.app/). Funput là phần mềm mã nguồn mở; chúng tôi ưu tiên minh bạch và quyền riêng tư theo thiết kế.
@@ -38,6 +38,12 @@ Trong ứng dụng Funput, chúng tôi không:
Funput có thể lưu các tùy chọn cấu hình trên thiết bị của bạn (ví dụ kiểu gõ, giao diện, hoặc trạng thái thiết lập) để ứng dụng hoạt động đúng như bạn mong muốn. Dữ liệu này được lưu cục bộ và không được Funput thu thập về máy chủ của chúng tôi.
+### Tin tức trong ứng dụng
+
+Khi bạn mở mục Tin tức trong ứng dụng Funput, ứng dụng tải danh sách bài viết công khai từ `https://funput.app/blog/feed.json`. Yêu cầu này không kèm mã nhận dạng, tài khoản, cookie hay bất kỳ nội dung nào bạn gõ, và chỉ diễn ra khi bạn mở mục Tin tức. Danh sách được lưu tạm trên thiết bị để xem khi không có mạng. Bàn phím Funput không tham gia vào việc này.
+
+Khi bạn chạm vào một bài viết, bài đó mở trên website funput.app, nên mục 6 dưới đây được áp dụng.
+
## 6. Website và Google Analytics
Website funput.app sử dụng Google Analytics để hiểu mức độ sử dụng trang (ví dụ số lượt xem, thiết bị, nguồn truy cập ở dạng tổng hợp). Google Analytics có thể dùng cookie hoặc mã nhận dạng tương tự do Google cung cấp.
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index 5a04576..3fc32a1 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -31,6 +31,7 @@ const {
+
;
+
+/** Published posts, newest first: the order the blog index and the feed share. */
+export async function getPublishedPosts(): Promise {
+ const posts = await getCollection('blog', ({ data }) => !data.draft);
+ return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
+}
+
+/** The canonical, trailing-slash URL of a post (the site sets `trailingSlash: 'always'`). */
+export function postUrl(post: BlogPost, site: URL | string): string {
+ return new URL(`/blog/${post.id}/`, site).href;
+}
diff --git a/src/pages/blog/[id].astro b/src/pages/blog/[id].astro
index 1db052a..ceda823 100644
--- a/src/pages/blog/[id].astro
+++ b/src/pages/blog/[id].astro
@@ -1,14 +1,15 @@
---
-import { getCollection, render } from 'astro:content';
-import type { CollectionEntry } from 'astro:content';
+import { render } from 'astro:content';
import ReadingLayout from '../../layouts/ReadingLayout.astro';
+import { getPublishedPosts } from '../../lib/blog';
+import type { BlogPost } from '../../lib/blog';
export async function getStaticPaths() {
- const posts = await getCollection('blog', ({ data }) => !data.draft);
+ const posts = await getPublishedPosts();
return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}
interface Props {
- post: CollectionEntry<'blog'>;
+ post: BlogPost;
}
const { post } = Astro.props;
const { Content, headings } = await render(post);
diff --git a/src/pages/blog/feed.json.ts b/src/pages/blog/feed.json.ts
new file mode 100644
index 0000000..889bb3e
--- /dev/null
+++ b/src/pages/blog/feed.json.ts
@@ -0,0 +1,35 @@
+import type { APIRoute } from 'astro';
+import { getPublishedPosts, postUrl } from '../../lib/blog';
+import { FUNPUT_CONSTANTS as links } from '../../lib/constants';
+
+/**
+ * JSON Feed 1.1 of the blog (https://jsonfeed.org/version/1.1).
+ *
+ * The Funput apps read it for their news tab, and any feed reader can subscribe to it.
+ * It carries metadata only: articles open on the website, so their content and the
+ * site's privacy terms stay in one place.
+ */
+export const GET: APIRoute = async ({ site }) => {
+ const origin = site ?? links.SITE_URL;
+ const posts = await getPublishedPosts();
+ const feed = {
+ version: 'https://jsonfeed.org/version/1.1',
+ title: 'Funput Blog',
+ home_page_url: new URL('/blog/', origin).href,
+ feed_url: new URL('/blog/feed.json', origin).href,
+ description: 'Cập nhật và hướng dẫn từ Funput — bộ gõ tiếng Việt mã nguồn mở.',
+ language: 'vi',
+ items: posts.map((post) => ({
+ id: post.id,
+ url: postUrl(post, origin),
+ title: post.data.title,
+ summary: post.data.description,
+ date_published: post.data.pubDate.toISOString(),
+ ...(post.data.updatedDate && { date_modified: post.data.updatedDate.toISOString() }),
+ tags: post.data.tags,
+ })),
+ };
+ return new Response(JSON.stringify(feed), {
+ headers: { 'Content-Type': 'application/feed+json; charset=utf-8' },
+ });
+};
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
index bcd2d92..1d7c0ab 100644
--- a/src/pages/blog/index.astro
+++ b/src/pages/blog/index.astro
@@ -1,10 +1,8 @@
---
-import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
+import { getPublishedPosts } from '../../lib/blog';
import { FUNPUT_CONSTANTS as links } from '../../lib/constants';
-const posts = (await getCollection('blog'))
- .filter((post) => !post.data.draft)
- .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
+const posts = await getPublishedPosts();
const formatDate = (date: Date) =>
date.toLocaleDateString('vi-VN', {
year: 'numeric',