この記事はRust+SvelteKit+CDK で RSS 要約アプリを作ってみる Advent Calendar 2025の 17 日目の記事になります。
また、筆者が属している株式会社野村総合研究所のアドベントカレンダーもあるので、ぜひ購読ください。
はじめに
SSG(静的サイトジェネレーター)のメリットに「あらかじめデータがレンダリングされているので表示が高速」であることが挙げられます。しかし、そのためには「ビルド時にデータを取得して HTML に焼き込む」という工程が必要になります。
SvelteKit でこれを実現するために、今回は「ビルド前スクリプトでデータを JSON 化し、それを読み込む」というアプローチを紹介します。
動的パスを含む SSG で気をつけること
動的部分の決定
動的パス(例:/blog/[slug]のような URL 設計で、[slug]の部分が動的である)で定義されるルートが存在する場合、そのルートの prerender(SSG においてあらかじめレンダリングすること)の実装が難しくなります。
オンデマンドでレンダリングする場合は、リクエスト時点でその[slug]の値が決まっているのですが、SSG のタイミングではどのような値が[slug]に入りうるか決まっていないためです。
Sveltekit ではそれを決定するentriesという機能を持っています。この機能を使うことで、SSG の際にどのような[slug]に対して prerender すればいいのかを定義することができます。
ISG でない辛さ
SvelteKit は、執筆時点では ISG (Incremental Static Generation) に対応していません(そのはず)。ISG を使うと、初回アクセスされたルートはオンデマンドでレンダリングされますが、その結果が静的に保存されるため、2回目以降に同じルートにアクセスした際は SSG したときと同じような挙動となります。これが使えればいいとこどりではあったのですが、その機能はありませんでした。
また、今回動的に生成する部分は日付ごとになるのですが、「どの日付からの範囲で生成するのか」をどのように決定するのかがポイントでした。DB に持つ、ファイルに持つ、などが考えられましたが、今回は「S3 のパスを見る」方法で解決しました。ビルドされた成果物は S3 に日付ごとに配置されるので、そのファイルパスを走査すれば、最も古い日付がわかるため、そこの日付を基準日として扱えます。
SumaRSS での実装
動的ルートのパラメータ定義(entries)
web/src/routes/blog/[slug]/+page.server.tsにおいて、entries関数をエクスポートします。
これは、動的ルート([slug])が取りうる値を配列として返す関数で、adapter-staticはこれを元にページを生成します。
// web/src/routes/blog/[slug]/+page.server.ts
import { getDatesToBuild } from "$lib/build";
import type { EntryGenerator, PageServerLoad } from "./$types";
import { listArticlesByDate } from "$lib/articles";
// 生成するページのパラメータ(slug=日付)を返す
export const entries: EntryGenerator = async () => {
// S3から既存の日付を取得し、今日の日付などを追加して返す
return (await getDatesToBuild()).map((date) => ({
slug: date,
}));
};
getDatesToBuild関数(web/src/lib/build.ts)は、S3 上の既存の日付フォルダをリストアップし、それに「今日」を加えたリストを返します。これにより、過去の記事ページも再生成され、新規記事も反映されます。
ページごとのデータ取得(load)
各ページのデータは、通常の SvelteKit アプリと同様にload関数で取得します。
SSG(prerender = true)の場合、このload関数はビルド時にのみ実行されます(AWS へのアクセスもビルド時のみ)。
// web/src/routes/blog/[slug]/+page.server.ts
export const load: PageServerLoad = async ({ params }) => {
const { slug } = params;
// DynamoDBからその日付の記事一覧を取得
const articles = await listArticlesByDate(slug);
// 要約済みの記事のみを表示対象とする
return {
articles: articles.filter((a) => a.state === "Summarized"),
date: slug,
};
};
記事データの取得処理(listArticlesByDate)
load関数内で呼び出されているlistArticlesByDateの実装も見てみます。
web/src/lib/articles.tsにて、DynamoDB の GSI(ByDate)を使用して、指定された日付(date)に合致する記事をクエリしています。
// web/src/lib/articles.ts
export const listArticlesByDate = async (date: string): Promise<Article[]> => {
const result = await docClient.send(
new QueryCommand({
TableName: ARTICLE_DATABASE_NAME,
IndexName: "ByDate",
KeyConditionExpression: "#dt = :dateValue",
ExpressionAttributeNames: {
"#dt": "date",
},
ExpressionAttributeValues: {
":dateValue": date,
},
})
);
return (result.Items ?? []) as Article[];
};
まとめ
SSG における動的パスの扱いは難しいですが、何らかの方法を使って「取りうる値の範囲」を定義することが重要です。今回は S3 パスから導出していますが、DB に持つなどするのもありだと思います。(というか ISG や ISR が最適な気もしなくもない・・・)