<h1 class="admin-page-title">GEO ダッシュボード</h1> <p class="admin-page-meta">最終更新: {{ site.data.geo.generated_at | default: "—" }}</p>
<script id="geo-data" type="application/json">{{ site.data.geo | jsonify }}</script>
<div class="geo-notice"> <p class="geo-notice-title">観測限界の注意</p> <ul> {% for note in site.data.geo.notes.limitations_jp %} <li>{{ note }}</li> {% endfor %} </ul> </div>
{% assign s7 = site.data.geo.summary["7d"] %} {% assign s30 = site.data.geo.summary["30d"] %}
<h2 class="admin-section-title">AI 検索流入</h2>
<div class="geo-period-toggle"> <button class="is-active" id="btn-7d" aria-label="7日間を表示">7日間</button> <button id="btn-30d" aria-label="30日間を表示">30日間</button> </div>
<div class="geo-hero"> <div class="geo-big-num"> <div class="geo-big-value" id="geo-ai-share">{{ s7.ai_share_pct | default: "0" }}%</div> <div class="geo-big-label">AI 流入比率</div> </div> <div style="flex:1;min-width:220px;max-width:320px;"> <canvas id="chart-geo-donut" aria-label="流入元内訳ドーナツチャート" role="img"></canvas> </div> </div>
<h2 class="admin-section-title">流入元サマリー</h2>
<div class="admin-table-wrap"> <table class="admin-table"> <thead> <tr> <th>種別</th> <th>7日 セッション</th> <th>30日 セッション</th> </tr> </thead> <tbody> <tr><td>AI 検索</td><td class="td-mono" id="sum-ai-7">{{ s7.ai_search_sessions | default: "0" }}</td><td class="td-mono">{{ s30.ai_search_sessions | default: "0" }}</td></tr> <tr><td>通常検索</td><td class="td-mono">{{ s7.organic_search_sessions | default: "0" }}</td><td class="td-mono">{{ s30.organic_search_sessions | default: "0" }}</td></tr> <tr><td>Direct</td><td class="td-mono">{{ s7.direct_sessions | default: "0" }}</td><td class="td-mono">{{ s30.direct_sessions | default: "0" }}</td></tr> <tr><td>Social</td><td class="td-mono">{{ s7.social_sessions | default: "0" }}</td><td class="td-mono">{{ s30.social_sessions | default: "0" }}</td></tr> <tr><td>Referral</td><td class="td-mono">{{ s7.referral_sessions | default: "0" }}</td><td class="td-mono">{{ s30.referral_sessions | default: "0" }}</td></tr> <tr><td>その他</td><td class="td-mono">{{ s7.other_sessions | default: "0" }}</td><td class="td-mono">{{ s30.other_sessions | default: "0" }}</td></tr> </tbody> </table> </div>
<h2 class="admin-section-title">AI ソース一覧</h2>
{% assign ai_sources = site.data.geo.ai_sources %} {% if ai_sources and ai_sources.size > 0 %} <div class="admin-table-wrap"> <table class="admin-table"> <thead> <tr> <th>名称</th> <th>ソースドメイン</th> <th>7日 セッション</th> <th>30日 セッション</th> <th>7日 ユーザー</th> <th>エンゲージ率</th> <th>平均滞在 (秒)</th> </tr> </thead> <tbody> {% assign sorted_sources = ai_sources | sort: "sessions_7d" | reverse %} {% for src in sorted_sources %} <tr> <td>{{ src.name_jp | default: src.source }}</td> <td class="td-mono">{{ src.source }}</td> <td class="td-mono">{{ src.sessions_7d | default: "0" }}</td> <td class="td-mono">{{ src.sessions_30d | default: "0" }}</td> <td class="td-mono">{{ src.users_7d | default: "0" }}</td> <td class="td-mono">{% if src.engagement_rate_7d %}{{ src.engagement_rate_7d | times: 100 | round: 1 }}%{% else %}—{% endif %}</td> <td class="td-mono">{{ src.avg_session_s_7d | default: "—" }}</td> </tr> {% endfor %} </tbody> </table> </div> {% else %} <div class="admin-empty">AI ソースデータなし。</div> {% endif %}
{% assign per_art = site.data.geo.per_article %} {% if per_art and per_art.size > 0 %} <h2 class="admin-section-title">記事別 AI 流入</h2> <div class="admin-table-wrap"> <table class="admin-table"> <thead> <tr> <th>記事パス</th> <th>7日 AI セッション</th> <th>30日 AI セッション</th> <th>流入元内訳</th> </tr> </thead> <tbody> {% assign sorted_art = per_art | sort: "ai_sessions_7d" | reverse %} {% for a in sorted_art %} <tr> <td class="td-mono">{{ a.page_path_guess | default: a.page_path | default: "—" }}</td> <td class="td-mono">{{ a.ai_sessions_7d | default: "0" }}</td> <td class="td-mono">{{ a.ai_sessions_30d | default: "0" }}</td> <td> {% for src in a.ai_sources_breakdown_7d %} <span class="tag-chip">{{ src.source }}: {{ src.sessions }}</span> {% endfor %} </td> </tr> {% endfor %} </tbody> </table> </div>
<h2 class="admin-section-title">AI 流入トップ記事</h2> <div class="top-articles-grid" id="top-articles-grid"> </div> {% else %} <div class="admin-empty"> 現状 AI 流入セッション 0 件。<br> Cloudflare 前段導入で AI クローラ計測も追加可能です。<br> <small style="color:var(--color-fg-muted);">GA4 参照元ベースの計測のため、referrer を送らない AI クライアントは捕捉できません。</small> </div> {% endif %}
<script defer> (function() { var rawGeo = document.getElementById('geo-data'); if (!rawGeo) return; var geo; try { geo = JSON.parse(rawGeo.textContent); } catch(e) { return; }
var s7 = (geo.summary || {})['7d'] || {}; var s30 = (geo.summary || {})['30d'] || {}; var perArt = geo.per_article || [];
function whenChart(fn) { if (typeof Chart !== 'undefined') { fn(); return; } var t = setInterval(function() { if (typeof Chart !== 'undefined') { clearInterval(t); fn(); } }, 100); }
// Period toggle var btn7 = document.getElementById('btn-7d'); var btn30 = document.getElementById('btn-30d'); var aiShareEl = document.getElementById('geo-ai-share'); var donutChart = null;
function makeDonutData(s) { return { labels: ['AI 検索', '通常検索', 'Direct', 'Social', 'Referral', 'その他'], datasets: [{ data: [ s.ai_search_sessions || 0, s.organic_search_sessions || 0, s.direct_sessions || 0, s.social_sessions || 0, s.referral_sessions || 0, s.other_sessions || 0 ], backgroundColor: ['#A1BAEC','#C8D5F4','#EBE8E4','#E8DCC8','#A59F97','#FAFAF9'], borderWidth: 1, borderColor: '#E5E5E5' }] }; }
whenChart(function() { var cvs = document.getElementById('chart-geo-donut'); if (cvs) { donutChart = new Chart(cvs, { type: 'doughnut', data: makeDonutData(s7), options: { responsive: true, plugins: { legend: { position: 'right', labels: { font: { family: 'Inter', size: 11 }, boxWidth: 12 } } } } }); } });
if (btn7) btn7.addEventListener('click', function() { btn7.classList.add('is-active'); btn30.classList.remove('is-active'); if (aiShareEl) aiShareEl.textContent = (s7.ai_share_pct || 0) + '%'; if (donutChart) { donutChart.data = makeDonutData(s7); donutChart.update(); } }); if (btn30) btn30.addEventListener('click', function() { btn30.classList.add('is-active'); btn7.classList.remove('is-active'); if (aiShareEl) aiShareEl.textContent = (s30.ai_share_pct || 0) + '%'; if (donutChart) { donutChart.data = makeDonutData(s30); donutChart.update(); } });
// Top 3 articles var grid = document.getElementById('top-articles-grid'); if (grid && perArt.length) { var sorted = perArt.slice().sort(function(a,b){ return (b.ai_sessions_7d||0) - (a.ai_sessions_7d||0); }); var top3 = sorted.slice(0, 3); var html = ''; top3.forEach(function(a, i) { var path = a.page_path_guess || a.page_path || '—'; html += '<div class="top-article-card">'; html += '<p class="top-rank">No.' + (i+1) + '</p>'; html += '<p class="top-title">' + path + '</p>'; html += '<div class="top-value">' + (a.ai_sessions_7d||0) + '</div>'; html += '<div class="top-label">7日 AI セッション</div>'; html += '</div>'; }); grid.innerHTML = html; } })(); </script>