thymeleaf について
thymeleafは、Thymeleafテンプレートを扱う開発者向けのクイックリファレンススキルです。構文、フォームバインディング、レイアウト再利用、安全な出力、トラブルシューティングを扱います。
主な機能
- 01変数、ループ、条件分岐の構文
- 02フラグメントとレイアウトの再利用
- 03フォームバインディングとJavaScriptインライン化
- 04式とユーティリティオブジェクトの参照
- 05安全な出力の実践と問題の診断
活用シーン
- 01Springのページテンプレート作成
- 02フォームデータのバインドとエラー表示
- 03再利用可能なページフラグメントの抽出
- 04変数やテンプレート読込問題の診断
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill thymeleafインストール後、Claude Code で/thymeleafを入力して読み込まれたか確認します。
詳細
Thymeleaf - クイックリファレンス
詳細な知識:technology に
thymeleafを指定してmcp__documentation__fetch_docsを使用すると、包括的なドキュメントを参照できます。
基本パターン
基本構文
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${title}">Default</title>
</head>
<body>
<h1 th:text="${message}">Hello</h1>
<p th:text="'Welcome, ' + ${name}">Welcome, User</p>
</body>
</html>
変数とループ
<p th:text="${user.name}">Name</p>
<tr th:each="user : ${users}">
<td th:text="${user.name}">Name</td>
<td th:text="${user.email}">Email</td>
</tr>
<p th:if="${user.active}">Active</p>
<p th:unless="${user.active}">Inactive</p>
メールサービス
@Service
@RequiredArgsConstructor
public class EmailService {
private final TemplateEngine templateEngine;
private final JavaMailSender mailSender;
public void sendWelcome(User user) {
Context ctx = new Context();
ctx.setVariable("userName", user.getName());
ctx.setVariable("actionUrl", "https://app.com/verify");
String html = templateEngine.process("emails/welcome", ctx);
MimeMessage msg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg, true);
helper.setTo(user.getEmail());
helper.setSubject("Welcome!");
helper.setText(html, true);
mailSender.send(msg);
}
}
フラグメントとレイアウト
<nav th:fragment="navigation">
<ul>
<li><a th:href="@{/}">Home</a></li>
<li><a th:href="@{/about}">About</a></li>
</ul>
</nav>
<footer th:fragment="footer(year)">
<p th:text="'© ' + ${year} + ' My Company'">© 2024 My Company</p>
</footer>
<div th:replace="~{fragments/common :: navigation}"></div>
<div th:insert="~{fragments/common :: navigation}"></div>
<div th:replace="~{fragments/common :: footer(${#dates.year(#dates.createNow())})}"></div>
レイアウトダイアレクト
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<title layout:title-pattern="$CONTENT_TITLE - $LAYOUT_TITLE">My App</title>
<link rel="stylesheet" th:href="@{/css/main.css}"/>
</head>
<body>
<header th:replace="~{fragments/common :: navigation}"></header>
<main layout:fragment="content">
</main>
<footer th:replace="~{fragments/common :: footer}"></footer>
<script layout:fragment="scripts"></script>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/main}">
<head>
<title>Home</title>
</head>
<body>
<main layout:fragment="content">
<h1>Welcome!</h1>
<p th:text="${message}">Content</p>
</main>
<th:block layout:fragment="scripts">
<script th:src="@{/js/home.js}"></script>
</th:block>
</body>
</html>
フォームバインディング
<form th:action="@{/users}" th:object="${userForm}" method="post">
<div>
<label for="name">Name:</label>
<input type="text" id="name" th:field="*{name}"
th:classappend="${#fields.hasErrors('name')} ? 'error' : ''"/>
<span th:if="${#fields.hasErrors('name')}"
th:errors="*{name}" class="error-msg">Name error</span>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" th:field="*{email}"/>
<span th:if="${#fields.hasErrors('email')}"
th:errors="*{email}" class="error-msg">Email error</span>
</div>
<div>
<label for="role">Role:</label>
<select id="role" th:field="*{role}">
<option value="">-- Select --</option>
<option th:each="role : ${roles}"
th:value="${role}"
th:text="${role.displayName}">Role</option>
</select>
</div>
<button type="submit">Save</button>
</form>
JavaScript のインライン化
<script th:inline="javascript">
// Natural templates with fallback
const user = /*[[${user}]]*/ { name: 'default' };
const userId = /*[[${user.id}]]*/ 0;
const isAdmin = /*[[${user.admin}]]*/ false;
// Array
const items = /*[[${items}]]*/ [];
// Conditional in JS
/*[# th:if="${user != null}"]*/
console.log('User:', user.name);
/*[/]*/
</script>
<style th:inline="css">
.user-bg {
background-color: [[${user.favoriteColor}]];
}
</style>
ユーティリティオブジェクト
<p th:text="${#dates.format(user.createdAt, 'dd/MM/yyyy HH:mm')}">Date</p>
<p th:text="${#dates.dayOfWeekName(date)}">Monday</p>
<p th:text="${#strings.toUpperCase(name)}">NAME</p>
<p th:text="${#strings.abbreviate(text, 100)}">Truncated...</p>
<p th:text="${#strings.isEmpty(value) ? 'N/A' : value}">Value</p>
<p th:text="${#strings.listJoin(items, ', ')}">a, b, c</p>
<p th:text="${#numbers.formatDecimal(price, 1, 2)}">10.50</p>
<p th:text="${#numbers.formatCurrency(amount)}">$1,234.56</p>
<p th:text="${#lists.size(users)}">Count</p>
<p th:if="${#lists.isEmpty(users)}">No users</p>
<p th:text="${#lists.contains(roles, 'ADMIN')}">Has admin</p>
<p th:text="${#aggregates.sum(prices)}">Total</p>
<p th:text="${#aggregates.avg(scores)}">Average</p>
Switch/Case
<div th:switch="${user.role}">
<p th:case="'ADMIN'">Administrator</p>
<p th:case="'MANAGER'">Manager</p>
<p th:case="'USER'">Regular User</p>
<p th:case="*">Unknown Role</p>
</div>
反復ステータス
<tr th:each="user, stat : ${users}"
th:class="${stat.odd} ? 'odd' : 'even'">
<td th:text="${stat.index}">0</td>
<td th:text="${stat.count}">1</td>
<td th:text="${user.name}">Name</td>
<td th:if="${stat.first}">First!</td>
<td th:if="${stat.last}">Last!</td>
</tr>
式
| 式 | 用途 |
|---|---|
${var} | 変数 |
*{prop} | 選択(th:object を使用) |
@{/url} | リンク URL |
#{msg} | i18n メッセージ |
~{frag} | フラグメント |
ベストプラクティス
| 推奨事項 | 非推奨事項 |
|---|---|
| エスケープされた出力には th:text を使用する | ユーザー入力に th:utext を使用する(XSS) |
| 再利用可能なコンポーネントにはフラグメントを使用する | テンプレート間で HTML を重複させる |
| テンプレートをシンプルに保つ | 複雑なロジックをテンプレートに記述する |
| i18n メッセージを使用する | テキスト文字列をハードコードする |
| レイアウトダイアレクトを使用する | すべてのページでレイアウトを繰り返す |
このスキルを使用しない場合
- REST API - JSON レスポンスには
spring-restスキルを使用します - SPA フロントエンド - React、Vue、または Angular を使用します
- PDF レポート - JasperReports または iText を使用します
- 高トラフィックの API - 静的フロントエンドを検討します
アンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
| ユーザー入力に th:utext を使用する | XSS の脆弱性 | エスケープには th:text を使用する |
| テンプレート内の複雑なロジック | テストや保守が困難 | ロジックをコントローラーに移す |
| th 名前空間がない | エラーが通知されずに失敗する | 常に xmlns:th を宣言する |
| あらゆる場所でインラインスタイルを使用する | UI に一貫性がなくなる | CSS クラスを使用する |
| フラグメントを再利用しない | コードが重複する | 共通のフラグメントを抽出する |
トラブルシューティング早見表
| 問題 | 診断 | 修正方法 |
|---|---|---|
| 変数が表示されない | th: プレフィックスを確認する | th:text、th:value を使用する |
| テンプレートが見つからない | パスを確認する | templates/ フォルダーに配置する |
| 反復が機能しない | th:each の構文を確認する | コレクションが渡されていることを確認する |
| フラグメントが含まれない | パスの構文を確認する | ~{fragments/name :: fragment} を使用する |
| i18n が機能しない | messages.properties を確認する | ファイルの場所とキーを確認する |
リファレンスドキュメント
コメント
Markdown 対応