📘 HubSpot CMS 組み込み構築 教科書
Chapter 8

パフォーマンス・SEO・アクセシビリティ

Core Web Vitals の最適化・画像の遅延読み込みと次世代フォーマット・HubSpot CDN の活用・構造化データ・キーボード操作とARIA実装まで。サイト品質を担保するすべての技術を体系化します。

🎯 対象レベル:中級〜上級
⏱ 読了目安:60〜90分
🔗 前章:第7章 フォーム・CTA・コンバージョン設計

この章の内容

  1. Core Web Vitals の概要と HubSpot での計測
  2. LCP 最適化:ヒーロー画像・フォント・プリロード
  3. CLS 最適化:画像サイズ明示・フォントスワップ・広告スペース
  4. 画像最適化完全ガイド(遅延読み込み・srcset・WebP・HubSpot変換)
  5. CSS・JavaScript の最適化
  6. HubSpot CDN の活用と get_asset_url
  7. SEO 実装完全ガイド(構造化データ・canonical・sitemap)
  8. アクセシビリティ(WCAG)実装ガイド
  9. HubSpot SEOツールとの連携
  10. 第8章まとめ
Section 8-1

Core Web Vitals の概要と HubSpot での計測

Core Web Vitals(コアウェブバイタル)はGoogleが定義するウェブ品質の指標です。 2021年以降、Googleの検索ランキングアルゴリズムの一部として組み込まれており、 SEOと直結します。HubSpot CMSでも意識的に最適化する必要があります。

LCP
Largest Contentful Paint
最大コンテンツの描画
良好:≤ 2.5s
要改善:≤ 4.0s
不良:> 4.0s
CLS
Cumulative Layout Shift
累積レイアウトシフト
良好:≤ 0.1
要改善:≤ 0.25
不良:> 0.25
INP
Interaction to Next Paint
操作から次の描画まで
良好:≤ 200ms
要改善:≤ 500ms
不良:> 500ms
FCP
First Contentful Paint
最初のコンテンツ描画
良好:≤ 1.8s
要改善:≤ 3.0s
不良:> 3.0s
TTFB
Time to First Byte
最初のバイトまでの時間
良好:≤ 800ms
要改善:≤ 1.8s
不良:> 1.8s

HubSpot CMSのCWV有利点と注意点

項目HubSpotの状況対応が必要か
TTFBHubSpotのグローバルCDNにより低レイテンシー。通常は良好。自動対応済み
LCPヒーロー画像の最適化は開発者の実装に依存する。要対応
CLS画像サイズ未指定・フォントスワップ・モジュール読み込みによるズレが起きやすい。要対応
INPHubSpotのトラッキングスクリプトやフォームスクリプトがメインスレッドをブロックすることがある。要注意
FCPレンダーブロッキングCSSを最小化することで改善できる。要対応

計測ツール


Section 8-2

LCP 最適化:ヒーロー画像・フォント・プリロード

LCPはページの「見えた感」に直結する指標です。 多くのサイトでLCP要素はヒーローバナーの画像になります。 「画像を早く表示する」ことが最優先の対策です。

① LCP画像のプリロード

ヒーローバナーのような LCP 要素になる画像は、 通常の画像読み込みより前にプリロードすることでLCPを大幅に改善できます。

layouts/base.html — 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 と fetchpriority

module.html — LCP要素になるヒーロー画像の正しい実装
{# ===== ヒーローバナーの画像 =====
   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 %}

③ フォントの最適化

layouts/base.html — フォント読み込みの最適化
{# ===== 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をゼロにする
}
#}

Section 8-3

CLS 最適化:画像サイズ明示・フォントスワップ

CLSはページ読み込み中にコンテンツが突然ずれる現象を数値化した指標です。 HubSpotサイトで CLS が悪化する主な原因と対策を整理します。

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
CSS — フォントスワップによるCLS対策
/* ===== フォント表示の最適化 =====
   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 — 画像コンテナへのアスペクト比確保
/* ===== 画像コンテナのアスペクト比を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;
}

Section 8-4

画像最適化完全ガイド

HubSpotのファイルマネージャーにアップロードされた画像は URLパラメータでリサイズ・フォーマット変換できます。 この機能を活用することで、表示サイズに最適化された画像を配信できます。

HubSpot 画像変換パラメータ

パラメータ説明
width=N幅をピクセル指定でリサイズ?width=800
height=N高さをピクセル指定でリサイズ?height=600
format=webpWebP形式に変換(非対応ブラウザは元形式)?format=webp
quality=N圧縮品質(1〜100)デフォルト80?quality=75
fit=クロップ方法:cover / contain / fill?fit=cover
upscale=false元画像より拡大しない?upscale=false

srcset によるレスポンシブ画像の実装

HubL — srcset を使ったレスポンシブ画像(完全実装)
{# ===== 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"
  )
}}

Section 8-5

CSS・JavaScript の最適化

レンダーブロッキングの排除

layouts/base.html — CSS・JS の最適化された読み込み順
<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>

HubSpot CDN と get_asset_url

HubL — get_asset_url の正しい使い方
{# ===== 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 #}

Section 8-6

SEO 実装完全ガイド

構造化データ(JSON-LD)の実装パターン

第3章で実装したブログ記事の Article スキーマに加え、よく使われる構造化データをまとめます。

HubL — Organization スキーマ(全ページ共通)
{# 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>
HubL — BreadcrumbList スキーマ(内部ページ共通)
{# テンプレートごとにパンくずリストのスキーマを出力 #}
{# 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 %}
HubL — FAQPage スキーマ(FAQ モジュール)
{# 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 と hreflang の実装

layouts/base.html — canonical と hreflang の実装
{# ===== 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 %}

メタ description の品質チェックリスト

📋 メタ description の実装チェック
文字数:60〜120文字 PCで70文字前後、SPで50文字前後が目安。短すぎず長すぎず。
フォールバック設定 meta_description が未設定の場合、本文先頭から自動生成する処理を入れる。
特殊文字のエスケープ | (パイプ)や " (ダブルクォート)をそのまま出力しない。
ページネーションページの扱い /blog/2 等には「2ページ目」を示す文言を付加する。
layouts/base.html — メタ description の完全実装
{% 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 %}

Section 8-7

アクセシビリティ(WCAG)実装ガイド

アクセシビリティはすべてのユーザーがサイトを利用できるようにするための実装です。 WCAG 2.1 AA 準拠を目標とし、スクリーンリーダー対応・キーボード操作・色覚対応を実装します。

⌨️

キーボード操作

すべての操作がキーボードのみで完結できること。フォーカス順序が論理的であること。

🔊

スクリーンリーダー

画像のalt属性・ARIAラベル・見出し構造・ランドマークロールが適切に設定されていること。

🎨

色覚対応

テキストと背景のコントラスト比が WCAG AA 基準(4.5:1以上)を満たしていること。

📱

タッチ操作

タップターゲットが44×44px以上であること。隣接する操作要素に十分な間隔があること。

ランドマークロールとスキップナビゲーション

layouts/base.html — ランドマーク・スキップナビゲーション
{# ===== スキップナビゲーション(キーボードユーザー向け)=====
   先頭にフォーカスが来た時だけ表示され、メインコンテンツへジャンプできる
===================================================== #}
<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>

インタラクティブ要素のアクセシビリティ

HubL + HTML — アクセシブルなナビゲーション・モーダル・フォーム
{# ===== アクセシブルなハンバーガーメニュー ===== #}
<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 以上

Section 8-8

HubSpot SEOツールとの連携

HubSpot には SEO に特化した管理ツールが内蔵されており、 コード実装と組み合わせることでより効果的なSEO施策が展開できます。

HubSpot SEOツールと実装の連携ポイント

HubSpotのSEO機能開発者が対応すること
SEOレコメンデーション 管理画面が提案する改善項目(alt属性不足・見出し構造の問題等)を実装で解決する
トピッククラスター ピラーページとクラスターページのテンプレートを適切な内部リンク構造で設計する
XMLサイトマップ HubSpotが自動生成。/sitemap.xml が有効か確認し、管理画面の「サイトマップ」設定で除外ページを管理する
robots.txt HubSpotの設定から編集可能。開発環境(sandbox)のクロールを禁止するよう設定する
コンテンツ戦略ツール キーワードのターゲット設定はマーケターが行う。テンプレートはheading構造・alt・canonicalを正確に実装する

納品前 SEO・パフォーマンス・アクセシビリティ チェックリスト

🚀 パフォーマンス
LCP 要素の画像に fetchpriority="high" と loading="eager" を設定 ヒーローバナー・アイキャッチ画像が対象。PageSpeed Insights で LCP 要素を確認する。
LCP
スクロール下の画像に loading="lazy" を設定 Above-the-fold 以外のすべての img タグに適用する。
LCP
全画像に width・height 属性を明示 数値が未定の場合は CSS で aspect-ratio を設定して代替する。
CLS
get_asset_url() でアセットを参照 CSS・JS・画像をすべて get_asset_url() 経由で読み込む。
TTFB
JS ファイルに defer 属性を付加 レンダーブロッキングを防ぐ。async ではなく defer が推奨。
FCP
🔍 SEO
全ページに canonical URL が設定されている ページネーションページも含め確認する。
meta description のフォールバック処理が実装されている 未設定ページでも意味のある文章が出力されること。
OGP・Twitter Card が全テンプレートに実装されている 画像のフォールバックも含めて動作確認する。
ブログ記事に Article スキーマが実装されている Google Rich Results Test で確認する。
サンクスページに noindex が設定されている HubSpot管理画面のページ設定から確認する。
♿ アクセシビリティ
全画像に適切な alt 属性が設定されている 装飾目的の画像は alt="" とする。
スキップナビゲーションが実装されている キーボードで Tab キーを押した際に先頭でフォーカスされること。
インタラクティブ要素に適切な ARIA 属性が設定されている ハンバーガーメニュー・アコーディオン・モーダルを確認する。
カラーコントラスト比が WCAG AA 基準を満たしている Chrome DevTools の Accessibility タブで全テキスト要素を確認する。
キーボードのみで全操作が完結できる Tab・Enter・Space・矢印キーで全インタラクションを操作できること。

Section 8-9

第8章まとめ

📌 この章で押さえるべきポイント

Core Web Vitals の優先対応

HubSpotのCDNでTTFBは自動改善。LCP(ヒーロー画像のプリロード・eager/high)とCLS(width/height明示・フォント最適化)が開発者の主要課題。

画像最適化の3点セット

HubSpot URLパラメータ(?width=N&format=webp)+ srcset/picture タグ + loading属性の使い分け(LCP=eager, それ以外=lazy)。

get_asset_url() は必須

CSS・JS・テーマ内画像はすべて get_asset_url() で参照する。CDNキャッシュとキャッシュバスティングが自動で有効になる。

構造化データ3種

Organization(全ページ)/ Article(ブログ記事)/ FAQPage(FAQモジュール)を実装。BreadcrumbList・HowTo等は必要に応じて追加する。

アクセシビリティ4本柱

スキップナビゲーション / ランドマークロール / ARIAでインタラクション状態を伝達 / カラーコントラスト AA 基準(4.5:1)。納品前に必ずキーボード操作テストを行う。

納品前チェックリスト

PageSpeed Insights・Google Rich Results Test・Chrome DevTools Lighthouse・Search Console でパフォーマンス・SEO・アクセシビリティを総合確認する。

次章:第9章 開発環境・CLI・デプロイワークフロー

HubSpot CLIの全コマンド・ローカル開発環境の構築・GitHubとの連携・本番デプロイのベストプラクティスまで解説します。

第9章へ →