HubSpot CMS development is determined by "template design." Systematizes implementation patterns from inheritance structure, DnD area, partial usage, complete reference of HubL variables, to blogs, LPs, and error pages. Contains best practices derived from analysis of actual projects (964 templates).
HubSpot CMS templatesThe “skeleton” of the pageDefine. When marketers create a page, they choose which template to use. The starting point for design is to first organize the types and roles of templates.
| Template type | File name conventions | Applications/Features |
|---|---|---|
| General purpose page | page.html | Company profile, service introduction, recruitment, etc. With header/footer. Place modules freely in the DnD area. |
| landing page | landing-page.html | LP for advertising and campaigns. Minimize or none header/footer. Layout to maximize conversions. |
| Blog list | blog-listing.html | List of blog articles. Linked to HubSpot blog. Implemented pagination tag filter. |
| Blog article | blog-post.html | Blog article details page. Linked to HubSpot blog. Implement structured data, previous and subsequent articles, and related articles. |
| Search results | search-results.html | Site search results page. Integrates with HubSpot's search API. |
| error page | error-page.html | Error pages such as 404. No DnD/Simple configuration. |
| Password protected page | password-prompt.html | Membership/limited content access control (Enterprise). |
In the analysis of the actual project code base,964 templatehas been confirmed. This is the scale of managing multiple themes, clients, and brand variations in one portal. In a typical configuration for a single site6-12 templatesis standard.
HubSpot CMS templatesInheritanceIt has a mechanism of
{% extends %} Specify the base template with
{% block %} Defines the area to be overwritten.
With this mechanism, changes to the header, footer, and head tag can be reflected in all templates in one file.
{# ===== Parent template (layouts/base.html) ===== #} <!DOCTYPE html> <html lang="ja"> <head> {% block head_meta %}{% endblock %} {# ← You can add OGP etc. as a child #} {{ standard_header_includes }} {# HubSpot required #} </head> <body> {% block header %} {% include "../partials/header.html" %} {% endblock %} <main> {% block main_content %}{% endblock %} {# ← Each template implements this #} </main> {% block footer %} {% include "../partials/footer.html" %} {% endblock %} {% block extra_js %}{% endblock %} {# ← Page-specific JS #} {{ standard_footer_includes }} {# HubSpot required #} </body> </html>{# ===== Child template (templates/page.html) ===== #} {% extends "../layouts/base.html" %} {# Add OGP to head_meta block #} {% block head_meta %} <meta property="og:title" content="{{ content.html_title }}"> <meta property="og:description" content="{{ content.meta_description }}">{% endblock %} {# Implement main content in DnD area #} {% block main_content %} {% dnd_area "main_dnd" label="Main content" %} {# Default module placement #} {% end_dnd_area %} {% endblock %}
| block name | role | Handling in child templates |
|---|---|---|
| head_meta | Page-specific meta information such as OGP, canonical, structured data, etc. | Override if necessary |
| extra_css | Page-specific CSS links and inline styles | Add as needed |
| header | global header | Override to minimize/hide in LP |
| main_content | Page-specific main content | Must be implemented (required block) |
| footer | global footer | Override to minimize in LP |
| extra_js | Page-specific JS (form initialization, GTM events, etc.) | Add as needed |
base.html is the inheritance source for all templates.
The code you write here will affect all pages.
It is possible to accurately implement the head tag, OGP, CSS loading, and placement of HubSpot required tags.
It affects the overall quality of the site.
{# ===================================================== base.html — where all templates are inherited from ===================================================== [Block list] head_meta: Page-specific meta information (OGP, canonical, etc.) extra_css : Page-specific CSS header: Global header (overridden with LP, etc.) main_content: ★ Main content (must be implemented) footer: Global footer (overridden with LP, etc.) extra_js: page-specific JS ===================================================== #} <!DOCTYPE html> <html lang="{{ content.language|default("ja") }}" prefix="og: https://ogp.me/ns#"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> {# ===== Page title ===== #} <title>{{ content.html_title }}</title> {# ===== meta description ===== #} {% set desc = "" %} {% if content.meta_description %} {% set desc = content.meta_description %} {% elif content.post_body %} {% set desc = content.post_body|striptags|truncate(110,end="") %} {% else %} {% set desc = site_settings.meta_description|default("") %} {% endif %} <meta name="description" content="{{ desc|escape }}"> {# ===== canonical ===== #} <link rel="canonical" href="{{ content.absolute_url }}"> {# ===== OGP (common part) ===== #} <meta property="og:site_name" content="{{ site_settings.company_name|escape }}"> <meta property="og:locale" content="ja_JP"> <meta property="og:url" content="{{ content.absolute_url }}"> <meta property="og:title" content="{{ content.html_title|escape }}"> <meta property="og:description" content="{{ desc|escape }}"> {# OGP image (with fallback) #} {% set ogp_image = content.featured_image |default(site_settings.favicon.src) |default(get_asset_url("../images/ogp-default.png")) %} <meta property="og:image" content="{{ ogp_image }}"> <meta property="og:type" content="website"> {# Twitter Card #} <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="{{ content.html_title|escape }}"> <meta name="twitter:description" content="{{ desc|escape }}"> <meta name="twitter:image" content="{{ ogp_image }}"> {# ===== Organization structured data (common to all pages) ===== #} <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Organization", "name": "{{ site_settings.company_name|escape }}", "url": "{{ site_settings.website_url }}", "logo": { "@type": "ImageObject", "url": "{{ get_asset_url("../images/logo.png") }}" } } </script> {# ===== Page-specific meta (added in child template) ===== #} {% block head_meta %}{% endblock %} {# ===== Global CSS (via CDN/asynchronous loading) ===== #} <link rel="preload" as="style" href="{{ get_asset_url('../css/main.css') }}" onload="this.rel='stylesheet'"> <noscript> <link rel="stylesheet" href="{{ get_asset_url('../css/main.css') }}"> </noscript> {# ===== Expand theme.json value as CSS variable ===== #} <style> :root { --color-primary : {{ theme.colors.primary_color.color }}; --color-secondary : {{ theme.colors.secondary_color.color }}; --color-text : {{ theme.colors.text_color.color }}; --font-heading : '{{ theme.typography.heading_font.font }}', sans-serif; --font-body : '{{ theme.typography.body_font.font }}', sans-serif; --container-max : {{ theme.layout.container_max_width }}px; --section-padding : {{ theme.layout.section_padding_v }}px; --border-radius : {{ theme.layout.border_radius }}px; } </style> {# ===== Page-specific CSS (added in child template) ===== #} {% block extra_css %}{% endblock %} {# ===== HubSpot required tag (placed at the end of head) ===== #} {{ standard_header_includes }} </head> <body> {# ===== Skip navigation (accessibility) ===== #} <a href="#main-content" class="skip-link">メインコンテンツへスキップ</a> {# ===== Global header (can be overridden in children) ===== #} {% block header %} {% include "../partials/header.html" %} {% endblock %} {# ===== Main content (implemented by child template) ===== #} <main id="main-content" role="main" tabindex="-1"> {% block main_content %}{% endblock %} </main> {# ===== Global footer (can be overridden in children) ===== #} {% block footer %} {% include "../partials/footer.html" %} {% endblock %} {# ===== Global JS ===== #} <script src="{{ get_asset_url('../js/main.js') }}" defer></script> {# ===== Page-specific JS (added in child template) ===== #} {% block extra_js %}{% endblock %} {# ===== HubSpot required tag (placed at the end of body) ===== #} {{ standard_footer_includes }} </body> </html>
{{ standard_header_includes }}(in head) and
{{ standard_footer_includes }}(body end) is
Automatically insert essential scripts for HubSpot tracking, forms, live chat, etc.
If you omit this, serious problems will occur such as the form not working or analysis data not being collected.
Be sure to include both in all templates.
The DnD area is an area where marketers can freely add and rearrange modules using the page editor. "Marketers can freely place content within the layout designed by engineers." That is the essence of DnD.
{% extends "../layouts/base.html" %} {% block main_content %} {# ===== DnD area definition ===== label: the name displayed in the page editor ===================================================== #} {% dnd_area "main_dnd" label="Main content" %} {# --- Default content (initial display when creating a new page) --- #} {% dnd_section %} {% dnd_column %} {% dnd_module path="../modules/hero-banner" label="Hero Banner" heading="Please enter a heading" subheading="Please enter a subheading" %} {% end_dnd_column %} {% end_dnd_section %} {% end_dnd_area %} {% endblock %}
{# Style parameters can be passed to dnd_section However, these are settings that can be overwritten in the page editor. If you want to force a fixed appearance, it is safer to control it with CSS. #} {% dnd_section vertical_alignment="MIDDLE" padding={{ { "top" : { "value": 80, "units": "px" }, "bottom": { "value": 80, "units": "px" }, "left" : { "value": 20, "units": "px" }, "right" : { "value": 20, "units": "px" } } }} background_color={{ { "css": "#f8fafc" } }}%} {% dnd_column %} {% dnd_module path="../modules/card-grid" %} {% end_dnd_column %} {% end_dnd_section %} {# ===== Two-column layout section ===== #} {% dnd_section %} {% dnd_column width=7 %} {# 7/12 column width #} {% dnd_module path="../modules/rich-text" %} {% end_dnd_column %} {% dnd_column width=5 %} {# 5/12 column width #} {% dnd_module path="../modules/form-section" %} {% end_dnd_column %} {% end_dnd_section %}
In the code base analysis of the actual project,dnd_area to allowed_modules The limit of
Almost no settings were made. this is"Flexibility to place all custom modules on any page"
This is a design decision that prioritizes.
If you want to set a limit, write it like this:
{% dnd_area "main" allowed_modules=["../modules/hero-banner", "../modules/card-grid"] %}
HubSpot offers multiple ways to reuse files. It is important to use them appropriately depending on the purpose.
| method | HubL syntax | Applications/Features |
|---|---|---|
| include (partial) | {% include "path" %} | Load files in a theme statically. Common HTML such as headers and footers. Cannot be edited directly from the management screen. |
| include_dnd_partial | {% include_dnd_partial path="...", context={} %} | An extended version that allows you to include while passing the context variable. Used to switch variants (transparent header, etc.). |
| global_module | {% global_module "module_id" %} | A global module created in the HubSpot admin screen. Marketers can edit directly from the admin screen. |
| global_partial | {% global_partial path="path" %} | Partials that can be edited on the admin screen. Use this when you want to open the header and footer to marketers. |
What was distinctive about the actual case was include_dnd_partial to context This is a pattern to pass.
Even with the same header, you can switch between variants such as "transparent" and "white background" for each page.
{# ===== LP/top page that requires transparent header ===== #} {% block header %} {% include_dnd_partial path="../partials/header.html", context={ "header_variant": "transparent" } %} {% endblock %} {# ===== Normal page (no context → default white header) ===== #} {# {% block header %} in base.html is used as is #} {# ===== partials/header.html side implementation ===== #} <header class="site-header site-header--{{ header_variant|default("default") }}" role="banner"> {% if header_variant == "transparent" %} {# Override CSS variables for transparent headers #} <style> .site-header--transparent { --header-bg : transparent; --header-color: #ffffff; } </style> {% endif %} {# ... Implementing navigation ... #} </header>
<nav id="global-nav" class="global-nav" aria-label="Global navigation" aria-hidden="true"> {% set nav_items = [ { "label": "service", "url": "/service" }, { "label": "Case study", "url": "/case" }, { "label": "Blog", "url": "/blog" }, { "label": "Company Profile", "url": "/about" }, { "label": "inquiry", "url": "/contact" } ] %} <ul class="global-nav__list" role="list"> {% for item in nav_items %} {% set is_current = request.path starts_with item.url %} <li class="global-nav__item{% if is_current %} is-current{% endif %}"> <a href="{{ item.url }}" class="global-nav__link" {% if is_current %}aria-current="page"{% endif %}> {{ item.label }} </a> </li> {% endfor %} </ul> </nav>{# Hamburger button (open/close state managed with aria-expanded) #} <button class="hamburger" aria-controls="global-nav" aria-expanded="false" aria-label="Open menu"> <span aria-hidden="true"></span> <span aria-hidden="true"></span> <span aria-hidden="true"></span> </button>
The entire template of the actual project was scanned and extracted.HubL elements actually usedorganize.
| filter | Purpose | Implementation example |
|---|---|---|
| | truncate(N) | truncate string to N characters | {{ content.name | truncate(50) }} |
| | escape | HTML escape (XSS prevention) | {{ module.text | escape }} |
| | striptags | Remove HTML tags and convert to plain text | {{ content.post_body | striptags | truncate(100) }} |
| | datetimeformat | Format date and time in specified format | {{ content.publish_date | datetimeformat("%Y年%m月%d日") }} |
| | replace(a, b) | String replacement | {{ tag.slug | replace("type:", "") }} |
| | default(val) | Specify default value if value is missing | {{ module.label | default("詳しく見る") }} |
| | lower | lowercase | {{ tag.slug | lower }} |
| | upper | capitalization | {{ content.language | upper }} |
| | length | Get list count | {{ module.items | length }} |
| | pprint | Debug output of variable contents | {{ module | pprint }}(Deleted in production) |
| function | Purpose |
|---|---|
| blog_recent_posts(blog_id, count) | Get N latest articles of specified blog |
| blog_recent_posts("default", count) | Get the latest articles of default blog |
| blog_tag_posts(blog_id, tag_slug, count) | Get articles with specific tags |
| blog_total_post_count(blog_id) | Get the total number of blog articles |
| hubdb_table_rows(table, opts) | Get records from HubDB table |
| hubdb_table_row(table, row_id) | Get one specific record from HubDB |
| get_asset_url("path") | Get the CDN URL of the assets in the theme (required) |
| content_by_id(id) | Get page object with specified ID |
{% extends "../layouts/base.html" %} {% block head_meta %} {# Add "Nth page" to the description for the second and subsequent pagination pages #} {% if current_page_num and current_page_num > 1 %} <meta name="robots" content="noindex, follow"> {% endif %} {# Pagination prev/next links #} {% if current_page_num > 1 %} <link rel="prev" href="{{ blog_page_link(current_page_num - 1) }}"> {% endif %} {% if current_page_num < last_page_num %} <link rel="next" href="{{ blog_page_link(current_page_num + 1) }}"> {% endif %} {% endblock %} {% block main_content %} {# ===== Page header ===== #} <div class="blog-listing-hero"> {% if active_tag %} <h1>タグ:{{ active_tag.name }}</h1> {% elif active_author %} <h1>{{ active_author.full_name }} の記事</h1> {% else %} <h1>ブログ</h1> {% endif %} </div> <div class="blog-listing container"> {# ===== Tag filter ===== #} {% set all_tags = blog_all_tags("default") %} {% if all_tags %} <nav class="blog-listing__tags" aria-label="Filter by tag"> <a href="{{ blog_all_posts_url("default") }}" class="tag-btn{% if not active_tag %} is-active{% endif %}">すべて</a> {% for tag in all_tags %} <a href="{{ tag.absolute_url }}" class="tag-btn{% if active_tag and active_tag.slug == tag.slug %} is-active{% endif %}" {% if active_tag and active_tag.slug == tag.slug %} aria-current="true" {% endif %}> {{ tag.name }} </a> {% endfor %} </nav> {% endif %} {# ===== Article list loop ===== #} {% if contents %} <ul class="blog-listing__grid" role="list"> {% for post in contents %} <li class="blog-card"> <a href="{{ post.absolute_url }}" class="blog-card__link"> {# Thumbnail #} {% if post.featured_image %} <div class="blog-card__thumb"> <img src="{{ post.featured_image }}?width=640&format=webp" alt="{{ post.featured_image_alt_text|default(post.name)|escape }}" loading="lazy" decoding="async" width="640" height="360"> </div> {% endif %} {# tag (displayed excluding type: prefix) #} {% set display_tags = [] %} {% for tag in post.tag_list %} {% if not tag.slug starts_with "type:" %} {% set display_tags = display_tags|list + [tag] %} {% endif %} {% endfor %} {% if display_tags %} <div class="blog-card__tags"> {% for tag in display_tags|slice(0,2) %} <span class="tag-badge">{{ tag.name }}</span> {% endfor %} </div> {% endif %} <h2 class="blog-card__title">{{ post.name }}</h2> <p class="blog-card__excerpt"> {{ post.post_body|striptags|truncate(80,end="…") }} </p> <time class="blog-card__date" datetime="{{ post.publish_date|datetimeformat("%Y-%m-%d") }}"> {{ post.publish_date|datetimeformat("%Y year %m month %d day") }} </time> </a> </li> {% endfor %} </ul> {# ===== Pagination ===== #} {% if last_page_num > 1 %} <nav class="pagination" aria-label="Page navigation"> {% if current_page_num > 1 %} <a href="{{ blog_page_link(current_page_num - 1) }}" class="pagination__btn" aria-label="Previous page">← 前へ</a> {% endif %} {% for page_num in range(1, last_page_num + 1) %} {% if page_num == current_page_num %} <span class="pagination__num is-current" aria-current="page">{{ page_num }}</span> {% else %} <a href="{{ blog_page_link(page_num) }}" class="pagination__num">{{ page_num }}</a> {% endif %} {% endfor %} {% if current_page_num < last_page_num %} <a href="{{ blog_page_link(current_page_num + 1) }}" class="pagination__btn" aria-label="Next page">次へ →</a> {% endif %} </nav> {% endif %} {% else %} <p class="blog-listing__empty">記事が見つかりませんでした。</p> {% endif %} </div>{% endblock %}
{% extends "../layouts/base.html" %} {# ===== Article structured data + OGP override ===== #} {% block head_meta %} <meta property="og:type" content="article"> {% if content.featured_image %} <meta property="og:image" content="{{ content.featured_image }}"> {% endif %} {# Article schema #} <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Article", "headline" : "{{ content.name|escape }}", "datePublished" : "{{ content.publish_date|datetimeformat("%Y-%m-%dT%H:%M:%S") }}+09:00", "dateModified" : "{{ content.updated|datetimeformat("%Y-%m-%dT%H:%M:%S") }}+09:00", "image" : "{{ content.featured_image|default(get_asset_url("../images/ogp-default.png")) }}", "author": { "@type": "Person", "name" : "{{ content.blog_post_author.full_name|default(site_settings.company_name)|escape }}" }, "publisher": { "@type": "Organization", "name" : "{{ site_settings.company_name|escape }}", "logo" : { "@type": "ImageObject", "url": "{{ get_asset_url("../images/logo.png") }}" } }, "mainEntityOfPage": { "@type": "WebPage", "@id": "{{ content.absolute_url }}" } } </script> {# Preloading LCP elements (featured images) #} {% if content.featured_image %} <link rel="preload" as="image" href="{{ content.featured_image }}?width=900&format=webp" fetchpriority="high"> {% endif %} {% endblock %} {% block main_content %} {# ===== Content type determination (tag prefix) ===== #} {% set ns = namespace(content_type="blog") %} {% for tag in content.tag_list %} {% if tag.slug starts_with "type:" %} {% set ns.content_type = tag.slug|replace("type:","") %} {% endif %} {% endfor %} <article class="post post--{{ ns.content_type }}"> {# ===== Header ===== #} <header class="post__header container"> {# Tag display (type: prefix excluded) #} {% set display_tags = [] %} {% for tag in content.tag_list %} {% if not tag.slug starts_with "type:" %} {% set display_tags = display_tags|list + [tag] %} {% endif %} {% endfor %} {% for tag in display_tags %} <a href="{{ tag.absolute_url }}" class="tag-badge">{{ tag.name }}</a> {% endfor %} <h1 class="post__title">{{ content.name }}</h1> <div class="post__meta"> {% if content.blog_post_author.avatar %} <img src="{{ content.blog_post_author.avatar }}" alt="{{ content.blog_post_author.full_name }}" class="post__author-avatar" width="40" height="40" loading="lazy"> {% endif %} <span>{{ content.blog_post_author.full_name }}</span> <time datetime="{{ content.publish_date|datetimeformat("%Y-%m-%d") }}"> {{ content.publish_date|datetimeformat("%Y year %m month %d day") }} </time> </div> </header> {# ===== Eye-catching image (LCP element) ===== #} {% if content.featured_image %} <div class="post__eyecatch"> <img src="{{ content.featured_image }}?width=900&format=webp" alt="{{ content.featured_image_alt_text|default(content.name)|escape }}" width="900" height="506" loading="eager" fetchpriority="high" decoding="sync"> </div> {% endif %} {# ===== Body ===== #} <div class="post__body container"> {{ content.post_body }} </div> {# ===== Previous/previous article navigation ===== #} <nav class="post-nav container" aria-label="Previous and subsequent articles"> {% if content.prev_post_featured_image or content.prev_post_name %} <a href="{{ content.prev_post_url }}" class="post-nav__prev"> <span>← 前の記事</span> <p>{{ content.prev_post_name }}</p> </a> {% endif %} {% if content.next_post_name %} <a href="{{ content.next_post_url }}" class="post-nav__next"> <span>次の記事 →</span> <p>{{ content.next_post_name }}</p> </a> {% endif %} </nav> {# ===== Related articles (latest 3 with same tag) ===== #} {% if content.tag_list %} {% set related_tag = display_tags|first %} {% if related_tag %} {% set related_posts = blog_tag_posts("default", related_tag.slug, 4) %} {% set filtered = [] %} {% for p in related_posts %} {% if p.id != content.id and filtered|length < 3 %} {% set filtered = filtered|list + [p] %} {% endif %} {% endfor %} {% if filtered %} <section class="post-related container"> <h2>関連記事</h2> <ul class="blog-listing__grid"> {% for p in filtered %} <li><a href="{{ p.absolute_url }}">{{ p.name }}</a></li> {% endfor %} </ul> </section> {% endif %} {% endif %} {% endif %} </article>{% endblock %}
LP minimizes headers and footers to focus on conversions.
{% block header %} and {% block footer %} override.
{% extends "../layouts/base.html" %} {# ===== LP header (logo only, no navigation) ===== #} {% block header %} <header class="lp-header" role="banner"> <a href="/" class="lp-header__logo"> <img src="{{ get_asset_url('../images/logo.svg') }}" alt="{{ site_settings.company_name }}" height="40" loading="eager"> </a> </header>{% endblock %} {# ===== Footer for LP (copyright only) ===== #} {% block footer %} <footer class="lp-footer" role="contentinfo"> <p>© {{ "now"|datetimeformat("%Y") }} {{ site_settings.company_name }}</p> </footer>{% endblock %} {# ===== DnD area (freely place sections) ===== #} {% block main_content %} {% dnd_area "lp_dnd" label="LP content" %} {% dnd_section %} {% dnd_column %} {% dnd_module path="../modules/hero-banner" %} {% end_dnd_column %} {% end_dnd_section %} {% end_dnd_area %} {% endblock %}
{% extends "../layouts/base.html" %} {% block main_content %} <div class="error-page container"> <h1 class="error-page__code">404</h1> <p class="error-page__message"> お探しのページは見つかりませんでした。 </p> <p> <a href="/" class="btn btn--primary">トップページへ</a> </p> {# Display the latest articles to encourage people to browse the site #} {% set recent = blog_recent_posts("default", 3) %} {% if recent %} <section> <h2>新着記事</h2> <ul> {% for post in recent %} <li><a href="{{ post.absolute_url }}">{{ post.name }}</a></li> {% endfor %} </ul> </section> {% endif %} </div>{% endblock %}
Confirmed in actual case {% macro %} It's a pattern.
When reusing the same HTML structure in multiple places, you can define and call it like a function.
{# ===== Macro definition (place at the beginning of the file or in partials) ===== #} {% macro render_post_card(post, show_excerpt=true, show_tags=true) %} <article class="blog-card"> {% if post.featured_image %} <a href="{{ post.absolute_url }}"> <img src="{{ post.featured_image }}?width=640&format=webp" alt="{{ post.featured_image_alt_text|default(post.name)|escape }}" loading="lazy" width="640" height="360"> </a> {% endif %} <div class="blog-card__body"> {% if show_tags %} {% for tag in post.tag_list|slice(0,2) %} {% if not tag.slug starts_with "type:" %} <span class="tag-badge">{{ tag.name }}</span> {% endif %} {% endfor %} {% endif %} <h3><a href="{{ post.absolute_url }}">{{ post.name }}</a></h3> {% if show_excerpt %} <p>{{ post.post_body|striptags|truncate(80,end="…") }}</p> {% endif %} </div> </article>{% endmacro %} {# ===== Call (can be used from anywhere, such as blog list, related articles, etc.) ===== #} {% for post in contents %} {{ render_post_card(post) }} {% endfor %} {# Version without excerpts and tags (compact display) #} {{ render_post_card(post, show_excerpt=false, show_tags=false) }}
is_in_editor when viewed in HubSpot's page editor. true It will be.
Confirmed in actual case"Do not load heavy scripts in the editor"It's a pattern.
{# ===== Do not load script in editor ===== Reason: Animation/external API calls reduce editor performance. Because it may be dropped. It does not affect the actual page display. ===================================================== #} {% if not is_in_editor %} <script src="//js.hsforms.net/forms/embed/v2.js"></script> <script> hbspt.forms.create({ portalId: "{{ hub_id }}", formId : "FORM_ID", target : "#hs-form-target" }); </script>{% endif %} {# Show placeholder only in editor #} {% if is_in_editor %} <div class="editor-placeholder"> [ここにフォームが表示されます] </div>{% endif %}
This is an advanced pattern confirmed in actual cases. Pass complex data to the module in JSON format and process it on the JavaScript side.
{# ===== Pass HubDB data to module in JSON ===== Usage: UI components initialized with JS, such as sliders, maps, charts, etc. ===================================================== #} {% set rows = hubdb_table_rows("staff", { "orderBy": "order", "limit": 20 }) %} {% set staff_data = [] %} {% for row in rows %} {% set staff_data = staff_data|list + [{ "name" : row.name, "title" : row.title, "image" : row.photo.url, "profile": row.profile|striptags }] %} {% endfor %} {# Embed JSON in data-* attributes #} <div id="staff-slider" class="staff-slider" data-staff='{{ staff_data|tojson }}'> {# JS reads data-staff attribute and initializes slider #} </div> <script> var staffData = JSON.parse( document.getElementById('staff-slider').dataset.staff ); // Initialize the slider using staffData </script>
Collect elements common to all templates in base.html, and override only the necessary parts of each template using {% extends %} + {% block %}. Changes to head, header, and footer can be completed in one base.html file.
If standard_header_includes (in the head) and standard_footer_includes (at the end of the body) are omitted, forms, tracking, chat, etc. will not work. Must be included in all templates.
Four-layer structure: dnd_area → dnd_section → dnd_column → dnd_module. It is a design decision whether to restrict placement modules using allowed_modules. In actual projects, there are often no restrictions (all modules can be placed).
Even for the same header, you can manage "transparent header" and "regular header" in one file by passing variants using the context variable. Project-specific ideas confirmed in actual projects.
The five core categories are content (page information) / request (request) / site_settings (site settings) / theme (design token) / is_in_editor (editor judgment).
macro = reuse of the same HTML structure. is_in_editor = Disable heavy scripts in editor. tojson + data attribute = bridge HubL data to JS. These three occur frequently in actual cases.