niki-til

<h1 class="admin-page-title">記事マップ</h1>

<div class="admin-summary-bar"> <div class="summary-stat"> <span class="summary-num">{{ site.data.articles.total_articles | default: 0 }}</span> <span class="summary-lbl">総記事数</span> </div> <div class="summary-stat"> <span class="summary-num" id="max-depth-val">—</span> <span class="summary-lbl">最大階層深さ</span> </div> <div class="summary-stat"> <span class="summary-num">{{ site.data.articles.orphans | size }}</span> <span class="summary-lbl">孤立記事</span> </div> </div>

<script id="articles-data" type="application/json">{{ site.data.articles | jsonify }}</script> <script id="taxonomy-data" type="application/json">{{ site.data.taxonomy | jsonify }}</script>

<div id="articles-tree-root"></div>

{% if site.data.articles.orphans and site.data.articles.orphans.size > 0 %} <h2 class="admin-section-title">孤立記事</h2> <div class="admin-table-wrap"> <table class="admin-table"> <thead> <tr><th>記事</th><th>カテゴリ</th></tr> </thead> <tbody> {% for orphan in site.data.articles.orphans %} <tr> <td><a href="{{ orphan.url }}">{{ orphan.title | default: orphan.path }}</a></td> <td><span class="badge badge--cat">{{ orphan.category | default: "—" }}</span></td> </tr> {% endfor %} </tbody> </table> </div> {% endif %}

<script defer> (function() { var rawArticles = document.getElementById('articles-data'); var rawTax = document.getElementById('taxonomy-data'); if (!rawArticles) return;

var data, tax; try { data = JSON.parse(rawArticles.textContent); tax = JSON.parse(rawTax.textContent); } catch(e) { return; }

var tree = data.tree_by_category || {}; var cats = (tax.categories || []);

// compute max depth var maxD = 0; (data.articles || []).forEach(function(a) { if ((a.depth || 0) > maxD) maxD = a.depth; }); var mdEl = document.getElementById('max-depth-val'); if (mdEl) mdEl.textContent = maxD;

var catMap = {}; cats.forEach(function(c) { catMap[c.id] = c; });

function escHtml(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }

var baseUrl = '{{ site.baseurl }}';

function renderNode(node, isLast, prefix) { var connector = isLast ? '└─ ' : '├─ '; var childPrefix = prefix + (isLast ? ' ' : '│ ');

// find article data for tags var art = (data.articles || []).find(function(a){ return a.path === node.path; }) || {}; var tags = (art.tags || []).slice(0, 3); // art.url already includes baseurl (e.g. /til/mind/cognition-basics/); fallback construct it var url = node.url || art.url || (baseUrl + '/' + node.path.replace(/.md$/, '/'));

var tagsHtml = tags.map(function(t){ return '<span class="tag-chip">' + escHtml(t) + '</span>'; }).join('');

var html = '<div class="tree-node">'; html += '<span class="tree-indent">' + escHtml(prefix + connector) + '</span>'; html += '<a class="tree-title" href="' + escHtml(url) + '" title="' + escHtml(node.title) + '">' + escHtml(node.title) + '</a>'; if (tagsHtml) html += '<span class="tree-tags">' + tagsHtml + '</span>'; html += '<a class="tree-link" href="' + escHtml(url) + '" aria-label="' + escHtml(node.title) + ' を開く">開く</a>'; html += '</div>';

var children = node.children || []; children.forEach(function(child, idx) { html += renderNode(child, idx === children.length - 1, childPrefix); });

return html; }

var root = document.getElementById('articles-tree-root'); if (!root) return;

var catIds = Object.keys(tree); var html = '';

catIds.forEach(function(catId) { var nodes = tree[catId] || []; if (nodes.length === 0) return; var catInfo = catMap[catId] || {}; var catName = catInfo.name_jp || catId; var catCount = catInfo.article_count || nodes.length;

html += '<details class="tree-cat" open>'; html += '<summary class="tree-category-header">'; html += '<span class="cat-name">' + escHtml(catName) + '</span>'; html += '<span class="cat-count">' + catCount + ' 件</span>'; html += '</summary>'; html += '<div class="article-tree">'; nodes.forEach(function(node, idx) { html += renderNode(node, idx === nodes.length - 1, ''); }); html += '</div>'; html += '</details>'; });

root.innerHTML = html; })(); </script>