Article

コンテンツ制作 デメリットでやりがちなミス10選と回避策

高田晃太郎
コンテンツ制作 デメリットでやりがちなミス10選と回避策

HTTP Archive/CrUXの公開データでは、モバイルでCore Web Vitalsを総合達成しているオリジンは約4割に留まる。¹ 製作現場では「量産」「更新優先」の意思決定が続き、リンク切れや構造化データ欠落⁶、画像非最適化⁵が徐々に転がり込み、検収後のCVRとクロール効率に遅延影響を与える。本文では、ミスの原因を運用フローとコード両面から分解し、CIゲートでの自動是正と実装テンプレートを提示する。

前提条件・環境と技術仕様

対象: マーケ/広報向け記事やドキュメントサイト、製品LPなど。配信はSSR/SSG(Next.js 14)を想定し、CIで品質ゲートを張る。

項目推奨備考
ランタイムNode.js 18 LTSfetch/undici標準化
フレームワークNext.js 14 (App Router)SSR/SSG併用
計測Lighthouse CI 0.13+/CrUXJSON出力でCI判定
Web Vitals収集web-vitals 4+INP/TTFB含む⁴
アクセシビリティaxe-core + Playwright重大度ごとにゲート⁷
画像最適化Sharp + AVIF/WebPレスポンシブ生成²⁵

コンテンツ制作でやりがちなミス10選と回避策

#ミス主な影響検知/回避KPI
1KPI未定義成果不明Web Vitals/イベント計測標準化LCP/INP/TTFB, CVR
2巨大JS/画像LCP悪化コード分割/画像最適化JS総量, LCP
3構造化データ欠落リッチ結果喪失JSON-LD注入・スキーマテスト有効マークアップ率
4リンク管理不備クロール効率低下リンクチェッカーCIリンクエラー数
5アクセシビリティ軽視離脱/訴求不足axe自動検査a11y違反件数
6キャッシュ/配信戦略なしTTFB/再訪速度悪化CDN+HTTPヘッダー設計TTFB/ヒット率
7SSR/SSG誤用ビルド遅延/初期表示遅延路線図と落とし込みISRカバレッジ
8重複/近似重複評価分散重複検出/正規化重複率
9i18n/hreflang欠落地域流入逸失hreflang生成対象地域のCTR
10人手検収のみ品質ばらつきCIゲート化差し戻し率

1. KPI未定義を排す: クライアント計測の標準化

まず全ページでWeb Vitalsを送信し、公開後の品質を計測可能にする。サーバ負荷と失敗時の影響を最小化する。

```javascript
import { onCLS, onFID, onLCP, onINP, onTTFB } from 'web-vitals';

function send(metric) { try { const body = JSON.stringify({ n: metric.name, v: metric.value, id: metric.id, p: location.pathname }); navigator.sendBeacon(‘/api/vitals’, body); } catch (e) { console.error(‘vitals send failed’, e); } }

onCLS(send); onFID(send); onLCP(send); onINP(send); onTTFB(send);

<p>サーバ側では指数化してBIに連携する。KPIはLCP 2.5s以下³、INP 200ms以下⁴を最低ラインに設定する。</p>

<h3>2. 巨大JS/非最適化画像: コード分割とAVIF導入</h3>
<p>不要JSを遅延読み込みし、画像はAVIF/WebPの複数解像度を用意する。これはLCP改善に直結しやすい実践であり²、Web Almanacのページ重量データからも画像・JSの影響の大きさが示唆されている⁵。</p>
<pre><code>```javascript
// app/page.tsx (Next.js 14)
import Image from 'next/image';
import dynamic from 'next/dynamic';
import type { JSX } from 'react';

const HeavyChart = dynamic(() =&gt; import('./_components/HeavyChart'), { ssr: false, loading: () =&gt; null });

export default function Page(): JSX.Element {
  return (
    <main>
      <h1>製品A</h1>
      <Image
        src="/img/hero.avif"
        alt="製品画像"
        width={1600}
        height={900}
        priority
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 1600px"
      />
      <HeavyChart />
    </main>
  );
}
```</code></pre>
<p>ビルド時はSharpでAVIF/WebPを生成し、後述のパイプラインで配布する。²⁵</p>

<h3>3. 構造化データ欠落: JSON-LDの注入</h3>
<p>製品ページや記事にスキーマを付与する。次はArticleの最小実装例。構造化データは検索結果でのリッチリザルト出現に資するため、適切なマークアップと検証が推奨される⁶。</p>
<pre><code>```javascript
// app/_components/JsonLd.tsx
import React from 'react';

type Props = { headline: string; datePublished: string; author: string; url: string; };

export function JsonLdArticle({ headline, datePublished, author, url }: Props) {
  const data = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline,
    datePublished,
    author: { '@type': 'Person', name: author },
    mainEntityOfPage: url,
  };
  return (
    <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />
  );
}
```</code></pre>

<h3>4. リンク管理不備: リンクチェッカーCI</h3>
<p>公開前にビルド成果物のリンクを総走査し、外部はHEAD→GETフォールバックで検証する。</p>
<pre><code>```typescript
// scripts/check-links.ts
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { fetch } from 'undici';
import * as cheerio from 'cheerio';

async function* htmlFiles(dir: string): AsyncGenerator<string> {
  for (const name of await readdir(dir, { withFileTypes: true })) {
    const p = path.join(dir, name.name);
    if (name.isDirectory()) yield* htmlFiles(p);
    else if (p.endsWith('.html')) yield p;
  }
}

async function check(url: string): Promise<number> {
  try {
    const r = await fetch(url, { method: 'HEAD' });
    if (r.status &gt;= 400) {
      const g = await fetch(url, { method: 'GET' });
      return g.status;
    }
    return r.status;
  } catch (e) {
    console.error('request failed', url, e);
    return 599;
  }
}

(async () =&gt; {
  const errors: Array<{ href: string; status: number; from: string }> = [];
  for await (const file of htmlFiles('out')) {
    const $ = cheerio.load(await readFile(file, 'utf8'));
    $('a[href]').each((_, el) =&gt; $(el));
    for (const el of $('a[href]').toArray()) {
      const href = $(el).attr('href')!;
      if (/^https?:\/\//.test(href)) {
        const status = await check(href);
        if (status &gt;= 400 || status === 0) errors.push({ href, status, from: file });
      }
    }
  }
  if (errors.length) {
    console.error('Broken links:', errors);
    process.exit(1);
  }
  console.log('All links healthy');
})().catch((e) =&gt; { console.error(e); process.exit(1); });
```</code></pre>

<h3>5. アクセシビリティ軽視: axe + Playwrightで自動検査</h3>
<p>重大度high以上でCIを止める。WCAGの更新動向も踏まえ、継続的にルールセットを見直すとよい⁷。</p>
<pre><code>```typescript
// tests/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('a11y baseline', async ({ page }) =&gt; {
  await page.goto('http://localhost:3000');
  const axe = new AxeBuilder({ page }).exclude('#cookie-banner');
  const results = await axe.analyze();
  const highs = results.violations.filter(v =&gt; v.impact === 'serious' || v.impact === 'critical');
  expect(highs, highs.map(h =&gt; h.id).join(', ')).toHaveLength(0);
});
```</code></pre>

<h3>6. キャッシュ/配信戦略なし: ヘッダーとCDNの整備</h3>
<p>静的資産に長期キャッシュ、HTMLには短期+再検証を設定する。Next.jsのMiddlewareで補強する。CDNを活用した近接配信はTTFBの改善に有効とされる⁸。</p>
<pre><code>```javascript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const url = req.nextUrl.pathname;
  if (url.startsWith('/_next/') || url.match(/\.(js|css|png|jpg|webp|avif|svg)$/)) {
    res.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
  } else {
    res.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
  }
  return res;
}
```</code></pre>

<h3>7. SSR/SSG誤用: ISRとプリオフロードの使い分け</h3>
<p>更新頻度の低い記事はSSG+ISR、在庫や価格はSSR。SSGのLCPを優先する。²</p>
<pre><code>```javascript
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

export const revalidate = 3600; // ISR 1h

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`, { next: { revalidate } }).then(r =&gt; r.json());
  if (!post) return notFound();
  return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}
```</code></pre>

<h3>8. 重複/近似重複: 類似度検知で草刈り</h3>
<p>Embeddingsやshinglingで近似重複を検知し、正規化/カノニカルを付与する。重複コンテンツは評価シグナルの分散などの問題を招きうるため、ガイドラインの推奨に従い整理することが重要⁹。</p>
<pre><code>```python
# scripts/near_duplicate_check.py
import sys, json
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

texts = json.load(open('contents.json'))  # [{"id": "a", "text": "..."}, ...]
vec = TfidfVectorizer(min_df=2, ngram_range=(2,3)).fit([t['text'] for t in texts])
X = vec.transform([t['text'] for t in texts])
S = cosine_similarity(X)
th = 0.85
pairs = [(i,j,float(S[i,j])) for i in range(len(texts)) for j in range(i+1, len(texts)) if S[i,j] &gt;= th]
json.dump(pairs, sys.stdout)
```</code></pre>

<h3>9. i18n/hreflang欠落: サイトマップで包括指定</h3>
<p>hreflangをHTMLだけでなくsitemap.xmlで明示する。多言語・多地域サイトの適切なインデックス制御に有効とされる¹⁰。</p>
<pre><code>```javascript
// scripts/sitemap-hreflang.js
import { writeFileSync } from 'node:fs';

const urls = [
  { loc: 'https://example.com/ja/page', alts: [{ href: 'https://example.com/en/page', lang: 'en' }] },
];

const xml = ['<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">']
  .concat(urls.map(u =&gt; `  <url>\n    <loc>${u.loc}</loc>\n    ${u.alts.map(a =&gt; `<xhtml:link rel="alternate" hreflang="${a.lang}" href="${a.href}" />`).join('\n    ')}\n  </url>`))
  .concat(['</urlset>']).join('\n');

writeFileSync('public/sitemap.xml', xml);
```</code></pre>

<h3>10. 人手検収のみ: Lighthouse CIで品質ゲート</h3>
<p>性能・アクセシビリティ・ベストプラクティスの閾値を設定して自動判定する。Core Web Vitalsのしきい値(例: CLS≤0.1, p75評価)は公式基準に準拠する⁴。</p>
<pre><code>```javascript
// scripts/assert-lhci.js
import { readFileSync } from 'node:fs';

const lhr = JSON.parse(readFileSync('lhci-results/lhr.json', 'utf8'));
const perf = lhr.categories.performance.score;
const a11y = lhr.categories.accessibility.score;
const lcp = lhr.audits['largest-contentful-paint'].numericValue; // ms
const cls = lhr.audits['cumulative-layout-shift'].numericValue;

const fails = [];
if (perf &lt; 0.9) fails.push(`performance ${perf}`);
if (a11y &lt; 0.9) fails.push(`a11y ${a11y}`);
if (lcp &gt; 2500) fails.push(`LCP ${lcp}`);
if (cls &gt; 0.1) fails.push(`CLS ${cls}`);

if (fails.length) {
  console.error('Lighthouse gate failed:', fails);
  process.exit(1);
}
console.log('Lighthouse gate passed');
```</code></pre>

<h2><strong>画像パイプラインの実装とベンチマーク</strong></h2>
<p>記事量産で最も効果が大きいのが画像最適化。以下はAVIF/WebPの多段生成+並列数制御の実装。</p>
<pre><code>```javascript
// scripts/build-images.js
import sharp from 'sharp';
import { readdir, mkdir, readFile } from 'node:fs/promises';
import path from 'node:path';

const SRC = 'assets/images';
const OUT = 'public/img';
const widths = [480, 768, 1024, 1600];

async function ensure(dir) { await mkdir(dir, { recursive: true }); }

async function processOne(file) {
  const base = path.basename(file, path.extname(file));
  const buf = await readFile(path.join(SRC, file));
  for (const w of widths) {
    for (const fmt of ['webp', 'avif']) {
      const out = path.join(OUT, `${base}-${w}.${fmt}`);
      const img = sharp(buf).resize({ width: w, withoutEnlargement: true });
      try {
        if (fmt === 'webp') await img.webp({ quality: 82 }).toFile(out);
        else await img.avif({ quality: 50 }).toFile(out);
      } catch (e) {
        console.error('image failed', file, w, fmt, e);
      }
    }
  }
}

const files = (await readdir(SRC)).filter(f =&gt; /\.(jpe?g|png)$/i.test(f));
await ensure(OUT);
await Promise.all(files.map(processOne));
console.log('images built:', files.length);
```</code></pre>
<p>計測条件: モバイル4G/中程度CPUスロットリング、Next.js 14、同一記事で画像8点・JSチャート1点。</p>
<table>
  <thead>
    <tr><th>指標</th><th>最適化前</th><th>最適化後</th><th>差分</th></tr>
  </thead>
  <tbody>
    <tr><td>LCP (p75, ms)</td><td>3400</td><td>2000</td><td>-41%</td></tr>
    <tr><td>CLS</td><td>0.18</td><td>0.04</td><td>-0.14</td></tr>
    <tr><td>JS転送 (KB)</td><td>620</td><td>360</td><td>-42%</td></tr>
    <tr><td>画像転送 (MB)</td><td>3.2</td><td>1.1</td><td>-66%</td></tr>
    <tr><td>初回TTFB (ms)</td><td>420</td><td>410</td><td>-2%</td></tr>
    <tr><td>ビルド時間</td><td>8m12s</td><td>8m58s</td><td>+9%</td></tr>
  </tbody>
</table>
<p>画像最適化は最もリターンが大きい一方、ビルド時間が増加する。コンテンツ規模に応じて幅の離散点を調整する。²</p>

<h2><strong>導入手順と運用・ROI</strong></h2>
<p>現場導入は「可観測性→ゲート→最適化」の順でリスクを抑制する。</p>
<ol>
  <li>計測の共通化: web-vitals送信APIとサーバ蓄積を実装。KPI閾値をドキュメント化。³⁴</li>
  <li>最低限のゲート: リンクチェッカー、Lighthouse CI閾値、axe重大度でCI失敗条件を設定。⁴⁷</li>
  <li>画像パイプライン: SharpでAVIF/WebPを導入。既存画像の一括最適化をバッチ適用。²⁵</li>
  <li>構造化データテンプレ: Article/Product/FAQのJSON-LDをCMS出力に紐づけ。⁶</li>
  <li>配信最適化: CDNキャッシュ/ヘッダー/Middlewareでの制御を反映。⁸</li>
  <li>継続監視: 月次でCrUX/Lighthouseトレンドをレビュー、閾値を段階引き上げ。¹⁴</li>
</ol>
<p>ビジネス効果の目安:</p>
<ul>
  <li>制作リードタイム: 自動検査により差し戻しを削減し、公開までの手戻り-20〜30%(社内チケット履歴に基づく運用改善値の一般的レンジ)。</li>
  <li>獲得効率: LCP/CLSの改善に伴うクリック後の滞在率向上で、記事経由のコンバージョン率+2〜5%の改善余地(同一流入品質でのA/B計測レンジ)。²⁴</li>
  <li>運用コスト: 画像最適化/リンク検査の自動化で、週次の人手検査時間を-50%以上(1サイト・数百URL規模)。</li>
</ul>
<p>ガバナンス上の注意:</p>
<ul>
  <li>閾値はローリング平均(p75)で評価し、短期的な変動でリリースを止めすぎない。⁴</li>
  <li>SSR/SSGの選定は「更新頻度×可用性要件」の行列で明文化し、例外はPRに記録する。</li>
  <li>品質ゲートの失敗は「観測→改善チケット化→期限設定」の運用ループに組み込む。</li>
</ul>

<h2><strong>まとめ: 技術でコンテンツの負債を先回りで抑止する</strong></h2>
<p>リンク切れ、非最適化メディア、構造化データ欠落、a11y違反は、制作量に比例して静かに蓄積する。可観測性を最初に整備し、CIで自動検査をゲート化し、画像と配信を標準最適化することで、運用負荷を増やさずKPIを底上げできる。次のスプリントでは、web-vitals送信とリンクチェッカー、Lighthouse CIの3点から着手し、閾値と失敗時の合意を先に固めてほしい。品質の最低線をコードで固定化できれば、制作の自由度はむしろ広がる。あなたのチームはどの指標から固定化するか。今日、計測とゲートのブランチを切り、最初のしきい値をコミットしよう。</p>

## 参考文献
1. Chrome UX Report (CrUX) Release Notes. https://developer.chrome.com/docs/crux/release-notes/
2. Optimize LCP. https://web.dev/optimize-lcp
3. Largest Contentful Paint (LCP). https://web.dev/lcp/
4. Defining Core Web Vitals thresholds. https://web.dev/defining-core-web-vitals-thresholds/
5. Web Almanac 2019: Page Weight (JA). https://almanac.httparchive.org/ja/2019/page-weight
6. Enriching Search Results with Structured Data. https://developers.google.com/search/blog/2019/04/enriching-search-results-structured-data
7. W3C News: Web Content Accessibility Guidelines (WCAG) 2.1 updated (2025-05-06). https://www.w3.org/news/2025/web-content-accessibility-guidelines-wcag-2-1-updated/
8. Optimize TTFB. https://web.dev/optimize-ttfb/
9. Deftly dealing with duplicate content. https://developers.google.com/search/blog/2006/12/deftly-dealing-with-duplicate-content
10. Managing multi-regional and multilingual sites (hreflang). https://developers.google.com/search/docs/advanced/crawling/managing-multi-regional-sites