Image Description
Takayuki KOBAYASHI

ホームページ、SSGのススメ

ホームページをSSG(静的サイト生成)で作るメリットと、11ty(Eleventy)を使った具体的な実装方法を解説します。共通部品・デザインテンプレート・ブログテンプレート・SEO対策・問い合わせフォーム・GitLab CIとDockerによるデプロイまで、実際の構成を交えてご紹介します。

Image Description

🧭 はじめに

「ホームページは、WordPress で作るもの」。そう考えている方は少なくありません。もちろん WordPress は今も有力な選択肢です。ただ、企業サイトやサービスサイトのように、更新頻度がそれほど高くなく、表示速度と安全性を重視したい サイトでは、SSG(Static Site Generator:静的サイトジェネレーター)が非常に相性のよい選択肢になります。

私たち PLAY TECHNOLOGY のコーポレートサイトも、11ty(Eleventy)を使ったSSGで構築しています。本記事では、SSG を選ぶ理由から、11ty での共通部品・テンプレート設計、SEO対策、問い合わせフォーム、GitLab CIとDocker によるデプロイまで、実際の構成に沿ってご紹介します。

これからホームページのリニューアルを検討している方、既存サイトの運用に課題を感じている方の参考になれば幸いです。

⚡ SSG(静的サイト生成)とは

SSGは、ビルド時に HTML を生成し、その静的なファイルを Webサーバーから配信する仕組みです。アクセスのたびにサーバー側で HTML を組み立てる SSR(サーバーサイドレンダリング)や、ブラウザ側で画面を描画する SPA(シングルページアプリケーション)とは、データの流れが異なります。

方式 HTMLの生成タイミング 得意なこと 注意点
SSG ビルド時 表示速度、安全性、運用コスト、SEO 更新にはビルドが必要
SSR リクエスト時 ユーザーごとの動的な表示 サーバーとアプリの運用が必要
SPA ブラウザ側 リッチな画面遷移、アプリ的な操作 初期表示とSEO対策に工夫が必要
CMS(従来型) リクエスト時 非エンジニアでも更新しやすい サーバー・DB・プラグインの保守が必要

SSG の大きな特徴は、公開されるのが HTML・CSS・画像などの静的ファイルだけ という点です。データベースやアプリケーションサーバーが公開環境に存在しないため、攻撃面を小さくできます。また、CDN にそのまま置けるので、国内外からのアクセスにも安定して高速に応答できます。

一方で、問い合わせフォームや全文検索、会員機能など「動き」が必要なものは、外部APIやサーバーレス関数と組み合わせる必要があります。この点は後ほど、問い合わせフォームの実装例で詳しく説明します。

🤔 なぜホームページに SSG なのか

企業ホームページの多くは、次のような特徴を持っています。

  • 公開するページ数が数十ページ程度
  • 更新は月に数回〜数週間に1回程度
  • 商品・サービス・会社情報など、内容が比較的安定している
  • 問い合わせや採用応募など、コンバージョンへの導線が重要
  • 表示速度とセキュリティを重視したい

このようなサイトでは、SSGのメリットを活かしやすい一方、デメリットは小さくなります。

観点 SSGのメリット
表示速度 事前生成したHTMLを配信するため、初期表示が速い
セキュリティ DBや管理画面を公開しなければ、攻撃面を大幅に減らせる
運用コスト 静的ファイル配信のみで済み、サーバー要件が軽い
SEO HTMLが最初から存在するため、クローラーが内容を理解しやすい
バックアップ Gitで履歴管理でき、復元も容易
開発体験 テンプレートとMarkdownで、構造を保ったまま量産できる

もちろん、すべてのサイトにSSGが向くわけではありません。更新を非エンジニアが頻繁に行う場合や、リアルタイムの在庫・価格・ユーザーごとの表示が必要な場合は、CMSやSSRのほうが適しています。サイトの目的と更新体制に合わせて、方式を選ぶこと が重要です。

🛠️ 11ty(Eleventy)を選んだ理由

SSGにはさまざまな選択肢があります。私たちが11tyを選んだ理由は、次のとおりです。

  • シンプルで学習コストが低い:Node.jsがあれば始められ、特別なフレームワークを覚える必要がない
  • テンプレートが柔軟:Nunjucks、Liquid、Markdownなどを組み合わせられる
  • データとテンプレートを分離できる:サイト設定やナビゲーションをJSONで管理し、全ページから参照できる
  • コレクションとページネーションが強力:ブログ記事の一覧・タグ・ページ送りを標準機能で組める
  • 出力が純粋な静的ファイル:特殊なランタイムを本番環境に持ち込まない
  • ビルドが速い:記事が増えても、日常的な更新のビルド時間が短い

11tyはバージョン3系で、Node.js 20以上での利用が前提になっています。最小構成はとてもシンプルです。

npm init -y
npm install -D @11ty/eleventy
npx @11ty/eleventy --serve --port=9002

--serve を付けると、ローカルサーバーが起動し、ファイル変更を検知して自動で再ビルドされます。開発中の確認が快適です。
--port を付けると、ローカルサーバーのポートを指定できます。

ディレクトリ構成

実際のサイトでは、次のような構成にしています。

project/
├── dist/               # 生成した静的HTML
├── src/
│   ├── _data/          # サイト設定、ナビゲーションなどの共通データ
│   ├── _includes/      # ヘッダー、フッター、CTAなどの共通部品
│   ├── _layouts/       # ページの土台となるレイアウト
│   ├── blog/           # ブログ記事(Markdown)
│   ├── assets/         # CSS、JavaScript、画像などの静的資産
│   ├── index.njk       # トップページ
│   ├── contacts.njk    # お問い合わせページ
│   ├── sitemap.njk     # サイトマップXML
│   └── robots.njk      # robots.txt
├── eleventy.config.js
└── package.json

src を入力、dist を出力にする設定は、eleventy.config.js で行います。

export default function (eleventyConfig) {
  return {
    templateFormats: ["md", "njk", "html", "liquid"],
    markdownTemplateEngine: "njk",
    htmlTemplateEngine: "njk",
    dir: {
      input: "src",
      includes: "_includes",
      layouts: "_layouts",
      output: "dist",
    },
  };
}

Markdownの中でもNunjucksの記法を使えるようにしておくと、記事内に共通部品を差し込んだり、データを参照したりできるようになります。

🧩 共通部品でサイト全体を揃える

ホームページ制作で重要になるのが、共通部品の設計 です。ヘッダー、フッター、ナビゲーション、CTA、パンくずリストなどを各ページにコピーしていると、修正のたびに全ページを直すことになり、表記ゆれや更新漏れの原因になります。

11tyでは、_includes に部品を置き、レイアウトから読み込むことで共通化できます。

{# src/_layouts/base.njk #}
<!DOCTYPE html>
<html lang="ja">
<head>
  {% include "_seo-head.njk" %}
</head>
<body>
  {% include "_header.njk" %}

  <main>
    {{ content | safe }}
  </main>

  {% include "_footer.njk" %}
</body>
</html>

各ページはレイアウトを指定するだけで、共通のヘッダーとフッターが適用されます。

---
layout: base
title: サービス紹介|PLAY TECHNOLOGY
description: クラウドを活用したシステム開発サービスのご紹介です。
---

<section class="container content-space-2">
  <h1 class="display-5">サービス紹介</h1>
  <p>私たちが提供するサービスの概要をご紹介します。</p>
</section>

ナビゲーションのように全ページで使う情報は、_data 配下のJSONに置くと管理しやすくなります。

{
  "items": [
    { "label": "サービス", "url": "/services/" },
    { "label": "実績", "url": "/works/" },
    { "label": "ブログ", "url": "/blog/" },
    { "label": "会社情報", "url": "/company/" },
    { "label": "お問い合わせ", "url": "/contacts" }
  ]
}

共通部品を徹底すると、デザインの一貫性が保たれるだけでなく、SEOの基本情報を全ページに漏れなく適用できる という副次的な効果もあります。

🎨 デザインテンプレート

デザインテンプレートは、ページの「型」です。トップページ、下層ページ、ブログ記事、問い合わせページなど、用途ごとにレイアウトを用意します。

11tyのレイアウトは、Nunjucksのテンプレート継承で組み立てられます。

{# src/_layouts/page.njk #}
{% extends "base.njk" %}

{% block content %}
  <div class="container content-space-t-3">
    <div class="w-lg-50">
      <h1 class="display-5">{{ title }}</h1>
      {% if description %}
        <p class="lead">{{ description }}</p>
      {% endif %}
    </div>

    <div class="row justify-content-lg-center">
      <div class="col-lg-8">
        {{ content | safe }}
      </div>
    </div>
  </div>
{% endblock %}

デザインをテンプレート化する際は、次の点を意識しています。

  • 余白とグリッドを統一する:セクション間の余白やコンテナ幅をテンプレート側で決める
  • 見出しレベルを守る:ページごとに h1 は1つ、以降は h2、h3 と階層を崩さない
  • 再利用できるパターンを部品化する:ヒーロー、カード、料金表、FAQ、CTAなど
  • レスポンシブを前提にする:画像や表が小さい画面で横にはみ出さないようにする
  • デザイン変更に強い構造にする:色や余白はユーティリティクラスやCSS変数に寄せる

テンプレートを整えておくと、新しいサービスページを追加するときも、文章と画像を用意するだけで一定の品質を保てます。

📝 ブログテンプレート

ブログは、ホームページの更新性とSEOの両面で重要な役割を担います。11tyでは、Markdownで書いた記事を「コレクション」としてまとめ、一覧ページやタグページを自動生成できます。

記事の front matter

各記事の先頭に、タイトル・説明・日付・タグなどを記載します。

---
title: ホームページ、SSGのススメ
description: SSGと11tyを使ったホームページ制作の実践的なポイントを解説します。
date: 2026-10-23 10:00:00
tags:
  - SSG
  - 11ty
  - SEO
layout: blog
image: "/assets/img/blog/example.jpg"
permalink: /blog/ssg-homepage.html
author: Takayuki KOBAYASHI
---

記事一覧とページ送り

eleventy.config.js でブログ用のコレクションを定義します。公開日が未来の記事を除外しておくと、予約投稿のような運用もできます。

eleventyConfig.addCollection("blogs", (collection) => {
  const now = new Date();
  return collection
    .getFilteredByGlob("./src/blog/*.md")
    .filter((item) => item.date <= now)
    .sort((a, b) => b.date - a.date);
});

一覧ページでは、コレクションをページネーションで分割します。

---
pagination:
  data: collections.blogs
  size: 6
  alias: posts
permalink: "/blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}index.html"
---

<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
  {% for post in posts %}
    <div class="col">
      <a class="card h-100" href="{{ post.url }}">
        {% if post.data.image %}
          <img class="card-img-top" src="{{ post.data.image }}" alt="{{ post.data.title }}">
        {% endif %}
        <div class="card-body">
          <time datetime="{{ post.date | htmlDateString }}">{{ post.date | htmlDateString }}</time>
          <h2 class="h5 mt-2">{{ post.data.title }}</h2>
          <p class="text-muted mb-0">{{ post.data.description }}</p>
        </div>
      </a>
    </div>
  {% endfor %}
</div>

タグページも、collections とページネーションを組み合わせて自動生成できます。記事を追加するたびに一覧・タグ・サイトマップが更新されるため、運用担当者の手作業を減らせます。

関連記事の表示

記事末尾には、同じタグを持つ記事を最大3件表示する、といった制御も容易です。

eleventyConfig.addNunjucksFilter("related", function (collection = []) {
  const { tags, page } = this.ctx;
  return collection
    .filter((post) => post.url !== page.url)
    .filter((post) => tags?.some((tag) => post.data.tags?.includes(tag)))
    .slice(0, 3);
});

ブログテンプレートを整えることで、記事を書く人がHTMLやデザインを意識せず、内容に集中できる 環境になります。

✍️ Markdown での記載

11tyのブログは、Markdownファイルを src/blog/ に追加するだけで記事になります。HTMLを直接書かなくても、見出し・表・コード・画像・リンクを表現できるため、エンジニア以外の方が原稿を書く場合でも扱いやすい形式です。

記事ファイルの作成

記事は src/blog/ 配下に、内容が分かる英数字のファイル名で作成します。ファイル名はURLとは直接連動させず、front matterの permalink で公開URLを明示すると管理しやすくなります。

src/blog/ssg-homepage.md

ファイルの先頭には、必ずfront matterを記載します。layout: blog を指定することで、ブログ用のデザインテンプレートが適用されます。

---
title: 記事のタイトル
description: 検索結果や一覧に表示される記事の要約です。120〜160文字程度を目安にします。
date: 2026-11-01 10:00:00
tags:
  - SSG
  - 11ty
  - SEO
layout: blog
image: "/assets/img/blog/example.jpg"
permalink: /blog/example.html
author: Takayuki KOBAYASHI
---

date を未来の日時にしておくと、コレクションのフィルタ設定により公開日までは一覧やサイトマップに掲載されません。予約投稿的な運用にも利用できます。

基本の Markdown 記法

ブログ記事でよく使う記法は、次のとおりです。

## 見出し2

本文です。段落を分ける場合は、空行を入れます。

**強調** と *斜体*、`インラインコード` を組み合わせられます。

- 箇条書き
- 箇条書き

1. 番号付きリスト
2. 番号付きリスト

> 引用文はこのように記載します。

[サービス紹介](/services/)

![画像の説明](/assets/img/blog/example.jpg)

| 項目 | 内容 |
| --- | --- |
| SSG | 静的サイト生成 |
| SEO | 検索エンジン最適化 |

ページタイトルはテンプレート側で h1 として出力されるため、本文の見出しは ## から始めるのが基本です。# を本文中で使うと、1ページに h1 が複数存在する状態になり、SEOやアクセシビリティの観点で好ましくありません。

また、日本語の文章では改行の扱いに注意が必要です。Markdownでは単純な改行がそのまま改行として表示されない場合があるため、段落を分けたいときは空行を入れます。同じ段落内で改行したい場合は、行末に半角スペースを2つ入れるか、HTMLの <br> を使います。

画像とキャプション

画像は src/assets/img/blog/ に配置し、記事からはサイトルートからの絶対パスで参照します。ファイル名は英小文字とハイフンで統一し、内容が分かる名前にすると管理しやすくなります。

![SSGのビルドから配信までの流れ](/assets/img/blog/ssg-flow.png)

画像には必ず alt(代替テキスト)を設定します。alt は画像が表示されない環境や、スクリーンリーダーを利用する方にとって重要な情報になります。記事の内容を説明する具体的な文言を入れ、単に「画像1」のような名前は避けます。

キャプションを付けたい場合は、Markdown 内に HTML を直接記載できます。

<figure>
  <img src="/assets/img/blog/ssg-flow.png" alt="SSGのビルドから配信までの流れ">
  <figcaption class="text-muted small">
    ビルド時にHTMLを生成し、静的ファイルとして配信します。
  </figcaption>
</figure>

画像は表示サイズに合わせて圧縮し、必要以上に大きなファイルをアップロードしないようにします。記事一覧のサムネイルには front matter の image が使われるため、一覧映えする画像を指定するとクリック率の改善が期待できます。

テーブルとコードブロック

比較表や手順の整理にはテーブルが便利です。ただし、列数を増やしすぎるとスマートフォンで読みにくくなるため、3〜4列程度を目安にします。表の前後には空行を入れ、Markdown として正しく認識されるようにしてください。

コードを掲載する場合は、言語名を付けたコードフェンスを使います。シンタックスハイライトが適用され、読みやすさが向上します。

```js
const message = "Hello, SSG!";
console.log(message);
```

主な言語指定は js、ts、bash、html、css、yaml、json、njk、text などです。長い行は適宜折り返し、コピーしやすい状態にしておきます。

注意点として、このサイトではMarkdownもNunjucksのテンプレートとして処理されます。記事内にテンプレート構文のサンプルをそのまま書くと、ビルド時に評価されてしまうことがあります。テンプレート記法を含むコード例を掲載する場合は、Eleventyのテンプレート処理を無効化するブロックで囲むか、表示用にエスケープしてください。

リンクとCTA

サイト内のページへリンクする場合は、/services/ や /contacts のようにサイトルートからのパスで記載します。外部サイトへのリンクは https:// から始まるURLを使い、必要に応じてリンク先の名称を明確にします。

[クラウド開発のご相談](/contacts)

記事の途中や末尾にCTAを置く場合は、共通のCTA部品やボタンのHTMLを利用できます。問い合わせページへの導線は、記事の内容と自然につながる位置に配置することが大切です。

<div class="d-grid d-sm-flex gap-3 mt-4">
  <a class="btn btn-primary btn-lg" href="/contacts">お問い合わせ</a>
  <a class="btn btn-outline-dark btn-lg" href="/blog/">他の記事を読む</a>
</div>

公開前のチェック

記事を公開する前に、次の項目を確認します。

  • title と description が記事の内容を正しく表しているか
  • 見出しが ## から始まり、階層が崩れていないか
  • 画像の alt が設定されているか
  • 内部リンク・外部リンクが正しいか
  • テーブルがスマートフォンで横にはみ出していないか
  • コードブロックの言語指定とインデントが正しいか
  • permalink が既存記事と重複していないか
  • date と tags が正しいか

ローカルでは npm run dev でプレビューし、npm run build で本番と同じビルドが通ることを確認します。公開後は、Search Consoleやアクセス解析の結果をもとに、タイトルや本文を継続的に改善していきます。

運用のコツ

Markdownでの記事運用では、次の点を意識すると品質が安定します。

  • 1記事につき1つのテーマに絞り、結論を先に示す
  • 冒頭で読者の課題を明確にし、目次代わりの見出しを置く
  • 比較には表、手順には番号付きリスト、実装にはコードブロックを使う
  • 専門用語には短い説明を添える
  • 記事の更新時は dateModified も更新する
  • 公開済みのURLは安易に変更せず、変更する場合はリダイレクトを設定する

Markdownは一見シンプルですが、構造を意識して書くことで、読みやすさとSEOの両方を高められます。テンプレートと組み合わせることで、少ない労力で品質の高い記事を継続的に公開できるようになります。

🔍 SEO 対策

SSGはSEOに有利な面がありますが、何もしなくても検索上位に表示されるわけではありません。ページごとの情報設計と、クローラーへの正しい伝達が必要です。

ページごとのメタ情報

title、description、keywords は、ページのfront matterから設定できるようにします。未設定の場合に備えて、サイト全体の既定値も用意します。

{% set defaultDescription = "クラウドを活用したシステム開発を行うPLAY TECHNOLOGYの公式サイトです。" %}
<title>{{ title }}</title>
<meta name="description" content="{{ description | default(defaultDescription) }}">
<meta name="keywords" content="{{ keywords }}">

<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description | default(defaultDescription) }}">
<meta property="og:image" content="{{ image | default('/assets/img/ogp-default.jpg') }}">
<meta property="og:url" content="{{ page.url | htmlBaseUrl(site.url) }}">
<meta property="og:type" content="article">
<meta name="twitter:card" content="summary_large_image">

description は検索結果のスニペットとして表示されるため、ページ内容を端的に表す120〜160文字程度 を目安にします。キーワードを詰め込みすぎず、読者が「このページに知りたいことがありそう」と判断できる文言にすることが大切です。

構造化データ

ブログ記事には、BlogPosting の構造化データを出力しています。検索エンジンに、記事のタイトル・公開日・著者・画像などを明示できます。

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "ホームページ、SSGのススメ",
  "description": "SSGと11tyを使ったホームページ制作の実践的なポイントを解説します。",
  "datePublished": "2026-10-23",
  "dateModified": "2026-10-23",
  "author": {
    "@type": "Person",
    "name": "Takayuki KOBAYASHI"
  },
  "publisher": {
    "@type": "Organization",
    "name": "PLAY TECHNOLOGY",
    "logo": {
      "@type": "ImageObject",
      "url": "https://playtechnology.jp/assets/img/play_logo2.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://playtechnology.jp/blog/ssg-homepage.html"
  }
}
</script>

sitemap.xml と robots.txt

11tyのテンプレート機能を使って、sitemap.xml と robots.txt も自動生成できます。記事を追加するたびにサイトマップを手で更新する必要はありません。

---
permalink: /sitemap.xml
layout: false
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in collections.all %}
  <url>
    <loc>{{ page.url | htmlBaseUrl(site.url) }}</loc>
    <lastmod>{{ page.date | htmlDateString }}</lastmod>
  </url>
{% endfor %}
</urlset>

検証環境では robots.txt でクロールを拒否し、noindex を付ける運用にしています。本番環境だけが検索対象になるようにしておくと、テスト用ページが意図せずインデックスされる事故を防げます。

---
permalink: /robots.txt
eleventyExcludeFromCollections: true
---
User-agent: *
{% if env == "production" %}
Allow: /
Sitemap: {{ site.url }}sitemap.xml
{% else %}
Disallow: /
{% endif %}

そのほかの SEO・品質対策

  • 見出し構造を守る:h1 はページに1つ、本文は h2 → h3 の順で組み立てる
  • 画像にaltを付ける:内容を説明する代替テキストを設定する
  • 内部リンクを設計する:関連記事やサービスページへ自然に導線を引く
  • 表示速度を改善する:画像の圧縮、不要な JavaScript の削減、CSS の最適化
  • モバイル表示を確認する:スマートフォンでの読みやすさとタップ領域を確保する
  • Search Consoleで監視する:インデックス状況、検索クエリ、表示回数を継続的に確認する

SEO は一度設定して終わりではありません。公開後も、検索結果での表示や問い合わせ数を見ながら、タイトルやコンテンツを継続的に改善していくことが重要です。

📬 問い合わせフォームの実現

静的サイトの注意点として挙げたのが、問い合わせフォームです。静的ファイルだけでは、フォームから送信されたデータを受け取って処理することができません。そこで、外部のフォームAPIやサーバーレス関数と組み合わせます。

私たちのサイトでは、フォーム送信 API に JSON を POST し、送信後に完了メッセージを表示する構成にしています。処理の流れは次のとおりです。

  1. ユーザーがフォームに入力する
  2. reCAPTCHA で認証する
  3. ブラウザからフォーム API へ JSON を POST する
  4. API 側でバリデーションとスパム判定を行う
  5. 成功したら完了メッセージを表示し、GA4 にコンバージョンイベントを送信する

フォームの HTML

フォームは、ラベルと入力欄を正しく関連付け、必須項目を明示します。個人情報の取り扱いへの同意も忘れずに配置します。

<form id="contactForm" novalidate>
  <div class="mb-4">
    <label class="form-label" for="name">お名前 <span class="text-danger">*</span></label>
    <input
      type="text"
      class="form-control"
      id="name"
      name="name"
      autocomplete="name"
      required
    >
  </div>

  <div class="mb-4">
    <label class="form-label" for="email">メールアドレス <span class="text-danger">*</span></label>
    <input
      type="email"
      class="form-control"
      id="email"
      name="email"
      autocomplete="email"
      required
    >
  </div>

  <div class="mb-4">
    <label class="form-label" for="message">お問い合わせ内容 <span class="text-danger">*</span></label>
    <textarea
      class="form-control"
      id="message"
      name="message"
      rows="4"
      required
    ></textarea>
  </div>

  <div class="form-check mb-4">
    <input
      class="form-check-input"
      type="checkbox"
      id="privacy"
      name="privacy_consent"
      value="同意する"
      required
    >
    <label class="form-check-label" for="privacy">
      <a href="/privacy/">プライバシーポリシー</a>に同意する
    </label>
  </div>

  <div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>

  <button type="submit" class="btn btn-primary btn-lg" id="submitButton">
    送信する
  </button>
</form>

autocomplete 属性を設定すると、ブラウザの自動入力が効きやすくなり、入力の負担を減らせます。ラベルと入力欄の for / id を一致させることは、アクセシビリティの観点でも重要です。

送信処理の JavaScript

送信時は、ブラウザの標準バリデーションと reCAPTCHA の認証を確認してから、API へ JSON を送ります。

const form = document.getElementById("contactForm");
const submitButton = document.getElementById("submitButton");

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  if (!form.checkValidity()) {
    form.classList.add("was-validated");
    return;
  }

  const token = grecaptcha.getResponse();
  if (!token) {
    alert("reCAPTCHAの認証を完了してください。");
    return;
  }

  const formData = new FormData(form);
  const data = Object.fromEntries(formData.entries());
  data.recaptcha_token = token;

  submitButton.disabled = true;
  submitButton.textContent = "送信中...";

  try {
    const response = await fetch("/api/contact", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ data }),
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    document.getElementById("contactFormWrapper").classList.add("d-none");
    document.getElementById("contactFormSuccess").classList.remove("d-none");
  } catch (error) {
    alert("送信できませんでした。時間をおいて再度お試しください。");
  } finally {
    submitButton.disabled = false;
    submitButton.textContent = "送信する";
  }
});

実際の構成では、フォームAPIのエンドポイントと公開用 API キーをサイト設定から注入しています。秘密鍵や管理者用の認証情報は、絶対に HTML や JavaScript に埋め込まない ようにしてください。クライアントに置いてよいのは、公開前提のキーだけです。

セキュリティと運用のポイント

  • reCAPTCHA や honeypot でスパムを防ぐ:自動投稿による迷惑メールを減らす
  • サーバー側でもバリデーションする:ブラウザのチェックは回避できるため、API 側で必須項目・形式・文字数を検証する
  • レート制限を設ける:同一IP からの短時間の大量送信を制限する
  • 個人情報の取り扱いを明示する:利用目的、保存期間、第三者提供の有無をプライバシーポリシーに記載する
  • 送信完了を明確に伝える:成功メッセージを表示し、問い合わせ番号や受付メールを送る
  • 到達監視を行う:フォーム API のエラー率や送信数を監視し、障害に気づけるようにする

静的サイトだからといって、フォームが作れないわけではありません。静的サイトは表示に専念し、動的な処理は外部サービスに任せる という役割分担が、シンプルで保守しやすい構成につながります。

🚀 デプロイ方法

11ty でビルドした dist ディレクトリを、Docker イメージに含めて Nginx で配信する構成にしています。CI/CD は GitLab CI を使い、ステージング環境と本番環境をブランチで分けています。

Dockerfile

Node.js でビルドし、完成した静的ファイルだけを Nginx イメージにコピーします。本番イメージにNode.js やソースコードを含めないため、イメージを小さく保てます。

FROM node:20 AS builder
WORKDIR /work

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
ARG MODE=production
ENV ELEVENTY_ENV=$MODE
ENV NODE_ENV=production
RUN npm run build

FROM nginx:1.27-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /work/dist /usr/share/nginx/html
EXPOSE 80

GitLab CI

staging ブランチへの push でステージング環境を、production ブランチへの push で本番環境をビルド・デプロイします。イメージにはコミット SHA やパイプラインID をタグとして付けるため、どのコミットが公開されているかを追跡できます。

stages:
  - build
  - deploy

build:
  stage: build
  image: docker:27-cli
  services:
    - docker:27-dind
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build --build-arg MODE=production -t "$CI_REGISTRY_IMAGE:$CI_PIPELINE_IID" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_PIPELINE_IID"
  only:
    - production

deploy:
  stage: deploy
  image: kroniak/ssh-client
  script:
    - mkdir -p ~/.ssh
    - echo "$SSH_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" \
        "docker pull $CI_REGISTRY_IMAGE:$CI_PIPELINE_IID && \
         docker rm -f $CONTAINER_NAME || true && \
         docker run -d --name $CONTAINER_NAME --restart=always \
           -p 8084:80 $CI_REGISTRY_IMAGE:$CI_PIPELINE_IID"
  only:
    - production

認証情報や SSH鍵は、GitLab の CI/CD変数に登録し、リポジトリには含めません。環境ごとの差異は ELEVENTY_ENV で切り替え、本番だけ解析タグや Search Console の確認トークンを有効にする、といった制御を行っています。

運用上のポイント

  • ロールバックしやすくする:イメージタグを残しておけば、以前のバージョンに戻せる
  • デプロイ前にビルドを検証する:リンク切れ、HTMLの構文、表示崩れを確認する
  • キャッシュを適切に設定する:CSSや画像などの静的資産は長めに、HTML は短めに設定する
  • 監視を入れる:死活監視、アクセスログ、エラーログを確認できるようにする
  • バックアップを取る:Git の履歴に加え、必要に応じて配信物のバックアップも保持する

SSG はビルドとデプロイの仕組みさえ整えてしまえば、その後の運用はとても軽くなります。記事更新も、Markdown を追加してマージするだけです。

💬 SSG 化のご依頼

ここまで、SSG と 11ty を使ったホームページ制作の要点をご紹介しました。PLAY TECHNOLOGY では、既存ホームページの SSG 化や、新規サイトの設計・構築を承っています。

こんなご相談をいただいています

  • 現在のホームページが遅く、スマートフォンでの表示に不満がある
  • WordPress のプラグイン更新やセキュリティ対応に手間がかかっている
  • デザインが古くなり、採用や問い合わせにつながる導線を見直したい
  • ブログやお知らせを、もっと更新しやすい仕組みにしたい
  • SEO を意識したページ構造・メタ情報・サイトマップを整えたい
  • 問い合わせフォームからのリード獲得を改善したい

ご支援の流れ

工程 内容
ヒアリング 現状の課題、サイトの目的、必要なページ、更新体制を整理します
設計 サイトマップ、情報設計、デザイン方針、技術構成を決定します
テンプレート構築 共通部品、デザインテンプレート、ブログテンプレートを実装します
コンテンツ移行 既存原稿や画像を移行し、必要に応じて原稿整理も支援します
SEO・フォーム実装 メタ情報、構造化データ、サイトマップ、問い合わせフォームを整備します
デプロイ CI/CD、Docker、Nginx などの配信基盤を構築します
運用支援 記事追加、改善提案、保守運用を必要に応じて支援します

ページ数やデザインの自由度、既存サイトからの移行範囲によって期間・費用は変わります。まずは現状のサイトと課題をお聞かせください。構成や進め方、概算のご提案をいたします。

✅ まとめ

ホームページに SSG を採用すると、表示速度・安全性・運用コスト・SEO の面で多くのメリットを得られます。一方で、問い合わせフォームや更新フローなど、動的な要素の扱いを事前に設計しておくことが重要です。

11ty は、共通部品・デザインテンプレート・ブログテンプレートをシンプルに組み立てられる SSG です。Markdownで記事を書き、Git で履歴を管理し、CI/CD で自動デプロイする。この流れを一度作ってしまえば、ホームページの運用は驚くほど軽くなります。

「今のホームページを、もっと速く、安全で、更新しやすくしたい」。そんなご相談があれば、ぜひお気軽にお問い合わせください。

関連コンテンツ