Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
8 changes: 7 additions & 1 deletion src/content/privacy.md
Original file line number Diff line number Diff line change
@@ -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ế.
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const {
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<link rel="alternate" type="application/feed+json" title="Funput Blog" href="/blog/feed.json" />
<SeoHead
title={title}
description={description}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';

export type BlogPost = CollectionEntry<'blog'>;

/** Published posts, newest first: the order the blog index and the feed share. */
export async function getPublishedPosts(): Promise<BlogPost[]> {
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;
}
9 changes: 5 additions & 4 deletions src/pages/blog/[id].astro
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
35 changes: 35 additions & 0 deletions src/pages/blog/feed.json.ts
Original file line number Diff line number Diff line change
@@ -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' },
});
};
6 changes: 2 additions & 4 deletions src/pages/blog/index.astro
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
Loading