Chuyển đến nội dung chính

レッスン 17: Keycloak インターフェースのカスタマイズ - テーマ

テーマの種類 (ログイン、アカウント、管理者、電子メール、ようこそ)、テーマのディレクトリ構造、カスタム テーマの作成、テーマ プロパティ ファイル、FreeMarker テンプレート エンジン、特定のテンプレートのオーバーライド、i18n メッセージ、カスタム CSS/JS の使用、テーマの拡張 (親)、テーマのデプロイ (スタンドアロン、Docker、JAR/SPI)、テーマのキャッシュおよびテーマ開発ワークフロー。

🔒 DevSecOps — レッスン 17 レッスン 17: Keycloak インターフェースのカスタマイズ - テーマ

基本から上級までの Keycloak

パート 5: テーマ、イベント、セキュリティ、および Vault

xdev.asia

1. Keycloakテーマの概要

Keycloakを使用すると、システムを通じてユーザーインターフェイスを完全にカスタマイズできますテーマ。各テーマは、ログインページから通知メールに至るまで、Keycloak の特定の部分のルックアンドフィールを制御します。

1.1 テーマの種類

キークロークのサポート5種類のテーマ:

テーマの種類説明する該当ページ
ログインログイン、登録、パスワードリセットインターフェイスログイン、登録、OTP、パスワードのリセット、エラー
アカウントユーザーアカウント管理ページアカウント コンソール (v3 React ベース)
管理者管理コンソールのインターフェース管理コンソール (React ベース)
電子メールユーザーに送信される電子メール テンプレート電子メールの確認、パスワードのリセット、イベント通知
いらっしゃいませデフォルトのようこそページルート URL にアクセスしたときのランディング ページ

1.2 デフォルトのテーマ

Keycloakには利用可能なテーマが付属しています:

  • キーマント— ログイン、アカウント、電子メールのデフォルトのテーマ
  • keycloak.v2— ログイン用の新しいテーマ (Keycloak 24+)、アカウントコンソール v3
  • ベース(非推奨) — 基本テーマ。親として使用しないでください。

2. テーマフォルダー構造

各テーマは、標準のフォルダー構造に従って編成されています。

themes/
└── my-custom-theme/
    ├── login/
    │   ├── theme.properties
    │   ├── resources/
    │   │   ├── css/
    │   │   │   └── styles.css
    │   │   ├── js/
    │   │   │   └── custom.js
    │   │   └── img/
    │   │       └── logo.png
    │   ├── templates/
    │   │   ├── login.ftl
    │   │   ├── register.ftl
    │   │   └── error.ftl
    │   └── messages/
    │       ├── messages_en.properties
    │       └── messages_vi.properties
    ├── account/
    │   ├── theme.properties
    │   └── ...
    └── email/
        ├── theme.properties
        ├── html/
        │   └── email-verification.ftl
        └── text/
            └── email-verification.ftl

2.1 メインディレクトリ

ディレクトリ目的
リソース/CSS、JavaScript、静的画像
テンプレート/FreeMarker テンプレート ファイル (.ftl)
メッセージ/多言語用のファイル i18n

3. テーマプロパティファイル

ファイルテーマ.プロパティは、各テーマ タイプの中心的な構成ファイルです。

# themes/my-custom-theme/login/theme.properties

# Kế thừa từ theme keycloak (sử dụng tất cả template/resource chưa override)
parent=keycloak.v2

# Import resources từ theme khác (common resources)
import=common/keycloak

# CSS files (thêm custom CSS)
styles=css/login.css css/styles.css

# Locales hỗ trợ
locales=en,vi

# Script files
scripts=js/custom.js

# Cache control (cho production)
cacheControl=max-age=2592000

3.1 重要な特性

財産説明する例えば
親。親継承する親テーマ親=keycloak.v2
輸入別のテーマからリソースをインポートするimport=common/keycloak
スタイル。スタイルCSS ファイルのリスト (スペース区切り)スタイル=css/login.css css/custom.css
スクリプトJavaScript ファイルスクリプト=js/app.js
ロケール言語サポートロケール=en、vi、ja

4.FreeMarkerテンプレートエンジン

キークロークの使用Apache フリーマーカーログインおよび電子メールテーマのテンプレートエンジンとして。 FreeMarker を使用すると、変数とロジックを使用して動的な HTML を生成できます。

4.1 基本的な構文

<!-- Biến -->
${realm.displayName}
${url.loginAction}
${messagesPerField.printIfExists('username','has-error')}

<!-- Điều kiện -->
<#if realm.password>
  <!-- Form đăng nhập bằng password -->
</#if>

<#if social.providers??>
  <!-- Hiển thị social login buttons -->
  <#list social.providers as p>
    <a href="${p.loginUrl}">${p.displayName}</a>
  </#list>
</#if>

<!-- Macro -->
<#macro registrationLayout>
  <!-- Layout wrapper -->
</#macro>

4.2 ログインテーマで利用可能な変数

変数説明する
領域。レルムレルム情報 (表示名、登録許可、パスワード、ソーシャル...)
URLURL (loginAction、registrationUrl、loginResetCredentialsUrl...)
クライアントクライアントがログインを要求しています (名前、クライアント ID...)
ログインフォームデータ (入力されたユーザー名...)
メッセージ。メッセージエラー/成功メッセージ
フィールドごとのメッセージ各フィールドの検証メッセージ
社交。社交ソーシャルログインプロバイダー
ロケール現在のロケールとサポートされているロケールのリスト

5. 特定のテンプレートをオーバーライドする

変更したいテンプレートをオーバーライドするだけです。残りのテンプレートは親テーマから継承されます。

5.1 ログインページの上書き

<!-- themes/my-custom-theme/login/templates/login.ftl -->
<#import "template.ftl" as layout>

<@layout.registrationLayout
    displayMessage=!messagesPerField.existsError('username','password')
    displayInfo=realm.password && realm.registrationAllowed
    ; section>

    <#if section = "header">
        <div class="custom-header">
            <img src="${url.resourcesPath}/img/logo.png" alt="Logo" class="login-logo" />
            <h1>${msg("loginAccountTitle")}</h1>
        </div>
    <#elseif section = "form">
        <div class="custom-login-form">
            <form action="${url.loginAction}" method="post">
                <div class="form-group">
                    <label for="username">
                        <#if !realm.loginWithEmailAllowed>
                            ${msg("username")}
                        <#elseif !realm.registrationEmailAsUsername>
                            ${msg("usernameOrEmail")}
                        <#else>
                            ${msg("email")}
                        </#if>
                    </label>
                    <input id="username" name="username" type="text"
                           value="${(login.username!'')}"
                           class="form-control ${messagesPerField.printIfExists('username','has-error')}"
                           autofocus autocomplete="username" />
                    <#if messagesPerField.existsError('username')>
                        <span class="error-message">${messagesPerField.get('username')}</span>
                    </#if>
                </div>

                <div class="form-group">
                    <label for="password">${msg("password")}</label>
                    <input id="password" name="password" type="password"
                           class="form-control ${messagesPerField.printIfExists('password','has-error')}"
                           autocomplete="current-password" />
                    <#if messagesPerField.existsError('password')>
                        <span class="error-message">${messagesPerField.get('password')}</span>
                    </#if>
                </div>

                <div class="form-actions">
                    <#if realm.rememberMe && !usernameHidden??>
                        <label class="remember-me">
                            <input id="rememberMe" name="rememberMe" type="checkbox"
                                   <#if login.rememberMe??>checked</#if>>
                            ${msg("rememberMe")}
                        </label>
                    </#if>

                    <button type="submit" class="btn btn-primary">
                        ${msg("doLogIn")}
                    </button>
                </div>
            </form>

            <#if realm.password && realm.registrationAllowed>
                <div class="register-link">
                    ${msg("noAccount")}
                    <a href="${url.registrationUrl}">${msg("doRegister")}</a>
                </div>
            </#if>
        </div>
    </#if>
</@layout.registrationLayout>

5.2 登録ページの上書き

<!-- themes/my-custom-theme/login/templates/register.ftl -->
<#import "template.ftl" as layout>

<@layout.registrationLayout
    displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm')
    ; section>

    <#if section = "header">
        <h1>${msg("registerTitle")}</h1>
    <#elseif section = "form">
        <form action="${url.registrationAction}" method="post">
            <div class="form-group">
                <label for="firstName">${msg("firstName")}</label>
                <input id="firstName" name="firstName" type="text"
                       value="${(register.formData.firstName!'')}"
                       class="form-control" />
            </div>

            <div class="form-group">
                <label for="lastName">${msg("lastName")}</label>
                <input id="lastName" name="lastName" type="text"
                       value="${(register.formData.lastName!'')}"
                       class="form-control" />
            </div>

            <div class="form-group">
                <label for="email">${msg("email")}</label>
                <input id="email" name="email" type="email"
                       value="${(register.formData.email!'')}"
                       class="form-control" autocomplete="email" />
            </div>

            <button type="submit" class="btn btn-primary">
                ${msg("doRegister")}
            </button>

            <div class="back-to-login">
                <a href="${url.loginUrl}">${msg("backToLogin")}</a>
            </div>
        </form>
    </#if>
</@layout.registrationLayout>

5.3 オーバーライドエラーページ

<!-- themes/my-custom-theme/login/templates/error.ftl -->
<#import "template.ftl" as layout>

<@layout.registrationLayout displayMessage=false; section>
    <#if section = "header">
        ${kcSanitize(msg("errorTitle"))?no_esc}
    <#elseif section = "form">
        <div class="error-container">
            <div class="alert alert-error">
                <span class="error-icon">⚠</span>
                ${kcSanitize(message.summary)?no_esc}
            </div>
            <#if skipLink??>
            <#else>
                <#if client?? && client.baseUrl?has_content>
                    <a href="${client.baseUrl}" class="btn btn-primary">
                        ${kcSanitize(msg("backToApplication"))?no_esc}
                    </a>
                </#if>
            </#if>
        </div>
    </#if>
</@layout.registrationLayout>

6. カスタム CSS と JavaScript

6.1 カスタムCSSの追加

/* themes/my-custom-theme/login/resources/css/styles.css */

/* Override login page styles */
.login-pf body {
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
    font-family: 'Inter', -apple-system, sans-serif;
}

/* Custom login card */
#kc-login {
    background: rgba(255, 255, 255, 0.95);
    border-radius: 16px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    padding: 40px;
    max-width: 440px;
    margin: 0 auto;
}

/* Logo */
.login-logo {
    max-width: 180px;
    margin: 0 auto 24px;
    display: block;
}

/* Form inputs */
.form-control {
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    padding: 12px 16px;
    font-size: 14px;
    transition: border-color 0.2s;
}

.form-control:focus {
    border-color: #0f3460;
    box-shadow: 0 0 0 3px rgba(15, 52, 96, 0.1);
    outline: none;
}

.form-control.has-error {
    border-color: #e74c3c;
}

/* Primary button */
.btn-primary {
    background: #0f3460;
    color: white;
    border: none;
    border-radius: 8px;
    padding: 12px 24px;
    font-size: 16px;
    font-weight: 600;
    width: 100%;
    cursor: pointer;
    transition: background 0.2s;
}

.btn-primary:hover {
    background: #1a4a7a;
}

/* Social login buttons */
.social-links {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 16px;
}

6.2 カスタム JavaScript の追加

// themes/my-custom-theme/login/resources/js/custom.js

document.addEventListener('DOMContentLoaded', function() {
    // Password visibility toggle
    const passwordField = document.getElementById('password');
    if (passwordField) {
        const toggleBtn = document.createElement('button');
        toggleBtn.type = 'button';
        toggleBtn.className = 'password-toggle';
        toggleBtn.textContent = '👁';
        toggleBtn.addEventListener('click', function() {
            passwordField.type = passwordField.type === 'password' ? 'text' : 'password';
        });
        passwordField.parentNode.appendChild(toggleBtn);
    }

    // Form validation enhancement
    const form = document.querySelector('form');
    if (form) {
        form.addEventListener('submit', function(e) {
            const submitBtn = form.querySelector('button[type="submit"]');
            if (submitBtn) {
                submitBtn.disabled = true;
                submitBtn.textContent = 'Đang xử lý...';
            }
        });
    }
});

7. 国際化 (i18n)

Keycloakはファイルを通じて複数の言語をサポートしますメッセージ_{ロケール}.properties.

7.1 ベトナム語ファイルの作成

# themes/my-custom-theme/login/messages/messages_vi.properties

# Login form
loginAccountTitle=Đăng nhập vào tài khoản
username=Tên đăng nhập
usernameOrEmail=Tên đăng nhập hoặc Email
password=Mật khẩu
rememberMe=Ghi nhớ đăng nhập
doLogIn=Đăng nhập
noAccount=Chưa có tài khoản?
doRegister=Đăng ký ngay

# Registration
registerTitle=Tạo tài khoản mới
firstName=Họ
lastName=Tên
email=Địa chỉ email
doRegister=Đăng ký

# Errors
invalidUserMessage=Tên đăng nhập hoặc mật khẩu không đúng.
accountDisabledMessage=Tài khoản đã bị vô hiệu hóa. Vui lòng liên hệ quản trị viên.

# Password reset
emailForgotTitle=Quên mật khẩu?
backToLogin=Quay lại đăng nhập

# Custom messages
welcomeMessage=Chào mừng bạn đến với hệ thống
companyName=Công ty của bạn

7.2 英語メッセージを上書きする

# themes/my-custom-theme/login/messages/messages_en.properties

# Override default messages
loginAccountTitle=Sign in to your account
welcomeMessage=Welcome to our platform
companyName=Your Company

# Custom messages cho branding
loginTitleHtml=<strong>Welcome</strong> to {0}

7.3 テンプレートでの i18n の使用

<!-- Sử dụng message key -->
<h1>${msg("loginAccountTitle")}</h1>

<!-- Message với tham số -->
<p>${msg("loginTitleHtml", realm.displayName)}</p>

<!-- Kiểm tra message tồn tại -->
<#if msg("welcomeMessage")?has_content>
    <p class="welcome">${msg("welcomeMessage")}</p>
</#if>

<!-- Locale selector -->
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
    <div class="locale-selector">
        <#list locale.supported as l>
            <a href="${l.url}" class="${(l.label == locale.current)?then('active','')}">
                ${l.label}
            </a>
        </#list>
    </div>
</#if>

8. 保護者と一緒にテーマを拡張する

テーマの継承により、既存のテーマに基づいて新しいテーマを作成し、変更が必要な部分のみをオーバーライドできます。

8.1 仕組み

Thứ tự tìm kiếm resource/template:
1. Current theme (my-custom-theme/login/)
2. Parent theme (keycloak.v2/login/)
3. Parent của parent (nếu có)
4. Base resources (import)

8.2 マルチレベルの継承

# themes/company-base/login/theme.properties
parent=keycloak.v2
styles=css/login.css css/company-base.css
locales=en,vi

# themes/company-product-a/login/theme.properties
parent=company-base
styles=css/login.css css/company-base.css css/product-a.css

8.3 テーマのバリエーション

Keycloak 24以降では、次のように定義できます。テーマのバリエーション同じテーマの異なるバージョンを使用するには:

# theme.properties
parent=keycloak.v2

# Variant definition (admin có thể chọn trong Realm Settings)
variant.light.styles=css/login.css css/light.css
variant.dark.styles=css/login.css css/dark.css

9. テーマの展開

9.1 スタンドアロン展開

テーマをフォルダーにコピーテーマ/キークロークの:

# Cấu trúc standalone
$KEYCLOAK_HOME/
├── themes/
│   └── my-custom-theme/
│       ├── login/
│       │   ├── theme.properties
│       │   ├── resources/
│       │   ├── templates/
│       │   └── messages/
│       └── email/
│           └── ...

9.2 Docker のデプロイメント

# Dockerfile - Simple COPY
FROM quay.io/keycloak/keycloak:26.1 AS builder

# Copy custom theme
COPY themes/my-custom-theme /opt/keycloak/themes/my-custom-theme

# Build optimized Keycloak
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:26.1

COPY --from=builder /opt/keycloak/ /opt/keycloak/

ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

9.3 ノードを使用した Docker マルチステージ ビルド (アカウント v3)

# Dockerfile - Multi-stage build cho Account Console v3 customization
FROM node:20-alpine AS theme-builder

WORKDIR /theme
COPY account-theme/ .
RUN npm ci && npm run build

FROM quay.io/keycloak/keycloak:26.1 AS keycloak-builder

# Copy login/email themes
COPY themes/my-custom-theme /opt/keycloak/themes/my-custom-theme

# Copy built Account v3 theme
COPY --from=theme-builder /theme/dist /opt/keycloak/themes/my-custom-theme/account

RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:26.1
COPY --from=keycloak-builder /opt/keycloak/ /opt/keycloak/

ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

9.4 JAR/SPIのデプロイメント

テーマを JAR ファイルとしてパッケージ化し、SPI プロバイダーとしてデプロイします。

my-theme.jar
├── META-INF/
│   └── keycloak-themes.json
└── theme/
    └── my-custom-theme/
        ├── login/
        └── email/
// META-INF/keycloak-themes.json
{
    "themes": [
        {
            "name": "my-custom-theme",
            "types": ["login", "email"]
        }
    ]
}
# Deploy JAR
cp my-theme.jar $KEYCLOAK_HOME/providers/
$KEYCLOAK_HOME/bin/kc.sh build

10. テーマ開発ワークフロー

10.1 開発時にキャッシュをオフにする

Keycloak キャッシュ テーマはデフォルトで、開発時にはオフにする必要があります。

# Tắt theme caching cho development
bin/kc.sh start-dev \
  --spi-theme-static-max-age=-1 \
  --spi-theme-cache-themes=false \
  --spi-theme-cache-templates=false

10.2 開発用の Docker Compose

# docker-compose.dev.yml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.1
    command:
      - start-dev
      - --spi-theme-static-max-age=-1
      - --spi-theme-cache-themes=false
      - --spi-theme-cache-templates=false
    volumes:
      # Mount theme directory cho hot-reload
      - ./themes/my-custom-theme:/opt/keycloak/themes/my-custom-theme
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
    ports:
      - "8080:8080"
    depends_on:
      - postgres

  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

10.3 開発ワークフロー

# 1. Khởi chạy dev environment
docker compose -f docker-compose.dev.yml up -d

# 2. Chỉnh sửa file theme (CSV, FTL, properties)
# Thay đổi sẽ tự động reflect (vì cache đã tắt)

# 3. Refresh browser để xem thay đổi
# Không cần restart Keycloak!

# 4. Áp dụng theme vào Realm
# Admin Console → Realm Settings → Themes → Login Theme: my-custom-theme

11. React アカウント コンソール v3 のカスタマイズ

アカウント コンソール v3 が使用するもの反応 + パターンフライ。カスタマイズするには、カスタム React アプリを構築する必要があります。

11.1 Keycloakify (推奨)

使用Keycloakify— React で Keycloak テーマを作成するための特殊なフレームワーク:

# Tạo project mới với Keycloakify
npx create-keycloakify-project my-keycloak-theme
cd my-keycloak-theme

# Cấu trúc project
my-keycloak-theme/
├── src/
│   ├── login/          # Login theme pages
│   │   ├── KcPage.tsx
│   │   ├── pages/
│   │   │   ├── Login.tsx
│   │   │   ├── Register.tsx
│   │   │   └── ...
│   │   └── i18n.ts
│   └── account/        # Account theme pages
├── public/
│   └── ...
├── package.json
└── vite.config.ts

# Dev mode
npm run dev

# Build JAR
npm run build-keycloak-theme
# Output: dist_keycloak/my-keycloak-theme.jar

11.2 Keycloakify ログインページの例

// src/login/pages/Login.tsx
import { type PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";

export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
    const { kcContext, i18n, Template } = props;
    const { url, realm, social, login } = kcContext;
    const { msg } = i18n;

    return (
        <Template kcContext={kcContext} i18n={i18n}>
            <div className="custom-login">
                <img src={`${url.resourcesPath}/img/logo.png`} alt="Logo" />
                <h1>{msg("loginAccountTitle")}</h1>

                <form action={url.loginAction} method="post">
                    <input
                        name="username"
                        defaultValue={login.username ?? ""}
                        placeholder={msg("usernameOrEmail")}
                    />
                    <input
                        name="password"
                        type="password"
                        placeholder={msg("password")}
                    />
                    <button type="submit">{msg("doLogIn")}</button>
                </form>

                {social?.providers && (
                    <div className="social-providers">
                        {social.providers.map(p => (
                            <a key={p.alias} href={p.loginUrl}>
                                {p.displayName}
                            </a>
                        ))}
                    </div>
                )}
            </div>
        </Template>
    );
}

12. 電子メールのテーマのカスタマイズ

電子メールのテーマには、テンプレートごとに HTML とプレーン テキストの 2 つのバージョンがあります。

<!-- themes/my-custom-theme/email/html/email-verification.ftl -->
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
    <div style="background: #0f3460; padding: 20px; text-align: center;">
        <img src="${url.resourcesPath}/img/logo-white.png" alt="Logo" style="max-width: 150px;" />
    </div>
    <div style="padding: 30px; background: #ffffff;">
        <h2>${msg("emailVerificationSubject")}</h2>
        <p>${msg("emailVerificationBody", linkExpiration, realmName)}</p>
        <a href="${link}" style="display: inline-block; background: #0f3460; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px;">
            ${msg("emailVerificationLinkText")}
        </a>
    </div>
    <div style="padding: 15px; text-align: center; color: #888; font-size: 12px;">
        <p>${msg("emailFooter")}</p>
    </div>
</body>
</html>
<!-- themes/my-custom-theme/email/text/email-verification.ftl -->
${msg("emailVerificationSubject")}

${msg("emailVerificationBodyPlainText", link, linkExpiration, realmName)}

13. テーマをレルムに適用する

13.1 管理コンソール経由

  1. 管理コンソールにログインします
  2. 構成するレルムを選択してください
  3. 入力レルム設定 → テーマ
  4. タイプごとにテーマを選択します。
    • ログインテーマ:私のカスタムテーマ
    • アカウントのテーマ:私のカスタムテーマ
    • 管理コンソールのテーマ: (デフォルトのまま)
    • 電子メールのテーマ:私のカスタムテーマ
  5. プレス保存

13.2 REST API経由

# Cập nhật theme cho realm qua API
curl -X PUT "http://localhost:8080/admin/realms/my-realm" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "loginTheme": "my-custom-theme",
    "accountTheme": "my-custom-theme",
    "emailTheme": "my-custom-theme"
  }'

14. ベストプラクティス

  • 常に親テーマを使用する— から継承keycloak.v2最初から書くのではなく。 Keycloakをアップグレードする際は互換性を確保してください。
  • 変更する必要があるもののみをオーバーライドする— テンプレート全体をコピーしないでください。カスタマイズするテンプレートのファイルのみを作成します。
  • クライアントごとにテーマを分ける— 複数のアプリケーションがある場合は、クライアントごとに異なるテーマを使用できます (レルム設定またはクライアントレベルのテーマのオーバーライド)。
  • 複数のブラウザでテスト済み— 特に、ログイン ページはモバイルでも機能する必要があります。
  • i18n を使用する— テンプレートではハードコードされたテキストではなく、常にメッセージ キーを使用します。
  • バージョン管理テーマ— テーマを別の Git リポジトリまたは同じプロジェクトで管理します。
  • CI/CD パイプライン— CIでJARテーマを構築し、Keycloakコンテナでテストし、自動的にデプロイします。