Article

内部リンク最適化:サイト内リンク構造を整えて評価UP

高田晃太郎
内部リンク最適化:サイト内リンク構造を整えて評価UP

Ahrefsの調査では、全URLのおよそ66%が外部被リンクをひとつも持たないと報告されています¹。外部から推されにくいページを、あなたのサイト内でどう救い上げるか。答えのひとつが内部リンクです。Google 検索セントラルのドキュメントでも、内部リンクはクロール(検索エンジンがページを発見して巡回すること)、インデックス(検索対象として登録すること)、意味理解に不可欠なシグナルと明言されています²。実務の現場でも、内部リンクの再設計がクロール効率とユーザー回遊の双方に寄与したという報告は少なくありません。CTOやエンジニアリーダーにとって重要なのは、思いつきの「関連記事を増やす」ではなく、測定可能なKPIとコードで運用できる仕組みに落とし込むことです。この記事では、情報設計から計測・実装までを一気通貫で解像度高く示し、チームで再現できる内製フレームとして提案します。

内部リンクは評価の土台:数値目標と原則を揃える

内部リンクは三つの面で効きます。第一にクロールパスの短縮です。クリック深度(トップから目標ページまでのリンク数)が浅いほどクロールの到達性が高まり、更新検知も早くなります。第二に意味の伝達です。アンカーテキスト(リンクの文言)は、コンテンツ同士の関係や対象ページの主題を検索エンジンに説明する役目を担います。第三に相対的な重要度の表明です。どのページに多くのリンクを集約し、どこから受けるかという構造は、内部版のPageRankのように重みの分配として振る舞います(内部PageRankはサイト内リンクだけで計算する相対重みの簡易指標です⁶)。これらは単なる概念ではなく、運用指標に落とし込むと機能します。たとえば平均クリック深度を3以内に保つ、孤立ページ(サイト内から1本もリンクされないページ)を0にする、戦略ハブの受リンクをカテゴリ配下の大半から確実に通す、といった目標は、ログとグラフ解析で月次に追えます。Googleは合理的なリンク数を推奨しており、無差別な自動リンクは逆効果になり得ます³。重要なのは、意味の近いページ同士を、説明的なアンカーでつなぎ、サイト全体の論理的な骨格を形成することです。たとえばドキュメントサイトなら「導入ガイド → 設定 → トラブルシューティング」という階層をハブで明示し、各ページから相互に自然な導線を返すだけでも、深さと意味の両面で改善を狙えます。

トピッククラスタとハブで“面”をつくる

単発の記事や商品詳細を量産しても、評価は面で伝わりにくいものです。そこで、包括ガイドやカテゴリトップ、ユースケース別のランディングのようなハブを設定し、配下の詳細ページから双方向にリンクさせます。ハブは、ナビゲーション、見出し下の導線、本文中の文脈リンクという複数の入口を用意すると機能します。関連度の低い大量リンクで水増しするのではなく、同一トピッククラスタ内の関係に限定し、アンカーは「こちら」ではなく、検索語に近い説明文で表現します(例:「内部リンク最適化の基礎」や「トピッククラスタ設計の手順」など)。これにより検索エンジンは、ハブと子ページの関係をより正確に理解できます⁴。

ファセットと重複の制御で“漏れ”を防ぐ

ECやSaaSのドキュメントでは、並び替えやフィルタでURLが増殖しがちです(これをファセットナビゲーションと言います)。内部リンクの最適化は、リンクを増やすだけでなく、意味を持たないパラメータや重複パスへのリンクを減らすことも含みます。正規化のために rel="canonical" を適用し、必要に応じて検索用のファセットは noindex を使います。ナビゲーションからのリンク先が常に正規URL(代表URL)になるようテンプレートで統一すれば、クロールバジェット(検索エンジンの巡回リソース)の浪費を防げます⁵。

計測フレーム:深さ、孤立、内部PageRankを見える化する

内部リンクの運用は、測れなければ継続できません。クローラの出力とアクセスログをデータレイクに集約し、グラフとして扱える形に整えるのが近道です。以下では、孤立ページ検出、クリック深度推定、内部PageRankの算出⁶を、SQLとPythonで再現可能な形に示します。非エンジニアの読者向けに補足すると、いずれも「どのページがつながり、どれが重要か」を定量化するための最低限の計測ステップです。

孤立ページを特定するSQLの一例

クローラの結果を edges(src, dst) と pages(url) に格納したと仮定し、参照されていないURLを抽出します。PostgreSQLの例です。

-- PostgreSQL
-- 孤立ページの抽出(サイト外からの流入考慮なし)
WITH inbound AS (
  SELECT dst AS url, COUNT(*) AS inlinks
  FROM edges
  GROUP BY dst
)
SELECT p.url
FROM pages p
LEFT JOIN inbound i ON p.url = i.url
WHERE COALESCE(i.inlinks, 0) = 0;

クリック深さを再帰CTEで推定する

ホームや主要ハブを種集合として深さを広げます。テンプレートのリンク群が安定していれば、実運用の近似として使えます。

-- PostgreSQL
-- 種URLからの最短クリック深度
WITH RECURSIVE seeds AS (
  SELECT url FROM pages WHERE url IN ('https://example.com/', 'https://example.com/docs/')
),
walk AS (
  SELECT s.url, 0 AS depth FROM seeds s
  UNION ALL
  SELECT e.dst, w.depth + 1
  FROM walk w
  JOIN edges e ON e.src = w.url
  WHERE w.depth < 10
),
min_depth AS (
  SELECT url, MIN(depth) AS depth FROM walk GROUP BY url
)
SELECT p.url, COALESCE(m.depth, NULL) AS click_depth
FROM pages p
LEFT JOIN min_depth m ON p.url = m.url;

内部PageRankをPythonで算出し、ハブの重みを検証する

NetworkX(Pythonのグラフ分析ライブラリ)を用いて内部グラフにPageRankを適用し、上位ノードが期待するハブになっているかを確認します。

# Python 3.x
import csv
import networkx as nx
from pathlib import Path

# edges.csv: src,dst の有向グラフ
G = nx.DiGraph()
with Path('edges.csv').open() as f:
    reader = csv.DictReader(f)
    for row in reader:
        G.add_edge(row['src'], row['dst'])

pr = nx.pagerank(G, alpha=0.85, max_iter=100)

# 上位20件を出力
for url, score in sorted(pr.items(), key=lambda x: x[1], reverse=True)[:20]:
    print(f"{score:.6f}\t{url}")

社内クロールで隠れた断絶を見つけるNode.jsスクリプト

公開サイトの内部リンクを数千URL規模で収集する簡易クローラです。robotsの配慮やレート制御は本番では強化してください。非同期クロールの結果をCSVで吐き出し、前掲のSQLやPythonに流し込むと一連の分析が回ります。

// Node.js (TypeScript)
import fetch from 'node-fetch';
import * as cheerio from 'cheerio';

const seen = new Set<string>();
const edges: Array<{ src: string; dst: string }> = [];
const origin = new URL('https://example.com');

async function crawl(url: string, depth = 0) {
  if (seen.has(url) || depth > 5) return;
  seen.add(url);
  const res = await fetch(url);
  if (!res.ok) return;
  const html = await res.text();
  const $ = cheerio.load(html);
  $('a[href]').each((_, el) => {
    const href = $(el).attr('href');
    if (!href) return;
    const u = new URL(href, url);
    if (u.origin === origin.origin) {
      const src = url.split('#')[0];
      const dst = u.toString().split('#')[0];
      edges.push({ src, dst });
      crawl(dst, depth + 1).catch(() => {});
    }
  });
}

crawl(origin.toString()).then(() => {
  console.log('src,dst');
  for (const e of edges) console.log(`${e.src},${e.dst}`);
});

実装パターン:テンプレート、スキーマ、計測を一体化する

内部リンクはテンプレートで半自動化しつつ、エディタが文脈リンクを加えられる余白を残すと運用が安定します。フロントエンド、構造化データ、アナリティクスを組み合わせ、ユーザーとクローラ双方にわかりやすい導線を提供します。

Next.jsでサイトワイドのリンク品質を担保する

フッターやサイドナビだけに頼らず、本文下にクラスタの代表記事を差し込むコンポーネントを用意します。Linkのプリフェッチはトラフィックに応じて制御します(大量プリフェッチは帯域を圧迫します)。

// Next.js / React (TypeScript)
import Link from 'next/link';
import { FC } from 'react';

type Related = { href: string; title: string; anchor: string };

export const ClusterLinks: FC<{ items: Related[] }> = ({ items }) => {
  if (!items?.length) return null;
  return (
    <nav aria-label="関連コンテンツ">
      <h2 className="visually-hidden">関連コンテンツ</h2>
      <ul className="related">
        {items.map((it) => (
          <li key={it.href}>
            <Link href={it.href} prefetch={false} title={it.title}>
              {it.anchor}
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  );
};

パンくずの構造化データで階層を明示する

パンくずはナビゲーションとしてだけでなく、構造化データ(検索エンジンに意味を伝えるための機械可読データ)で階層を検索エンジンに伝えられます。Next.jsのScriptでJSON-LD(JSON形式の構造化データ)を埋め込みます⁷。

// Next.js / JSON-LD
import Script from 'next/script';
import { FC } from 'react';

export const BreadcrumbLD: FC<{ items: { name: string; url: string }[] }> = ({ items }) => {
  const json = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: items.map((it, i) => ({
      '@type': 'ListItem',
      position: i + 1,
      name: it.name,
      item: it.url,
    })),
  };
  return (
    <Script type="application/ld+json" id="ld-breadcrumb">
      {JSON.stringify(json)}
    </Script>
  );
};

アンカーテキストのクリックをGA4で計測する

どのアンカーが実際に使われているかをイベントで追えば、無駄なリンクの削減と強い導線の強化に繋がります。Google Analytics 4(GA4)のイベントとして送信し、レポートで「リンク文言 × 遷移先」の実利用を可視化します。

// GA4 計測ユーティリティ (TypeScript)
import { isProd } from './env';

export function trackInternalLink(anchor: string, href: string) {
  if (!isProd) return;
  // @ts-ignore
  window.gtag?.('event', 'internal_link_click', {
    event_category: 'navigation',
    link_text: anchor,
    link_url: href,
  });
}

正規URLへの統一をサーバで保証する

リンクの正規化は、テンプレートだけに任せず、サーバ側でのリダイレクト・スラッシュ統一でも担保します。以下はNginxの例です。トレーリングスラッシュ(末尾の/)の扱いを統一すると、重複URLの発生を抑えられます。

# Nginx
# www へ統一とトレーリングスラッシュ統一
server {
  listen 80;
  server_name example.com;
  return 301 https://www.example.com$request_uri;
}

server {
  listen 443 ssl;
  server_name www.example.com;

  # ディレクトリはスラッシュ付きへ
  if (-d $request_filename) {
    rewrite ^(.+[^/])$ $1/ permanent;
  }

  # パラメータの正規化はアプリ側で処理
  # ...
}

運用ガバナンス:CMSルールとKPIレビューで継続させる

内部リンクは一度構築して終わりではありません。新規ページが毎月追加される限り、孤立や分断は再発します。そこで、CMSの入力ルールで最低限のリンクを保証し、KPIレビューで劣化を検知する体制が必要です。記事テンプレートにクラスタの推奨リンク候補を自動提案する仕組みを用意し、エディタは文脈に沿って選択・編集するだけで十分な内部リンク密度が担保されるようにします。提案の根拠は、先に算出した内部PageRankやトピック類似度、閲覧ログの回遊データを使うと実務で回ります。類似度計算には、タイトルと見出しをベクトル化して近傍を推薦する簡易モデルでも効果があります(過学習を避け、運用で理解できる振る舞いを優先しましょう)。

GoでHTMLからリンクを安全に抽出し、CIで検査する

ビルド時にリンク品質を自動検査できると、人手のレビュー負担が減ります。以下はGoでHTMLのa要素を抽出する最小例です。CIではアンカーのテキスト長や禁止語(「こちら」単独など)も検査します。たとえば「テキストが5文字未満のアンカー比率」を閾値で警告すると、説明的アンカーの徹底に役立ちます。

// Go 1.22
package main

import (
    "fmt"
    "log"
    "net/url"
    "golang.org/x/net/html"
    "strings"
)

func main() {
    htmlStr := `<html><body><a href="/docs/intro">内部リンク最適化ガイド</a></body></html>`
    r := strings.NewReader(htmlStr)
    z := html.NewTokenizer(r)
    for {
        tt := z.Next()
        if tt == html.ErrorToken { break }
        if tt == html.StartTagToken {
            t := z.Token()
            if t.Data == "a" {
                for _, a := range t.Attr {
                    if a.Key == "href" {
                        u, _ := url.Parse(a.Val)
                        if u.Scheme == "" { fmt.Println("internal:", u.Path) }
                    }
                }
            }
        }
    }
}

編集体験と検索体験を両立させる

エディタにとって過剰な作業は長続きしません。WYSIWYGにアンカーテキストの品質評価をインライン表示し、推奨アンカー候補を提示すれば、表現の自由を保ちつつ品質を底上げできます。ユーザー側では、本文中のリンクは説明的で、パンくずとサイドナビは位置情報を提供し、本文末の導線は次に読むべき一歩を提案する、という役割分担が理想です。検索エンジンに対しては、これらの導線が一貫して正規URLを向き、クラスタ内に閉じた関連である限り、評価の分散は起きにくくなります。

成果の読み解き:ビジネスKPIに接続する

内部リンクの改善は、単に「SEOスコア」を上げるためではなく、事業のKPIに接続してこそ価値があります。平均クリック深度が縮むと、新着や更新の検出が速まり、インデックス率が改善します。ハブページの内部PageRankが高まると、関連クエリでの可視性が整い、自然検索からの回遊が持続的に生まれます。これらは最終的にセッションの質、つまりコンテンツ間の連続閲覧やドキュメントからの製品導線のクリック増加として観測できます。たとえば「孤立ページ数がゼロ、平均クリック深度が3以内、主要ハブの内部PageRankが上位10%」という状態を目標にし、Search Consoleのインデックスカバレッジと内部リンクレポート、アナリティクスのイベント、サーバログのクロール頻度を組み合わせて月次のダッシュボードに可視化すると、改善の手応えが組織で共有できます。技術チームはグラフ指標の変化を、ビジネス側は回遊とCVRの変化を、それぞれの言語で同じ現象として語れる状態が理想です。

アンカーテキスト多様性と重複抑制のバランス

アンカーは説明的であるほど良い一方で、不自然なキーワードの硬直化は避けるべきです。同一URLに対して完全一致の同一アンカーが大量に繰り返されると、ユーザー体験も検索エンジンの評価も損ないがちです。クラスタ内で語彙のバリエーションをつけつつ、意図する主要語は含める、という運用が現実的です。CMSのサジェストで言い換え候補を提示し、編集者が自然な文として選べるようにする仕組み化が有効です。

避けたい落とし穴

内部リンクで nofollow を用いて重要度の調整を試みる「スカルプティング」は、現代の検索エンジンの挙動と相性が良くありません⁸。基本的にサイト内では dofollow を前提に、意味のあるリンクだけを掲載する設計で臨むべきです。また、robots.txtで重要なディレクトリを誤ってブロックし、サイト内の導線が死ぬ事故は珍しくありません。変更は必ずステージングでクロールテストを行い、重要導線の可視化を定期的に確認する体制を保ってください。

まとめ:リンクは“増やす”より“設計して運用する”

内部リンク最適化は、単なる数合わせではありません。クラスタとハブで面を作り、クリック深度を3以内に整え、孤立を0に保ち、アンカーで意味を丁寧に伝える。その原則を、SQLとPythonでの指標算出、Next.jsとJSON-LDでの実装、GA4での行動計測にまで落とし込めば、チームは再現可能な改善サイクルを手に入れます。外部リンクに頼れないページが多いからこそ、内部から評価を育てる設計が効きます。明日から着手するなら、まず現状の深さと孤立を測り、最重要ハブの受リンクを増やすテンプレート改修から始めてみてください。数週間でクロールや回遊の改善がダッシュボードに表れることがあり、数カ月で検索流入の質が変化する可能性も十分にあります。チームの次の一手はどこに置きますか。あなたのサイトの骨格を、いま設計し直してみましょう。

参考文献

  1. Barry Schwartz. Over 65% Of Web Pages Have No Links, Ahrefs Study. SERoundtable. (https://www.seroundtable.com/over-65-of-web-pages-have-no-links-31134.html#:~:text=Ahrefs%20published%20a%20study%20a,have%20even%20a%20single%20backlink)
  2. Google Search Central. Make your links crawlable. (https://developers.google.com/search/docs/crawling-indexing/links-crawlable#:~:text=You%20may%20usually%20think%20about,to%20those%20pages%20in%20context)
  3. Google Search Central. Link best practices — There’s no magical ideal number of links on a page. (https://developers.google.com/search/docs/crawling-indexing/links-crawlable#:~:text=There%27s%20no%20magical%20ideal%20number,much%2C%20then%20it%20probably%20is)
  4. Conductor Academy. Topic Clusters: The Hub-and-Spoke SEO Strategy. (https://www.conductor.com/academy/topic-clusters/#:~:text=Both%20content%20marketing%20strategies%20enhance,search%20engine%20algorithms%20like%20Google)
  5. Google Search Central. Manage faceted navigation. (https://developers.google.com/search/docs/crawling-indexing/crawling-managing-faceted-navigation#:~:text=%2A%20Using%20%60rel%3D,in%20order%20for%20it%20to)
  6. Patrick Stox. Improve internal linking: Calculate Internal PageRank in R. Search Engine Land. (https://searchengineland.com/improve-internal-linking-calculate-internal-pagerank-r-246883#:~:text=For%20this%20reason%2C%20the%20way,a%20metric%20I%20call%20Internal)
  7. Google Search Central. Breadcrumb structured data. (https://developers.google.com/search/docs/appearance/structured-data/breadcrumb#:~:text=A%20breadcrumb%20trail%20on%20a,breadcrumb%20in%20the%20breadcrumb%20trail)
  8. Michael Martinez. How To Screw Your Web Site With Nofollow. SEO Theory. (https://a2006.seo-theory.com/how-to-screw-your-web-site-with-nofollow/#:~:text=Matt%20Cutts%20is%20on%20record,as%20saying)