Article

SEOを損なわないリニューアル:検索順位を維持するための10の対策

高田晃太郎
SEOを損なわないリニューアル:検索順位を維持するための10の対策

Akamai等のレポートでは、100msの遅延がコンバージョンを大幅に下げることが示されています¹。さらにGoogleはCore Web Vitalsをページ体験の主要指標として定義しており、検索での成功に重要ですが、単独でランキングを決めるシグナルではありません²³。URL構造やリダイレクトの不備はクロール効率と評価移譲に直結します⁴⁵。実務では、リニューアル直後に有機流入が乱高下するのは珍しくありません。つまり「見た目を変える」よりも、「シグナルを毀損しない」設計と移行の精度が成果を左右します。本稿は、検索順位を維持するための10の対策を、実装・ベンチマーク・監視まで含めて示します。

前提条件とアーキテクチャ:順位維持の設計原則

まず、移行プロジェクトの前提を定義します。対象はSPA/SSR(Next.js等)やHeadless CMS構成を想定し、検索評価の継承を最優先に設計します。

項目推奨スタック/設定目的
レンダリングSSR/SSG(Next.js 13+ App Router)初期HTMLを確実に返しLCP改善¹
配信Nginx/Cloud CDN + HTTP/2TTFB短縮・安定化
計測GSC・GA4・CrUX・Lighthouse CI順位・CVとCWVの継続計測²
ログアクセスログ+404/410監視URL欠落・リダイレクト漏れ検知⁵

実装手順(推奨)

  1. 現行URL・内部リンク・構造化データを完全クロール(Screaming Frog等)
  2. 1:1のリダイレクトマップを作成し、テスト環境で自動検証⁵
  3. ステージングをGSCに別プロパティとして登録し、robotsでnoindex⁵
  4. XMLサイトマップとrobots.txtを新構成で先行生成⁶
  5. 部分ローンチ(1〜5%トラフィック)でバグ検知→全量切替
  6. 切替後72時間は404/5xx・CLS/INPを重点監視¹³

検索順位を維持するための10の対策と実装

1. 301リダイレクトを厳密に(チェーン禁止・1ホップ)

評価移譲は永久(301)で一回で到達させるのが原則。サーバ層での高速判定が望ましいですが、アプリ層でも整合性検証用に実装します⁵。

import express from 'express';
import fs from 'fs/promises';
import path from 'path';

const app = express(); let map = new Map();

async function loadMap() { try { const json = await fs.readFile(path.join(process.cwd(), ‘redirects.json’), ‘utf-8’); const arr = JSON.parse(json); map = new Map(arr.map(({ from, to }) => [from, to])); console.log(Loaded ${map.size} redirects); } catch (e) { console.error(‘Failed to load redirects’, e); process.exit(1); } }

app.use((req, res, next) => { try { const dest = map.get(req.path); if (dest) return res.redirect(301, dest); next(); } catch (e) { console.error(‘Redirect middleware error’, e); next(); } });

loadMap().then(() => app.listen(3000, () => console.log(‘redirects ready’)));

パフォーマンス指標: リダイレクト判定までの99pが1ms未満(Nodeプロセス内)を目標。Nginx mapを使えばミクロ秒オーダになります。

2. URL正規化(トレーリングスラッシュ・小文字化)

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) { const url = req.nextUrl; if (url.pathname !== ’/’ && url.pathname.endsWith(’/’)) { url.pathname = url.pathname.slice(0, -1); return NextResponse.redirect(url, 301); } if (/[A-Z]/.test(url.pathname)) { url.pathname = url.pathname.toLowerCase(); return NextResponse.redirect(url, 301); } return NextResponse.next(); }

正規化は重複URLの分散を防ぎ、canonicalの依存度を下げます⁴。

3. 構造化データとメタデータの継承

JSON-LDのtype・必須プロパティは旧サイト同等以上を保証。ページタイプ別にスキーマをスナップショット比較します。

4. robots.txt と noindex の環境分離

# nginx snippet
map $request_uri $redirect_to { include /etc/nginx/redirects.map; }
server {
  listen 443 ssl; server_name example.com;
  if ($redirect_to) { return 301 $redirect_to; }
  location = /robots.txt { try_files $uri =404; }
  location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; }
  error_page 410 = @gone; location @gone { return 410; }
}

ステージングではX-Robots-Tag: noindexヘッダを付与してインデックス混入を防ぎます⁵。

5. XMLサイトマップの自動生成と即時送信

import xml.etree.ElementTree as ET
from datetime import datetime
from urllib.parse import urljoin
import json, sys

def generate(base, routes): urlset = ET.Element(‘urlset’, xmlns=“http://www.sitemaps.org/schemas/sitemap/0.9”) for r in routes: if r.get(‘noindex’): continue url = ET.SubElement(urlset, ‘url’) ET.SubElement(url, ‘loc’).text = urljoin(base, r[‘path’]) ET.SubElement(url, ‘lastmod’).text = datetime.utcnow().strftime(‘%Y-%m-%dT%H:%M:%SZ’) ET.SubElement(url, ‘changefreq’).text = r.get(‘freq’, ‘weekly’) ET.SubElement(url, ‘priority’).text = str(r.get(‘priority’, 0.5)) return ET.tostring(urlset, encoding=‘utf-8’, xml_declaration=True)

if name == ‘main’: try: data = json.load(sys.stdin) xml = generate(data[‘base’], data[‘routes’]) sys.stdout.buffer.write(xml) except Exception as e: sys.stderr.write(f’ERROR: {e}\n’) sys.exit(1)

生成後はSearch Console APIで送信。差分検知で更新を最小化するとクロール効率が上がります⁶。

6. hreflang と canonical の整合

hreflangは相互参照とx-defaultを徹底。canonicalと矛盾させないこと。サイトマップでのhreflang宣言も有効です⁷⁸。

7. Core Web Vitals(LCP/CLS/INP)の改善

KPI目標(P75)¹主要アクション
TTFB< 200msエッジキャッシュ、keep-alive、SSR最適化
LCP< 2.5s¹画像のpreload/priority・レスポンシブ画像
CLS< 0.1¹サイズ属性指定・フォントディスプレイ
INP< 200ms¹³遅延ローディング、メインスレッド軽量化
import puppeteer from 'puppeteer';
(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'networkidle0' });
  const perf = JSON.parse(await page.evaluate(() => JSON.stringify(window.performance.timing)));
  console.log('TTFB(ms):', perf.responseStart - perf.requestStart);
  await browser.close();
})().catch(e => { console.error(e); process.exit(1); });

計測はCIに組み込み、回帰を自動検知します。注:INPは2024年にFIDを置き換えた指標です³。

8. 見出し階層と内部リンクの保持

H1は1ページ1つ、H2/H3は意味の連続性を保ちます。主要ランディングの内部リンクは「旧→新」で同等以上の露出に再配置します。

9. 404/410の扱いとフェイルセーフ

import re, sys
from collections import Counter, deque
pattern = re.compile(r'\"GET (.*?) HTTP/.*\" (\d{3})')
window = deque(maxlen=3000)
hist = Counter()
for line in sys.stdin:
  m = pattern.search(line)
  if not m: continue
  path, status = m.group(1), int(m.group(2))
  window.append(status == 404)
  if status == 404: hist[path] += 1
  if len(window) == window.maxlen and sum(window)/len(window) > 0.05:
    print('ALERT 404 rate >5%')
for p, c in hist.most_common(20):
  print(c, p)

消滅URLは410を返し、類似意図のページがある場合のみ301で誘導します⁵。

10. ローンチ後の監視(GSC+ログ+RUM)

GSCの「ページのインデックス未登録」「検出 - インデックス未登録」を日次で監視。RUMでINP/CLSをP75で可視化し、404率・5xx率は5分粒度でアラート¹。

パフォーマンス最適化とベンチマーク結果

キャッシュ戦略とネットワーク最適化でTTFB/LCPを改善します。画像はnext/image等で最適化、フォントはpreloadとdisplay: swapを適用。

-- 旧CMSからリダイレクトマップを抽出
SELECT old_url AS from, new_url AS to
FROM url_redirects
WHERE enabled = TRUE;

社内ベンチマーク結果(例):

指標新(移行翌週)改善率
TTFB(東京, p95)320ms180ms43.7%
LCP(全体, p75)3.1s2.2s29.0%
CLS(全体, p75)0.180.0761.1%
INP(全体, p75)260ms170ms34.6%

測定条件: Cloud CDN有効、画像はAVIF優先、SSR+静的キャッシュ、回線はLTE相当。Lighthouse CIはモバイルシミュレーションで固定。

Nginx/エッジでの高速化例

# 画像/フォントは長期キャッシュ
location ~* \.(avif|webp|jpg|png|woff2)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTMLは短期+stale-while-revalidate
location / {
  add_header Cache-Control "public, max-age=60, stale-while-revalidate=300";
}

コストとROIの目安

想定: 月間自然流入50万、CVR2.0%、平均CV価値1万円。移行で流入維持+LCP改善によりCVR相対+5%とすると、月+100件≒1,000万円/年の増益。実装/計測工数(4〜8週間, 3人月)に対しROIは十分に成立します。

移行のガバナンス:計測・リスク低減・導入期間

ガードレール(失敗を防ぐ仕組み)

  1. CIでURL差分テスト(旧→新のHTTPステータス/タイトル/H1/構造化データを自動比較)
  2. リダイレクトチェーン検査(3xx連鎖を0に)
  3. RUMでCWVのP75をデプロイごとにトレンド化¹
  4. アクセスログの404率>5%で自動アラート(前掲スクリプト)

導入期間の目安

要件定義1〜2週、クロール/マッピング1〜2週、実装/CI組込2〜3週、段階ローンチ1週、安定化1〜2週。合計4〜8週間が標準レンジです。

追加の検証コード(簡易クロール計測)

import fetch from 'node-fetch';
const urls = ['https://example.com/', 'https://example.com/about'];
(async () => {
  for (const u of urls) {
    const t0 = Date.now();
    const r = await fetch(u, { redirect: 'manual' });
    const ttfb = Date.now() - t0;
    console.log(u, r.status, 'TTFB:', ttfb, 'ms', 'Cache:', r.headers.get('cf-cache-status'));
  }
})().catch(e => { console.error(e); process.exit(1); });

本番切替前に主要ランディング1000URLで上記のTTFB/ステータス検証を回し、閾値逸脱をブロックします。

多言語/hreflangの自動検証スニペット

import { JSDOM } from 'jsdom';
import fetch from 'node-fetch';

async function check(u) { const html = await (await fetch(u)).text(); const dom = new JSDOM(html); const links = […dom.window.document.querySelectorAll(‘link[rel=“alternate”][hreflang]’)] .map(l => ({ lang: l.getAttribute(‘hreflang’), href: l.getAttribute(‘href’) })); if (!links.find(l => l.lang === ‘x-default’)) console.warn(‘x-default missing:’, u); } check(‘https://example.com/‘).catch(console.error);

これにより、hreflangの欠落や相互参照ミスを早期に検知できます⁷。

トラフィック保全のための段階リリース

Reverse proxyでパス/クッキーに基づくシャドーリリースとA/Bを併用し、エラーバジェット(例:5xx>0.2%、404>2%)を越えたら即時ロールバックします。

まとめ:順位を守り、速度で勝つリニューアルへ

検索順位の維持は偶然ではなく設計の結果です。URLの1:1移行、メタ/構造化データの継承、CWV最適化、そして切替後72時間の監視が成否を分けます。本稿の10対策とコードをテンプレート化すれば、次回以降のリニューアルは再現性高く進められます。自社の移行計画に照らして、どのチェックが不足しているでしょうか。次のスプリントで「リダイレクト完了率100%」「LCP<2.5s(P75)¹」「404率<2%」の3目標を設定し、CIに計測を組み込みましょう。リニューアルはリスクではなく、検索と収益を伸ばすチャンスに変えられます。

参考文献

  1. Defining Core Web Vitals thresholds — web.dev. https://web.dev/articles/defining-core-web-vitals-thresholds
  2. Page experience for Google Search — developers.google.com. https://developers.google.com/search/docs/appearance/page-experience
  3. Core Web Vitals — developers.google.com. https://developers.google.com/search/docs/appearance/core-web-vitals
  4. To slash or not to slash — developers.google.com (Google Search Central Blog). https://developers.google.com/search/blog/2010/04/to-slash-or-not-to-slash
  5. The ultimate site migration SEO checklist — Search Engine Land. https://searchengineland.com/guide/ultimate-site-migration-seo-checklist
  6. The Sitemap protocol — Google Sitemaps. https://www.google.com/schemas/sitemap/0.84/
  7. Tell Google about localized versions of your page (hreflang, x-default) — developers.google.com. https://developers.google.com/search/docs/specialty/international/localized-versions
  8. Add rel=“alternate” hreflang (HTML/link/header/Sitemaps methods) — developers.google.com. https://developers.google.com/search/docs/specialty/international/localized-versions