Files
igny8/v2/Igny8 V2 New Final Plans/Linker Internal & External/IGNY8-Linker-Module-Visualization.html
IGNY8 VPS (Salman) 128b186865 temproary docs uplaoded
2026-03-23 09:02:49 +00:00

579 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1200">
<title>IGNY8 Linker Module — SAG Authority Flow</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0e1a;
--surface: #111827;
--surface-light: #1a2236;
--border: #2a3654;
--text: #e2e8f0;
--text-muted: #94a3b8;
--text-dim: #64748b;
--homepage: #f59e0b;
--hub: #3b82f6;
--blog: #10b981;
--product: #ec4899;
--service: #8b5cf6;
--term: #06b6d4;
--category: #f97316;
--brand: #ef4444;
--comparison: #14b8a6;
--vertical-up: #fbbf24;
--vertical-down: #60a5fa;
--sibling: #34d399;
--cross-cluster: #f472b6;
--taxonomy-ctx: #22d3ee;
--breadcrumb: #a78bfa;
--related: #fb923c;
--ext-t1: #fcd34d;
--ext-t2: #93c5fd;
--ext-t3: #a5b4fc;
--ext-t4: #86efac;
--ext-t5: #fca5a5;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Outfit', sans-serif;
overflow-x: hidden;
min-height: 100vh;
}
.header {
border-bottom: 1px solid var(--border);
padding: 14px 24px;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(180deg, var(--surface-light) 0%, var(--bg) 100%);
}
.header-left { display: flex; align-items: center; gap: 14px; }
.logo {
width: 36px; height: 36px; border-radius: 10px;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
display: flex; align-items: center; justify-content: center;
font-size: 16px; font-weight: 800; color: #fff;
}
.header-title { font-size: 16px; font-weight: 700; letter-spacing: -0.3px; }
.header-sub { font-size: 11px; color: var(--text-dim); font-family: 'JetBrains Mono', monospace; }
.view-toggle {
display: flex; gap: 4px; background: var(--surface); border-radius: 8px; padding: 3px;
}
.view-btn {
padding: 6px 14px; border-radius: 6px; border: none;
background: transparent; color: var(--text-dim);
font-size: 11px; font-weight: 600; cursor: pointer;
font-family: 'Outfit', sans-serif; text-transform: uppercase; letter-spacing: 0.5px;
transition: all 0.2s;
}
.view-btn.active {
background: linear-gradient(135deg, #3b82f6, #6366f1); color: #fff;
}
.layout { display: flex; height: calc(100vh - 65px); }
.sidebar {
width: 220px; border-right: 1px solid var(--border);
padding: 16px; overflow-y: auto; background: var(--surface);
flex-shrink: 0;
}
.sidebar-label {
font-size: 9px; font-weight: 700; color: var(--text-dim);
letter-spacing: 1.5px; margin-bottom: 10px; margin-top: 4px;
}
.filter-btn {
display: flex; align-items: center; gap: 8px; width: 100%;
padding: 7px 10px; margin-bottom: 3px; border-radius: 6px;
border: 1px solid transparent; background: transparent;
color: var(--text-muted); font-size: 10px; font-weight: 500;
cursor: pointer; text-align: left; font-family: 'Outfit', sans-serif;
transition: all 0.2s;
}
.filter-btn:hover { background: rgba(255,255,255,0.03); }
.filter-btn.active { border-color: rgba(255,255,255,0.1); }
.filter-dot {
width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0;
}
.divider { height: 1px; background: var(--border); margin: 14px 0; }
.page-type-item {
display: flex; align-items: center; gap: 8px; padding: 4px 10px; margin-bottom: 2px;
}
.page-type-dot {
width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
}
.page-type-label { font-size: 10px; color: var(--text-muted); }
.page-type-count { font-size: 8px; color: var(--text-dim); font-family: 'JetBrains Mono', monospace; }
.health-box {
padding: 10px; border-radius: 8px; margin-top: 4px;
}
.health-row { display: flex; justify-content: space-between; margin-bottom: 6px; }
.health-label { font-size: 10px; color: var(--text-muted); }
.health-value { font-size: 12px; font-weight: 700; font-family: 'JetBrains Mono', monospace; }
.health-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
.health-fill { height: 100%; border-radius: 2px; }
.health-stats {
margin-top: 8px; padding: 8px 10px; border-radius: 6px;
}
.health-stat { font-size: 9px; color: var(--text-dim); margin-bottom: 2px; }
.health-stat span { font-weight: 700; }
.canvas-area { flex: 1; position: relative; overflow: hidden; }
.stats-bar {
position: absolute; top: 12px; left: 50%; transform: translateX(-50%); z-index: 10;
display: flex; gap: 8px; background: rgba(17,24,39,0.9); border-radius: 10px;
padding: 6px 12px; border: 1px solid var(--border); backdrop-filter: blur(8px);
}
.stat-item {
text-align: center; padding: 2px 10px;
border-right: 1px solid var(--border);
}
.stat-item:last-child { border-right: none; }
.stat-value { font-size: 16px; font-weight: 800; font-family: 'JetBrains Mono', monospace; }
.stat-label { font-size: 7px; color: var(--text-dim); letter-spacing: 0.5px; }
svg text { user-select: none; }
.link-line { transition: opacity 0.35s ease; }
.node-group { cursor: pointer; transition: opacity 0.35s ease; }
@keyframes pulse-ring {
0%, 100% { r: 58; opacity: 0.25; }
50% { r: 66; opacity: 0.08; }
}
@keyframes flow-dot {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { offset-distance: 100%; opacity: 0; }
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<div class="header-left">
<div class="logo"></div>
<div>
<div class="header-title">IGNY8 Linker Module</div>
<div class="header-sub">SAG Authority Flow Visualization</div>
</div>
</div>
<div class="view-toggle">
<button class="view-btn active" data-view="internal">Internal</button>
<button class="view-btn" data-view="external">External</button>
<button class="view-btn" data-view="combined">Combined</button>
</div>
</div>
<div class="layout">
<!-- Sidebar -->
<div class="sidebar" id="sidebar"></div>
<!-- Canvas -->
<div class="canvas-area">
<div class="stats-bar" id="stats-bar"></div>
<svg id="canvas" width="100%" height="100%" viewBox="0 0 1020 720" preserveAspectRatio="xMidYMid meet"></svg>
</div>
</div>
<script>
(function() {
// ─── STATE ───
let activeFilter = 'all';
let activeView = 'internal';
// ─── COLORS ───
const C = {
homepage:'#f59e0b', hub:'#3b82f6', blog:'#10b981', product:'#ec4899',
service:'#8b5cf6', term:'#06b6d4', category:'#f97316', brand:'#ef4444',
comparison:'#14b8a6',
verticalUp:'#fbbf24', verticalDown:'#60a5fa', sibling:'#34d399',
crossCluster:'#f472b6', taxonomyCtx:'#22d3ee', breadcrumb:'#a78bfa', related:'#fb923c',
extT1:'#fcd34d', extT2:'#93c5fd', extT3:'#a5b4fc', extT4:'#86efac', extT5:'#fca5a5',
text:'#e2e8f0', muted:'#94a3b8', dim:'#64748b', border:'#2a3654',
bg:'#0a0e1a', surface:'#111827'
};
// ─── DATA ───
const filters = [
{ key:'all', label:'All Links', color: C.text },
{ key:'vertical', label:'Vertical (Hub ↕ Blog)', color: C.verticalUp },
{ key:'sibling', label:'Sibling (Blog ↔ Blog)', color: C.sibling },
{ key:'cross', label:'Cross-Cluster (Hub ↔ Hub)', color: C.crossCluster },
{ key:'taxonomy', label:'Taxonomy (Term → Hubs)', color: C.taxonomyCtx },
{ key:'breadcrumb', label:'Breadcrumb (Structural)', color: C.breadcrumb },
{ key:'related', label:'Related (Cross-Cluster)', color: C.related },
];
const pageTypes = [
{ type:'homepage', label:'Homepage', count:'1 page' },
{ type:'hub', label:'Cluster Hubs', count:'12 pages' },
{ type:'blog', label:'Blog Posts', count:'36 pages' },
{ type:'term', label:'Term Pages', count:'24 pages' },
{ type:'product', label:'Products', count:'50 pages' },
{ type:'service', label:'Services', count:'6 pages' },
{ type:'category', label:'Categories', count:'4 pages' },
{ type:'brand', label:'Brand Pages', count:'3 pages' },
{ type:'comparison', label:'Comparisons', count:'5 pages' },
];
const nodes = [
{ id:'home', x:500, y:95, label:'Homepage', type:'homepage', tier:'T1', info:'15 outbound', size:54 },
{ id:'cat1', x:160, y:178, label:'Category: Foot', type:'category', info:'8 links', size:34 },
{ id:'cat2', x:840, y:178, label:'Category: Neck', type:'category', info:'6 links', size:34 },
{ id:'hub1', x:300, y:295, label:'Foot × Neuro', type:'hub', tier:'T2', info:'Hub · 12 links', size:46 },
{ id:'hub2', x:500, y:198, label:'EMS × Foot', type:'hub', tier:'T2', info:'Hub · 10 links', size:44 },
{ id:'hub3', x:700, y:295, label:'Heated × Neck', type:'hub', tier:'T3', info:'Hub · 9 links', size:42 },
{ id:'term1', x:155, y:300, label:'Neuropathy', type:'term', info:'→ 3 hubs', size:34 },
{ id:'term2', x:845, y:300, label:'Heated', type:'term', info:'→ 2 hubs', size:34 },
{ id:'blog1', x:235, y:462, label:'Doctor Picks', type:'blog', tier:'T4', info:'6 links', size:32 },
{ id:'blog2', x:330, y:430, label:'EMS vs TENS', type:'blog', tier:'T4', info:'5 links', size:32 },
{ id:'blog3', x:395, y:478, label:'Does It Help?', type:'blog', tier:'T4', info:'4 links', size:32 },
{ id:'blog4', x:625, y:430, label:'Best Heated', type:'blog', tier:'T4', info:'5 links', size:32 },
{ id:'blog5', x:720, y:462, label:'Neck Pain Fix', type:'blog', tier:'T4', info:'4 links', size:32 },
{ id:'blog6', x:575, y:478, label:'Heat Therapy', type:'blog', tier:'T4', info:'4 links', size:32 },
{ id:'prod1', x:175, y:418, label:'RENPHO Pro', type:'product', info:'3 links', size:30 },
{ id:'prod2', x:825, y:418, label:'Homedics Heat', type:'product', info:'3 links', size:30 },
{ id:'comp1', x:500, y:558, label:'Brand Compare', type:'comparison', tier:'T5', info:'8 links', size:34 },
];
const links = [
// Type 1: Vertical Up — Blog → Hub (mandatory)
{ from:'blog1', to:'hub1', type:'vertical', color:C.verticalUp, label:'MANDATORY' },
{ from:'blog2', to:'hub1', type:'vertical', color:C.verticalUp },
{ from:'blog3', to:'hub1', type:'vertical', color:C.verticalUp },
{ from:'blog4', to:'hub3', type:'vertical', color:C.verticalUp, label:'MANDATORY' },
{ from:'blog5', to:'hub3', type:'vertical', color:C.verticalUp },
{ from:'blog6', to:'hub3', type:'vertical', color:C.verticalUp },
// Type 2: Vertical Down — Hub → Blog/Product
{ from:'hub1', to:'blog1', type:'vertical', color:C.verticalDown },
{ from:'hub1', to:'blog2', type:'vertical', color:C.verticalDown },
{ from:'hub3', to:'blog4', type:'vertical', color:C.verticalDown },
{ from:'hub3', to:'blog5', type:'vertical', color:C.verticalDown },
{ from:'hub1', to:'prod1', type:'vertical', color:C.verticalDown },
{ from:'hub3', to:'prod2', type:'vertical', color:C.verticalDown },
// Type 3: Sibling — Blog ↔ Blog
{ from:'blog1', to:'blog2', type:'sibling', color:C.sibling, label:'MAX 2', curve:-22 },
{ from:'blog2', to:'blog3', type:'sibling', color:C.sibling, curve:-18 },
{ from:'blog4', to:'blog5', type:'sibling', color:C.sibling, label:'MAX 2', curve:-22 },
// Type 4: Cross-Cluster — Hub ↔ Hub
{ from:'hub1', to:'hub3', type:'cross', color:C.crossCluster, label:'SHARED: "Foot"', curve:35 },
{ from:'hub1', to:'hub2', type:'cross', color:C.crossCluster, curve:20 },
{ from:'hub2', to:'hub3', type:'cross', color:C.crossCluster, curve:20 },
// Type 5: Taxonomy — Term → Hubs
{ from:'term1', to:'hub1', type:'taxonomy', color:C.taxonomyCtx, label:'ALL HUBS' },
{ from:'term1', to:'hub2', type:'taxonomy', color:C.taxonomyCtx },
{ from:'term2', to:'hub3', type:'taxonomy', color:C.taxonomyCtx, label:'ALL HUBS' },
{ from:'term2', to:'hub2', type:'taxonomy', color:C.taxonomyCtx },
// Type 6: Breadcrumb
{ from:'home', to:'hub1', type:'breadcrumb', color:C.breadcrumb },
{ from:'home', to:'hub2', type:'breadcrumb', color:C.breadcrumb },
{ from:'home', to:'hub3', type:'breadcrumb', color:C.breadcrumb },
{ from:'home', to:'cat1', type:'breadcrumb', color:C.breadcrumb },
{ from:'home', to:'cat2', type:'breadcrumb', color:C.breadcrumb },
// Type 7: Related (cross-cluster blogs)
{ from:'blog3', to:'blog4', type:'related', color:C.related, label:'CROSS-CLUSTER', curve:25 },
{ from:'blog6', to:'blog2', type:'related', color:C.related, curve:-25 },
];
const externalLinks = [
{ target:'home', count:'10-30', dr:'30-60', tier:'T1', color:C.extT1, ox:-70, oy:-50 },
{ target:'hub1', count:'5-15', dr:'25-50', tier:'T2', color:C.extT2, ox:-75, oy:-45 },
{ target:'hub2', count:'5-15', dr:'25-50', tier:'T2', color:C.extT2, ox:-70, oy:-50 },
{ target:'hub3', count:'5-15', dr:'25-50', tier:'T2', color:C.extT2, ox:75, oy:-45 },
{ target:'blog1', count:'1-2', dr:'20-35', tier:'T4', color:C.extT4, ox:-55, oy:-35 },
{ target:'comp1', count:'2-6', dr:'25-40', tier:'T5', color:C.extT5, ox:-70, oy:-45 },
];
// ─── HELPERS ───
function nodeById(id) { return nodes.find(n => n.id === id); }
function svgEl(tag, attrs, parent) {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const [k, v] of Object.entries(attrs || {})) {
if (k === 'textContent') el.textContent = v;
else el.setAttribute(k, v);
}
if (parent) parent.appendChild(el);
return el;
}
function isVisible(linkType) {
if (activeFilter === 'all') return true;
return linkType === activeFilter;
}
function curvePath(x1, y1, x2, y2, curveAmt) {
if (!curveAmt) return `M${x1},${y1} L${x2},${y2}`;
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
const mx = (x1+x2)/2, my = (y1+y2)/2;
const cx = mx + (dy/len)*curveAmt;
const cy = my - (dx/len)*curveAmt;
return `M${x1},${y1} Q${cx},${cy} ${x2},${y2}`;
}
// ─── RENDER SIDEBAR ───
function renderSidebar() {
const sb = document.getElementById('sidebar');
sb.innerHTML = '';
// Link type filters
const l1 = document.createElement('div'); l1.className = 'sidebar-label'; l1.textContent = 'LINK TYPES'; sb.appendChild(l1);
filters.forEach(f => {
const btn = document.createElement('button');
btn.className = 'filter-btn' + (activeFilter === f.key ? ' active' : '');
btn.style.borderColor = activeFilter === f.key ? f.color + '40' : 'transparent';
btn.style.background = activeFilter === f.key ? f.color + '12' : 'transparent';
btn.style.color = activeFilter === f.key ? f.color : '';
btn.innerHTML = `<div class="filter-dot" style="background:${f.color}"></div>${f.label}`;
btn.onclick = () => { activeFilter = activeFilter === f.key ? 'all' : f.key; render(); };
sb.appendChild(btn);
});
// Divider
sb.appendChild(Object.assign(document.createElement('div'), { className: 'divider' }));
// Page types
const l2 = document.createElement('div'); l2.className = 'sidebar-label'; l2.textContent = 'PAGE TYPES'; sb.appendChild(l2);
pageTypes.forEach(p => {
const d = document.createElement('div'); d.className = 'page-type-item';
d.innerHTML = `<div class="page-type-dot" style="background:${C[p.type]}40;border:1.5px solid ${C[p.type]}"></div>
<div><div class="page-type-label">${p.label}</div><div class="page-type-count">${p.count}</div></div>`;
sb.appendChild(d);
});
sb.appendChild(Object.assign(document.createElement('div'), { className: 'divider' }));
// Health
const l3 = document.createElement('div'); l3.className = 'sidebar-label'; l3.textContent = 'HEALTH'; sb.appendChild(l3);
const hbox = document.createElement('div'); hbox.className = 'health-box';
hbox.style.background = C.hub + '08'; hbox.style.border = `1px solid ${C.hub}20`;
hbox.innerHTML = `
<div class="health-row"><span class="health-label">Link Coverage</span><span class="health-value" style="color:${C.hub}">87/100</span></div>
<div class="health-bar"><div class="health-fill" style="width:87%;background:linear-gradient(90deg,${C.hub},${C.term})"></div></div>`;
sb.appendChild(hbox);
const hs = document.createElement('div'); hs.className = 'health-stats';
hs.style.background = C.sibling + '08';
hs.innerHTML = `
<div class="health-stat">Orphan Pages: <span style="color:#ef4444">0</span></div>
<div class="health-stat">Broken Links: <span style="color:#ef4444">2</span></div>
<div class="health-stat">Over-linked: <span style="color:${C.homepage}">1</span></div>
<div class="health-stat">Avg Links/Page: <span style="color:${C.hub}">6.2</span></div>`;
sb.appendChild(hs);
}
// ─── RENDER STATS BAR ───
function renderStats() {
const bar = document.getElementById('stats-bar');
const stats = [
{ value:'215', label:'Total Links', color:C.hub },
{ value:'141', label:'Internal', color:C.sibling },
{ value:'83', label:'External', color:C.crossCluster },
{ value:'7', label:'Link Types', color:C.term },
{ value:'0', label:'Orphans', color:C.sibling },
];
bar.innerHTML = stats.map((s,i) =>
`<div class="stat-item" ${i===stats.length-1?'style="border-right:none"':''}>
<div class="stat-value" style="color:${s.color}">${s.value}</div>
<div class="stat-label">${s.label}</div>
</div>`
).join('');
}
// ─── RENDER SVG CANVAS ───
function renderCanvas() {
const svg = document.getElementById('canvas');
svg.innerHTML = '';
// Defs
const defs = svgEl('defs', {}, svg);
defs.innerHTML = `
<marker id="ah" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="${C.dim}"/>
</marker>
<radialGradient id="glow-c" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="${C.homepage}12"/><stop offset="100%" stop-color="transparent"/>
</radialGradient>`;
// Background glow + rings
svgEl('circle', { cx:500, cy:330, r:300, fill:'url(#glow-c)' }, svg);
[80,150,220,290,360].forEach(r => svgEl('circle', { cx:500, cy:330, r, fill:'none', stroke:C.border+'28', 'stroke-width':'0.5', 'stroke-dasharray':'4,8' }, svg));
// Cluster boundaries
const clG = svgEl('g', { opacity:'0.5' }, svg);
svgEl('rect', { x:185, y:255, width:270, height:265, rx:16, fill:'none', stroke:C.hub+'20', 'stroke-width':'1', 'stroke-dasharray':'6,4' }, clG);
svgEl('text', { x:195, y:272, fill:C.hub+'35', 'font-size':'8', 'font-family':"'JetBrains Mono', monospace", textContent:'CLUSTER: Foot × Neuropathy' }, clG);
svgEl('rect', { x:535, y:255, width:270, height:265, rx:16, fill:'none', stroke:C.hub+'20', 'stroke-width':'1', 'stroke-dasharray':'6,4' }, clG);
svgEl('text', { x:545, y:272, fill:C.hub+'35', 'font-size':'8', 'font-family':"'JetBrains Mono', monospace", textContent:'CLUSTER: Heated × Neck' }, clG);
// ─── DRAW LINKS ───
const linkGroup = svgEl('g', { id:'link-layer' }, svg);
if (activeView === 'internal' || activeView === 'combined') {
links.forEach(lk => {
const from = nodeById(lk.from), to = nodeById(lk.to);
if (!from || !to) return;
const vis = isVisible(lk.type);
const g = svgEl('g', { class:'link-line', opacity: vis ? '0.75' : '0.05' }, linkGroup);
const dash = lk.type === 'breadcrumb' ? '4,4' : lk.type === 'related' ? '8,4' : 'none';
const path = curvePath(from.x, from.y, to.x, to.y, lk.curve || (lk.type==='cross'?30 : 0));
const pathId = 'p-' + lk.from + '-' + lk.to;
svgEl('path', { id: pathId, d: path, fill:'none', stroke: lk.color, 'stroke-width': vis && activeFilter !== 'all' ? '2.2' : '1.2', 'stroke-dasharray': dash }, g);
// Animated dot for active filter
if (vis && activeFilter !== 'all') {
const dot = svgEl('circle', { r:'3', fill: lk.color, opacity:'0.9' }, g);
const anim = svgEl('animateMotion', { dur: '2.5s', repeatCount:'indefinite' }, dot);
svgEl('mpath', { href: '#'+pathId }, anim);
}
// Label
if (lk.label && vis) {
const mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2;
const dx = to.x-from.x, dy = to.y-from.y, len = Math.sqrt(dx*dx+dy*dy)||1;
const off = lk.curve || (lk.type==='cross'?30:0);
const cx = mx + (dy/len)*off*0.5, cy = my - (dx/len)*off*0.5;
svgEl('rect', { x:cx-38, y:cy-9, width:76, height:15, rx:3, fill:C.bg, stroke:lk.color+'35', 'stroke-width':'0.5' }, g);
svgEl('text', { x:cx, y:cy+2, 'text-anchor':'middle', fill:lk.color+'cc', 'font-size':'7', 'font-family':"'JetBrains Mono', monospace", textContent: lk.label }, g);
}
});
}
// ─── EXTERNAL LINKS ───
if (activeView === 'external' || activeView === 'combined') {
const extG = svgEl('g', { id:'ext-layer' }, svg);
externalLinks.forEach(el => {
const n = nodeById(el.target);
if (!n) return;
const sx = n.x + el.ox, sy = n.y + el.oy;
const ex = n.x + (el.ox > 0 ? 12 : -12) * (Math.abs(el.ox)/el.ox || 1);
const ey = n.y - 5;
const g = svgEl('g', {}, extG);
svgEl('line', { x1:sx, y1:sy, x2:n.x + (el.ox>0?10:-10), y2:n.y-8, stroke:el.color, 'stroke-width':'2', 'marker-end':'url(#ah)' }, g);
svgEl('rect', { x:sx-30, y:sy-18, width:60, height:26, rx:4, fill:el.color+'18', stroke:el.color+'45', 'stroke-width':'1' }, g);
svgEl('text', { x:sx, y:sy-6, 'text-anchor':'middle', fill:el.color, 'font-size':'8', 'font-weight':'600', 'font-family':"'JetBrains Mono', monospace", textContent: el.count+' links' }, g);
svgEl('text', { x:sx, y:sy+5, 'text-anchor':'middle', fill:C.dim, 'font-size':'7', 'font-family':"'JetBrains Mono', monospace", textContent: 'DR '+el.dr }, g);
svgEl('text', { x:sx, y:sy-22, 'text-anchor':'middle', fill:el.color+'90', 'font-size':'7', 'font-weight':'700', 'font-family':"'JetBrains Mono', monospace", textContent: el.tier+' EXTERNAL' }, g);
});
// Authority flow label
svgEl('text', { x:430, y:155, 'text-anchor':'middle', fill:C.homepage+'45', 'font-size':'8', 'font-family':"'JetBrains Mono', monospace", textContent:'AUTHORITY FLOWS DOWN ▼', transform:'rotate(-12 430 155)' }, extG);
svgEl('text', { x:500, y:395, 'text-anchor':'middle', fill:C.verticalUp+'35', 'font-size':'8', 'font-family':"'JetBrains Mono', monospace", textContent:'▲ AUTHORITY FLOWS UP' }, extG);
}
// ─── DRAW NODES ───
const nodeLayer = svgEl('g', { id:'node-layer' }, svg);
nodes.forEach(n => {
const col = C[n.type] || '#666';
const sz = n.size || 36;
const g = svgEl('g', { class:'node-group' }, nodeLayer);
// Outer ring
svgEl('circle', { cx:n.x, cy:n.y, r:sz, fill:col+'15', stroke:col+'55', 'stroke-width':'1.5' }, g);
svgEl('circle', { cx:n.x, cy:n.y, r:sz-5, fill:col+'08', stroke:'none' }, g);
// Tier badge
if (n.tier) {
svgEl('rect', { x:n.x+sz*0.48, y:n.y-sz+1, width:28, height:16, rx:8, fill:col }, g);
svgEl('text', { x:n.x+sz*0.48+14, y:n.y-sz+12.5, 'text-anchor':'middle', fill:'#000', 'font-size':'9', 'font-weight':'700', 'font-family':"'JetBrains Mono', monospace", textContent:n.tier }, g);
}
// Labels
const lbl = n.label.length > 15 ? n.label.slice(0,14)+'…' : n.label;
svgEl('text', { x:n.x, y:n.y-4, 'text-anchor':'middle', fill:C.text, 'font-size':'10.5', 'font-weight':'600', 'font-family':"'Outfit', sans-serif", textContent:lbl }, g);
svgEl('text', { x:n.x, y:n.y+10, 'text-anchor':'middle', fill:C.muted, 'font-size':'8', 'font-family':"'JetBrains Mono', monospace", textContent:n.type.toUpperCase() }, g);
if (n.info) {
svgEl('text', { x:n.x, y:n.y+22, 'text-anchor':'middle', fill:C.dim, 'font-size':'7.5', 'font-family':"'JetBrains Mono', monospace", textContent:n.info }, g);
}
});
// ─── LEGENDS ───
const legG = svgEl('g', {}, svg);
// Internal legend
const iLeg = [
{ color:C.verticalUp, label:'Blog → Hub (mandatory)' },
{ color:C.verticalDown, label:'Hub → Blog / Product' },
{ color:C.sibling, label:'Blog ↔ Blog (max 2)' },
{ color:C.crossCluster, label:'Hub ↔ Hub (shared attr)' },
{ color:C.taxonomyCtx, label:'Term → All Connected Hubs' },
{ color:C.breadcrumb, label:'Breadcrumb (structural)', dash:'4,4' },
{ color:C.related, label:'Related (cross-cluster)', dash:'8,4' },
];
svgEl('text', { x:30, y:562, fill:C.muted, 'font-size':'9', 'font-weight':'700', 'font-family':"'Outfit', sans-serif", 'letter-spacing':'1.5', textContent:'INTERNAL LINKS' }, legG);
iLeg.forEach((it, i) => {
svgEl('line', { x1:30, y1:578+i*17, x2:48, y2:578+i*17, stroke:it.color, 'stroke-width':'2', 'stroke-dasharray': it.dash||'none' }, legG);
svgEl('text', { x:56, y:582+i*17, fill:C.dim, 'font-size':'8.5', 'font-family':"'JetBrains Mono', monospace", textContent:it.label }, legG);
});
// External legend (when visible)
if (activeView === 'external' || activeView === 'combined') {
const eLeg = [
{ color:C.extT1, label:'T1 → Homepage (10-30)' },
{ color:C.extT2, label:'T2 → Top Hubs (5-15)' },
{ color:C.extT3, label:'T3 → Other Hubs (3-10)' },
{ color:C.extT4, label:'T4 → Select Blogs (1-4)' },
{ color:C.extT5, label:'T5 → Auth Magnets (2-6)' },
];
svgEl('text', { x:800, y:562, fill:C.muted, 'font-size':'9', 'font-weight':'700', 'font-family':"'Outfit', sans-serif", 'letter-spacing':'1.5', textContent:'EXTERNAL LINKS' }, legG);
eLeg.forEach((it, i) => {
svgEl('line', { x1:800, y1:578+i*17, x2:818, y2:578+i*17, stroke:it.color, 'stroke-width':'2' }, legG);
svgEl('text', { x:826, y:582+i*17, fill:C.dim, 'font-size':'8.5', 'font-family':"'JetBrains Mono', monospace", textContent:it.label }, legG);
});
}
// Density rule box
svgEl('rect', { x:370, y:622, width:260, height:42, rx:8, fill:C.surface, stroke:C.border, 'stroke-width':'1' }, svg);
svgEl('text', { x:500, y:639, 'text-anchor':'middle', fill:C.muted, 'font-size':'9', 'font-weight':'600', 'font-family':"'Outfit', sans-serif", textContent:'DENSITY RULE' }, svg);
svgEl('text', { x:500, y:653, 'text-anchor':'middle', fill:C.dim, 'font-size':'7.5', 'font-family':"'JetBrains Mono', monospace", textContent:'1-2 links per 300 words · No orphans · Max 8 words/anchor' }, svg);
}
// ─── VIEW TOGGLE ───
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeView = btn.dataset.view;
render();
});
});
// ─── RENDER ALL ───
function render() {
renderSidebar();
renderStats();
renderCanvas();
}
render();
})();
</script>
</body>
</html>