Core Web Vitals の最適化・画像の遅延読み込みと次世代フォーマット・HubSpot CDN の活用・構造化データ・キーボード操作とARIA実装まで。サイト品質を担保するすべての技術を体系化します。
Core Web Vitals(コアウェブバイタル)はGoogleが定義するウェブ品質の指標です。 2021年以降、Googleの検索ランキングアルゴリズムの一部として組み込まれており、 SEOと直結します。HubSpot CMSでも意識的に最適化する必要があります。
| 項目 | HubSpotの状況 | 対応が必要か |
|---|---|---|
| TTFB | HubSpotのグローバルCDNにより低レイテンシー。通常は良好。 | 自動対応済み |
| LCP | ヒーロー画像の最適化は開発者の実装に依存する。 | 要対応 |
| CLS | 画像サイズ未指定・フォントスワップ・モジュール読み込みによるズレが起きやすい。 | 要対応 |
| INP | HubSpotのトラッキングスクリプトやフォームスクリプトがメインスレッドをブロックすることがある。 | 要注意 |
| FCP | レンダーブロッキングCSSを最小化することで改善できる。 | 要対応 |
LCPはページの「見えた感」に直結する指標です。 多くのサイトでLCP要素はヒーローバナーの画像になります。 「画像を早く表示する」ことが最優先の対策です。
ヒーローバナーのような LCP 要素になる画像は、 通常の画像読み込みより前にプリロードすることでLCPを大幅に改善できます。
{# ====== head 内でプリロードを指定 ====== #} {# 方法①:テンプレートで固定のヒーロー画像をプリロード #} {% if module.hero_image.src %} <link rel="preload" as="image" href="{{ module.hero_image.src }}" fetchpriority="high"> {% endif %} {# 方法②:ブログ記事のアイキャッチ画像をプリロード(blog-post.html のheadブロックで)#} {% block extra_head %} {% if content.featured_image %} <link rel="preload" as="image" href="{{ content.featured_image }}" fetchpriority="high"> {% endif %} {% endblock %}
{# ===== ヒーローバナーの画像 ===== LCP 要素になる画像には以下を必ず設定: - loading="eager"(遅延読み込みを無効) - fetchpriority="high"(ブラウザへの優先度ヒント) - decoding="sync"(非同期デコードを無効) - width / height を必ず明示(CLSの防止にも必要) ===================================================== #} {% if module.hero_image.src %} <img src="{{ module.hero_image.src }}" alt="{{ module.hero_image.alt|default("") }}" width="{{ module.hero_image.width|default(1440) }}" height="{{ module.hero_image.height|default(640) }}" loading="eager" fetchpriority="high" decoding="sync"> {% endif %} {# ===== スクロール後に見える画像(LCP要素でない)===== - loading="lazy" で遅延読み込み - fetchpriority は指定不要(デフォルトが "auto") ===================================================== #} {% for card in module.cards %} {% if card.image.src %} <img src="{{ card.image.src }}" alt="{{ card.image.alt|default("") }}" width="{{ card.image.width|default(400) }}" height="{{ card.image.height|default(300) }}" loading="lazy" decoding="async"> {% endif %} {% endfor %}
{# ===== Google Fonts の最適化読み込み ===== display=swap でフォント読み込み中もテキストを表示(CLSの原因になるが FCP は改善) preconnect でドメイン接続を先行して確立する ===================================================== #} {# ① preconnect でGoogleの接続を先行確立 #} <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> {# ② フォントCSSをプリロードしてからロード #} {% set heading_font = theme.settings.typography.heading_font %} {% if heading_font.font_set == "GOOGLE" %} {% set font_family_encoded = heading_font.font|replace(" ", "+") %} <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={{ font_family_encoded }}:wght@400;700;900&display=swap" media="print" onload="this.media='all'"> {# media="print" で最初は印刷専用として読み込み、onloadでallに切り替える → レンダーブロッキングを回避しつつフォントを非同期読み込み #} <noscript> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={{ font_family_encoded }}:wght@400;700;900&display=swap"> </noscript> {% endif %} {# ③ 本文フォントのプリロード(CLS対策:font-display: optional 推奨)#} {# CSSで以下を設定: @font-face { font-family: 'Noto Sans JP'; font-display: optional; ← フォール バックフォントとのCLSをゼロにする } #}
CLSはページ読み込み中にコンテンツが突然ずれる現象を数値化した指標です。 HubSpotサイトで CLS が悪化する主な原因と対策を整理します。
| 原因 | 対策 | HubSpot実装箇所 |
|---|---|---|
| 画像に width/height 未指定 | すべての img タグに width・height 属性を明示する | 全モジュールの module.html |
| Webフォントのスワップ | font-display: optional または size-adjust で補正 | variables.css の @font-face |
| 広告・外部ウィジェットの挿入 | 挿入先に min-height を事前確保する | モジュールCSS / グローバルCSS |
| 動的に挿入されるコンテンツ | スケルトンUIを表示してスペースを事前確保 | JS実装 |
| HubSpotチャットウィジェット | チャットウィジェット読み込みタイミングを遅延させる | HubSpot設定 / JS |
/* ===== フォント表示の最適化 ===== font-display: optional → フォントが300ms以内に読み込めない場合はフォールバックフォントを使い続ける → スワップが起きないためCLSがゼロになる(ただしフォントが表示されない場合がある) size-adjust → システムフォントのサイズをWebフォントに合わせて補正 → スワップ時のレイアウトシフトを最小化 ===================================================== */ @font-face { font-family: 'Noto Sans JP'; font-style: normal; font-weight: 400; font-display: optional; /* CLSをゼロにする最強設定 */ src: url('...') format('woff2'); } /* size-adjust でフォールバックフォントのサイズを補正 */ @font-face { font-family: 'Noto-Sans-JP-fallback'; src: local('Hiragino Sans'), local('Meiryo'); size-adjust: 95%; /* Webフォントとのサイズ差を補正 */ ascent-override: 105%; /* アセント高さの補正 */ descent-override: 25%; /* ディセント深さの補正 */ } body { font-family: 'Noto Sans JP', 'Noto-Sans-JP-fallback', sans-serif; }
/* ===== 画像コンテナのアスペクト比をCSSで確保 ===== 画像が読み込まれる前からスペースを確保し、CLS を防ぐ ===================================================== */ /* ブログカードのサムネイル(16:9)*/ .blog-card__thumb { aspect-ratio: 16 / 9; overflow: hidden; background: #edf2f7; /* 読み込み前のプレースホルダー色 */ } .blog-card__thumb img { width: 100%; height: 100%; object-fit: cover; } /* アバター画像(1:1)*/ .author-avatar { aspect-ratio: 1 / 1; overflow: hidden; border-radius: 50%; background: #edf2f7; } .author-avatar img { width: 100%; height: 100%; object-fit: cover; }
HubSpotのファイルマネージャーにアップロードされた画像は URLパラメータでリサイズ・フォーマット変換できます。 この機能を活用することで、表示サイズに最適化された画像を配信できます。
| パラメータ | 説明 | 例 |
|---|---|---|
| width=N | 幅をピクセル指定でリサイズ | ?width=800 |
| height=N | 高さをピクセル指定でリサイズ | ?height=600 |
| format=webp | WebP形式に変換(非対応ブラウザは元形式) | ?format=webp |
| quality=N | 圧縮品質(1〜100)デフォルト80 | ?quality=75 |
| fit= | クロップ方法:cover / contain / fill | ?fit=cover |
| upscale=false | 元画像より拡大しない | ?upscale=false |
{# ===== HubSpot画像変換パラメータ + srcset の組み合わせ ===== デバイスの解像度・ビューポートに最適な画像を自動選択させる ===================================================== #} {% macro responsive_image(src, alt, widths, sizes, is_lcp=false, class="") %} {% if src %} {% set base = src|split("?")|first %} {# picture タグで WebP と元フォーマットを提供 #} <picture> {# WebP source #} <source type="image/webp" sizes="{{ sizes }}" srcset=" {% for w in widths %} {{ base }}?width={{ w }}&format=webp&quality=80 {{ w }}w{% if not loop.last %},{% endif %} {% endfor %} "> {# 元フォーマット(フォールバック)#} <source sizes="{{ sizes }}" srcset=" {% for w in widths %} {{ base }}?width={{ w }}&quality=80 {{ w }}w{% if not loop.last %},{% endif %} {% endfor %} "> {# img タグ(フォールバック) #} <img src="{{ base }}?width={{ widths|first }}&quality=80" alt="{{ alt }}" loading="{% if is_lcp %}eager{% else %}lazy{% endif %}" fetchpriority="{% if is_lcp %}high{% else %}auto{% endif %}" decoding="{% if is_lcp %}sync{% else %}async{% endif %}" {% if class %}class="{{ class }}"{% endif %}> </picture> {% endif %} {% endmacro %} {# ===== 使用例 ===== #} {# ヒーローバナー(LCP要素)#} {{ responsive_image( module.hero_image.src, module.hero_image.alt, widths = [400, 800, 1200, 1600], sizes = "(max-width: 640px) 100vw, (max-width: 1024px) 100vw, 1200px", is_lcp = true, class = "hero__img" ) }} {# ブログカードのサムネイル(通常画像)#} {{ responsive_image( post.featured_image, post.featured_image_alt_text|default(post.name), widths = [320, 640, 800], sizes = "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw", is_lcp = false, class = "blog-card__img" ) }}
<head> {# ① クリティカルCSSをインラインで記述(above-the-fold の表示に必要な最小CSS)#} <style> /* ヒーロー・ヘッダーの表示に必要な最低限のスタイル */ :root { --color-primary: {{ theme.settings.color.primary_color.color }}; } body { margin: 0; font-family: 'Noto Sans JP', sans-serif; } .site-header { position: sticky; top: 0; z-index: 100; background: #fff; } </style> {# ② グローバルCSS:メディア属性を使った非同期読み込み #} <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> {# ③ HubSpot 必須タグ(LCPよりも前に入れない) #} {{ standard_header_includes }} </head> <body> {# ... コンテンツ ... #} {# ④ JSはbody末尾に defer で読み込む(デフォルト推奨) #} <script src="{{ get_asset_url('../js/main.js') }}" defer> </script> {# ⑤ サードパーティスクリプトは最後に・可能なら遅延読み込み #} <script> // チャットウィジェットを5秒後に読み込む(LCP/INPの妨害を防ぐ) setTimeout(function() { var s = document.createElement('script'); s.src = '//js.hs-scripts.com/PORTAL_ID.js'; s.async = true; document.head.appendChild(s); }, 5000); </script> {# ⑥ HubSpot 必須タグ(body末尾) #} {{ standard_footer_includes }} </body>
{# ===== get_asset_url でCDNキャッシュを活用する ===== テーマ内のアセットは get_asset_url() を使って参照する。 HubSpotがCDN最適化したURLに自動変換し、 ファイルハッシュをURLに付与してキャッシュバスティングも自動で行う。 ===================================================== #} {# CSS #} <link rel="stylesheet" href="{{ get_asset_url('../css/main.css') }}"> {# JavaScript #} <script src="{{ get_asset_url('../js/main.js') }}" defer></script> {# 画像(テーマ内のSVGアイコン等) #} <img src="{{ get_asset_url('../images/logo.svg') }}" alt="ロゴ"> {# ❌ 相対パスで直接書くのはNG(CDNを経由せずオリジンから配信される)#} {# <link rel="stylesheet" href="../css/main.css"> ← NG #} {# ✅ get_asset_url() で生成されるURLの例 #} {# https://hs-XXXXXXXXXX.hubspotusercontent-na1.net/hubfs/PORTAL_ID/ my-theme/css/main.css?t=1234567890 #}
第3章で実装したブログ記事の Article スキーマに加え、よく使われる構造化データをまとめます。
{# layouts/base.html の <head> 内に追加 #} <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") }}" }, "sameAs": [ "{{ site_settings.twitter_url }}", "{{ site_settings.linkedin_url }}" ] } </script>
{# テンプレートごとにパンくずリストのスキーマを出力 #} {# blog-post.html の extra_head ブロック内で #} {% block extra_head %} <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "ホーム", "item": "{{ site_settings.website_url }}" }, { "@type": "ListItem", "position": 2, "name": "ブログ", "item": "{{ site_settings.website_url }}/blog" }, { "@type": "ListItem", "position": 3, "name": "{{ content.name|escape }}", "item": "{{ content.absolute_url }}" } ] } </script> {% endblock %}
{# faq.module / module.html の末尾に追加 #} {% if module.items %} <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ {% for item in module.items %} { "@type": "Question", "name": "{{ item.question|escape }}", "acceptedAnswer": { "@type": "Answer", "text": "{{ item.answer|striptags|escape }}" } }{% if not loop.last %},{% endif %} {% endfor %} ] } </script> {% endif %}
{# ===== canonical URL ===== HubSpotが content.absolute_url を自動設定してくれる ページネーションページ(/blog/2 等)にも正しく設定される ===================================================== #} <link rel="canonical" href="{{ content.absolute_url }}"> {# ===== hreflang(多言語サイトの場合)===== HubSpotの多言語グループ設定をしていれば自動挿入もされるが 手動で確認・補完することを推奨 ===================================================== #} {% if content.translated_content %} {# 現在のページ #} <link rel="alternate" hreflang="{{ content.language }}" href="{{ content.absolute_url }}"> {# 他言語バージョン #} {% for lang, trans in content.translated_content.items() %} {% if trans.state == "PUBLISHED" %} <link rel="alternate" hreflang="{{ lang }}" href="{{ trans.absolute_url }}"> {% endif %} {% endfor %} {# x-default:言語検出できない場合のデフォルト #} <link rel="alternate" hreflang="x-default" href="{{ content.absolute_url }}"> {% endif %}
{% block meta_description %} {% set desc = "" %} {# 優先順位①:ページで設定したメタ description #} {% if content.meta_description %} {% set desc = content.meta_description %} {# 優先順位②:ブログ記事の場合は本文先頭から自動生成 #} {% elif content.post_body %} {% set desc = content.post_body|striptags|truncate(110, end="") %} {# 優先順位③:サイト全体のデフォルト description #} {% else %} {% set desc = site_settings.meta_description|default("サイトのデフォルト説明文") %} {% endif %} {# ページネーションページに「N ページ目」を付加 #} {% if current_page_num and current_page_num > 1 %} {% set desc = desc ~ "(" ~ current_page_num ~ "ページ目)" %} {% endif %} <meta name="description" content="{{ desc|escape }}"> {% endblock %}
アクセシビリティはすべてのユーザーがサイトを利用できるようにするための実装です。 WCAG 2.1 AA 準拠を目標とし、スクリーンリーダー対応・キーボード操作・色覚対応を実装します。
すべての操作がキーボードのみで完結できること。フォーカス順序が論理的であること。
画像のalt属性・ARIAラベル・見出し構造・ランドマークロールが適切に設定されていること。
テキストと背景のコントラスト比が WCAG AA 基準(4.5:1以上)を満たしていること。
タップターゲットが44×44px以上であること。隣接する操作要素に十分な間隔があること。
{# ===== スキップナビゲーション(キーボードユーザー向け)===== 先頭にフォーカスが来た時だけ表示され、メインコンテンツへジャンプできる ===================================================== #} <a href="#main-content" class="skip-link"> メインコンテンツへスキップ </a> <style> .skip-link { position: absolute; top: -100%; left: 0; background: var(--color-primary); color: white; padding: 8px 16px; font-weight: 700; z-index: 9999; border-radius: 0 0 8px 0; transition: top 0.2s; } .skip-link:focus { top: 0; /* フォーカス時に表示 */ } </style> {# ===== ランドマークロールの正しい実装 ===== #} <body> {# バナー:サイトのヘッダー(header要素 + role="banner")#} <header role="banner"> {# ナビゲーション(nav + aria-label で識別)#} <nav aria-label="グローバルナビゲーション">...</nav> </header> {# メインコンテンツ(main + id でスキップリンクのターゲットに)#} <main id="main-content" role="main" tabindex="-1"> {% block main_content %}{% endblock %} </main> {# コンテントインフォ:サイトのフッター #} <footer role="contentinfo">...</footer> </body>
{# ===== アクセシブルなハンバーガーメニュー ===== #} <button class="hamburger" aria-controls="global-nav" aria-expanded="false" aria-label="メニューを開く"> <span aria-hidden="true"></span> <span aria-hidden="true"></span> <span aria-hidden="true"></span> </button> <nav id="global-nav" aria-label="グローバルナビゲーション" aria-hidden="true"> <ul role="list"> <li><a href="/">ホーム</a></li> <li><a href="/about">会社概要</a></li> </ul> </nav> <script> var btn = document.querySelector('.hamburger'); var nav = document.getElementById('global-nav'); btn.addEventListener('click', function() { var isOpen = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!isOpen)); btn.setAttribute('aria-label', isOpen ? 'メニューを開く' : 'メニューを閉じる'); nav.setAttribute('aria-hidden', String(isOpen)); nav.classList.toggle('is-open', !isOpen); }); </script> {# ===== アクセシブルなアコーディオン(FAQ)===== #} {% for item in module.items %} {% set faq_id = "faq-" ~ loop.index %} <div class="faq-item"> <h3> <button id="{{ faq_id }}-btn" class="faq-item__btn" aria-controls="{{ faq_id }}-panel" aria-expanded="false"> {{ item.question }} <span class="faq-item__icon" aria-hidden="true">▼</span> </button> </h3> <div id="{{ faq_id }}-panel" role="region" aria-labelledby="{{ faq_id }}-btn" hidden> {{ item.answer }} </div> </div> {% endfor %} <script> document.querySelectorAll('.faq-item__btn').forEach(function(btn) { btn.addEventListener('click', function() { var expanded = this.getAttribute('aria-expanded') === 'true'; var panel = document.getElementById(this.getAttribute('aria-controls')); this.setAttribute('aria-expanded', String(!expanded)); panel.hidden = expanded; }); }); </script> {# ===== フォームのアクセシビリティ強化(HubSpotフォーム上書きJS)===== #} <script> // HubSpotフォームがDOMに追加された後にアクセシビリティ属性を補完 document.addEventListener('DOMContentLoaded', function() { var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.nodeType === 1 && node.classList.contains('hs-form')) { // エラーメッセージを aria-describedby で入力フィールドに紐付け node.querySelectorAll('.hs-form-field').forEach(function(field) { var input = field.querySelector('input, select, textarea'); var errors = field.querySelector('.hs-error-msgs'); if (input && errors) { var errorId = 'error-' + input.name; errors.id = errorId; input.setAttribute('aria-describedby', errorId); } }); observer.disconnect(); } }); }); }); observer.observe(document.getElementById('hs-post-form'), { childList: true }); }); </script>
| 用途 | WCAG AA 基準 | WCAG AAA 基準 | 確認ツール |
|---|---|---|---|
| 通常テキスト(18px未満) | 4.5:1 以上 | 7:1 以上 | Chrome DevTools の Accessibility タブ / contrast ratio checker |
| 大きいテキスト(18px以上 or 太字14px以上) | 3:1 以上 | 4.5:1 以上 | |
| UIコンポーネント・グラフィック | 3:1 以上 | — |
HubSpot には SEO に特化した管理ツールが内蔵されており、 コード実装と組み合わせることでより効果的なSEO施策が展開できます。
| HubSpotのSEO機能 | 開発者が対応すること |
|---|---|
| SEOレコメンデーション | 管理画面が提案する改善項目(alt属性不足・見出し構造の問題等)を実装で解決する |
| トピッククラスター | ピラーページとクラスターページのテンプレートを適切な内部リンク構造で設計する |
| XMLサイトマップ | HubSpotが自動生成。/sitemap.xml が有効か確認し、管理画面の「サイトマップ」設定で除外ページを管理する |
| robots.txt | HubSpotの設定から編集可能。開発環境(sandbox)のクロールを禁止するよう設定する |
| コンテンツ戦略ツール | キーワードのターゲット設定はマーケターが行う。テンプレートはheading構造・alt・canonicalを正確に実装する |
HubSpotのCDNでTTFBは自動改善。LCP(ヒーロー画像のプリロード・eager/high)とCLS(width/height明示・フォント最適化)が開発者の主要課題。
HubSpot URLパラメータ(?width=N&format=webp)+ srcset/picture タグ + loading属性の使い分け(LCP=eager, それ以外=lazy)。
CSS・JS・テーマ内画像はすべて get_asset_url() で参照する。CDNキャッシュとキャッシュバスティングが自動で有効になる。
Organization(全ページ)/ Article(ブログ記事)/ FAQPage(FAQモジュール)を実装。BreadcrumbList・HowTo等は必要に応じて追加する。
スキップナビゲーション / ランドマークロール / ARIAでインタラクション状態を伝達 / カラーコントラスト AA 基準(4.5:1)。納品前に必ずキーボード操作テストを行う。
PageSpeed Insights・Google Rich Results Test・Chrome DevTools Lighthouse・Search Console でパフォーマンス・SEO・アクセシビリティを総合確認する。