0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript + D3.js】SEO/GEO/LLMO/AIO対応度を30問のチェックリストでスコア化するツールを作った手順

0
Posted at

seo-geo-llmo-checker.jpg

この記事でできること

  • GEO/LLMO/AIOのスコアリングロジック(6カテゴリ×各5問、100点満点)の実装手順を再現できる
  • D3.jsのレーダーチャートをWordPress/SWELL環境で動かすための設計パターンを学べる
  • WordPress特有のJS制約(&& 禁止・<script src>無視)への対処法を理解できる

完成形のツールはこちらで公開中: SEO/GEO/LLMOスコアチェッカー(AI Japan Index)


環境・前提

  • JavaScript: ES5互換(const/let使用可、&&禁止はWordPress固有制約)
  • D3.js: v7(動的ロード方式)
  • デプロイ先: WordPress / SWELLテーマ(HTMLブロック埋め込み)
  • バックエンド: なし(フロントエンド完結)
  • 外部API: 使用しない

完成形

6カテゴリ(コンテンツ品質・構造化データ・E-E-A-T・AI引用可能性・鮮度・UX)の30問に「はい/一部/いいえ」で回答すると、100点満点スコア・D3.jsレーダーチャート・A〜Eグレード・改善提案が表示される。動作確認はこちらでできる。


手順

Step 1: データ構造を定義する

まず6カテゴリ30問のデータをJSONとして定義する。各質問は id, categoryId, text, points を持つ。

var QUESTIONS = [
  // カテゴリA: コンテンツ品質(合計25点)
  {
    id: 'A1',
    categoryId: 'content',
    text: 'ページ冒頭200語以内でメインの質問に直接回答しているか',
    points: 5
  },
  {
    id: 'A2',
    categoryId: 'content',
    text: '150〜200語ごとに統計データ(出典付き)を含んでいるか',
    points: 5
  },
  // ... A3, A4, A5

  // カテゴリB: 構造化データ・技術基盤(合計20点)
  {
    id: 'B1',
    categoryId: 'technical',
    text: 'JSON-LD Schema Markup(Article/FAQ/HowTo等)を実装しているか',
    points: 4
  },
  // ... B2〜B5

  // カテゴリC〜F: 同様に定義
];

var CATEGORIES = [
  { id: 'content',    label: 'コンテンツ品質',       maxScore: 25 },
  { id: 'technical',  label: '構造化データ・技術基盤', maxScore: 20 },
  { id: 'eeat',       label: 'E-E-A-T・権威性',       maxScore: 20 },
  { id: 'citation',   label: 'AI引用可能性',           maxScore: 15 },
  { id: 'freshness',  label: '鮮度・更新頻度',         maxScore: 10 },
  { id: 'ux',         label: 'モバイル・UX',           maxScore: 10 }
];

ポイント: && を使う場合はWordPressで破壊されるため、後述のStep 4で対策する。


Step 2: スコア計算ロジックを実装する

3段階回答(はい=1.0、一部=0.5、いいえ=0)でスコアを計算する。

function calcCategoryScore(categoryId, answers) {
  var questions = QUESTIONS.filter(function(q) {
    return q.categoryId === categoryId;
  });

  var score = 0;
  var maxScore = 0;

  questions.forEach(function(q) {
    maxScore += q.points;
    var ans = answers[q.id];
    if (ans === 'yes') {
      score += q.points;
    } else if (ans === 'partial') {
      score += q.points * 0.5;
    }
    // 'no' は 0点(加算なし)
  });

  return { score: score, max: maxScore };
}

function calcTotalScore(answers) {
  var total = 0;
  CATEGORIES.forEach(function(cat) {
    var result = calcCategoryScore(cat.id, answers);
    total += result.score;
  });
  return Math.round(total);
}

Step 3: グレード判定を実装する

function getGrade(totalScore) {
  if (totalScore >= 85) { return { grade: 'A', label: 'AI検索最適化済み' }; }
  if (totalScore >= 70) { return { grade: 'B', label: 'AI対応済み' }; }
  if (totalScore >= 55) { return { grade: 'C', label: '改善必要' }; }
  if (totalScore >= 40) { return { grade: 'D', label: '大幅改善必要' }; }
  return { grade: 'E', label: '未対応' };
}

Step 4: D3.jsをWordPress対応で動的ロードする

つまずきポイント1: <script src="https://cdn.d3js.org/..."> を投稿コンテンツ内に書いても、SWELLテーマが読み込みを無視する。

OK: 動的ロード方式(D3ブートストラップ)

function loadD3AndInit() {
  if (typeof d3 !== 'undefined') {
    initAll();
    return;
  }
  var script = document.createElement('script');
  script.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
  script.onload = function() { initAll(); };
  document.head.appendChild(script);
}

document.addEventListener('DOMContentLoaded', function() {
  loadD3AndInit();
});

つまずきポイント2: && 演算子の禁止。WordPressのwpautopフィルターが &&&#038;&#038; にHTMLエンティティ変換し、JSが構文エラーで停止する。

// NG: WordPressで破壊される
if (a && b) { doSomething(); }

// OK: ネストifで書き換え
if (a) {
  if (b) { doSomething(); }
}

Step 5: D3.jsレーダーチャートを描画する

function drawRadar(svgEl, categoryScores) {
  var n = CATEGORIES.length;
  var angleSlice = (Math.PI * 2) / n;
  var radius = 120; // レーダー半径(px)

  var g = d3.select(svgEl)
    .append('g')
    .attr('transform', 'translate(' + (svgEl.clientWidth / 2) + ',' + (svgEl.clientHeight / 2) + ')');

  // 背景グリッド線(25/50/75/100のレベル)
  [0.25, 0.5, 0.75, 1.0].forEach(function(level) {
    var points = CATEGORIES.map(function(cat, i) {
      var angle = angleSlice * i - Math.PI / 2;
      return [
        Math.cos(angle) * radius * level,
        Math.sin(angle) * radius * level
      ];
    });
    g.append('polygon')
      .attr('points', points.map(function(p) { return p[0] + ',' + p[1]; }).join(' '))
      .attr('fill', 'none')
      .attr('stroke', '#2a2a4a')
      .attr('stroke-width', 1);
  });

  // 軸線
  CATEGORIES.forEach(function(cat, i) {
    var angle = angleSlice * i - Math.PI / 2;
    g.append('line')
      .attr('x1', 0).attr('y1', 0)
      .attr('x2', Math.cos(angle) * radius)
      .attr('y2', Math.sin(angle) * radius)
      .attr('stroke', '#2a2a4a')
      .attr('stroke-width', 1);
  });

  // スコアの多角形
  var dataPoints = CATEGORIES.map(function(cat, i) {
    var angle = angleSlice * i - Math.PI / 2;
    var r = (categoryScores[cat.id].score / categoryScores[cat.id].max) * radius;
    return [Math.cos(angle) * r, Math.sin(angle) * r];
  });

  g.append('polygon')
    .attr('points', dataPoints.map(function(p) { return p[0] + ',' + p[1]; }).join(' '))
    .attr('fill', 'rgba(147, 105, 168, 0.3)')
    .attr('stroke', '#9369a8')
    .attr('stroke-width', 2);
}

Step 6: カテゴリラベルをHTMLで配置する

つまずきポイント3: SVGの <text> でカテゴリ名(日本語6〜8文字)を描くと、スマホ幅で見切れる。

OK: HTMLのdivをposition absoluteで配置する

function placeRadarLabels(containerEl, svgEl, radius) {
  var n = CATEGORIES.length;
  var angleSlice = (Math.PI * 2) / n;
  var svgRect = svgEl.getBoundingClientRect();
  var containerRect = containerEl.getBoundingClientRect();
  var cx = svgRect.left - containerRect.left + svgEl.clientWidth / 2;
  var cy = svgRect.top - containerRect.top + svgEl.clientHeight / 2;

  // 既存ラベルをクリア
  var existing = containerEl.querySelectorAll('.radar-label');
  existing.forEach(function(el) { el.remove(); });

  CATEGORIES.forEach(function(cat, i) {
    var angle = angleSlice * i - Math.PI / 2;
    var x = cx + Math.cos(angle) * (radius + 28);
    var y = cy + Math.sin(angle) * (radius + 28);

    var label = document.createElement('div');
    label.className = 'radar-label';
    label.textContent = cat.label;
    label.style.cssText = [
      'position:absolute',
      'left:' + x + 'px',
      'top:' + y + 'px',
      'transform:translate(-50%,-50%)',
      'font-size:11px',
      'color:#a7a9be',
      'white-space:nowrap',
      'max-width:80px',
      'word-break:keep-all',
      'overflow-wrap:break-word',
      'text-align:center'
    ].join(';');
    containerEl.appendChild(label);
  });
}

Step 7: 動作確認

  1. ブラウザで開き、30問全てに「はい」と回答 → スコアが100点になるか確認する
  2. 全て「いいえ」と回答 → スコアが0点・グレードEになるか確認する
  3. スマホ幅(375px)でカテゴリラベルが枠内に収まっているか確認する
  4. WordPress環境にデプロイ後、DevToolsのConsoleにJSエラーが出ていないか確認する

つまずきポイントまとめ

  • <script src="CDN">がWordPress/SWELLで無視される: document.createElement('script') での動的ロードに変更する
  • && がWordPressに破壊される: ネストifまたは三項演算子に書き換える(全JSファイルでgrep必須)
  • SVGテキストがスマホで見切れる: 日本語5文字超のラベルはHTMLのdivで代替する
  • <!-- wp:html --> ブロックを使う: フルHTMLドキュメント(DOCTYPE)形式だとwpautopがscriptタグ内に<p>を挿入してJSが壊れる

まとめ

今回学んだこと:

  • D3.jsレーダーチャートの基本実装(グリッド・軸線・データポリゴン・ラベル配置)
  • WordPress/SWELL固有の3つのJS制約とその回避パターン
  • 6カテゴリスコアリングの配点設計における一次ソース(Princeton GEO研究等)の使い方
  • フロントエンド完結でセルフ診断ツールを作る場合の設計トレードオフ

関連ツール・データ: AI Japan Index — 全ツール一覧

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?