Chapter 5 · CMS Theme & Template Development
CMS Hub 開発——
テーマ & テンプレート
HubSpot テーマの構造・HubL テンプレート言語の実践・グローバルパーシャル・
レスポンシブ設計まで。フロントエンド開発者が知るべき CMS 開発の全体像を体系的に学ぶ。
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, ',', '.') }}
<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 }}">
<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 %}
{{ require_css("{{ get_asset_url('./css/main.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 — ブログ記事テンプレート(抜粋)
<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>
{% 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 — グローバルヘッダー
<header class=
"site-header">
<div class=
"header-inner">
<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
%}
{% 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>
<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 データをテンプレートで表示する
{% 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 — モバイルファーストのブレークポイント設計
:root {
--breakpoint-sm:
576px;
--breakpoint-md:
768px;
--breakpoint-lg:
992px;
--breakpoint-xl:
1200px;
}
.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;
}
}
.dnd-section { width:
100%; }
.dnd-column { min-width:
0; }
HubL — レスポンシブ画像と遅延読み込み
<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
Sandbox でテーマをダウンロード
hs cms fetch "my-theme" ./themes/my-theme --portal dev-sandbox でローカルに取得。
-
2
Watch モードで開発開始
hs cms watch ./themes/my-theme my-theme --portal dev-sandbox でファイル保存時に自動アップロード。
-
3
ブラウザでプレビュー確認
HubSpot の「プレビュー」機能で実際のレンダリングを確認。デベロッパーツールでレスポンシブ確認も行う。
-
4
Git にコミット
変更を commit & push。PR を作成してチームレビューを受ける。
-
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 の設定・
カスタムモジュールのベストプラクティスを実践的に解説します。