A deep dive into blog functions, tag design strategies, complete pagination implementation, dynamic data management using HubDB, and personalization using smart content. This is a chapter to master HubSpot's data layer.
The first decision in blog design for HubSpot CMS is “Should I operate multiple content types on one blog or use multiple blogs?”is. This choice depends on both your plan and your operational structure.
| plan | Number of blogs | Recommended design |
|---|---|---|
| Starter | only one | Type management with 1 blog + tags (tag design strategy described below is required) |
| Professional / Enterprise | max 100 | The ideal configuration is to set up separate blogs for each type of content. |
If you use multiple blogs on Professional or higher, you can have separate URLs and templates for each content type.
| Blog name (admin screen) | URL | Purpose | template |
|---|---|---|---|
| Main blog | /blog | Blog articles/columns | blog-listing / blog-post |
| Seminar | /seminar | Seminar/event information | seminar-listing / seminar-post |
| notice | /news | Press release/update information | news-listing / news-post |
| case study | /case | Case Studies/Customer Stories | case-listing / case-post |
When using multiple blogs,blog_recent_posts() Specify the blog ID or blog slug in the first argument.
"default" is an alias for the default blog.
The blog ID is the URL of the HubSpot admin screen (app.hubspot.com/blog/PORTAL_ID/BLOG_ID/posts) to check.
{# Define and manage blog ID as a variable (maintainability improved) #} {% set blog_ids = { "main" : "default", "seminar" : "12345678", {# Seminar blog ID #} "news" : "23456789", {# Notification blog ID #} "case" : "34567890" {# Case blog ID #} } %} {# Get the latest articles from each blog and display them on the top page #} {% set latest_seminars = blog_recent_posts(blog_ids.seminar, 3) %} {% set latest_cases = blog_recent_posts(blog_ids.case, 3) %} {% set latest_news = blog_recent_posts(blog_ids.news, 5) %} {# Check number of seminars #} {% set seminar_total = blog_total_post_count(blog_ids.seminar) %} <p>全{{ seminar_total }}件のセミナー情報</p>{# Cross-search of multiple blogs (obtain the latest articles all at once) #} {% set all_recent = blog_recent_posts("default", 3) %} {% set all_recent = all_recent + blog_recent_posts(blog_ids.seminar, 3) %} {% set all_recent = all_recent + blog_recent_posts(blog_ids.news, 3) %}
If you want to display articles retrieved from multiple blogs sorted by publication date,
HubL's sort You can use filters, butpublish_date is for unix timestamp
Sorting directly requires some ingenuity.
If you need a complete cross-searchImplementation with HubSpot Search API (JavaScript)Please consider.
HubSpot does not have a concept equivalent to WordPress' "categories", Tags also serve as categories. Especially if you can only use one blog with the Starter plan, Tag design is the most important element that affects the information design of the entire site.
{# ======================================================= Pattern for determining multiple roles at the same time from tags Execute at the beginning of article details (blog-post.html) ======================================================= #} {% set ns = namespace( content_type = "blog", categories = [], is_featured = false, is_pickup = false ) %} {% for tag in content.tag_list %} {# ① Content type determination #} {% if tag.slug starts_with "type:" %} {% set ns.content_type = tag.slug|replace("type:", "") %} {# ② Collect categories (add to array) #} {% elif tag.slug starts_with "cat:" %} {% set ns.categories = ns.categories + [tag.slug|replace("cat:", "")] %} {# ③ Status flag determination #} {% elif tag.slug == "status:featured" %} {% set ns.is_featured = true %} {% elif tag.slug == "status:pickup" %} {% set ns.is_pickup = true %} {% endif %} {% endfor %} {# Utilize judgment results #} <article class="post post--{{ ns.content_type }} {% if ns.is_featured %} post--featured{% endif %} {% if ns.is_pickup %} post--pickup{% endif %}"> {% if ns.is_featured %} <span class="badge badge--featured">注目</span> {% endif %} {% if ns.categories %} <div class="post-categories"> {% for cat in ns.categories %} <a href="{{ blog_tag_url("default", "cat:" ~ cat) }}" class="category-link">{{ cat }}</a> {% endfor %} </div> {% endif %} </article>
Prefixed tag (type: / cat: / status:) is
In many cases, you do not want to display it in the article's display tag list.
Prepare a pattern to filter.
{# Extract only "display tags" excluding type: / cat: / status: #} {% set display_tags = [] %} {% for tag in content.tag_list %} {% if not ( tag.slug starts_with "type:" or tag.slug starts_with "cat:" or tag.slug starts_with "status:" ) %} {% set display_tags = display_tags + [tag] %} {% endif %} {% endfor %} {# Output display tag only #} {% if display_tags %} <div class="post-tags"> {% for tag in display_tags %} <a href="{{ tag.tag_url }}" class="post-tag"># {{ tag.name }}</a> {% endfor %} </div>{% endif %}
① Clarify role with prefix: Create rules for different uses such as type: / cat: / status: etc.
② Only alphanumeric characters and hyphens can be used as slugs: Avoid Japanese as it is included in the URL
③ One type: tag per article: Attaching multiple type: tags will complicate the judgment.
④ Create a tag list document: Document naming conventions to avoid confusion for operators
HubSpot's blog list/tag archive page includes:
current_page_num / last_page_num / blog_page_link()
variables and functions can be used automatically.
Use this to implement pagination that is optimal for both SEO and UX.
A method that displays the N page numbers before and after the current page, and inserts an abbreviation (...) at both ends. Ideal if you have a large number of pages.
{% if last_page_num > 1 %} {# Window size: How many items to display before and after the current page #} {% set win = 2 %} <nav class="pagination" aria-label="Pagination"> <ul class="pagination__list"> {# ← Previous button #} <li class="pagination__item pagination__item--prev {% if current_page_num == 1 %} is-disabled{% endif %}"> {% if current_page_num > 1 %} <a href="{{ blog_page_link(current_page_num - 1) }}" aria-label="Go to previous page">←</a> {% else %} <span aria-hidden="true">←</span> {% endif %} </li> {# First page + ellipsis (if current is far from the beginning) #} {% if current_page_num > win + 2 %} <li class="pagination__item"> <a href="{{ blog_page_link(1) }}">1</a> </li> {% if current_page_num > win + 3 %} <li class="pagination__item pagination__item--ellipsis" aria-hidden="true"><span>…</span></li> {% endif %} {% endif %} {# Page number in window #} {% for i in range(1, last_page_num + 1) %} {% if i >= current_page_num - win and i <= current_page_num + win %} <li class="pagination__item {% if i == current_page_num %} is-current{% endif %}"> {% if i == current_page_num %} <span aria-current="page">{{ i }}</span> {% else %} <a href="{{ blog_page_link(i) }}" aria-label="{{ i }}Page number">{{ i }}</a> {% endif %} </li> {% endif %} {% endfor %} {# ellipsis + last page (if current is far from the end) #} {% if current_page_num < last_page_num - win - 1 %} {% if current_page_num < last_page_num - win - 2 %} <li class="pagination__item pagination__item--ellipsis" aria-hidden="true"><span>…</span></li> {% endif %} <li class="pagination__item"> <a href="{{ blog_page_link(last_page_num) }}">{{ last_page_num }}</a> </li> {% endif %} {# Next → button #} <li class="pagination__item pagination__item--next {% if current_page_num == last_page_num %} is-disabled{% endif %}"> {% if current_page_num < last_page_num %} <a href="{{ blog_page_link(current_page_num + 1) }}" aria-label="Next page">→</a> {% else %} <span aria-hidden="true">→</span> {% endif %} </li> </ul> {# Display count information #} <p class="pagination__info"> {{ current_page_num }} / {{ last_page_num }} ページ (全 {{ contents.total_count }} 件) </p> </nav>{% endif %}
This pattern is suitable if you have a small number of articles or want to keep the design simple.
<nav class="pagination pagination--simple" aria-label="Page navigation"> {% if current_page_num > 1 %} <a class="pagination__prev" href="{{ blog_page_link(current_page_num - 1) }}"> ← 新しい記事 </a> {% endif %} {% if current_page_num < last_page_num %} <a class="pagination__next" href="{{ blog_page_link(current_page_num + 1) }}"> 古い記事 → </a> {% endif %} </nav>
HubDB(HubDB) is a cloud-based relational database that can be used on HubSpot (Professional or higher). Data can be managed with a spreadsheet-like operation, and data can be retrieved and displayed from HubL in real time.
| Column name | slug | type | explanation |
|---|---|---|---|
| Row Name (automatic) | name | text | Name (row identifier) |
| display name | display_name | text | Name to display on page |
| post | title | text | Position/Title |
| Department | department | choices | Sales / Marketing / Development / CS |
| face photo | avatar | image | profile photo |
| profile | bio | rich text | Self-introduction text |
| text | Display email address | ||
| linkedin_url | URL | LinkedIn profile URL | |
| Display order | sort_order | numerical value | Display order in list |
| display | is_published | Boolean value | false=hidden |
| function | argument | explanation |
|---|---|---|
| hubdb_table_rows(table, query) | table: table ID or slug query: query parameters (optional) |
Get table row data as an array. Most frequently used functions. |
| hubdb_table_row(table, row_id) | table / row_id: row ID | Get one specific row. Used in dynamic page generation. |
| hubdb_table(table) | table | Obtain table meta information (column definitions, etc.). |
{# ====== Basic data acquisition ====== #} {# Get all results (only display flag = true, sort by display order) #} {% set members = hubdb_table_rows( "team_members", "orderBy=sort_order&is_published=true" ) %} {# Specify upper limit of number of items #} {% set members = hubdb_table_rows( "team_members", "orderBy=sort_order&limit=6" ) %} {# Get only specific departments #} {% set sales_team = hubdb_table_rows( "team_members", "department=Sales&orderBy=sort_order" ) %} {# List data #} {% if members %} <div class="team-grid"> {% for member in members %} <div class="member-card"> {% if member.avatar %} <img src="{{ member.avatar.url }}" alt="{{ member.display_name }}" width="200" height="200" loading="lazy"> {% endif %} <h3>{{ member.display_name }}</h3> <p class="title">{{ member.title }}</p> {% if member.bio %} <div class="bio">{{ member.bio }}</div> {% endif %} {% if member.linkedin_url %} <a href="{{ member.linkedin_url }}" target="_blank" rel="noopener noreferrer"> LinkedIn </a> {% endif %} </div> {% endfor %} </div>{% else %} <p>メンバー情報がありません。</p>{% endif %}
| parameters | How to use | example |
|---|---|---|
| orderBy | Sort (column name, - in descending order) | orderBy=-sort_order(descending order) |
| limit | Maximum number of acquisitions | limit=6 |
| offset | Acquisition start position | offset=6(From the 7th item) |
| column name=value | Filter by value | is_published=true |
| Column name__gt | greater than | price__gt=1000 |
| Column name__lt | less than | price__lt=5000 |
| Column name__contains | contains string | name__contains=東京 |
HubDB's powerful featuresDynamic Pagesis.
One row in HubDB automatically becomes one page,
/team/tanaka-taro individual details page like
It can be automatically generated from DB records.
| element | explanation |
|---|---|
| base url | Specified in the page settings of the HubSpot admin screen (e.g. /team) |
| line slug | HubDB's "Row name" field becomes the URL path (e.g. tanaka-taro → /team/tanaka-taro) |
| template | Prepare one template for Dynamic Pages and share it with all records |
| data access | within the template dynamic_page_hubdb_row You can get the current row data from variables |
{% extends "./layouts/base.html" %} {# Get current row data with dynamic_page_hubdb_row #} {% set member = dynamic_page_hubdb_row %} {# Overwrite page title with member name #} {% block meta_title %} <title>{{ member.display_name }} | チーム | {{ site_settings.company_name }}</title>{% endblock %} {% block og_tags %} <meta property="og:title" content="{{ member.display_name }}"> <meta property="og:type" content="profile"> {% if member.avatar %} <meta property="og:image" content="{{ member.avatar.url }}"> {% endif %} {% endblock %} {% block main_content %} <article class="member-detail"> {# Breadcrumb #} <nav class="breadcrumb"> <ol> <li><a href="/">ホーム</a></li> <li><a href="/team">チーム</a></li> <li aria-current="page">{{ member.display_name }}</li> </ol> </nav> <div class="member-detail__profile"> {% if member.avatar %} <img src="{{ member.avatar.url }}" alt="{{ member.display_name }}" width="240" height="240"> {% endif %} <div> <h1>{{ member.display_name }}</h1> <p class="title">{{ member.title }}</p> {% if member.bio %} <div class="bio">{{ member.bio }}</div> {% endif %} {% if member.email %} <a href="mailto:{{ member.email }}">{{ member.email }}</a> {% endif %} </div> </div> {# List of members in the same department (related content) #} {% set same_dept = hubdb_table_rows( "team_members", "department=" ~ member.department ~ "&is_published=true&orderBy=sort_order" ) %} {% if same_dept|length > 1 %} <section class="same-dept"> <h2>{{ member.department }}のメンバー</h2> {% for m in same_dept %} {% if m.hs_id != member.hs_id %} <a href="/team/{{ m.hs_path }}">{{ m.display_name }}</a> {% endif %} {% endfor %} </section> {% endif %} </article>{% endblock %}
Create a new page from "Website → Website Page" in the HubSpot admin screen,
Select Dynamic Pages template as the template.
If you specify the HubDB table in "Data Source" in the page settings,
A page for all rows of the table will be automatically generated.
dynamic_page_hubdb_row is a variable specific to Dynamic Pages templates.
smart contentdepending on visitor attributes (known contact information, device, referrer, list affiliation) This is a function that dynamically changes the content displayed on the page (Professional or higher). You can achieve personalization unique to HubSpot's CRM integration.
Vary content based on CRM property values of known contacts (form filled).
The display changes depending on whether you are in HubSpot's contact list (segment).
Display different content for PCs, smartphones, and tablets.
Change the content depending on the reference source such as search, SNS, email, advertisement, etc.
Smart content is how to make modules smart on the HubSpot admin screen,
HubL's contact There is a way to access contact data directly with variables.
{# ====== contact variable: access known contact information ====== #} {# For unknown visitors, contact will be empty (is not defined) #} {# ① Switch display between known vs. unknown visitors #} {% if contact %} {# Known contacts: Greet by name #} <p class="personalized-greeting"> {{ contact.firstname }}さん、おかえりなさい! </p>{% else %} {# Unknown visitor: Show generic CTA #} <p>まずは無料で試してみませんか?</p>{% endif %} {# ② Switch display by lifecycle stage #} {% if contact.lifecyclestage == "customer" %} {# For existing customers: Upsell CTA #} {% module "upsell_cta" path="../modules/cta-banner.module" %} {% elif contact.lifecyclestage == "lead" or contact.lifecyclestage == "marketingqualifiedlead" %} {# For leads: Demo application CTA #} {% module "demo_cta" path="../modules/cta-banner.module" %} {% else %} {# Unknown / For subscribers: Material DL form #} {% module "download_cta" path="../modules/form-section.module" %} {% endif %} {# ③ Switch content by industry #} {% set industry_cases = { "Manufacturing" : "manufacturing", "IT" : "it", "real estate" : "realestate", "Finance" : "finance" } %} {% if contact and contact.industry and industry_cases[contact.industry] %} {% set case_tag = industry_cases[contact.industry] %} {% set related_cases = blog_posts("case", 3, 0, case_tag) %} {% if related_cases %} <section class="industry-cases"> <h2>{{ contact.industry }}の導入事例</h2> {% for case in related_cases %} <a href="{{ case.absolute_url }}">{{ case.name }}</a> {% endfor %} </section> {% endif %} {% endif %}
| properties | Content | Example value |
|---|---|---|
| contact.firstname | first name | Taro |
| contact.lastname | Surname (last name) | Tanaka |
| contact.email | email address | taro@example.com |
| contact.company | Company Name | Sample Co., Ltd. |
| contact.industry | Industry | IT/Communication |
| contact.lifecyclestage | life cycle stage | lead / customer etc. |
| contact.jobtitle | post | marketing manager |
| contact.phone | telephone number | 03-xxxx-xxxx |
contact Variables can be used on Professional or higher plans, and
Only known contacts with HubSpot tracking cookies enabled (e.g., form submissions)is.
It will be empty for unknown visitors and visitors who have blocked cookies.
surely {% if contact %} Please check the existence before accessing.
{# Get the latest articles of each content type (1 blog + tag filtering) #} {% set tab_config = [ {"id": "all", "label": "all", "tag": ""}, {"id": "blog", "label": "Blog", "tag": "type:blog"}, {"id": "seminar", "label": "Seminar", "tag": "type:seminar"}, {"id": "news", "label": "notice", "tag": "type:news"}, {"id": "case", "label": "Introduction example", "tag": "type:case"} ] %} {# Tab navigation #} <div class="content-tabs" role="tablist"> {% for tab in tab_config %} <button class="content-tab{% if loop.first %} is-active{% endif %}" role="tab" data-target="#tab-{{ tab.id }}" {% if loop.first %}aria-selected="true"{% endif %}> {{ tab.label }} </button> {% endfor %} </div>{# Content panel for each tab #} {% for tab in tab_config %} {% if tab.tag %} {% set tab_posts = blog_posts("default", 6, 0, tab.tag) %} {% else %} {% set tab_posts = blog_recent_posts("default", 6) %} {% endif %} <div id="tab-{{ tab.id }}" role="tabpanel" class="tab-panel{% if not loop.first %} is-hidden{% endif %}"> {% if tab_posts %} <ul class="post-grid"> {% for post in tab_posts %} <li> <a href="{{ post.absolute_url }}"> {% if post.featured_image %} <img src="{{ post.featured_image }}" alt="{{ post.featured_image_alt_text|default(post.name) }}" loading="lazy"> {% endif %} <p>{{ post.name }}</p> <time>{{ post.publish_date|datetimeformat("%Y.%m.%d") }}</time> </a> </li> {% endfor %} </ul> {% else %} <p>コンテンツがまだありません</p> {% endif %} </div>{% endfor %} {# Tab switching JS (using ID generated by HubL) #} <script> document.querySelectorAll('.content-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.content-tab, .tab-panel').forEach(el => { el.classList.remove('is-active'); el.classList.add('is-hidden'); el.removeAttribute('aria-selected'); }); tab.classList.add('is-active'); tab.setAttribute('aria-selected', 'true'); const panel = document.querySelector(tab.dataset.target); if (panel) { panel.classList.remove('is-hidden'); } }); }); </script>
{# Variables such as next_post_featured_image that can be used in the blog_post template #} <nav class="post-nav" aria-label="Previous and subsequent articles"> {% if next_post_featured_image or next_post_name %} <a class="post-nav__prev" href="{{ next_post_url }}" rel="prev"> <span>← 次の記事</span> {% if next_post_featured_image %} <img src="{{ next_post_featured_image }}" alt="{{ next_post_name }}" loading="lazy"> {% endif %} <p>{{ next_post_name }}</p> </a> {% endif %} {% if last_post_featured_image or last_post_name %} <a class="post-nav__next" href="{{ last_post_url }}" rel="next"> <span>前の記事 →</span> {% if last_post_featured_image %} <img src="{{ last_post_featured_image }}" alt="{{ last_post_name }}" loading="lazy"> {% endif %} <p>{{ last_post_name }}</p> </a> {% endif %} </nav>
{% set total = blog_total_post_count("default") %} {% if total == 0 %} {# If there are no articles yet for the whole site #} <div class="blog-empty"> <p>まもなく記事を公開します。しばらくお待ちください。</p> </div>{% elif contents|length == 0 %} {# If there are 0 corresponding articles on the tag page etc. #} <div class="blog-empty"> {% if content.tag %} <p>「{{ content.tag.name }}」の記事はまだありません。</p> {% else %} <p>記事がまだありません。</p> {% endif %} <a href="/blog">すべての記事を見る</a> </div>{% else %} {# Normal display with article #} <ul class="post-grid"> {% for post in contents %} {# ... #} {% endfor %} </ul>{% endif %}
Starter manages types with 1 blog + tag design. For Professionals and above, separate blogs are set up for each type. Multiple blogs are managed by converting IDs into variables.
Separate roles with type: / cat: / status: prefix. Simultaneously determine multiple attributes using namespace patterns. Implementation that filters only display tags is required.
The sliding window method is optimal for both UX and SEO. Make sure to have an accessible implementation including aria-current / aria-label. Don't forget to display the count information.
Ideal for managing structured data such as employee introductions, product lists, FAQs, etc. Use the query parameters of hubdb_table_rows() to retrieve only the necessary data.
One row in HubDB automatically becomes one page. Access row data with the dynamic_page_hubdb_row variable. One template shares all records.
Information on known contacts can be accessed using the contact variable (Professional and above). Be sure to check the presence and branch the displayed content based on life cycle stage, industry, etc.