カスタムモジュール
本ページでは、カスタムモジュールについてより詳しく解説していきます。作成の仕方や、使用方法などはモジュールページを確認してください。
またカスタムモジュールについては、公式ドキュメントにおいても詳しく解説されています。公式ドキュメントは英語ではありますが、重複する内容を解説しても仕方ありませんので、本ページではカスタムモジュールを使用するにおいて特に重要な点や、実務において役立つ使い方などを解説していきます。
【公式ドキュメントのカスタムモジュールに関するページ一覧】
目次
カスタムモジュール ≒ Single File Component
カスタムモジュールは
- HTML + HUBL
- CSS
- JavaScript
- フィールド
- 付随する依存アセット(画像や他のCSS、JavaScriptなど)
から成り立っており、1つのUIに関する表示(HTMLやHubL、CSS)から挙動(JavaScript)を1つの「カスタムモジュール」という単位にまとめることができるようになっています。
また特にJavaScriptに関しては、クロージャを用いて独立したネームスペースを自動的に形成します(Scoped CSSについては後述します)。
このことからカスタムモジュールは「入力UI(フィールド)を備える、Single File Componentに近しい存在」ということができ、ページ編集画面を介さずに、テンプレートから直接カスタムモジュールにデータを渡し出力することも可能です。
※ただしカスタムモジュール内で別のカスタムモジュールを呼ぶことはできないため、純粋なSFCと比較して一定の制限があることも事実です。
つまりいかにカスタムモジュールを上手く設計するか、再利用するかがWebサイトの寿命に関わるといっても過言ではありません。
フィールドの値をCSSやJavaScriptに渡す
例えば
- 「ページ編集画面で選択した色を反映させたい」
- 「ページ編集画面の値に合わせて、JavaScriptの挙動を変更したい」
というようなことがあるでしょう。これについては開発画面の見方(Code editor details)でも触れられていますが、結論から言うとCSS欄及びJavaScript欄では module_asset_url
変数以外のHubLは展開できません。
そのため、HTML + HubL欄で
- styleタグやscriptタグを書く
- HTMLのstyle属性やdata属性に値を渡す
などの対応をする必要があります。例えばフォントフィールドを利用してページ編集画面で選択したフォントをCSSに反映させたい場合は、HTML + HUBL欄において次のような記述になります。
<h2 class="el_lv2Heading"d>{{ module.title }}</h2d> <style> .el_lv2Heading { color: {{ module.font_field.color }}; font-size: {{ module.font_field.size }}{{module.font_field.size_unit }}; {{ module.font_field.style }}; /* この変数は複数のプロパティ名と値を出力します */ } </style> ↓ 出力結果 <style> .el_lv2Heading { color: #000000; font-size: 24px; font-weight: normal; font-family: 'courier new', courier; font-style: italic; } </style>
CSS欄に記載したコードはページ読み込み時にHubSpotが自動的に1つのCSSファイルにまとめ、かつminifyしてくれます。しかしこのstyleタグのコードはそれらの対象ではなく、また同じカスタムモジュールを同一ページ内で複数使用すれば、同じ内容がその分だけ出力されてしまいます。
そのためHubLを受けるコードのみをHTML + HUBL欄に記載し、それ以外のコードはCSSまたはJavaScript欄に書くべきでしょう。ただし、JavaScript欄のコードは独自のネームスペースを形成するため、HTML + HUBL欄に必要分だけJavaScriptを書くのはかえって少々面倒です。
またHTML + HUBL欄に書いたstyleタグ及びscriptタグは、bodyタグ内のカスタムモジュールが埋め込まれた箇所に当然そのまま出力されます。しかし require_css 及び require_js HubLタグでコードをラップすることでheadタグ内に出力することができ、これはドキュメントでも推奨されているベストプラクティスです。
CSSをScopedにする
上記のコードは実は問題があります。というのも、同一ページ内でこのカスタムモジュールを複数使用し、それぞれに異なるフォント設定をしても、全て最後に出力されたstyleタグのスタイリングで統一されてしまうのです。
効率的にカスタムモジュールを開発する
現在のカスタムモジュールはグループ化や繰り返しがあり、HubSpotの管理画面上でもとても効率的に開発できるようになっています(昔はどちらもなかったため、同じようなフィールドをひたすら量産し、【グループ1】【グループ2】などとしてグループ化と繰り返しを擬似的に再現していました。つらかった…)。
しかし、HubSpot FTPまたはLocal Development Toolsを使用してカスタムモジュールをローカルマシンにダウンロードし、コードベースで編集することでカスタムモジュールの開発はさらに高速になります。
カスタムモジュールのディレクトリ構成
カスタムモジュールをダウンロードすると、「モジュール名.module」というディレクトリが作成されますが、このディレクトリが1つのカスタムモジュールとなります。
さらにディレクトリの中を覗くと、次のような構成になっています。
└── デモモジュール.module ├── _locales ├── fields.json ├── meta.json ├── module.css ├── module.html └── module.js
これらのうち「module.*」 となっているファイルは、それぞれHTML + HUBL、CSS、JavaScript欄に対応するファイルです。その他は次の通りです。
- _localesディレクトリ
- モジュールオプションの「フィールド翻訳」にて翻訳を行っていると、_locales内に翻訳用のディレクトリとファイルが生成されます
- fields.json
- カスタムモジュールのフィールドを管理しているファイルです
- meta.json
- カスタムモジュールの外部アセットへの依存や、使用できるテンプレートタイプ、グローバルかどうかなどメタ情報を管理しているファイルです
このうちfields.jsonがカスタムモジュール開発において1番深く関わるファイルですので、詳細に解説します。
fields.jsonの書式
fields.json内にJSONオブジェクトを追加していくことで、モジュールを追加していきます。例えば先ほどから使用している
- タイトル(テキストフィールド)
- フォント設定(フォントフィールド)
を持つデモモジュールのfields.jsonを見てみると、次のようになっています。
[ { "allow_new_line": false, "default": null, "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "7a2c8137-bea8-13f8-2857-77561d0f79c4", "label": "タイトル", "locked": false, "type": "text", "required": false }, { "load_external_fonts": true, "default": { "size": 12, "color": "#000", "styles": {}, "size_unit": "px" }, "name": "font_field", "id": "f5285ab9-24a9-f8b5-8d98-763da55aa64e", "label": "フォント", "locked": false, "type": "font", "required": false } ]
各フィールドによって利用可能なプロパティは異なりますが、全てのフィールドに共通のプロパティは次の通りです。
- type
- id
- label
- locked
- help_text
- name
- required
- visibility
- default
これらの詳細な解説、及び各フィールドのプロパティ一覧については公式ドキュメントにありますので、そちらに任せます。1点特筆すべき点としては、上記のコードではidはランダムなものとなっています。これはHubSpot CMSが自動的にユニークなIDを割り振った結果であり、ローカル開発を行う際は、このidは省略して構いません。
グループの形成
管理画面上で複数のフィールドを選択してグループを形成することができますが、ローカル開発でもフィールドのグループ化は可能です。先ほどから使用しているカスタムモジュールにリッチテキストフィールドを追加し、タイトルとリッチテキストを1つのグループとしてみましょう。まずはグループ化していない状態では、次のようなJSONになります。
[ { "allow_new_line": false, "default": null, "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "7a2c8137-bea8-13f8-2857-77561d0f79c4", "label": "タイトル", "locked": false, "type": "text", "required": false }, { // 追加 "default": null, "name": "text", "id": "6678c5f6-00d1-d02b-5cf2-579fabf43a77", "label": "テキスト", "locked": false, "type": "richtext", "required": false }, { "load_external_fonts": true, "default": { "size": 12, "color": "#000", "styles": {}, "size_unit": "px" }, "name": "font_field", "id": "f5285ab9-24a9-f8b5-8d98-763da55aa64e", "label": "フォント", "locked": false, "type": "font", "required": false } ]
タイトルとテキストをグループ化すると、次のようなJSONになります。
[ { "default": {}, "tab": "CONTENT", "name": "cont_gruop", "id": "40d7e908-3168-90e5-06fc-9177734e8875", "label": "コンテンツグループ", "locked": false, "type": "group", "required": false, "children": [ { "allow_new_line": false, "default": null, "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "7a2c8137-bea8-13f8-2857-77561d0f79c4", "label": "タイトル", "locked": false, "type": "text", "required": false }, { "default": null, "name": "text", "id": "6678c5f6-00d1-d02b-5cf2-579fabf43a77", "label": "テキスト", "locked": false, "type": "richtext", "required": false } ] }, { // フォントの内容が続きます } ]
着目すべき点は、まずtypeに「gruop」と指定されている点です。typeは text
や richtext
などのフィールドの種類を表していましたので、ここに「gruop」と指定することによりグループの使用を宣言します。
次にchildrenプロパティを見てみると、先ほど第1階層に存在していたタイトルとテキストがchildren配列の中に丸々移動しています。グループ形成の基本はこれだけで、意外と簡単と思えるのではないでしょうか。
しかし、ややこしいのはタイトルとテキストにデフォルト値を設定した場合です。それぞれ「タイトルが入ります」「テキストが入ります」と設定すると、JSONは次のようになります。
[ { "default": { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" }, "tab": "CONTENT", "name": "cont_gruop", "id": "40d7e908-3168-90e5-06fc-9177734e8875", "label": "コンテンツグループ", "locked": true, "type": "group", "required": false, "children": [ { "allow_new_line": false, "default": "タイトルが入ります", "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "7a2c8137-bea8-13f8-2857-77561d0f79c4", "label": "タイトル", "locked": false, "type": "text", "required": false }, { "default": "<p>テキストが入ります<\/p>", "name": "text", "id": "6678c5f6-00d1-d02b-5cf2-579fabf43a77", "label": "テキスト", "locked": false, "type": "richtext", "required": false } ] }, { // フォントの内容が続きます } ]
children内の各フィールドのdefaultに値が入るのは想像がつく挙動ですが、それだけでなく、3行目のグループ自体のdefaultプロパティにも各フィールドの変数名をキーとして同じ値が設定されます。この2度手間のような挙動が、正直ややこしいところです。
繰り返しの設定
繰り返しは、それぞれのモジュールに「occurrence」というプロパティを追加することで実装します。先ほどのグループに繰り返しを設定すると、次のようなコードになります。
[ { "default": { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" }, "tab": "CONTENT", "name": "cont_gruop", "id": "40d7e908-3168-90e5-06fc-9177734e8875", "label": "コンテンツグループ", "locked": true, "type": "group", "required": false, "children": [ { "allow_new_line": false, "default": "タイトルが入ります", "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "7a2c8137-bea8-13f8-2857-77561d0f79c4", "label": "タイトル", "locked": false, "type": "text", "required": false }, { "default": "<p>テキストが入ります<\/p>", "name": "text", "id": "6678c5f6-00d1-d02b-5cf2-579fabf43a77", "label": "テキスト", "locked": false, "type": "richtext", "required": false } ], "occurrence" : { "min" : null, "max" : null, "sorting_label_field" : null, "default" : null } }, { // フォントの内容が続きます } ]
occurrenceプロパティを追加しただけで、その中のプロパティは全てnullとしています。しかしこれだけでも(occurrenceプロパティが存在するだけで)、リピーターオプションは有効になります。
なお、sorting_label_fieldプロパティは「オブジェクト並べ替えラベルにどのフィールドを使用するか」の指定で、グループに対する繰り返し時のみ指定した値が適用されます。値は各フィールドのidを指定します。
"sorting_label_field": "6678c5f6-00d1-d02b-5cf2-579fabf43a77"
ただしid値はローカル開発においては省略することができるため、このランダムな値は一度HubSpotからfields.jsonをダウンロードしないと知ることができません。そんな面倒を解決する手段として、ローカル開発で予めidに任意の文字列を入れてしまう方法もあります。要はid値はカスタムモジュール内においてユニークでさえあればよいので、次のように書くことが可能です。
[ { "default": [ { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります1" } ], "tab": "CONTENT", "name": "cont_gruop", "id": "40d7e908-3168-90e5-06fc-9177734e8875", "label": "コンテンツグループ", "locked": false, "type": "group", "required": false, "children": [ { "allow_new_line": false, "default": "タイトルが入ります", "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "title", // ここでidを可読的なものに決めてしまう "label": "タイトル", "locked": false, "type": "text", "required": false }, { "default": "<p>テキストが入ります<\/p>", "name": "text", "id": "text", // ここでidを可読的なものに決めてしまう "label": "テキスト", "locked": false, "type": "richtext", "required": false } ], "occurrence": { "default": null, "min": null, "max": null, "sorting_label_field": "text" // idの指定 } }, { // フォントの内容が続きます } ]
意外とシンプルなので、一度書式さえ覚えてしまえばサクサクとローカル開発を進められるのではないでしょうか。しかし、またしても複雑なのは
- グループ内のフィールドにデフォルト値を入れた状態で
- 繰り返しの初期値を設定
したときです。グループ内のフィールドには既にデフォルト値を入れていますので、繰り返しの初期値を3と設定してみましょう。すると、JSONは次のようになります。
[ { "default": [ { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" }, { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" }, { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" } ], "tab": "CONTENT", "name": "cont_gruop", "id": "40d7e908-3168-90e5-06fc-9177734e8875", "label": "コンテンツグループ", "locked": false, "type": "group", "required": false, "children": [ { "allow_new_line": false, "default": "タイトルが入ります", "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "title", "label": "タイトル", "locked": false, "type": "text", "required": false }, { "default": "<p>テキストが入ります<\/p>", "name": "text", "id": "text", "label": "テキスト", "locked": false, "type": "richtext", "required": false } ], "occurrence": { "default": 3, "min": null, "max": null, "sorting_label_field": "text" } }, { // フォントの内容が続きます } ]
グループのdefalutが、繰り返しの初期値の分だけ増えました。ではそれぞれの値を書き換えることで、1つ目、2つ目、3つ目にそれぞれ異なる初期値を設定できるのかと思いきや、そういう訳でもありません。上から「タイトルが入ります1」「タイトルが入ります2」「タイトルが入ります3」とした場合、ページ編集画面では次のような挙動になります。
つまり、値そのものには影響せず、ラベルだけに反映された形となります。不思議な挙動ですね。恐らく、そのうち何らかの修正が行われるでしょう。
また、今まで紹介したJSONは実は私が自分の読みやすいようプロパティ順を入れ替えたもので、HubSpotからfileds.jsonをダウンロードしたときのプロパティの並び順は次のようになっています。自分で入れ替えた形でも動作に問題はありませんので、ローカル開発に終始する分には下記の並び順を目にすることはないでしょう。
しかし実際のところ、これより多くのフィールドをグループ内に抱えていたり、さらに表示条件などを設定するとなると、JSONだけで全てを制御するローカル開発はなかなかの熟練者でないと厳しいでしょう。
複雑な設定は適宜HubSpotの管理画面を利用した方が結果として早いことも多いので、HubSpotが標準で出力するプロパティの並び順も見慣れておくとよいでしょう。
[ { "default": [ { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" }, { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" }, { "text": "<p>テキストが入ります<\/p>", "title": "タイトルが入ります" } ], "tab": "CONTENT", "children": [ { "allow_new_line": false, "default": "タイトルが入ります", "validation_regex": "", "name": "title", "show_emoji_picker": false, "id": "title", "label": "タイトル", "locked": false, "type": "text", "required": false }, { "default": "<p>テキストが入ります<\/p>", "name": "text", "id": "text", "label": "テキスト", "locked": false, "type": "richtext", "required": false } ], "name": "cont_gruop", "id": "40d7e908-3168-90e5-06fc-9177734e8875", "label": "コンテンツグループ", "occurrence": { "default": 3, "min": null, "max": null, "sorting_label_field": "title" }, "locked": false, "type": "group", "required": false }, { // フォントの内容が続きます } ]
meta.jsonの書式
meta.jsonについても各プロパティの詳細はドキュメントに記載されていますので、全ては解説しません。よく使うものは、カスタムモジュールにファイルをリンクした際の
- js_assets
- css_assets
- other_assets
ですが、これらに関してはHubSpotの管理画面上で指定しまった方が楽でしょう。fields.jsonに比べ、meta.jsonはローカルで開発をするメリットがあまりないのが正直なところです。
もう1点特筆しておくと、Local Development Toolsの npx hs create module
コマンドを利用してカスタムモジュールを生成した場合、デフォルトで「is_available_for_new_content」がfalseになっています。つまりページ編集画面で追加しようとしても選択肢として出てきませんので、おかしいと思ったらここをチェックするようにしてください。
以下が、meta.jsonの例です。
{ "master_language": "ja", "js_assets": [{ "path": "/pensees-corp/system/assets/js/vendor/slick.min.js", "name": "slick.min.js", "autoload": true }], "host_template_types": [ "PAGE", "BLOG_POST", "BLOG_LISTING" ], "placement_rules": [], "global": false, "content_tags": [], "smart_type": "NOT_SMART", "other_assets": [{ "name": "lightbox-loading-1.gif", "resource_id": null, "autoload": false, "url": "https://cdn2.hubspot.net/hubfs/5445294/blog/assets/img/common/lightbox-loading-1.gif" }], "is_available_for_new_content": true, "css_assets": [{ "path": "/pensees-corp/system/assets/css/slick.css", "name": "slick.css", "autoload": true }], "module_id": 10324363, "external_js": [], "extra_classes": "" }
カスタムモジュールの制限事項
カスタムモジュールのHTML + HUBL欄はページテンプレートと同じように全てのHubLがサポートされている訳ではなく、幾つかの制限事項があります。まず、下記に列挙するタグ、関数はカスタムモジュールでは使用できません。
- タグ
- custom_module
- extends
- import
- include
- module
- raw_jinja
- raw_html
- widget_container
- 関数
- get_public_template_url
- get_public_template_url_by_id
- footer_js
- head_css
- head_js
- include_attached_css
- include_default_custom_css
- include_css
- include_javascript
- include_targeted_definitions
- js_integration_head
- js_integration_body_start
- js_integration_body_end
- require_attached_css
- require_default_custom_css
またカスタムモジュールにおいてフィールドを追加せず、HTML + HUBL欄に下記のようにタグを記述してモジュールを直接埋め込むことも可能です。
{% logo "my_logo" width='200' %}
しかしカスタムモジュール内にタグで埋め込まれたモジュールはoverrideable属性が常にFalseになり、ページ編集画面などにおいて値の編集ができません。またexport_to_template_context属性も使用できないため、「値(とそれに付随するHTML)の出力専用」のような使い方となります。
実際の利用例として、例えばCTAフィールドで得られるのはCTAのguidだけですので、その値をctaタグに渡してCTAを出力する、というような使い方をします(もっとも、CTAに関しては出力用の関数も用意されていますが)。
カスタムモジュールのデータを再利用する
通常のモジュールと同じように、カスタムモジュールにも export_to_template_context=True
設定することで、カスタムモジュールのコンテンツを純粋なデータとして利用することができます。この仕組みを用いて、ブログ詳細のカスタムモジュールのデータをブログアーカイブページに出力したり、あるいはページテンプレートの設定をカスタムモジュールにまとめたりすることもできます。
アイデア次第でいくらでも幅が広がる使い方なので、ぜひ試してみてください。