🟢 HubSpot 開発者向け実践教科書 — 2026年版 Developer Edition
Chapter 5  ·  CMS Theme & Template Development

CMS Hub 開発——
テーマ & テンプレート

HubSpot テーマの構造・HubL テンプレート言語の実践・グローバルパーシャル・ レスポンシブ設計まで。フロントエンド開発者が知るべき CMS 開発の全体像を体系的に学ぶ。

HubL テンプレート言語
CLI v8 ウォッチモード
所要時間:約120分
5-1 HubSpot CMS の開発モデル

HubSpot CMS のレンダリング構造と、開発者が操作できるレイヤーを整理する。

🏗 CMS の構成要素

HubSpot CMS はテーマ(Theme)を基盤に、テンプレート(Template)パーシャル(Partial)モジュール(Module)の3層で構成されます。 ページコンテンツはドラッグ&ドロップエディタで編集でき、開発者は HTML・CSS・HubL・JavaScript を使って その土台を構築します。

構成要素役割ファイル拡張子編集者
テーマ サイト全体のデザイン・設定を管理するルートコンテナ theme.json 開発者
テンプレート ページのレイアウト骨格(ヘッダー・フッター・メインエリア) .html(HubL) 開発者
グローバルパーシャル ヘッダー・フッターなど全ページ共通のパーツ .html(HubL) 開発者 / マーケター
モジュール ドラッグ&ドロップで配置できる再利用可能なコンポーネント .module/(ディレクトリ) 開発者
CSS / JS スタイル・インタラクション .css / .js 開発者
デザインマネージャ vs CLI: HubSpot のデザインマネージャ(ブラウザ上のファイルエディタ)でも開発できますが、 ローカルエディタ(VS Code 等)+ CLI による開発が圧倒的に効率的です。 Git によるバージョン管理・チーム開発・CI/CD 自動デプロイができる CLI 開発を推奨します。
5-2 テーマのディレクトリ構造

標準的なテーマ構成を把握し、各ファイルの役割を理解する。

my-theme/
theme.json← テーマ設定・フィールド定義
templates/← ページテンプレート
home.html← トップページ用
page.html← 汎用ページ用
blog-listing.html← ブログ一覧
blog-post.html← ブログ記事詳細
landing-page.html← LP(フォームなど)
error-page.html← 404 など
partials/← 共通パーツ
header.html
footer.html
navigation.html
modules/← カスタムモジュール(6章で詳説)
hero-banner.module/
cta-section.module/
css/
main.css← メインスタイルシート
theme-overrides.css
js/
main.js
images/
logo.svg
fonts/
デザインマネージャのパス: CLI でアップロードするとき、HubSpot 側のパスは @hubspot/my-theme/templates/page.html のように @hubspot/ プレフィックス + フォルダ名になります。 hs cms upload ./my-theme my-theme の第2引数がそのまま HubSpot 側のルートパスになります。
5-3 theme.json の設定

テーマ全体の設定・編集可能フィールド・カラーパレットを定義する。

theme.json
{ "label": "My Company Theme", "preview_path": "./images/preview.png", "author": { "name": "My Company", "url": "https://example.com" }, "version": "1.0.0", "isAvailableForNewContent": true, // マーケターが編集できるフィールドを定義 "fields": [ { "type": "font", "label": "見出しフォント", "name": "heading_font", "default": { "font": "Noto Sans JP", "font_set": "GOOGLE", "size": 36, "size_unit": "px", "bold": true } }, { "type": "color", "label": "ブランドカラー(プライマリ)", "name": "primary_color", "default": { "color": "#10b981" } }, { "type": "color", "label": "ブランドカラー(セカンダリ)", "name": "secondary_color", "default": { "color": "#059669" } }, { "type": "image", "label": "サイトロゴ", "name": "site_logo", "default": { "src": "", "alt": "Company Logo" } }, { "type": "group", "label": "SNS リンク", "name": "social_links", "children": [ { "type": "text", "label": "Twitter / X", "name": "twitter", "default": "" }, { "type": "text", "label": "LinkedIn", "name": "linkedin", "default": "" } ] } ] }
🎨 theme.json フィールド型一覧

color — カラーピッカー  |  font — フォント・サイズ・スタイル  |  text — テキスト入力  |  number — 数値入力  |  boolean — オン/オフ  |  image — 画像選択  |  url — URL 入力  |  choice — ドロップダウン  |  group — フィールドのグルーピング

5-4 HubL テンプレート言語の基礎

HubL(HubSpot Markup Language)は Jinja2 ベースのテンプレートエンジン。構文と主要タグを学ぶ。

構文用途
{{ ... }} 変数・式の出力 {{ content.title }}
{% ... %} タグ(制御構文・関数) {% if ... %}{% endif %}
{# ... #} コメント(出力されない) {# TODO: 後で修正 #}
{{ theme.フィールド名 }} theme.json フィールドの値を取得 {{ theme.primary_color.color }}
{% module %} モジュールを配置 {% module "hero" path="./modules/hero-banner" %}
{% dnd_area %} ドラッグ&ドロップ領域を定義 コンテンツエリアの定義
{% include %} パーシャルを読み込む {% include "./partials/header.html" %}
{% global_partial %} グローバルパーシャルを配置 全ページ共通の header/footer
HubL — 基本構文
{# 変数出力 #} <h1>{{ content.title }}</h1> <meta name="description" content="{{ content.meta_description }}"> {# 条件分岐 #} {% if content.featured_image %} <img src="{{ content.featured_image }}" alt="{{ content.featured_image_alt_text }}"> {% endif %} {# ループ #} {% for tag in content.tag_list %} <span class="tag">{{ tag.name }}</span> {% endfor %} {# フィルター #} {{ content.title | truncate(60) }} {{ post.publish_date | datetimeformat('%Y年%m月%d日') }} {{ price | number_format(0, ',', '.') }} {# theme.json フィールドを CSS に使う #} <style> :root { --primary-color: {{ theme.primary_color.color }}; --secondary-color: {{ theme.secondary_color.color }}; --heading-font: "{{ theme.heading_font.font }}", sans-serif; } </style>
5-5 テンプレートファイルの実装

汎用ページテンプレートとブログ記事テンプレートを実際に実装する。

templates/page.html — 汎用ページテンプレート
{# テンプレートのメタ設定 #} <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ content.html_title }}</title> <meta name="description" content="{{ content.meta_description }}"> {# OGP タグ #} <meta property="og:title" content="{{ content.html_title }}"> <meta property="og:description" content="{{ content.meta_description }}"> {% if content.featured_image %} <meta property="og:image" content="{{ content.featured_image }}"> {% endif %} {# テーマの CSS を読み込む #} {{ require_css("{{ get_asset_url('./css/main.css') }}") }} {# theme.json のカラーを CSS 変数として注入 #} <style> :root { --color-primary: {{ theme.primary_color.color }}; --color-secondary: {{ theme.secondary_color.color }}; } </style> </head> <body> {# グローバルヘッダー(全ページ共通) #} {% global_partial path="./partials/header.html" %} <main id="main-content"> {# ドラッグ&ドロップ エリア #} {% dnd_area "main_content" label="メインコンテンツ", class="page-main-content" %} {# デフォルトで配置するモジュール #} {% dnd_section %} {% dnd_column %} {% dnd_row %} {% dnd_module "rich_text" path="@hubspot/rich_text", label="リッチテキスト" %} {% end_dnd_row %} {% end_dnd_column %} {% end_dnd_section %} {% end_dnd_area %} </main> {# グローバルフッター #} {% global_partial path="./partials/footer.html" %} {{ require_js("{{ get_asset_url('./js/main.js') }}", "footer") }} </body> </html>
templates/blog-post.html — ブログ記事テンプレート(抜粋)
{# templateType: blog_post #} <article class="blog-post"> <header class="post-header"> {# カテゴリ・タグ #} {% if content.tag_list %} <div class="post-tags"> {% for tag in content.tag_list %} <a href="{{ tag.url }}" class="tag">{{ tag.name }}</a> {% endfor %} </div> {% endif %} <h1>{{ content.name }}</h1> <div class="post-meta"> <time>{{ content.publish_date | datetimeformat('%Y年%m月%d日') }}</time> {% if content.blog_post_author %} <span class="author">{{ content.blog_post_author.display_name }}</span> {% endif %} </div> {% if content.featured_image %} <img src="{{ content.featured_image }}" alt="{{ content.featured_image_alt_text }}" class="post-featured-image" loading="lazy" > {% endif %} </header> {# 本文 #} <div class="post-body"> {{ content.post_body }} </div> {# 関連記事(同じタグの記事を3件表示) #} {% set related_posts = blog_recent_tag_posts( group.id, content.tag_list[0].id, 3 ) %} {% if related_posts %} <section class="related-posts"> <h2>関連記事</h2> {% for post in related_posts %} <a href="{{ post.absolute_url }}">{{ post.name }}</a> {% endfor %} </section> {% endif %} </article>
5-6 グローバルパーシャルの実装

ヘッダー・フッターなど全ページ共通のパーツを、マーケターが管理画面から編集できる形で実装する。

🌍 グローバルパーシャルの特徴

グローバルパーシャル({% global_partial %})は、1箇所を更新すると全ページに即時反映されます。 マーケターはコードに触れずに HubSpot の編集画面からナビゲーションリンクやフッターテキストを変更できます。 開発者はあらかじめ編集可能なモジュールをパーシャル内に配置しておきます。

partials/header.html — グローバルヘッダー
{# isGlobalPartial: true とコメントで宣言 #} {# templateType: global_partial label: グローバルヘッダー isAvailableForNewContent: false #} <header class="site-header"> <div class="header-inner"> {# ロゴ(theme.json から取得) #} <a href="/" class="site-logo"> {% if theme.site_logo.src %} <img src="{{ theme.site_logo.src }}" alt="{{ theme.site_logo.alt }}" width="160" height="40" > {% else %} <span class="logo-text">{{ site_settings.company_name }}</span> {% endif %} </a> {# ナビゲーション(マーケターが編集可能) #} {% module "navigation" path="@hubspot/simple_menu", label="メインナビゲーション", overrideable=true %} {# CTA ボタン(マーケターが編集可能) #} {% module "header_cta" path="@hubspot/cta", label="ヘッダーCTA", overrideable=true %} </div> </header>
partials/footer.html — グローバルフッター(抜粋)
<footer class="site-footer"> <div class="footer-inner"> {# フッターロゴ #} <a href="/"> <img src="{{ theme.site_logo.src }}" alt="{{ theme.site_logo.alt }}"> </a> {# SNS リンク(theme.json の social_links グループから取得) #} <div class="social-links"> {% if theme.social_links.twitter %} <a href="{{ theme.social_links.twitter }}" target="_blank" rel="noopener">X</a> {% endif %} {% if theme.social_links.linkedin %} <a href="{{ theme.social_links.linkedin }}" target="_blank" rel="noopener">LinkedIn</a> {% endif %} </div> {# コピーライト(現在年を動的に表示) #} <p class="copyright"> © {{ "now"|datetimeformat("%Y") }} {{ site_settings.company_name }} </p> </div> </footer>
5-7 HubL の実践テクニック

よく使う HubL 関数・フィルター・変数をまとめて把握する。

カテゴリHubL 式説明
文字列フィルター {{ text | truncate(100) }} 100文字で切り詰め(末尾に…)
{{ text | striptags }} HTML タグを除去
{{ text | escape }} HTML エスケープ
{{ text | lower }} 小文字に変換
日付フィルター {{ date | datetimeformat('%Y年%m月%d日') }} 日付フォーマット
{{ "now" | datetimeformat('%Y') }} 現在年を取得
{{ date | timeago }} 「3日前」形式で表示
ページ情報 {{ content.absolute_url }} 現在のページの絶対 URL
{{ content.html_title }} ページタイトル
{{ request.path }} 現在のパス(/about など)
アセット {{ get_asset_url('./css/main.css') }} テーマ内アセットの URL を取得
{{ require_css("URL") }} CSS を head に読み込む
{{ require_js("URL", "footer") }} JS を body 末尾に読み込む
ブログ関数 {% blog_recent_posts "blog" 5 %} 最新記事を5件取得
{% blog_recent_tag_posts group.id tag.id 3 %} 同じタグの記事を3件取得
HubL — HubDB データをテンプレートで表示する
{# HubDB テーブルから店舗データを取得して一覧表示 #} {% set store_table = hubdb_table_rows("store_locations", "is_open=true&orderBy=store_name") %} <div class="store-list"> {% for store in store_table %} <div class="store-card"> <h3>{{ store.store_name }}</h3> <p>{{ store.address }}</p> <p>{{ store.phone }}</p> {% if store.latitude and store.longitude %} <a href="https://maps.google.com/?q={{ store.latitude }},{{ store.longitude }}" target="_blank" >地図を見る</a> {% endif %} </div> {% endfor %} </div>
5-8 レスポンシブ設計とパフォーマンス最適化

モバイルファーストの CSS 設計と、HubSpot CMS での画像最適化パターンを学ぶ。

CSS — モバイルファーストのブレークポイント設計
/* HubSpot テーマ推奨のブレークポイント */ :root { --breakpoint-sm: 576px; --breakpoint-md: 768px; --breakpoint-lg: 992px; --breakpoint-xl: 1200px; } /* モバイルファースト(min-width で上書き) */ .hero-section { padding: 40px 16px; flex-direction: column; } @media (min-width: 768px) { .hero-section { padding: 80px 40px; flex-direction: row; gap: 48px; } } @media (min-width: 1200px) { .hero-section { padding: 120px 80px; } } /* HubSpot DnD グリッドとの共存 */ .dnd-section { width: 100%; } .dnd-column { min-width: 0; }
HubL — レスポンシブ画像と遅延読み込み
{# HubSpot の resize_image_url フィルターで自動リサイズ #} <picture> <source media="(min-width: 768px)" srcset="{{ image.src | resize_image_url(1200, 630) }}" > <source media="(min-width: 375px)" srcset="{{ image.src | resize_image_url(768, 400) }}" > <img src="{{ image.src | resize_image_url(375, 200) }}" alt="{{ image.alt }}" loading="lazy" decoding="async" width="1200" height="630" > </picture>
パフォーマンス対策実装方法
画像の遅延読み込み loading="lazy" 属性を必ず付与
画像の自動リサイズ resize_image_url(w, h) フィルターで必要サイズのみ配信
CSS の非同期読み込み クリティカルでない CSS は require_css で body 末尾に
JS の defer require_js("URL", "footer") で body 末尾に配置
HubSpot CDN の活用 テーマのアセットは自動的に HubSpot CDN から配信される
フォントの最適化 font-display: swap + サブセット指定で FOUT を防ぐ
5-9 ローカル開発フローの実践

1章の CLI 知識を踏まえ、テーマ開発の具体的なフローを整理する。

  1. 1
    Sandbox でテーマをダウンロード hs cms fetch "my-theme" ./themes/my-theme --portal dev-sandbox でローカルに取得。
  2. 2
    Watch モードで開発開始 hs cms watch ./themes/my-theme my-theme --portal dev-sandbox でファイル保存時に自動アップロード。
  3. 3
    ブラウザでプレビュー確認 HubSpot の「プレビュー」機能で実際のレンダリングを確認。デベロッパーツールでレスポンシブ確認も行う。
  4. 4
    Git にコミット 変更を commit & push。PR を作成してチームレビューを受ける。
  5. 5
    GitHub Actions で本番デプロイ main マージで自動的に hs cms upload が実行され、本番に反映される。
⚠ watch 中は本番ポータルを指定しない: hs cms watch 実行中は保存のたびにアップロードされます。 必ず --portal dev-sandbox のように開発用ポータルを明示して、 本番ポータルへの誤アップロードを防いでください。
5-10 この章のまとめ

次章(CMS Hub 開発——モジュール設計)に進む前に確認する。

✅ Chapter 5 チェックリスト

  • HubSpot CMS の構成要素(テーマ・テンプレート・パーシャル・モジュール)を説明できる
  • 標準的なテーマのディレクトリ構造を理解した
  • theme.json でフォント・カラー・画像フィールドを定義できる
  • HubL の基本構文(変数・条件・ループ・フィルター)を使えるようになった
  • 汎用ページテンプレート(page.html)を実装できる
  • ブログ記事テンプレート(blog-post.html)を実装できる
  • {% dnd_area %} でドラッグ&ドロップ領域を定義できる
  • グローバルパーシャルでヘッダー・フッターを実装できる
  • hubdb_table_rows() で HubDB データをテンプレートに表示できる
  • resize_image_url フィルターでレスポンシブ画像を実装できる
  • Watch モードを使ったローカル開発フローを実践できる
次章(Chapter 6)について: CMS Hub のモジュール設計を学びます。fields.json によるフィールド定義・ module.html での HubL 実装・meta.json の設定・ カスタムモジュールのベストプラクティスを実践的に解説します。