SSGで静的Webサイトの開発と管理を効率化する
静的WebサイトとWebアプリケーション
ホームページのような、コンテンツの内容がユーザーによって変化しないようなWebサイトは静的Webサイトと呼ばれます。静的Webサイトは、主にHTML・CSS・JavaScriptの技術を使って作成することができます。
一方、ユーザーのアカウント管理をしてログイン可能にしたり、ログインしているユーザーによって表示するコンテンツを変えたりするような機能を実現しようとすると、HTML・CSS・JavaScriptだけでの実現は難しくなります。このような機能を実現するには、一般的にはサーバー側で動作するプログラムや、データを管理する仕組みが必要になります。
そのような機能を実現したWebサイトは、Webアプリケーション、あるいは動的Webサイトなどと呼ばれます。
Webアプリケーションでは、静的Webサイトに比べてできることが増え、ユーザーに対して多くの機能を提供することが可能になります。しかしその分、開発や管理にかかるコストは増える傾向にあります。開発には専門的な知識が必要で、時間もかかります。静的なWebサイトを運用するにはWebサーバーがあれば十分ですが、Webアプリケーションの場合は、アプリケーションの実行環境や、必要に応じてデータベースなどの環境も用意する必要があり、それに伴う運用のためのインフラ構築も必要となり、手間も金銭的なコストも増えます。また、セキュリティについても考慮すべきことが増えます。データの量やプログラムの作り方によっては、パフォーマンスも考慮する必要があり、SEO対策が必要な場合はその点の考慮も必要になります。
静的Webサイトは、Webアプリケーションに比べると考慮すべきことが圧倒的に少ないため運用がとても楽です。ユーザーがログインしたり、ユーザーがデータを登録するような処理が必要な場合はWebアプリケーションにしたくなりますが、ユーザーに依存しない参照用のコンテンツを提供するようなWebサイトの場合、静的なWebサイトを選ぶ方が運用は楽になります。
静的Webサイトの課題
静的なWebサイトの場合、Webアプリケーションに比べると開発の難易度は高くありませんが、サイトの中でページ数が増えてくると、ページの管理にも課題が出てきます。
1つのWebサイトの中ではヘッダーやフッター部分はどのページでも統一されていることが一般的です。フレームワークなどを使用せずに標準的なHTML・CSS・JavaScriptでWebサイトを開発していると、ページが増えるたびに同じヘッダーやフッターのコードを書く必要があり、重複したコードが増えてしまいます。重複したコードが増えると、変更があった際に対応漏れが起きる可能性も高くなり、修正や確認にも時間がかかってしまいます。
Webアプリケーションとして開発する場合、何かしらのフレームワーク(テンプレートエンジン)技術を使って開発することが多く、コードが重複する問題はその仕組みを使ってある程度解決できます。とはいえ、それだけのためにWebアプリケーションとして開発するのは、運用コストに見合いません。
ReactやVue.jsなどのJavaScriptフレームワークを使ってSPA(Single Page Application)構成にして、ヘッダーやフッターなどの共通コンテンツをコンポーネント化することができれば、動的なWebサイトにせずとも、コードの重複をなくすことができるようになります。
しかし、この方法も万能ではありません。一般的なCSR(Client-Side Rendering)構成のSPAでは、ブラウザでJavaScriptを実行してHTMLの内容を構築します。検索エンジンによってはJavaScriptの実行に制約があるため、SEOの観点で追加の考慮が必要になる場合があります。
SSGによる解決
静的なWebサイトとして運用したいが、コードの重複をなくして開発を楽にしたい。
このような課題の解決に役に立つのが、SSG(Static Site Generator:静的サイトジェネレーター)です。SSGは、テンプレートとなるコードを元にして、ビルド時にHTML・CSS・JavaScriptを生成するツール、およびフレームワークです。代表的なSSGフレームワークとしては、Astro、Hugo、Eleventy(11ty)などがあります。
Astroは、このブログサイトの構築でも使われているフレームワークで、別の記事でも簡単に紹介しているので、興味がある方はそちらも参照ください。
Eleventy(11ty)
理解を深めるために、簡単なサンプルを使ってSSGを試してみます。
ここでは、Eleventy(11ty)を使って、簡単なWebサイトを作ることにします。
ここでは環境構築は省略します。手元で動かして確認する場合は公式サイトなどを参考にしてください。
※サンプルでは、Eleventyの入力ディレクトリをsrcに指定し、WebCプラグインとコンポーネントの読み込みを設定していることを前提としています。
まずは、SSGを使わずに、3つのHTMLファイルを作ります。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ホーム</title>
</head>
<body>
<header>
<p>サンプルサイト</p>
<nav>
<a href="/">ホーム</a>
<a href="/services/">サービス</a>
<a href="/about/">会社概要</a>
</nav>
</header>
<main>
<h1>ホーム</h1>
<p>ようこそ、サンプルサイトへ!</p>
</main>
<footer>
<small>© 2026 サンプルサイト</small>
</footer>
</body>
</html>
※以下、head要素は省略
services/index.html
<body>
<header>
<p>サンプルサイト</p>
<nav>
<a href="/">ホーム</a>
<a href="/services/">サービス</a>
<a href="/about/">会社概要</a>
</nav>
</header>
<main>
<h1>サービス</h1>
<p>提供サービスを紹介します。</p>
</main>
<footer>
<small>© 2026 サンプルサイト</small>
</footer>
</body>
about/index.html
<body>
<header>
<p>サンプルサイト</p>
<nav>
<a href="/">ホーム</a>
<a href="/services/">サービス</a>
<a href="/about/">会社概要</a>
</nav>
</header>
<main>
<h1>会社概要</h1>
<p>私たちについて紹介します。</p>
</main>
<footer>
<small>© 2026 サンプルサイト</small>
</footer>
</body>
見比べてみると分かりますが、header要素とfooter要素は全く同じものがそれぞれのページに書かれています。今後ページが増えていくと重複したコードがより増えていくことになるので、うまくまとめられるとよさそうです。
また、各文言についても後々変更になる可能性があります。変更の可能性がある文言は、変数にして外出しで管理できると楽そうです。
SSGの技術を使うと、このような課題を解決できます。
Eleventyを使う場合でもやり方はいくつかありますが、ここではWebCというコンポーネントの仕組みとFront Matter(フロントマター)と呼ばれる仕組みを使うことにします。この2つを使った具体的なコードは以下になります。
共通データを扱うJSONファイル(src/_data/site.json)
{
"name": "サンプルサイト",
"copyright": "© 2026 サンプルサイト"
}
ヘッダーのコンポーネント(src/_components/site-header.webc)
<header>
<p @text="$data.site.name"></p>
<nav>
<a href="/">ホーム</a>
<a href="/services/">サービス</a>
<a href="/about/">会社概要</a>
</nav>
</header>
フッターのコンポーネント(src/_components/site-footer.webc)
<footer>
<small @text="$data.site.copyright"></small>
</footer>
共通レイアウト(src/_includes/base.webc)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title @text="title"></title>
</head>
<body>
<site-header></site-header>
<main @raw="content"></main>
<site-footer></site-footer>
</body>
</html>
トップページ(src/index.webc)
---
layout: base.webc
title: ホーム
description: ようこそ、サンプルサイトへ!
---
<h1 @text="title"></h1>
<p @text="description"></p>
サービスページ(src/services.webc)
---
layout: base.webc
title: サービス
description: 提供サービスを紹介します。
---
<h1 @text="title"></h1>
<p @text="description"></p>
会社概要ページ(src/about.webc)
---
layout: base.webc
title: 会社概要
description: 私たちについて紹介します。
---
<h1 @text="title"></h1>
<p @text="description"></p>
ここまでできたら、後はnpm run buildコマンドを実行することで(環境構築がうまくできていれば)、先のHTMLファイルが出力されます。
トータルのファイル数は増えていますが、ヘッダーやフッターの重複はなくなり、文言を外出しすることもでき、各ページはシンプルになっています。
さらに複雑なWebサイトの管理
より管理が複雑になる状況としては、似たような構成のWebサイトを複数管理しなければいけないケースが考えられます。
例えば、同一のサービスを展開する店舗を複数持つような企業で、店舗ごとにドメインを変えてWebサイトを運用するようなケースです。サロンやジムなどのイメージでしょうか。
この場合、サイト全体の構成や、ベースとなるレイアウトはどの店舗のWebサイトでも共通ではあるものの、コンテンツが店舗によって若干異なるケースが出てきます。
このようなケースもEleventyで実現可能ですが、今回はコンポーネントやTypeScriptを活用しやすいAstroを使って実装してみます。
ここでは、東京と大阪の2店舗で運営しているフィットネスジムをサンプルに考えます。それぞれの店舗ごとにWebサイトを持ち、異なるドメインで運用しているケースとします。コンテンツの内容は異なるものの、Webサイトの構成やレイアウトは同じと想定します。
ここでも環境構築やAstroの記述方法についての詳細な解説は省略します。「こういうことができる」というイメージを持ってもらうためのサンプルになります。詳しくは公式サイトなどを参照ください。
最終的なゴールとしては、ビルド時にパラメータを指定することで、それぞれの店舗ごとのWebサイトを出力できるようにします。
# 東京店舗用のビルド
npm run build:tokyo
# 大阪店舗用のビルド
npm run build:osaka
まずはpackage.jsonのscriptsで、店舗ごとのビルドや開発サーバーの起動ができるようにします。
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"dev:tokyo": "cross-env STORE=tokyo astro dev",
"dev:osaka": "cross-env STORE=osaka astro dev",
"build:tokyo": "cross-env STORE=tokyo astro build",
"build:osaka": "cross-env STORE=osaka astro build"
}
}
※WindowsとmacOSの両方で同じコマンドが使えるように、事前にcross-envをインストールしておきます。npm install -D cross-env でインストールします。
店舗ごとの情報を整理します。各店舗ごとの文言を定数データで定義します。
src/data/stores.ts
export const stores = {
tokyo: {
name: "Fit Studio 東京店",
description: "東京・渋谷のフィットネスジムです。",
address: "東京都渋谷区〇〇1-2-3",
hours: "9:00〜22:00",
message: "東京で楽しくフィットネスを始めましょう!",
},
osaka: {
name: "Fit Studio 大阪店",
description: "大阪・梅田のフィットネスジムです。",
address: "大阪府大阪市北区〇〇4-5-6",
hours: "10:00〜21:00",
message: "大阪で理想の身体を目指しましょう!",
},
};
export type StoreKey = keyof typeof stores;
環境変数から対象の店舗のデータを取得する処理を定義します。
src/data/currentStore.ts
import { stores, type StoreKey } from "./stores";
const storeKey = process.env.STORE ?? "tokyo";
if (!Object.hasOwn(stores, storeKey)) {
throw new Error(`店舗が見つかりません: ${storeKey}`);
}
export const currentStore = stores[storeKey as StoreKey];
共通のレイアウトを定義します。
ここではシンプルなレイアウトにしていますが、ヘッダーやフッターなども共通化することが可能です。
Astroでは、—で囲まれた部分にJavaScriptやTypeScriptを記述できます。<slot /> の部分は、コンテンツが挿入される部分になります。
src/layouts/BaseLayout.astro
---
interface Props {
title: string;
storeName: string;
}
const { title, storeName } = Astro.props;
---
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title} | {storeName}</title>
</head>
<body>
<main>
<slot />
</main>
</body>
</html>
トップページを作成します。
src/pages/index.astro
---
import BaseLayout from "../layouts/BaseLayout.astro";
import { currentStore } from "../data/currentStore";
const store = currentStore;
---
<BaseLayout title="ホーム" storeName={store.name}>
<h1>{store.name}へようこそ!</h1>
<p>{store.description}</p>
<p>{store.message}</p>
</BaseLayout>
最後に、ビルドの設定をします。
astro.config.mjs
import { defineConfig } from "astro/config";
const store = process.env.STORE ?? "tokyo";
const validStores = ["tokyo", "osaka"];
if (!validStores.includes(store)) {
throw new Error(`店舗が見つかりません: ${store}`);
}
export default defineConfig({
output: "static",
outDir: `./dist/${store}`,
});
ここまでできたら、npm run build:tokyoのコマンドを実行してビルドします。大阪用の店舗のサイトをビルドする場合はnpm run build:osakaとします。
ビルドが成功すると、dist/tokyo(またはdist/osaka)配下に、HTMLファイルが生成されます。
生成されたHTMLは以下のようになります。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>ホーム | Fit Studio 東京店</title>
</head>
<body>
<main>
<h1>Fit Studio 東京店へようこそ!</h1>
<p>東京・渋谷のフィットネスジムです。</p>
<p>東京で楽しくフィットネスを始めましょう!</p>
</main>
</body>
</html>
このようにして生成したHTML・CSS・JavaScriptなどのファイルは、一般的な静的Webサイトと同様に、Webサーバーや静的ホスティングサービスに配置するだけで公開できます。公開環境にAstroやNode.jsをインストールする必要はありません。
ここでは文言が異なるだけの想定で作りましたが、実際には店舗ごとに特定の要素が追加されるなど、もっと複雑になることが想定されます。その場合は必要に応じて分岐の処理なども増えていくことが予想されますが、ソースコードを一元管理できるのでメリットは大きいでしょう。
先に紹介したEleventyと比べると、個人的にはAstroの方が独自の仕組みや記法に慣れるまで時間がかかる印象です。しかし、その分コンポーネントやTypeScriptを活用して、柔軟にWebサイトを開発できるというメリットもあります。
開発効率と運用コストのバランス
この記事では、HTML・CSS・JavaScriptで構成された静的Webサイトを生成するSSGについて紹介しました。
実際のところ、AstroのようなSSGのフレームワークを使いこなすには、フレームワーク独自の仕組みやコンポーネントの扱い方などを学ぶ必要があり、フロントエンド開発のスキルが必要になるとも感じています。そのため、普段からHTMLやCSSを中心にWebサイトを開発している人にとっては、学習のハードルを高く感じるかもしれません。
一方で、PHPのようなサーバーサイドの言語を使ってHTMLを出力する仕組みであれば、既にその言語の開発経験がある人にとっては、比較的簡単に実装できるかもしれません。
そのため、新しくSSGのフレームワークを学習するよりも、使い慣れた言語やフレームワークでWebアプリケーションとして開発する方が、開発効率が良いと感じることもあるでしょう。
しかし、先にも述べた通り、Webアプリケーションとして開発すると、運用面で考慮しなければいけないことが増えて、運用コストが上がります。
2026年10月、PHPに重大な脆弱性があることが公表され、国内でも一部のWebサイトで緊急メンテナンスや一時停止が行われました(参考:IPA PHPの脆弱性対策について)。一般的なホームページでも問い合わせや受付フォームを設けることを目的に、PHPやWordpressが使われているWebサイトは多く、一時的にアクセスができなくなるホームページもありました。
PHPを使っているかどうかに限らず、Webアプリケーションを使っている場合、このようなセキュリティリスクの対応が求められる可能性があることを考慮しなくてはいけません。対応中は対象のWebサイトを止める必要も出てきます。一方、静的なWebサイトであれば、セキュリティ面や開発・運用面で考慮すべきことはWebアプリケーションに比べてかなり少なくなります。そのような観点から、コンテンツが静的なWebサイトでも完結できる内容であれば、静的なWebサイトを選択することで運用上のメリットが多くあります。
問い合わせフォームが必要なWebサイトであっても、静的なWebサイトで十分なところはSSGを使って作り、入力フォームが必要な部分は別サービスを利用してリンクを設置する、またはフォームを埋め込む、という運用も考えられます。もちろん、SSGを使えばセキュリティ上の問題がなくなるわけではありません。しかし、公開環境で動作するプログラムや管理すべきサーバーを減らすことで、攻撃対象や運用上の負担を小さくできる可能性があります。
Webサイトを開発する際には、どうしても開発時の効率や、実装のしやすさを重視した技術選定をしてしまいがちです。しかし、Webサイトは開発して終わりではなく、公開した後も長期間にわたって運用していくことになります。
開発時には少し手間がかかったとしても、運用の負担を減らすことができれば、長期的に見てメリットが大きくなることもあります。
もちろん、SSGがあらゆるWebサイトに適しているわけではありません。しかし、要件を満たすことができるのであれば、Webアプリケーションとして開発する前に、静的Webサイトとして実現できないかを検討してみる価値はあると思います。
技術選定をする際には、開発時の効率だけでなく、公開後の運用まで含めて考えることが大切です。
// related


