フルスタック開発
Next.jsの静的サイトをFirebase Hostingで公開する
このサイトの構成を解説します。Next.jsの静的エクスポートをFirebase Hostingで配信し、URL・キャッシュ・SEOメタデータの整合性をとる方法です。
著者:タンヴィル・アハメド・公開 ・更新 ・約5分で読めます
このサイトはNext.jsで作っていますが、サーバー上で動いている処理は一つもありません。すべてのページはビルド時にHTMLとして生成され、Firebase HostingにアップロードされてCDNから配信されます。この記事では、その構成にした理由と、全体を支える設定を紹介します。
なぜ静的サイトにしたのか
ポートフォリオやブログの内容が変わるのは、記事を公開したときであって、誰かがアクセスしたときではありません。リクエストのたびにレンダリングしても得られるものはなく、費用がかかり、パッチを当て、監視しなければならないサーバーが増えるだけです。
静的エクスポートにすると、次のような利点があります。
- すべてのページに完全なHTMLがある。 検索エンジンやリンクのプレビューは、JavaScriptを実行しなくても内容をすべて読み取れます。
- 運用するランタイムがない。 Node.jsのプロセスがないので、本番環境でスケールさせたりパッチを当てたりするものがありません。
- 無料でホスティングできる。 このような規模のサイトなら、独自ドメインとHTTPSを含めてFirebase HostingのSparkプランで収まります。
その代わり、公開するたびにビルドが必要です。個人サイトであれば問題なく、CIパイプラインを用意すればpush一回で済みます。
静的エクスポートの設定
中心になるのはnext.config.tsの3行です。
const nextConfig: NextConfig = {
output: "export",
trailingSlash: true,
images: { unoptimized: true },
};output: "export"を指定すると、next buildがout/にプレーンなファイルを書き出します。/blog/[slug]のような動的ルートも、generateStaticParamsでビルド時にすべてのslugを列挙すれば使えます。あわせてdynamicParams = falseを指定し、存在しないslugはビルドエラーではなく404になるようにしています。
images: { unoptimized: true }が必要なのは、標準の画像最適化がサーバー上で動く仕組みだからです。代わりに、画像はリサイズと圧縮を済ませてからpublic/に置き、すべての<img>にwidthとheightを明示して、読み込み中にレイアウトがずれないようにしています。
URLをそろえる
検索にとって最も重要な設定はtrailingSlash: trueです。これによりNext.jsは/about.htmlではなく/about/index.htmlを出力し、内部リンクもすべて/about/を指すようになります。
Firebase Hostingも同じ形で配信する必要があります。
{
"hosting": {
"public": "out",
"cleanUrls": true,
"trailingSlash": true
}
}両方の設定がそろっていれば、各ページのURLはちょうど一つになります。
| リクエスト | 結果 |
|---|---|
/about/ | 200、about/index.htmlを配信 |
/about | /about/へ301リダイレクト |
/about/index.html | /about/へ301リダイレクト |
もし両者が食い違うと、ページのメタデータにあるcanonical URLが、リダイレクトされるアドレスを指してしまうことがあります。小さなことですが、どのURLをインデックスすべきか検索エンジンを迷わせる、まさにそういう不整合です。そこで、canonicalリンク、サイトマップ、構造化データに入れる前に、すべてのパスを末尾スラッシュ付きの形にそろえるヘルパーを用意しています。
キャッシュ
Next.jsはJavaScriptとCSSを/_next/static/以下に置き、ファイル名に内容のハッシュを含めます。内容が変われば名前も変わるので、これらのファイルは1年間キャッシュし、immutableを付けられます。
HTMLはその逆です。/about/のファイルはデプロイのたびに同じ場所で中身が変わるため、毎回再検証させる必要があります。
[
{
"source": "**",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }]
},
{
"source": "/_next/static/**",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]全体に当てるルールを先に、個別のルールを後に書くことで、ハッシュ付きのファイルには長期キャッシュが、それ以外には再検証が適用されます。また、FirebaseはデプロイのたびにCDNのキャッシュを削除するので、新しいリリースはすぐに反映されます。
ビルド時のSEOメタデータ
クローラーが必要とする情報は、すべて生成されたHTMLに含まれています。
- タイトル、説明文、canonical URLはMetadata APIで設定し、
metadataBaseは環境変数から読み込みます。 - **
sitemap.xmlとrobots.txt**はsitemap.tsとrobots.tsで生成し、force-staticを指定して通常のファイルとしてout/に書き出します。 - 構造化データはJSON-LDのグラフです。すべてのページに
PersonとWebSiteを、記事にはBlogPostingを、階層のあるページにはBreadcrumbListを出力しています。 - Open Graph画像は
next/ogでページごとにビルド時に一度だけ生成します。
canonicalのオリジンはNEXT_PUBLIC_SITE_URLから取得し、これが未設定の本番ビルドは意図的に失敗させています。canonicalがlocalhostを指したまま公開されるくらいなら、ビルドが止まるほうがましです。
英語と日本語の2言語対応
このサイトは英語と日本語の両方で読めます。英語はルート(/about/)、日本語は/ja/以下(/ja/about/)です。
- 言語ごとにルートレイアウトを分け、
<html lang>がページの言語と一致するようにしています。 - 各ページは
hreflangで互いの言語版を示し、x-defaultは英語版を指します。サイトマップにも同じ対応関係を書き出しています。 - 翻訳がない記事には
hreflangを出力しません。存在しないページを指す代替リンクは、ないほうがよいからです。
あえて入れなかったもの
Firebaseなら、Firestore、Cloud Functions、Authenticationも簡単に追加できます。このサイトではどれも使っていません。コンテンツは型付きのTypeScriptファイルとMDXとしてリポジトリに置いているので、変更履歴、レビュー、型チェックが自然に手に入ります。問い合わせはフォームではなくメールとLinkedInで受けているため、インターネットからの書き込みを受け付けるエンドポイントもありません。
もし将来、問い合わせフォームのような動的な機能が必要になっても、ページの生成や配信の仕組みを変えずに、独立した小さなサービスとして追加できます。
- Next.js
- Firebase Hosting
- 静的サイト
- テクニカルSEO