本文へ

開発記録

ホーム、記事一覧、記事ページ、sitemapで公開クエリを共有する理由

`getPublishedNotes` の実際の利用箇所をもとに、ホーム、記事一覧、記事ページ、ページネーション、sitemapで公開条件を共有し、表示の違いをそれぞれの側で処理する仕組みを説明します。

林予晴
公開
  • コンテンツエンジニアリング
  • Astro
  • Content Collections
  • 公開クエリ

記事を公開するかどうかを、ホーム、記事一覧、記事ページ、sitemapが別々に判断してはいけません。どこか一か所でも余分な下書きが表示されたり、公開済みの記事が抜けたりすると、読者が見るサイトの状態に矛盾が生じます。

ここで扱うコンテンツシステムでは、「どの記事が公開条件を満たしているか」という判断を getPublishedNotes() に集約し、表示方法は各ページに任せています。公開条件の定義は一つにし、ページごとの差はクエリの後で処理する方針です。各入口に個別のフィルタールールを持たせません。

公開可能な記事の定義を一か所にまとめます

現在のデータの流れは、次のように整理できます。

Markdown
  → getAllNotes()
  → validateNoteIdentities()
  → selectPublishedNotes()
  → getPublishedNotes(locale?)
  → ホーム/記事一覧/記事ページ/sitemap/関連記事

getAllNotes() は、まず記事の識別情報、ディレクトリの言語、本文の見出し階層を検査します。その後、getPublishedNotes() が台北のタイムゾーンで今日の日付を取得し、selectPublishedNotes() に渡して公開条件を判定します。

記事は、次の条件をすべて満たす必要があります。

  • draft が false です。
  • publishedAt が設定されています。
  • publishedAt の日付が今日以前です。
  • クエリで言語を指定した場合は、locale が一致しています。

条件を満たした記事の集合は、公開日時の新しい順に並びます。秒単位の日時が一致するときだけ、stableId で順序を決めます。これらのルールは src/utils/notes-model.ts にまとめられており、各Astroページの条件式に分散しているわけではありません。

判定を集約すると、同じ記事に対して、すべてのページが同じ台北の日付、同じ下書き条件、同じ並び順のルールを使えます。公開契約を変更するときも、まず一つのコンテンツモデルを変更し、その後にすべての出力検証を再実行できます。

同じ公開対象の集合を、複数のページが使います

クエリを共有しても、すべてのページを同じ見た目にする必要はありません。クエリが先に答えるのは「どの記事を表示してよいか」です。各入口は、その集合を自分の役割に応じて使います。

以下の /notes で始まるパスは、繁体字中国語のルート例です。英語と日本語の記事ルートは別の行に記載しています。

利用箇所 集合の使い方 各ページの役割
ホーム getPublishedNotes(lang).slice(0, 3) その言語の最新記事を最大3件表示し、全件一覧への入口を用意します
/notes getPublishedNotes(lang) 全件一覧、記事がない場合の表示、一覧内のグループ分けやページネーションを扱います
/notes/page/[page] getPublishedNotes() と paginateNotes() 実際に必要な後続の一覧ページだけを生成します
/notes/[slug] getPublishedNotes() を使う getStaticPaths() 公開条件を満たす繁体字中国語の記事ページだけを生成します
/en/notes/[slug]、/ja/notes/[slug] 同じクエリの結果を言語で絞り込みます 実在し、公開条件を満たす翻訳記事だけを生成します
sitemap getPublishedNotes() 公開記事のURLと、実在する一覧ページだけを出力します
記事内の関連記事 同じ言語の getPublishedNotes() 公開対象の集合から現在の記事を除き、metadataに基づいて並べます

そのため、ホームには3件だけを表示し、一覧にはより多くの記事を表示できます。記事ページは個別のルートを生成し、sitemapは公開記事をすべて列挙できます。これらは表示上の役割の違いであって、公開ルールの違いではありません。

ページごとのフィルターでは、問題が出力に隠れます

たとえば、ホームが draft だけを確認し、未来の publishedAt を確認しなければ、公開予定の記事が先にホームへ出てしまう可能性があります。sitemapがすべてのMarkdownを走査するだけなら、一覧にカードがない下書きにもsitemapのURLが付く可能性があります。記事ページが同じ集合を使っていなければ、読者がリンク先を開いたとき、一覧には存在しない記事を読めてしまうこともあります。

こうした誤りは、TypeScriptの検査や個別ページのビルドでは見つかるとは限りません。各ページの条件式は、コードとしては正しい場合があるからです。問題は、入口ごとに「公開記事」の定義が異なることです。

公開クエリを集約すれば、各ページは自分の出力だけを扱えます。ホームは件数、一覧は配置、記事ページは本文、sitemapはURLを担当します。draft、日付、言語の判定を改めて複製する必要はありません。

モデルと生成物の両方をテストします

筆記のテストでは、まずコンテンツモデル自体について、次の項目を確認しています。

  • 下書きには publishedAt がなくても構いません。
  • 公開記事に publishedAt がない場合は検証に失敗します。
  • 無効な日付や、公開日時より前の updatedAt は拒否します。
  • 下書きと公開日がまだ来ていない記事は、公開クエリの結果に含めません。
  • 同じ秒に公開された記事は、stableId によって安定した順序を保ちます。
  • 言語の絞り込みと記事URLには、同じ識別ルールを使います。
  • プレビュールートはAstro devでのみ登録します。

さらに verify:notes が実際のビルド生成物を読み込み、元のMarkdown、dist/、sitemap、記事ページ、3言語のホームで件数とリンクが一致するかを検査します。この二つの層は、互いの代わりにはなりません。モデルのテストは条件を確認し、出力の検証は、その条件がページやsitemapに実際に反映されたことを確認します。

公開クエリの共有は、コンテンツ品質の検査ではありません

getPublishedNotes() が判断できるのは、記事が現在の公開条件を満たしているかどうかです。文章の出来がよいか、日付、タイトル、説明文が公開に適しているかまでは判断できません。SEOの順位、読者の理解、アクセス数、協業の成果を証明することもできません。

静的サイトとしての制約もあります。未来の日付を設定した記事が、午前0時に自動で公開されるわけではありません。日付の条件を満たした後にサイトを再ビルドして初めて、新しい公開対象の集合がホーム、一覧、記事ページ、sitemapへ書き込まれます。

公開クエリを単純に保つのは、このためです。担当するのは「どのコンテンツを正式な出力に含めてよいか」という狭い問いです。内容の正確さ、公開範囲、画面上での読みやすさ、公開の承認は、別の検査として残ります。

記事が増えたら、ルールを変える前に利用箇所を追います

記事の公開フローを変更するときは、まず getPublishedNotes の利用箇所をたどります。ホーム、一覧、ページネーション、記事ページ、関連記事、sitemapが、引き続き同じ公開対象の集合を使っているかを確認します。そのうえで、各ページが自分の表示処理だけを行っているかを確認します。

これは、特定のページに条件式を一つ追加するより確実です。公開ルールの問題は、一つの画面が間違っているというより、同じ記事が出力先ごとに異なる状態として扱われることにあります。公開条件を一つにまとめれば、ページごとに違う答えを返さずに済みます。

この記録の範囲

この記事は、getAllNotes、getPublishedNotes、selectPublishedNotes の実装と、ホーム、一覧、ページネーション、記事ルート、sitemap、記事レイアウト、筆記の検証スクリプトをもとに整理しています。ここで記録したコンテンツクエリの契約を説明するものであり、クエリの共有をコンテンツ品質、SEO、読者への効果の保証として扱ってはいません。