mcpField
T
thymeleaf

thymeleaf

github.com/claude-dev-suite開発4694 回インストール2 日前に更新
リポジトリ

thymeleaf について

thymeleafは、Thymeleafテンプレートを扱う開発者向けのクイックリファレンススキルです。構文、フォームバインディング、レイアウト再利用、安全な出力、トラブルシューティングを扱います。

主な機能

  1. 01変数、ループ、条件分岐の構文
  2. 02フラグメントとレイアウトの再利用
  3. 03フォームバインディングとJavaScriptインライン化
  4. 04式とユーティリティオブジェクトの参照
  5. 05安全な出力の実践と問題の診断

活用シーン

  1. 01Springのページテンプレート作成
  2. 02フォームデータのバインドとエラー表示
  3. 03再利用可能なページフラグメントの抽出
  4. 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 を確認するファイルの場所とキーを確認する

リファレンスドキュメント

コメント