Bài viết
Sử dụng giao diện người dùng thuần túy để tạo blog đa ngôn ngữ "động + bài viết"
Tôi đã biến trang web của mình thành một blog đa ngôn ngữ "động + bài viết": nội dung được quản lý bằng Markdown, bản dịch được hoàn thành khi đăng, các trang tĩnh được tạo trong quá trình xây dựng và trình duyệt chịu trách nhiệm đọc và chuyển đổi ngôn ngữ. Bài viết này phân tích mô hình nội dung, quy trình chia sẻ, sự cân bằng giữa tải theo yêu cầu và SEO cũng như chính xác những gì mà giao diện người dùng thuần túy tiết kiệm được.
· 11 phút đọc
Tôi hy vọng rằng trang web của tôi có thể được sử dụng để ghi lại một đoạn suy nghĩ hoặc viết ra một vấn đề kỹ thuật hoàn chỉnh một cách rõ ràng. Đặt "động lực" trong phần ngắn và "bài viết" trong phần dài. Cả hai có thể được liên kết với nhau và khi khách truy cập chuyển đổi ngôn ngữ, nội dung cũng phải được chuyển đổi cùng nhau.
Yêu cầu này có vẻ rất bình thường, nhưng nếu bạn thêm cơ sở dữ liệu, chương trình phụ trợ và API nội dung ngay từ đầu, sau đó xử lý phiên bản của từng ngôn ngữ, nó sẽ sớm trở thành một sản phẩm khác cần bảo trì lâu dài.
Trước tiên, tôi chọn thu hẹp các yêu cầu về những gì tôi thực sự có thể sử dụng: **Một tác giả, nhập nội dung vào Git và xây dựng lại trang web sau khi xuất bản. Người đọc chỉ cần các tập tin tĩnh và một trình duyệt. ** Đây là cấu trúc "động + bài viết" hiện tại của stephen.taipei.
Front-end thuần túy đề cập đến phần cuối đọc#
Trước tiên hãy làm rõ ranh giới. Dự án này sử dụng React, Vite và Tailwind. Node.js vẫn cần thiết để xử lý nội dung, chuyển đổi hình ảnh và tạo tập lệnh; bản dịch cũng sẽ gọi các dịch vụ bên ngoài trong giai đoạn xuất bản. Sau khi triển khai, các tệp tĩnh được Nginx cung cấp.
Vì vậy, ý tôi khi nói thuần túy front-end là khi người đọc mở trang web, họ không cần một Blog back-end đặc biệt để tìm kiếm thông tin, nhóm trang hoặc dịch theo thời gian thực. Nó có quy trình xây dựng và máy chủ lưu trữ nhưng không có API ứng dụng hoặc cơ sở dữ liệu liên tục để đọc bài viết.
Nếu công việc được thực hiện trước, việc đọc phần cuối sẽ dễ dàng hơn nhiều.
Tin, bài chia sẻ chung một nguồn, mỗi bài có một mẫu riêng#
Nội dung tập trung ở content/ và văn bản gốc đều là Markdown. Hai nội dung không được sắp xếp theo cùng một định dạng:
content/
tags.yaml
tags.locales.yaml
notes/2026/20261004-0944.md
notes/2026/20261004-0944.locales.yaml
articles/frontend-only-multilingual-blog/
index.md
locales.yaml
cover.webp
Động lực là thẻ ngắn. Tên tệp sử dụng phút của giờ Đài Bắc làm ID. Nó có thể được đánh dấu bằng ý tưởng, học tập, trực tuyến, gian lận hoặc đọc sách. Nó có thể được thiết lập chỉ bằng cách viết một đoạn văn mà không cần phải nghĩ đến tiêu đề mỗi lần. Một lời nhắc sẽ xuất hiện nếu nó vượt quá 500 từ và nó sẽ được chuyển thành một bài viết nếu vượt quá 1.000 từ.
Bài viết có slug, title, abstract, date cố định và cũng có thể có bìa, thời gian cập nhật. URL sẽ không bị thay đổi sau khi được đặt ở chế độ công khai để ngăn liên kết được chia sẻ ban đầu trở nên không hợp lệ.
Cả hai có liên quan bằng cách sử dụng article:. Để cập nhật, bạn có thể viết ra câu hỏi trước rồi liên kết đến bài viết đầy đủ; trang bài viết sẽ liệt kê các bản cập nhật tham chiếu ngược lại. Nội dung ngắn giữ lại điểm bắt đầu hiện tại và nội dung dài có nhiệm vụ hoàn thiện đạo hàm. Không cần thiết phải đăng toàn văn giống nhau ở cả hai mặt.
Để quy tắc nội dung chặn lỗi trước khi xây dựng#
Nếu Markdown chỉ được đưa vào hình ảnh, nhiều vấn đề sẽ không được nhìn thấy cho đến khi nó đi vào hoạt động: sai ngày, bài viết không được kết nối, hình ảnh không có thay thế hoặc cùng một chủ đề được viết bằng ba thẻ giống nhau.
Tôi đưa những quy tắc này vào hệ thống nội dung được chia sẻ. loadContent Đọc tệp, kiểm tra các trường bằng Zod, sau đó xử lý các mối quan hệ Markdown, hình ảnh, ngôn ngữ và nội dung. Cả hoạt động kiểm tra CLI và xây dựng Vite đều sử dụng quy trình này để giảm khoảng cách giữa "máy cục bộ cho biết nó hoạt động nhưng môi trường chính thức thì không".
Thẻ chỉ chấp nhận tags.yaml khóa đã đăng ký; các bài viết được tham chiếu động và các động lực khác phải tồn tại; bản xem trước cục bộ có thể đọc bản nháp và bản dựng chính thức sẽ loại trừ bản nháp. Những quy tắc này không dựa trên việc ghi nhớ chúng khi đăng bài, mà chúng thực sự cản trở việc kiểm tra.
Markdown cũng có ranh giới đầu ra rõ ràng: raw HTML không được xuất trực tiếp, HTML được tạo ra sẽ được làm sạch, còn các khối mã được tô sáng khi build. Hình ảnh nội dung được tham chiếu bằng đường dẫn tương đối cục bộ sẽ được chuyển thành WebP với chiều rộng tối đa 1600px và xóa metadata EXIF, bao gồm cả dữ liệu GPS. Bài viết, thẻ nội dung và bản xem trước chia sẻ dùng chung hình ảnh đã xử lý. Hình ảnh từ xa và hình ảnh được tham chiếu bằng đường dẫn tuyệt đối tính từ root không đi qua quy trình chuyển đổi cục bộ này.
Chi phí cho hệ thống đa ngôn ngữ sẽ được thanh toán khi xuất bản bài viết.#
Trang web hiện hỗ trợ 22 ngôn ngữ. Văn bản gốc tiếng Trung phồn thể được viết một lần và tiêu đề, tóm tắt, mô tả văn bản và hình ảnh của 21 ngôn ngữ khác được lưu trữ trong locales.yaml bên cạnh.
Đây là một quyết định mà tôi quan tâm: **Người đọc không phải đợi dịch. ** content:localize Tạo bản dịch khi đăng. Bản dựng chính thức chỉ đọc các tệp hiện có và không gọi lại dịch vụ dịch mỗi khi nó được triển khai.
Sử dụng thông báo nguồn để so sánh văn bản gốc và văn bản đã dịch. Nếu văn bản gốc bị thay đổi thì bản dịch cũ không còn được coi là bản mới nhất; nếu thiếu một họ ngôn ngữ hoặc bản dịch đã lỗi thời, content:check sẽ không thành công. Quá trình dịch bảo vệ mã, mã nội tuyến và mục tiêu liên kết khỏi việc dịch mã định danh kỹ thuật hoặc URL.
Điều này không có nghĩa là bản dịch tự động có chất lượng do con người biên tập. Nó đề cập đến tính đầy đủ và tính nhất quán được cập nhật, đồng thời ngôn ngữ và giọng điệu chuyên nghiệp vẫn cần được hiệu đính. Văn bản gốc chưa được thay đổi sẽ giữ lại bản dịch hiện có và văn bản được sửa đổi thủ công sẽ không bị ghi đè khi chạy lại.
Đọc tóm tắt trong danh sách, click để tải bài viết#
Sau khi tăng nội dung, nếu trang chủ tải toàn bộ nội dung của từng bài viết, từng họ ngôn ngữ thì front-end thuần túy sẽ rất chậm.
Vite plugin chia nội dung thành ba entry point: virtual:content/latest dành cho trang chủ, virtual:content/index dành cho các trang nội dung và một virtual:content/article/<slug> riêng cho từng bài viết. Trang chủ lấy ba ghi chú mới nhất và ba thẻ bài viết. Dữ liệu ghi chú vẫn chứa toàn văn và bản dịch; thẻ chỉ rút gọn phần hiển thị. Thẻ của bài viết dài chỉ mang theo phần tóm tắt và dữ liệu liên quan, còn toàn bộ nội dung được tải riêng cho từng bài bằng dynamic import.
Suspense của React chịu trách nhiệm chờ văn bản và ranh giới lỗi bên ngoài chịu trách nhiệm hiển thị thông báo lỗi tải. Khi yêu cầu văn bản không thành công, tiêu đề, tóm tắt và điều hướng trang vẫn được giữ nguyên.
Ở đây cũng có sự cân bằng: hiện tại, mỗi bài viết có một đoạn văn bản chứa các bản dịch của từng họ ngôn ngữ và nó vẫn chưa được chia thành một đoạn cho mỗi họ ngôn ngữ. Toàn văn động và bản dịch được nhập vào chỉ mục nội dung. Khi khối lượng nội dung tiếp tục tăng, kích thước chỉ mục và số lượt tải xuống của văn bản đa ngôn ngữ là những bước tiếp theo cần được đo lường.
Phải có chuyển đổi giao diện người dùng và lối vào để đọc tĩnh.#
Chúng tôi chỉ làm SPA, khi chia sẻ thường chỉ thấy tiêu đề và hình ảnh trên trang chủ. Điều này là không đủ cho bài viết.
Do đó, khi thiết lập, mỗi URL động, bài viết và thẻ đều có shell HTML riêng, chứa tiêu đề, mô tả, chuẩn, Open Graph và JSON-LD, đồng thời văn bản gốc có thể đọc được được đặt trong noscript. Bạn có thể lấy nội dung và trang bìa độc quyền của bài viết này mà không cần thực thi JavaScript.
Đoạn nội dung tương tự cũng sẽ tạo ra feed.xml, sitemap-content.xml, phiên bản Markdown của bài viết và chỉ mục bài viết cho llms.txt. Đây là những sản phẩm được xây dựng chứ không phải một bộ nội dung khác yêu cầu đồng bộ hóa thủ công.
Tuy nhiên, ranh giới của SEO đa ngôn ngữ cũng phải được làm rõ: **Hiện tại, các bản cập nhật và bài viết có chung một URL ở mỗi ngôn ngữ và ngôn ngữ được chuyển đổi bởi giao diện người dùng; các trang nội dung tĩnh, nguồn cấp dữ liệu và phiên bản Markdown duy trì văn bản gốc. ** Trang chủ cũng có tính năng kết xuất trước ngôn ngữ và chế độ sáng tối. Điều này không có nghĩa là mỗi bài viết cũng có 22 trang ngôn ngữ có thể lập chỉ mục độc lập.
Nếu bạn cần cho phép các công cụ tìm kiếm đưa từng họ ngôn ngữ vào trong tương lai, bạn sẽ cần thêm định tuyến họ ngôn ngữ, các trang tĩnh tương ứng và hreflang. Đó là một yêu cầu khác mà bộ này chưa đáp ứng được.
Đăng bài cũng là một quá trình phân phối#
Quy trình xuất bản của tôi là: viết văn bản gốc, tạo bản dịch, kiểm tra nội dung, gửi tới Git và sau đó hợp nhất nó vào main. Các hành động GitHub hiện tại sẽ xây dựng lại trang web và triển khai nó.
npm run content:localize
npm run content:check
npm run test:content
npm run build
Nếu bản dịch chưa đầy đủ ở mọi ngôn ngữ, tôi sẽ bổ sung trước. Bài viết còn ở trạng thái bản nháp thì chỉ nằm trong bản xem trước cục bộ. Bản build production cũng xác nhận không có trang nháp và quét các đầu ra công khai chính như feed, sitemap, trang chủ và JavaScript bundle để tìm dấu đánh dấu bản nháp. Nội dung cũng có diff và lịch sử phiên bản như mã nguồn, nên có thể khôi phục cùng lúc.
Phương pháp này hy sinh các bản cập nhật theo thời gian thực: nếu bạn thay đổi một đoạn văn bản, bạn phải chờ triển khai. Nhưng đối với trang web kỹ thuật của tôi, tôi sẵn sàng đánh đổi sự chờ đợi này để đổi lấy một ít phụ trợ hơn, ít phụ thuộc vào thời gian thực hiện hơn và một bản ghi thay đổi dễ theo dõi hơn.
Khi nào thì thích hợp để thực hiện việc này?#
Nếu bạn là tác giả, tần suất đăng bài có thể được xây dựng lại, nội dung chủ yếu là văn bản và bạn đã quen với Git thì kiến trúc này rất phù hợp. Các bản cập nhật ngắn hạ thấp ngưỡng viết, bài viết vẫn giữ nguyên nguồn gốc hoàn chỉnh và đa ngôn ngữ cho phép nhiều người đọc cùng một nội dung.
Nếu các yêu cầu trở thành sự cộng tác theo thời gian thực giữa nhiều người, quản lý quyền, nhận xét của người đọc, đăng nhập hoặc đăng ký thì CMS và chương trình phụ trợ cần phải được đánh giá lại. Front-end thuần túy sẽ không tự động làm cho những nhu cầu này biến mất, nhưng hiện tại tôi chưa đưa chúng vào phạm vi của blog này.
Các nguyên tắc thiết kế lần này rất gần với những gì tôi đã viết trong Mẫu thiết kế kiến trúc đa layout và đa theme: trước tiên hãy phân tách rõ ràng các trách nhiệm, sau đó để các quy tắc chung duy trì tính nhất quán.
Đối với trang web này, điểm mấu chốt là cho phép tôi tiếp tục viết và biết nội dung nào thực sự được cung cấp mỗi khi tôi xuất bản.