<h1 class="admin-page-title">SEO ダッシュボード</h1> <p class="admin-page-meta">最終更新: {{ site.data.seo.generated_at | default: "—" }}</p>
<script id="seo-data" type="application/json">{{ site.data.seo | jsonify }}</script>
{% assign s7 = site.data.seo.summary["7d"] %} {% assign s30 = site.data.seo.summary["30d"] %}
<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>表示回数</td> <td class="td-mono">{{ s7.impressions | default: "0" }}</td> <td class="td-mono">{{ s30.impressions | default: "0" }}</td> </tr> <tr> <td>クリック数</td> <td class="td-mono">{{ s7.clicks | default: "0" }}</td> <td class="td-mono">{{ s30.clicks | default: "0" }}</td> </tr> <tr> <td>平均順位</td> <td class="td-mono">{% if s7.avg_position and s7.avg_position != 0 %}{{ s7.avg_position | round: 1 }}{% else %}—{% endif %}</td> <td class="td-mono">{% if s30.avg_position and s30.avg_position != 0 %}{{ s30.avg_position | round: 1 }}{% else %}—{% endif %}</td> </tr> <tr> <td>CTR</td> <td class="td-mono">{% if s7.avg_ctr and s7.avg_ctr != 0 %}{{ s7.avg_ctr | times: 100 | round: 2 }}%{% else %}—{% endif %}</td> <td class="td-mono">{% if s30.avg_ctr and s30.avg_ctr != 0 %}{{ s30.avg_ctr | times: 100 | round: 2 }}%{% else %}—{% endif %}</td> </tr> </tbody> </table> </div>
<h2 class="admin-section-title">日次推移チャート(インプレッション / クリック)</h2> <div class="chart-wrap"> <p class="chart-title">表示回数 & クリック数</p> <canvas id="chart-daily-impr" aria-label="日次インプレッションとクリック数チャート" role="img"></canvas> </div>
<h2 class="admin-section-title">日次推移チャート(平均順位)</h2> <div class="chart-wrap"> <p class="chart-title">平均掲載順位(低い値 = 上位)</p> <canvas id="chart-daily-pos" aria-label="日次平均順位チャート" role="img"></canvas> </div>
<h2 class="admin-section-title">記事別データ</h2> <p style="font-size:12px;color:var(--color-fg-muted);margin-bottom:1rem;">30日間インプレッション降順。行クリックで直近 28 日の順位推移を展開します。</p>
{% assign per = site.data.seo.per_article %} {% if per and per.size > 0 %} <div class="admin-table-wrap"> <table class="admin-table" id="seo-article-table"> <thead> <tr> <th>記事パス</th> <th>7日 表示</th> <th>7日 クリック</th> <th>7日 順位</th> <th>30日 順位</th> <th>順位変化</th> </tr> </thead> <tbody> {% assign sorted_per = per | sort: "impressions_30d" | reverse %} {% for art in sorted_per %} <tr class="seo-row" data-idx="{{ forloop.index0 }}" style="cursor:pointer;" aria-expanded="false"> <td class="td-mono" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;" title="{{ art.page_path_guess | default: art.page_url }}">{{ art.page_path_guess | default: art.page_url | default: "—" }}</td> <td class="td-mono">{{ art.impressions_7d | default: "0" }}</td> <td class="td-mono">{{ art.clicks_7d | default: "0" }}</td> <td class="td-mono">{% if art.position_7d_avg and art.position_7d_avg != 0 %}{{ art.position_7d_avg | round: 1 }}{% else %}—{% endif %}</td> <td class="td-mono">{% if art.position_30d_avg and art.position_30d_avg != 0 %}{{ art.position_30d_avg | round: 1 }}{% else %}—{% endif %}</td> <td> {% assign rc = art.rank_change_7d_vs_prev_7d %} {% if rc == null or rc == 0 %} <span class="badge badge--flat">—</span> {% elsif rc < 0 %} <span class="badge badge--up">+{{ rc | abs }} 改善</span> {% else %} <span class="badge badge--down">-{{ rc }} 低下</span> {% endif %} </td> </tr> <tr class="expand-row" id="expand-{{ forloop.index0 }}"> <td class="expand-cell" colspan="6"> <div class="inline-chart-wrap"> <canvas id="pos-chart-{{ forloop.index0 }}" height="120" aria-label="{{ art.page_path_guess | default: art.page_url }} の順位推移" role="img"></canvas> </div> </td> </tr> {% endfor %} </tbody> </table> </div> {% else %} <div class="admin-empty"> データ蓄積待ち(GSC プロパティ検証直後)。<br> しばらく経ってから <code>npm run seo:build</code> を再実行してください。 </div> {% endif %}
<script defer> (function() { var rawSeo = document.getElementById('seo-data'); if (!rawSeo) return; var seo; try { seo = JSON.parse(rawSeo.textContent); } catch(e) { return; }
var daily = seo.site_daily || []; var perArt = seo.per_article || [];
var chartOpts = { responsive: true, maintainAspectRatio: true, interaction: { mode: 'index', intersect: false }, plugins: { legend: { position: 'bottom', labels: { font: { family: 'Inter', size: 11 } } } }, scales: { x: { ticks: { font: { family: 'Inter', size: 10 }, maxRotation: 45 }, grid: { color: 'rgba(0,0,0,0.04)' } }, y: { ticks: { font: { family: 'Inter', size: 11 } }, grid: { color: 'rgba(0,0,0,0.04)' }, beginAtZero: true } } };
// Wait for Chart.js to load function whenChart(fn) { if (typeof Chart !== 'undefined') { fn(); return; } document.addEventListener('DOMContentLoaded', function() { var t = setInterval(function() { if (typeof Chart !== 'undefined') { clearInterval(t); fn(); } }, 100); }); }
whenChart(function() { // Daily impressions + clicks var elImpr = document.getElementById('chart-daily-impr'); if (elImpr && daily.length) { new Chart(elImpr, { type: 'line', data: { labels: daily.map(function(d){ return d.date; }), datasets: [ { label: '表示回数', data: daily.map(function(d){ return d.impressions||0; }), borderColor: '#A1BAEC', backgroundColor: 'rgba(161,186,236,0.08)', tension: 0.3, pointRadius: 2, yAxisID: 'y' }, { label: 'クリック数', data: daily.map(function(d){ return d.clicks||0; }), borderColor: '#B22E20', backgroundColor: 'rgba(178,46,32,0.06)', tension: 0.3, pointRadius: 2, yAxisID: 'y1' } ] }, options: Object.assign({}, chartOpts, { scales: Object.assign({}, chartOpts.scales, { y: { position: 'left', title: { display: true, text: '表示回数', font: { size: 10 } }, ticks: { font: { size: 10 } }, beginAtZero: true }, y1: { position: 'right', title: { display: true, text: 'クリック', font: { size: 10 } }, ticks: { font: { size: 10 } }, beginAtZero: true, grid: { drawOnChartArea: false } } }) }) }); } else if (elImpr) { elImpr.parentElement.insertAdjacentHTML('beforeend', '<p style="font-size:12px;color:var(--color-fg-muted);">日次データ蓄積待ち。</p>'); }
// Daily position (Y-axis reversed) var elPos = document.getElementById('chart-daily-pos'); if (elPos && daily.length) { new Chart(elPos, { type: 'line', data: { labels: daily.map(function(d){ return d.date; }), datasets: [ { label: '平均順位', data: daily.map(function(d){ return d.position||null; }), borderColor: '#57534E', backgroundColor: 'rgba(87,83,78,0.06)', tension: 0.3, pointRadius: 2, spanGaps: false } ] }, options: Object.assign({}, chartOpts, { scales: Object.assign({}, chartOpts.scales, { y: { reverse: true, title: { display: true, text: '順位(低い=上位)', font: { size: 10 } }, ticks: { font: { size: 10 } }, beginAtZero: false } }) }) }); } else if (elPos) { elPos.parentElement.insertAdjacentHTML('beforeend', '<p style="font-size:12px;color:var(--color-fg-muted);">日次データ蓄積待ち。</p>'); }
// Expandable rows var rows = document.querySelectorAll('.seo-row'); var openIdx = null; rows.forEach(function(row) { row.addEventListener('click', function() { var idx = row.getAttribute('data-idx'); var expandRow = document.getElementById('expand-' + idx); if (!expandRow) return;
if (openIdx !== null && openIdx !== idx) { var prevRow = document.getElementById('expand-' + openIdx); if (prevRow) prevRow.classList.remove('is-open'); }
var isOpen = expandRow.classList.contains('is-open'); expandRow.classList.toggle('is-open', !isOpen); row.setAttribute('aria-expanded', String(!isOpen));
if (!isOpen && !expandRow.dataset.rendered) { expandRow.dataset.rendered = '1'; var artData = perArt[parseInt(idx, 10)]; if (!artData) return; var dailyArr = artData.daily || []; var cvs = document.getElementById('pos-chart-' + idx); if (cvs && dailyArr.length) { new Chart(cvs, { type: 'line', data: { labels: dailyArr.map(function(d){ return d.date; }), datasets: [ { label: '順位', data: dailyArr.map(function(d){ return d.position||null; }), borderColor: '#57534E', backgroundColor: 'rgba(87,83,78,0.06)', tension: 0.3, pointRadius: 2, spanGaps: false } ] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { font: { size: 9 }, maxRotation: 45 } }, y: { reverse: true, ticks: { font: { size: 10 } }, beginAtZero: false } } } }); } else if (cvs) { cvs.parentElement.insertAdjacentHTML('beforeend', '<p style="font-size:12px;color:var(--color-fg-muted);">日次データなし。</p>'); } }
openIdx = isOpen ? null : idx; }); }); }); })(); </script>