temproary docs uplaoded

This commit is contained in:
IGNY8 VPS (Salman)
2026-03-23 09:02:49 +00:00
parent cb6eca4483
commit 128b186865
113 changed files with 68897 additions and 0 deletions

View File

@@ -0,0 +1,578 @@
<!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>