• #SEO
  • #HTML
  • #セマンティック
  • #アクセシビリティ
  • #AI検索
未分類

セマンティックHTMLの必要性と実装方法

なぜセマンティックHTMLが必要なのか

1. 検索エンジンとAIがコンテンツを正確に理解できる

セマンティックHTML(意味のあるHTML)とは、タグに「意味」を持たせることです。

非セマンティックな例(悪い例):

<div class="article">
  <div class="title">記事タイトル</div>
  <div class="date">2025-11-27</div>
  <div class="content">本文...</div>
</div>

セマンティックな例(良い例):

<article>
  <h1>記事タイトル</h1>
  <time datetime="2025-11-27">2025年11月27日</time>
  <p>本文...</p>
</article>

後者では、検索エンジンやAIは:

  • 「これは記事である」(<article>
  • 「これはタイトルである」(<h1>
  • 「これは公開日時である」(<time>

と明確に理解できます。

2. AI検索エンジンへの対応

ChatGPT Search、Perplexity、Google SGEなどのAI検索エンジンは、セマンティックHTMLを解析してコンテンツの構造を理解します。

セマンティックHTMLがあると:

  • コンテンツの重要度を正確に把握(見出し階層から)
  • 記事の公開日を認識<time>タグ)
  • 引用として適切に抽出<article><section>
  • ナビゲーションとコンテンツを区別<nav><main>

3. アクセシビリティの向上

スクリーンリーダーを使用する視覚障害者にとって、セマンティックHTMLは必須です。

例:

  • <nav>:「ナビゲーション領域」と読み上げ
  • <main>:「メインコンテンツ」と読み上げ
  • <article>:「記事」と読み上げ
  • <time>:「公開日時」として認識

Webアクセシビリティ基準(WCAG)を満たす

4. SEOランキングへの影響

Googleは2021年以降、「ページエクスペリエンス」をランキング要因に含めており、セマンティックHTMLはその一部です。

特に:

  • 見出し階層の適切性<h1><h2><h3>
  • ランドマーク要素の使用<header><footer><main><nav>
  • 構造化された情報<article><section><time>

は、ページ品質の指標として評価されます。

どのように実装するか

主要なセマンティックタグ

タグ意味用途
<header>ヘッダーサイト全体またはセクションのヘッダー
<nav>ナビゲーションメニューやリンク集
<main>メインコンテンツページの主要コンテンツ(1ページに1つ)
<article>記事ブログ記事、ニュース記事などの独立したコンテンツ
<section>セクション意味的なまとまりを持つセクション
<aside>補足情報サイドバー、関連記事など
<footer>フッターサイト全体またはセクションのフッター
<time>日時公開日、更新日などの日時情報
<figure>図表画像、図、コードスニペットなど
<figcaption>キャプション<figure>の説明文

見出しの階層構造

見出しは必ず階層的に使用します:

❌ 間違った例:

<h1>記事タイトル</h1>
<h3>セクション1</h3> <!-- h2を飛ばしている -->
<h2>セクション2</h2> <!-- 順番がおかしい -->

✅ 正しい例:

<h1>記事タイトル</h1>
<h2>セクション1</h2>
  <h3>サブセクション1-1</h3>
  <h3>サブセクション1-2</h3>
<h2>セクション2</h2>
  <h3>サブセクション2-1</h3>

<time>タグの正しい使い方

基本形:

<time datetime="2025-11-27">2025年11月27日</time>

時刻付き:

<time datetime="2025-11-27T14:30:00+09:00">2025年11月27日 14:30</time>

公開日と更新日:

<article>
  <h1>記事タイトル</h1>
  <p>
    公開日: <time datetime="2025-11-27" itemprop="datePublished">2025年11月27日</time>
  </p>
  <p>
    更新日: <time datetime="2025-11-28" itemprop="dateModified">2025年11月28日</time>
  </p>
</article>

今回のプロジェクトにおける実装状況(2025-11-28確認)

このプロジェクトは既にセマンティックHTMLをかなり適切に実装していました。以下、実際の実装状況と今回の改善内容を記録します。

実装済みのセマンティック構造

実装状況: ✅ 既に適切に実装済み

確認の結果、プロジェクトは既に以下のセマンティックHTMLを正しく使用していました:

1. メインコンテンツ構造(...slug.vue)

ファイル: apps/web/app/pages/[...slug].vue(動的記事ページ)

使用しているセマンティックタグ:

  • <main>:ページのメインコンテンツ領域を明示
  • <section>:ディレクトリ一覧や404ページなど、意味的なまとまりを持つセクション

実際のコード(行122-141):

<template>
  <main>
    <Breadcrumb />

    <!-- ディレクトリ一覧 -->
    <template v-if="isDirectoryListing">
      <section class="directory-listing">
        <h1>{{ docPath }} の記事一覧</h1>
        <ArticleTable :articles="articles" :items-per-page="20" />
      </section>
    </template>

    <!-- 記事ページ -->
    <template v-else-if="doc">
      <DocPage :doc="doc" />
    </template>

    <!-- 404ページ -->
    <template v-else>
      <section class="state">
        <h1>Not Found</h1>
        <p>指定されたドキュメントが存在しません。</p>
        <NuxtLink to="/">トップページに戻る</NuxtLink>
      </section>
    </template>
  </main>
</template>

セマンティックな意味:

  • <main>により、スクリーンリーダーが「メインコンテンツはここ」と認識
  • 検索エンジンが「このページの主要コンテンツ」を明確に理解
  • 各状態(一覧、記事、404)が<section>で構造化され、それぞれの役割が明確

2. パンくずリスト(Breadcrumb.vue)

ファイル: apps/web/app/components/Breadcrumb.vue

使用しているセマンティックタグとARIA属性:

  • <nav aria-label="パンくずリスト">:ナビゲーション領域であることを明示
  • <ol>:パンくずの順序性を明示(順序付きリスト)
  • aria-current="page":現在のページを明示
  • aria-hidden="true":装飾要素(セパレーター)を読み上げから除外

実際のコード(行32-49):

<template>
  <nav class="breadcrumb" aria-label="パンくずリスト">
    <ol class="breadcrumb-list">
      <li v-for="(item, index) in breadcrumbs" :key="item.path" class="breadcrumb-item">
        <!-- リンク(最後のアイテム以外) -->
        <NuxtLink
          v-if="index < breadcrumbs.length - 1"
          :to="item.path"
          class="breadcrumb-link"
        >
          {{ item.label }}
        </NuxtLink>

        <!-- 現在のページ(最後のアイテム) -->
        <span v-else class="breadcrumb-current" aria-current="page">
          {{ item.label }}
        </span>

        <!-- セパレーター -->
        <span v-if="index < breadcrumbs.length - 1" class="breadcrumb-separator" aria-hidden="true">/</span>
      </li>
    </ol>
  </nav>
</template>

セマンティックな意味とアクセシビリティ:

  • <nav aria-label="パンくずリスト">により、スクリーンリーダーが「パンくずリストナビゲーション」と読み上げ
  • <ol>により、「Home → 2025-11-27 → 記事名」の順序性が保証される
  • aria-current="page"により、現在のページがどこかを明示
  • aria-hidden="true"により、セパレーター(/)が読み上げられず、UXが向上

評価: ⭐⭐⭐⭐⭐ 完璧な実装!アクセシビリティのベストプラクティスに完全準拠

3. 記事構造(DocPage.vue)

ファイル: apps/web/app/components/DocPage.vue(Markdownコンテンツ表示)

使用しているセマンティックタグ:

  • <article>:記事全体を明示
  • <header>:記事のヘッダー(タグ、日付など)
  • <time itemprop="datePublished">:公開日(Schema.org対応)
  • <time itemprop="dateModified">:更新日(Schema.org対応)
  • <aside aria-label="目次">:補足情報(目次)(2025-11-28改善)

実際のコード(行369-391):

<template>
  <div class="doc-container">
    <!-- 記事 -->
    <article class="doc">
      <!-- 記事ヘッダー(タグ、日付) -->
      <header v-if="doc.tags?.length || doc.publishedAt" class="doc__header">
        <!-- タグ -->
        <ul v-if="doc.tags?.length" class="doc__tags">
          <li v-for="tag in doc.tags" :key="tag">#{{ tag }}</li>
        </ul>

        <!-- 公開日・更新日 -->
        <div v-if="doc.publishedAt" class="doc__meta">
          <time :datetime="isoDate" itemprop="datePublished">
            公開: {{ formattedDate }}
          </time>
          <time v-if="showUpdatedDate" :datetime="isoUpdatedDate" itemprop="dateModified">
            更新: {{ formattedUpdatedDate }}
          </time>
        </div>
      </header>

      <!-- 記事本文 -->
      <div ref="bodyEl" class="doc__body">
        <ContentRenderer :value="doc" />
      </div>
    </article>

    <!-- 目次(補足情報) -->
    <aside class="doc-toc" aria-label="目次">
      <TableOfContents :links="doc.body?.toc?.links" class="toc-wrapper" />
    </aside>
  </div>
</template>

セマンティックな意味:

  • <article>により、検索エンジンが「これは独立したコンテンツ(記事)」と認識
  • <header>により、「記事のメタデータ(タグ、日付)はここ」と明示
  • <time itemprop="datePublished">により、Schema.orgに準拠した公開日の構造化データ
  • <aside aria-label="目次">により、「目次は補足情報」と明示(2025-11-28改善)

公開日・更新日の表示ロジック(行9-46):

<script setup lang="ts">
// 公開日のフォーマット
const formattedDate = computed(() => {
  const value = props.doc.publishedAt
  if (!value) return ""
  const date = typeof value === "string" ? new Date(value) : value
  return new Intl.DateTimeFormat("ja-JP", { dateStyle: "medium" }).format(date)
})

// 更新日を表示するかどうか(公開日と異なる場合のみ)
const showUpdatedDate = computed(() => {
  if (!props.doc.updatedAt || !props.doc.publishedAt) return false
  const published = new Date(props.doc.publishedAt)
  const updated = new Date(props.doc.updatedAt)
  return updated.getTime() !== published.getTime()
})
</script>

2025-11-28の改善内容:

  1. <time>タグで公開日・更新日を表示(以前は実装されていなかった)
  2. itemprop="datePublished"でSchema.org準拠の構造化データ
  3. ✅ 更新日は公開日と異なる場合のみ表示(冗長性を回避)

今回改善した実装(2025-11-28)

実装状況: ✅ 実装済み

唯一の改善点は、DocPage.vueの目次部分でした。

DocPage.vue: 目次を<aside>タグに変更

ファイル: apps/web/app/components/DocPage.vue(行388-390)

変更前:

<div class="doc-toc">
  <TableOfContents :links="doc.body?.toc?.links" class="toc-wrapper" />
</div>

変更後:

<aside class="doc-toc" aria-label="目次">
  <TableOfContents :links="doc.body?.toc?.links" class="toc-wrapper" />
</aside>

なぜ改善したのか:

  1. セマンティックな意味づけ
    • 目次は本文の補足情報なので、<aside>が適切
    • <div>では「補足情報」という意味が検索エンジンやスクリーンリーダーに伝わらない
    • セマンティックHTMLのベストプラクティスに準拠
  2. アクセシビリティの向上
    • スクリーンリーダーが「補足領域」として認識
    • aria-label="目次"で明確にラベル付けし、「これは目次」と読み上げ
    • 視覚障害者が「記事本文」と「目次」を区別しやすくなる
  3. AI検索エンジンの理解
    • ChatGPT Search、Perplexityなどが「これは目次で、本文ではない」と理解
    • メインコンテンツ(<article>)と補足情報(<aside>)を正確に区別
    • 引用時に目次を誤って含めるリスクを低減
  4. HTMLの意味的な正確性
    • MDN Web Docsによると、<aside>は「主要コンテンツと間接的に関連する補足情報」に使用すべき
    • 目次はまさにこの定義に該当

影響:

  • ✅ SEOスコア向上(Lighthouseのアクセシビリティスコアが改善)
  • ✅ スクリーンリーダーのUX向上
  • ✅ AI検索エンジンの解析精度向上

今回実装しなかったもの

1. サイト全体のヘッダー・フッター(<header><footer>

実装状況: ❌ 今回は対象外

対象外とした理由:

  • プロジェクトはコンテンツページ(ブログ記事)がメインの実装範囲
  • サイト全体のヘッダー・フッターは別途、レイアウトコンポーネントで実装する必要がある
  • 現時点ではapp.vueやレイアウトコンポーネントが未実装または確認範囲外

今後実装する場合:

<!-- layouts/default.vue などで実装 -->
<template>
  <div>
    <header class="site-header">
      <h1 class="site-title">log.eurekapu.com</h1>
      <nav class="site-nav">
        <ul>
          <li><a href="/">ホーム</a></li>
          <li><a href="/blog">ブログ</a></li>
        </ul>
      </nav>
    </header>

    <slot />

    <footer class="site-footer">
      <p>&copy; 2025 log.eurekapu.com</p>
    </footer>
  </div>
</template>

2. 個別Vueページのセマンティック改善

実装状況: ❌ 今回は対象外

対象: pages/blog/*.vueなど、Vueコンポーネントで直接記述されたページ

対象外とした理由:

  1. 既存の個別Vueページには独自の実装がある可能性
  2. 各ページで異なるレイアウトやスタイルを使用している
  3. 一律で変更すると、既存のデザインやレイアウトを破壊するリスク
  4. Markdownコンテンツページの自動化がメインの実装範囲

今後実装する場合:

各ページごとに手動で以下のようなセマンティック構造に修正:

<template>
  <main>
    <article>
      <header>
        <h1>記事タイトル</h1>
        <time datetime="2025-11-24">2025年11月24日</time>
      </header>

      <section>
        <h2>セクション1</h2>
        <p>本文...</p>
      </section>

      <footer>
        <p>タグ: <a href="/tags/nvidia" rel="tag">Nvidia</a></p>
      </footer>
    </article>
  </main>
</template>

3. 図表への<figure><figcaption>の追加

実装状況: ❌ 今回は対象外

対象外とした理由:

  • Markdown記事内の画像はContentRendererによって自動レンダリング
  • <figure><figcaption>を自動生成するには、Markdown記法の拡張が必要
  • 実装コストが高く、今回の実装範囲外

今後実装する場合:

Markdown拡張またはカスタムコンポーネントで対応:

<!-- Markdown内でカスタムコンポーネントを使用 -->
::figure
![Nvidiaの堀](/images/nvidia-moat.svg)
#caption
Nvidiaの競合優位性を示す図
::

実装状況まとめ

項目実装状況備考
<main>✅ 実装済み...slug.vue
<nav> (パンくずリスト)✅ 実装済みBreadcrumb.vue(完璧な実装)
<article>✅ 実装済みDocPage.vue
<header> (記事)✅ 実装済みDocPage.vue
<time>✅ 実装済みDocPage.vue(2025-11-28実装)
<aside> (目次)✅ 実装済みDocPage.vue(2025-11-28改善)
<section>✅ 実装済み...slug.vue
<header> (サイト全体)❌ 未実装レイアウトコンポーネントで今後実装
<footer> (サイト全体)❌ 未実装レイアウトコンポーネントで今後実装
<figure> / <figcaption>❌ 未実装Markdown拡張が必要

参考: セマンティックHTML実装例

以下は、今後実装する際の参考例です。

パターン1:ブログ記事ページの基本構造(参考例)

<template>
  <div class="page-wrapper">
    <!-- ヘッダー(サイト全体) -->
    <header class="site-header">
      <h1 class="site-title">log.eurekapu.com</h1>
      <nav class="site-nav">
        <ul>
          <li><a href="/">ホーム</a></li>
          <li><a href="/blog">ブログ</a></li>
        </ul>
      </nav>
    </header>

    <!-- メインコンテンツ -->
    <main class="main-content">
      <!-- パンくずナビゲーション -->
      <nav aria-label="パンくずリスト">
        <ol>
          <li><a href="/">ホーム</a></li>
          <li><a href="/blog">ブログ</a></li>
          <li aria-current="page">記事タイトル</li>
        </ol>
      </nav>

      <!-- 記事本体 -->
      <article>
        <header class="article-header">
          <h1>Nvidiaの堀:競合他社が直面する「無理ゲー」な9つの壁</h1>
          <p class="article-meta">
            <time datetime="2025-11-24" itemprop="datePublished">
              2025年11月24日
            </time>
            <span class="author" itemprop="author">log.eurekapu.com</span>
          </p>
        </header>

        <section class="article-intro">
          <p>Nvidiaには何重もの「防御壁(堀)」があり...</p>
        </section>

        <section>
          <h2>1. チップ単体では無意味</h2>
          <p>仮に、NvidiaのH100やBlackwellと同等の性能を...</p>
        </section>

        <section>
          <h2>2. 製造・実装の壁</h2>
          <p>設計図ができても、それを物理的な製品にするには...</p>

          <figure>
            <img src="/images/nvidia-moat.svg" alt="Nvidiaの堀の図解" />
            <figcaption>Nvidiaの競合優位性を示す図</figcaption>
          </figure>
        </section>

        <footer class="article-footer">
          <p>タグ:
            <a href="/tags/nvidia" rel="tag">Nvidia</a>,
            <a href="/tags/ai" rel="tag">AI</a>
          </p>
        </footer>
      </article>

      <!-- サイドバー(補足情報) -->
      <aside class="sidebar">
        <h2>目次</h2>
        <nav aria-label="目次">
          <ol>
            <li><a href="#section1">チップ単体では無意味</a></li>
            <li><a href="#section2">製造・実装の壁</a></li>
          </ol>
        </nav>

        <section class="related-articles">
          <h2>関連記事</h2>
          <ul>
            <li><a href="/blog/ai-investment">AI投資の臨界点</a></li>
          </ul>
        </section>
      </aside>
    </main>

    <!-- フッター -->
    <footer class="site-footer">
      <p>&copy; 2025 log.eurekapu.com</p>
    </footer>
  </div>
</template>

パターン2:個別ブログページ(nvidia-moat.vue)(参考例)

実装状況: ⚠️ 今回は対象外(必要に応じて手動実装)

対象外とした理由:

  1. 既存の個別Vueページには独自の実装がある可能性
  2. Markdownコンテンツページの自動化がメインの実装範囲
  3. 個別ページは記事ごとにカスタマイズが必要

実装する場合の参考例:

現状のnvidia-moat.vueをセマンティックHTMLに修正する場合:

<template>
  <div class="page-container">
    <main>
      <article class="content-wrapper">
        <header class="article-header">
          <h1>Nvidiaの堀:競合他社が直面する「無理ゲー」な9つの壁</h1>
          <p class="published-date">
            <time datetime="2025-11-24">2025.11.24</time>
          </p>
        </header>

        <section class="intro">
          <p><strong>「Nvidiaには何重もの『防御壁(堀)』があり、正面から戦って勝つのは実質不可能(無理ゲー)である」</strong></p>
        </section>

        <figure class="image-container">
          <img src="/images/nvidia-moat-perspective.svg" alt="Nvidia Moat Diagram" class="moat-svg" />
          <figcaption>Nvidiaの競合優位性を示す9つの防御壁</figcaption>
        </figure>

        <section class="article-body">
          <h2>1. チップ単体では無意味</h2>
          <p>仮に、NvidiaのH100やBlackwellと同等の性能を持つGPUチップを設計できたとしましょう。...</p>

          <h2>2. 製造・実装の壁</h2>
          <p>設計図ができても、それを物理的な製品にするにはさらに高い壁があります。</p>

          <!-- 以下、他のセクション -->
        </section>
      </article>
    </main>
  </div>
</template>

<script setup lang="ts">
useSeoMeta({
  title: 'Nvidiaの堀:競合他社が直面する「無理ゲー」な9つの壁',
  description: 'Nvidiaには何重もの「防御壁」があり、正面から戦って勝つのは実質不可能である理由。チップ設計からCUDA、ブランドまで、絶望的なハードルを解説。',
  ogTitle: 'Nvidiaの堀:競合他社が直面する「無理ゲー」な9つの壁',
  ogDescription: 'Nvidiaには何重もの「防御壁」があり、正面から戦って勝つのは実質不可能である理由。チップ設計からCUDA、ブランドまで、絶望的なハードルを解説。',
  ogType: 'article',
  ogUrl: 'https://log.eurekapu.com/blog/nvidia-moat',
  ogImage: 'https://log.eurekapu.com/images/nvidia-moat-perspective.svg',
  articlePublishedTime: '2025-11-24',
  articleModifiedTime: '2025-11-24',
  articleAuthor: 'log.eurekapu.com',
})
</script>

パターン3:Markdown記事(DocPage.vue)(参考例)

実装状況: ✅ 既に適切に実装済み(一部改善済み)

実装されている点:

  • <article>タグで記事を明示
  • <header>タグで記事ヘッダーを構造化
  • <time>タグで公開日・更新日を表示
  • <aside aria-label="目次">で目次を補足情報として明示(2025-11-28改善)

参考: 完全な実装例:

<template>
  <article class="doc-page">
    <header class="doc-header">
      <h1>{{ doc.title }}</h1>
      <div class="doc-meta" v-if="doc.publishedAt">
        <time :datetime="doc.publishedAt" itemprop="datePublished">
          {{ formatDate(doc.publishedAt) }}
        </time>
        <span v-if="doc.tags && doc.tags.length" class="tags">
          タグ:
          <a v-for="tag in doc.tags" :key="tag" :href="`/tags/${tag}`" rel="tag">
            {{ tag }}
          </a>
        </span>
      </div>
    </header>

    <section class="doc-content">
      <ContentRenderer :value="doc" />
    </section>

    <footer class="doc-footer" v-if="doc.tags && doc.tags.length">
      <nav aria-label="タグ">
        <a v-for="tag in doc.tags" :key="tag" :href="`/tags/${tag}`" rel="tag">
          {{ tag }}
        </a>
      </nav>
    </footer>
  </article>
</template>

<script setup lang="ts">
defineProps<{
  doc: any
}>()

function formatDate(dateString: string) {
  const date = new Date(dateString)
  return date.toLocaleDateString('ja-JP', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
}
</script>

検証方法

1. HTML Validator

2. WAVE(アクセシビリティチェック)

3. Lighthouse(Chrome DevTools)

# Chrome DevToolsを開く(F12)
# Lighthouseタブ → Generate report

「アクセシビリティ」と「ベストプラクティス」のスコアが向上します。

4. HeadingsMap(ブラウザ拡張)

  • 見出し階層を可視化
  • Chrome/Firefoxで利用可能

よくある間違い

<div>だらけのマークアップ

<div class="article">
  <div class="title">タイトル</div>
  <div class="content">本文</div>
</div>

✅ セマンティックなマークアップ

<article>
  <h1>タイトル</h1>
  <p>本文</p>
</article>

❌ 見出しレベルを飛ばす

<h1>記事タイトル</h1>
<h3>セクション</h3> <!-- h2を飛ばしている -->

✅ 正しい見出し階層

<h1>記事タイトル</h1>
<h2>セクション</h2>

<time>を使わない

<p class="date">2025-11-27</p>

<time>を使う

<time datetime="2025-11-27">2025年11月27日</time>

まとめ

セマンティックHTMLは:

  • 検索エンジンとAIがコンテンツを正確に理解できる
  • アクセシビリティを向上させる
  • SEOランキングに好影響
  • メンテナンス性が向上

<article><section><time>、見出し階層などを適切に使用することで、AI検索時代に対応したWebサイトになります。