Spring Boot × Thymeleafで「共通ヘッダー」「共通フッター」「共通CSS/JS」を1か所管理にする最短ルート。
th:fragment / th:replace を軸に、実務でもすぐ使えるディレクトリ構成・サンプルHTML・Maven/Gradle設定・FAQまで丁寧に解説します。
なぜ共通化が必要?(メリット3つ)
- 保守性UP:ヘッダーのリンク1つ直すだけで、全ページ一括反映。
- 表示品質の統一:UIのブレを排除して、ブランド/UXを守る。
- 開発速度UP:新規ページは「中身」だけ作ればOK。
メンター:まずは置き場所のルールを決めるのが成功のコツ!
プロジェクト構成(推奨ディレクトリ)
src/
└─ main/
├─ java/…(省略)
└─ resources/
├─ static/
│ ├─ css/
│ │ └─ app.css
│ └─ js/
│ └─ app.js
└─ templates/
├─ _fragments/
│ ├─ header.html
│ └─ footer.html
├─ _layout/
│ └─ base.html
└─ page/
└─ home.html
まーくん:「_fragments」「_layout」で役割が見える化されるね!
Thymeleafフラグメントの作り方
th:fragment で「この要素はパーツです」と宣言します。
header.html(共通ヘッダー)
<!-- templates/_fragments/header.html -->
<header th:fragment="siteHeader(title)" class="site-header">
<div class="container">
<a href="/" class="logo">MyApp</a>
<nav class="nav">
<a th:classappend="${active=='home'} ? 'active'" th:href="@{/}">Home</a>
<a th:classappend="${active=='products'} ? 'active'" th:href="@{/products}">Products</a>
<a th:classappend="${active=='about'} ? 'active'" th:href="@{/about}">About</a>
</nav>
<button class="nav-toggle" aria-label="メニュー">☰</button>
</div>
</header>
footer.html(共通フッター)
<!-- templates/_fragments/footer.html -->
<footer th:fragment="siteFooter" class="site-footer">
<div class="container">
<p>© <span th:text="${#calendars.format(#calendars.createNow(), 'yyyy')}">2025</span> MyApp. All rights reserved.</p>
</div>
</footer>
メンター:フラグメントは引数を受け取れる。例:siteHeader(title)。
レイアウト(ベースHTML)と差し込み
全ページの骨組みを _layout/base.html にまとめ、各ページは中身だけに集中。
base.html(ベース)
<!-- templates/_layout/base.html -->
<!doctype html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${title} ?: 'MyApp'">MyApp</title>
<link rel="stylesheet" th:href="@{/css/app.css(v=${appVersion})}">
</head>
<body>
<div th:replace="~{_fragments/header :: siteHeader(${title})}"></div>
<main class="container" th:fragment="content">
<!-- 子ページがここを差し替える -->
<div th:replace="${content}"></div>
</main>
<div th:replace="~{_fragments/footer :: siteFooter}"></div>
<script th:src="@{/js/app.js(v=${appVersion})}"></script>
</body>
</html>
home.html(ページ本体)
<!-- templates/page/home.html -->
<!doctype html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<body>
<div th:replace="~{_layout/base :: content(~{::section})}" th:with="title='ホーム', active='home'">
<section>
<h1>Welcome</h1>
<p>これはトップページの本文です。</p>
</section>
</div>
</body>
</html>
まーくん:base :: content(~{::section}) の書き方、パッと理解できた!
共通CSS/JSとキャッシュ対策
src/main/resources/static/ 配下に置くと、/css/app.css のように配信されます。
キャッシュ更新にはクエリ ?v=${appVersion} を付与。
app.css(レスポンシブ+ダークモード配慮)
:root { --bg:#0b1220; --text:#e5e7eb; --muted:#94a3b8; --brand:#22d3ee; --card:#0f172a; }
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font:16px/1.8 system-ui}
.container{max-width:1100px;margin:auto;padding:16px}
.site-header,.site-footer{background:#0c1526;border-bottom:1px solid #17223a}
.logo{font-weight:700;color:#e2e8f0;text-decoration:none}
.nav{display:flex;gap:16px}
.nav a{color:#cbd5e1;text-decoration:none;padding:8px 10px;border-radius:10px}
.nav a.active{outline:2px solid var(--brand)}
.nav-toggle{display:none}
@media (max-width:768px){
.nav{display:none}
.nav.open{display:flex;flex-direction:column;margin-top:8px}
.nav-toggle{display:inline-block;background:#0f172a;color:#e2e8f0;border:1px solid #1f2a44;border-radius:10px;padding:8px 12px}
}
.lead{color:var(--muted)}
.toc{background:var(--card);border:1px solid #1f2a44;border-radius:16px;padding:12px;margin:18px 0}
.eyecatch svg{width:100%;height:auto;border-radius:24px;display:block}
.balloon{display:flex;gap:12px;align-items:flex-start;margin:16px 0}
.balloon .avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#22d3ee,#a78bfa)}
.balloon p{background:#0f172a;border:1px solid #1f2a44;border-radius:14px;padding:10px 12px;margin:0;max-width:720px}
code{background:#0f172a;border:1px solid #1f2a44;border-radius:6px;padding:2px 6px}
pre{background:#0b1220;border:1px solid #1f2a44;border-radius:12px;padding:14px;overflow:auto}
app.js(モバイルナビ)
document.addEventListener('click', (e) => {
const btn = e.target.closest('.nav-toggle');
if (!btn) return;
const nav = document.querySelector('.nav');
nav?.classList.toggle('open');
});
バージョン付与(キャッシュバスティング)
アプリ起動時にバージョンをModelに入れておくと便利です。
// 例:起動時にバージョン文字列を用意しておく
@Bean
public String appVersion() { return String.valueOf(System.currentTimeMillis()); }
もしくは @ControllerAdvice で全コントローラへ配布(後述)。
全ページ共通のModel属性を配る(サイト名/ナビ状態/バージョンなど)
@ControllerAdvice
public class GlobalModelAdvice {
@ModelAttribute("siteName")
public String siteName() {
return "MyApp";
}
@ModelAttribute("appVersion")
public String appVersion() {
return String.valueOf(System.currentTimeMillis());
}
}
アクティブメニューの指定は各ページ側で active を渡せばOK。
メンター:@ControllerAdvice を使うと「どのページでも必要な定数」を安全に共有できる。
(発展)Thymeleaf Layout Dialectを使う方法
標準機能でも十分ですが、Thymeleaf Layout Dialect を使うと記述がさらに素直になります。
依存関係(どちらか)
<!-- Maven -->
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>3.3.0</version>
</dependency>
// Gradle
implementation "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0"
使い方(レイアウト適用)
<!-- 子ページ -->
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head><title>ページタイトル</title></head>
<body layout:decorate="~{_layout/base}">
<section layout:fragment="content">
<h1>Hello</h1>
</section>
</body>
</html>
まーくん:layout:decorate と layout:fragment、語感で覚えやすい!
よくある質問(FAQ)
Q1. th:include と th:replace の違いは?
th:replace は対象要素ごと置換、th:include は内部の子要素を挿入。ベース側の不要なラッパーを残したくない時は th:replace が明快。
Q2. CSSやJSの読み込み順は?
基本は CSSは<head>、JSは<body>の末尾。ページ固有JSはページ側で読みたい場合、th:fragmentで「scripts」領域を用意して差し込みましょう。
Q3. ページごとにタイトルやメタ説明を変えたい
<div th:replace="~{_layout/base :: content(~{::section})}"
th:with="title=${pageTitle}, metaDesc=${pageDesc}, active='about'">
<section>...</section>
</div>
Q4. 静的ファイルのキャッシュが強すぎる
バージョンパラメータを付与(例:/css/app.css?v=${appVersion})。本番はCDNやETag/Cache-Controlの設定も検討。
Q5. 国際化(i18n)は?
messages_xx.properties を用意し、#{key} で参照。ヘッダー内リンク文言も外出し可能。
デプロイ前チェックリスト
- 全ページでヘッダー/フッターが崩れていないか(スマホ幅含む)。
- 現在ページのナビに
.activeが付くか。 - CSS/JSにバージョン付与(キャッシュ切り替わるか)。
- タイトル/メタ説明がページごとに適切か。
- FCP/CLSが悪化していないか(Lighthouseで確認)。



コメント