記事
純粋なフロントエンドを使用して「動的 + 記事」の多言語ブログを作成する
私のウェブサイトを「動的 + 記事」多言語ブログにしました。コンテンツは Markdown で管理され、投稿時に翻訳が完了し、構築中に静的ページが生成され、言語の読み取りと切り替えはブラウザーが担当します。この記事では、コンテンツ モデル、共有パイプライン、オンデマンド読み込みと SEO の間のトレードオフ、そして純粋なフロントエンドによって何が節約されるのかを詳しく説明します。
· 11 分で読めます
私の Web サイトが、考えをひと段落書き留めたり、完全な技術的な問題を明確に書き留めたりするために使用できることを願っています。短いものには「ダイナミクス」を、長いものには「記事」を入れます。この 2 つは相互にリンクでき、訪問者が言語を切り替えると、コンテンツも一緒に切り替える必要があります。
ごく当たり前の要件に思えますが、最初からデータベースやバックエンド、コンテンツAPIを追加し、さらに各言語のバージョンを対応していくと、すぐにまた長期メンテナンスが必要な製品になってしまいます。
私はまず、実際に使用できるものに要件を絞り込むことにしました。 ** 1 人の作成者がコンテンツを Git に入力し、公開後に Web サイトを再構築します。リーダーに必要なのは静的ファイルとブラウザーだけです。 ** これが現在の stephen.taipei の「動的 + 記事」構造です。
純粋なフロントエンドは読み取りエンドを指します#
まずは境界線を明確にしましょう。このプロジェクトでは React、Vite、Tailwind を使用します。コンテンツ処理、画像変換、スクリプト作成には引き続き Node.js が必要です。翻訳は、公開段階で外部サービスも呼び出します。デプロイ後、静的ファイルが Nginx によって提供されます。
つまり、純粋なフロントエンドとは、読者が Web サイトを開いたときに、情報を検索したり、ページをグループ化したり、リアルタイムで翻訳したりするために特別なブログ バックエンドを必要としないということです。ビルドプロセスとホストはありますが、記事を読むための永続的なアプリケーション API やデータベースはありません。
事前に作業をしておくと読み終わりがとても楽になります。
ニュースと記事は同じソースを共有し、それぞれに独自のモデルがあります#
内容はcontent/に集中しており、原文は全てMarkdownです。 2 つのコンテンツは同じ形式にまとめられていません。
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
ダイナミクスは短いカードです。ファイル名は台北時間の分をIDとして使用します。アイデア、学習、オンライン、不正行為、読書などのマークを付けることができます。毎回タイトルを考えなくても、パラグラフを書くだけで成立します。 500文字を超えるとリマインダーが表示され、1,000文字を超えると記事化する必要があります。
記事には固定のスラッグ、タイトル、要約、日付があり、表紙や更新時刻を含めることもできます。元の共有リンクが無効になるのを防ぐため、URL は公開後に変更されません。
2 つは article: を使用して関連付けられます。最新情報については、最初に質問を書き留めてから、記事全体にリンクすることができます。記事ページには、それを参照する更新が逆にリストされます。短いコンテンツは現在の開始点を保持し、長いコンテンツは派生の終了を担当します。両面に同じ全文を投稿する必要はありません。
ビルドする前にコンテンツ ルールでエラーをブロックする#
Markdown が導入されただけであれば、公開されるまで多くの問題が発生しません。日付が間違っている、記事がつながっていない、画像に alt が含まれていない、同じトピックが 3 つの類似したタグで書かれているなどです。
これらのルールを共有コンテンツ パイプラインに追加します。 loadContent ファイルを読み取り、Zod でフィールドを確認し、Markdown、画像、言語、およびコンテンツの関係を処理します。 CLI 検査と Vite 構築はどちらもこのパイプラインを使用して、「ローカル マシンは動作していると言っているのに、公式環境では動作していない」というギャップを削減します。
タグは tags.yaml 登録されたキーのみを受け入れます。動的に参照される記事やその他のダイナミクスが存在する必要があります。ローカル プレビューではドラフトを読み取ることができますが、公式ビルドではドラフトが除外されます。これらのルールは投稿時に覚えておくことに基づいているわけではありませんが、実際には検査を妨げます。
Markdownにも明確な出力境界があります。raw HTMLはそのまま出力せず、生成したHTMLはサニタイズし、コードブロックのハイライトはビルド時に行います。ローカル相対パスで参照したコンテンツ画像は、最大幅1600pxのWebPに変換し、GPS情報を含むEXIFメタデータを削除します。記事、カード、共有プレビューでは変換後の画像を共用します。リモート画像とルート絶対パスで参照した画像は、このローカル変換処理の対象外です。
多言語システムの費用は記事掲載時にお支払いいただきます。#
このウェブサイトは現在 22 の言語をサポートしています。繁体字中国語の原文は 1 回書かれ、他の 21 言語のタイトル、要約、テキスト、画像の説明はその隣の locales.yaml に保存されます。
私が重視している決定は次のとおりです: **読者は翻訳を待つ必要はありません。 ** content:localize 投稿時に翻訳を生成します。公式ビルドは既存のファイルを読み取るだけであり、デプロイされるたびに翻訳サービスを再度呼び出すことはありません。
ソース ダイジェストを使用して、元のテキストと翻訳されたテキストを比較します。原文が変更された場合、古い翻訳は最新版とみなされなくなります。 1 つの言語ファミリーが欠落している場合、または翻訳が古い場合、content:check は失敗します。変換プロセスは、コード、インライン コード、およびリンク ターゲットを技術識別子や URL の変換から保護します。
これは、自動翻訳が人間による編集の品質を備えているという意味ではありません。完全性と最新の一貫性を重視しており、専門的な言葉遣いや口調には依然として校正が必要です。変更されていない元のテキストには既存の翻訳が保持され、手動で変更したテキストは再実行によって上書きされません。
リスト内の要約を読み、クリックして記事をロードします#
コンテンツが増えた後、ホームページが各記事および各言語ファミリーの全文を読み込むと、純粋なフロントエンドが非常に遅くなります。
Vite pluginはコンテンツを3つのエントリーポイントに分けます。ホームページ用のvirtual:content/latest、コンテンツページ用のvirtual:content/index、そして記事ごとのvirtual:content/article/<slug>です。ホームページには最新の短文投稿3件と記事カード3件を載せます。短文投稿のデータには全文と翻訳も含まれますが、カード上では表示を省略します。長文記事のカードが持つのは概要などのデータだけで、全文はdynamic importによって記事ごとに読み込まれます。
React の Suspense はテキストの待機を担当し、外側のエラー境界は読み込み失敗メッセージの表示を担当します。テキスト要求が失敗しても、タイトル、要約、ページ ナビゲーションは残ります。
ここにもトレードオフがあります。現在、記事ごとに 1 つのテキスト チャンクがあり、各言語ファミリーの翻訳が含まれていますが、まだ言語ファミリーごとに 1 つのチャンクに分割されていません。動的な全文と翻訳がコンテンツ インデックスに入力されます。コンテンツの量は増加し続けるため、インデックス サイズと多言語テキストのダウンロード数が次のステップで測定されます。
フロントエンドのスイッチングと静的読み取りの入り口が必要です。#
私たちはSPAしかやっていないので、それをシェアするときはトップページのタイトルと写真しか見ないことが多いです。これでは記事としては十分ではありません。
したがって、セットアップ時に、各動的 URL、記事 URL、およびタグ URL には、タイトル、説明、正規、Open Graph、および JSON-LD を含む独自の HTML シェルがあり、読み取り可能な元のテキストが noscript に配置されます。 JavaScript を実行しなくても、この記事の内容と独占表紙を入手できます。
同じコンテンツから、feed.xml、sitemap-content.xml、記事の Markdown バージョン、および llms.txt の記事インデックスも生成されます。これらは構築された製品であり、手動同期が必要な別のコンテンツ セットではありません。
ただし、多言語 SEO の境界も明確にする必要があります。 **現在、アップデートと記事は各言語で同じ URL を共有しており、言語はフロントエンドによって切り替えられます。静的コンテンツ ページ、フィード、およびマークダウン バージョンでは、元のテキストが維持されます。 ** ホームページには、言語およびライト モードとダーク モードのプリレンダリングもあります。これは、各記事に 22 の独立してインデックス可能な言語ページがあるという意味ではありません。
将来的に各言語ファミリーを検索エンジンに含めることを許可する必要がある場合は、言語ファミリーのルーティング、対応する静的ページ、および hreflang を追加する必要があります。これは、このセットがまだ満たしていないもう 1 つの要件です。
投稿も配信プロセスです#
私の公開プロセスは、原文を書き、翻訳を生成し、内容を確認し、Git に送信して、main にマージします。既存の GitHub アクションは、Web サイトを再構築してデプロイします。
npm run content:localize
npm run content:check
npm run test:content
npm run build
多言語の翻訳が揃っていなければ、先に補います。記事が下書きならローカルプレビューにとどめます。本番出力では下書きページが存在しないことも確認し、feed、sitemap、ホームページ、JavaScript bundleなど主要な公開成果物を走査して下書きの目印を探します。コンテンツにもコードと同じようにdiffとバージョン履歴があり、一緒にロールバックできます。
この方法ではリアルタイムの更新が犠牲になります。テキストの一部を変更すると、展開を待つ必要があります。しかし、私の技術的な Web サイトの場合、バックエンドを 1 つ減らし、実行期間の依存関係を 1 つ減らし、追跡しやすい変更記録と引き換えに、この待ち時間を犠牲にするつもりです。
これを行うのはいつが適切ですか?#
投稿頻度が再構築でき、コンテンツがテキスト中心で、Git に慣れている著者であれば、このアーキテクチャは非常に適しています。短い更新により執筆の敷居が低くなり、記事は完全な派生を保持し、多言語により同じコンテンツをより多くの人が読むことができます。
要件が複数人間のリアルタイムコラボレーション、権限管理、読者のコメント、ログインまたはサブスクリプションになる場合は、CMS とバックエンドを再評価する必要があります。純粋なフロントエンドによってこれらのニーズが自動的になくなるわけではありませんが、現時点ではこのブログの範囲には入れていません。
今回の設計原則は、マルチレイアウト・マルチテーマのアーキテクチャ設計パターン で書いたことに非常に近いです。まず責任を明確に分離し、次に共有ルールで一貫性を維持します。
このウェブサイトでは、書き続けられることと、公開するたびに実際にどのようなコンテンツが配信されているかを知ることが重要なポイントです。