WWN Components Reference
HTML, JavaScript, Mock Page, and State Machines
Table of Contents
HTML Components
Nav Element
The only required markup. Everything else is progressive enhancement.
<nav class="webring" data-wwn-position="1" data-wwn-show-status="true" data-wwn-rewrite-links="true" aria-label="Webring navigation"> <a id="webring-prev" rel="prev">← prev</a> <span id="webring-status"></span> <a id="webring-next" rel="next">next →</a> <span class="webring-info" tabindex="0" role="button" aria-label="What do these values mean?" title="n=sites in ring, p=this site's position, w=winding number (net revolutions). See: https://en.wikipedia.org/wiki/Winding_number">ⓘ</span> </nav> <script src="/scripts/webring.js" defer></script>
Data Attributes
| Attribute | Default | Effect |
|---|---|---|
data-wwn-position"N"= |
— | Required. Declares site position (1-indexed) |
data-wwn-show-status |
"true" |
Show/hide p=X w=Y n=Z status span |
data-wwn-rewrite-links |
"true" |
Inject ?w=N on internal same-origin links |
Configuration Modes
<!-- testbed: everything on (default) --> <nav class="webring" data-wwn-position="1" aria-label="Webring navigation"> <!-- production: hide status, keep link rewriting --> <nav class="webring" data-wwn-position="1" data-wwn-show-status="false" aria-label="Webring navigation"> <!-- minimal: no status, no link rewriting --> <nav class="webring" data-wwn-position="1" data-wwn-show-status="false" data-wwn-rewrite-links="false" aria-label="Webring navigation">
Absent attribute = on. Set "false" to disable.
Prev/next links always work regardless of flags.
CSS
Nine lines structural. Everything else is aesthetic.
/* structural (required) */ .webring { display: flex; align-items: center; justify-content: center; gap: 1em; padding: 1em 0; margin-top: 2em; border-top: 1px solid #333; } /* aesthetic (optional) */ .webring { font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; font-size: 0.85em; color: #999; } .webring a { color: #6af; text-decoration: none; } .webring a:hover { text-decoration: underline; } .webring a.wraparound { color: #f84; } .webring a.wraparound::after { content: ' \21BB'; font-size: 0.7em; opacity: 0.6; } #webring-status { font-size: 0.8em; opacity: 0.7; } .webring-info { cursor: help; opacity: 0.5; } .webring-info:hover, .webring-info:focus { opacity: 1; }
JavaScript
/** * WWN — Webring Winding Number * * State: C = ⟨ord, k, w, t, d⟩ * ord (code: n) - order of cyclic group |Z_ord| * k (code: p) - element in Z_ord (1-indexed) * w - winding number (net circuits) * t - turning count (direction reversals) * d - direction (+/−, + default) * * Two phases: * 1. Sync render as ord=1 (immediate, zero latency) * 2. Async fetch /.well-known/webring.json to upgrade * * Two data attributes control display: * data-wwn-show-status="false" → hide ord=X k=Y w=Z * data-wwn-rewrite-links="false" → don't inject ?w on internal links * * w changes ONLY on wraparound: * next from k=ord → k=1: w → w+1 * prev from k=1 → k=ord: w → w-1 */ (function () { var nav = document.querySelector('.webring[data-wwn-position]'); if (!nav) return; var params = new URLSearchParams(window.location.search); var w = parseInt(params.get('w'), 10) || 0; var PORT = location.port ? ':' + location.port : ''; var currentHost = location.hostname + PORT; // Read configuration from data attributes (absent = true) var showStatus = nav.getAttribute('data-wwn-show-status') !== 'false'; var rewriteLinks = nav.getAttribute('data-wwn-rewrite-links') !== 'false'; // Phase 1: sync render as n=1 render([currentHost], currentHost); // Phase 2: async upgrade from manifest fetch('/.well-known/webring.json') .then(function (r) { return r.ok ? r.json() : Promise.reject(); }) .then(function (manifest) { if (!manifest.ring || typeof manifest.ring !== 'string') return; if (!Array.isArray(manifest.members) || manifest.members.length === 0) return; var ring = manifest.members .filter(function (m) { return m && typeof m.domain === 'string'; }) .map(function (m) { return m.domain; }); if (ring.length > 0) render(ring, currentHost); }) .catch(function () { /* base case holds */ }); function render(ring, self) { var n = ring.length; var campaign = n > 1 ? 'literate-web' : self.replace(/[.:]/g, '-'); var utm = 'utm_source=ring&utm_medium=webring&utm_campaign=' + campaign; // Position: attribute is authority, indexOf is fallback var attrP = parseInt(nav.getAttribute('data-wwn-position'), 10); var detectedIndex = ring.indexOf(self); var p = (Number.isInteger(attrP) && attrP >= 1 && attrP <= n) ? attrP : (detectedIndex !== -1) ? (detectedIndex + 1) : attrP; var status = document.getElementById('webring-status'); var prev = document.getElementById('webring-prev'); var next = document.getElementById('webring-next'); var scheme = location.protocol + '//'; // Orphan: site not in ring if (!Number.isInteger(p) || p < 1 || p > n) { var entry = scheme + ring[0] + '/?w=' + w + '&' + utm; if (status && showStatus) { status.textContent = 'p=' + (attrP || '?') + ' w=' + w + ' n=' + n + ' (not in ring)'; } if (prev) { prev.href = entry; prev.textContent = '\u2190 enter ring'; } if (next) { next.href = entry; next.textContent = 'enter ring \u2192'; } return; } // Wraparound-only w var prevIndex = (p - 2 + n) % n; var nextIndex = p % n; var prevW = w - (p === 1 ? 1 : 0); var nextW = w + (p % n === 0 ? 1 : 0); var prevHref = scheme + ring[prevIndex] + '/?w=' + prevW + '&' + utm; var nextHref = scheme + ring[nextIndex] + '/?w=' + nextW + '&' + utm; if (status && showStatus) { status.textContent = 'p=' + p + ' w=' + w + ' n=' + n; } if (prev) { prev.href = prevHref; prev.classList.toggle('wraparound', p === 1); } if (next) { next.href = nextHref; next.classList.toggle('wraparound', p === n); } // Internal link rewriting (if enabled) if (!rewriteLinks) return; var links = document.querySelectorAll('a[href]'); for (var i = 0; i < links.length; i++) { var link = links[i]; if (link.id === 'webring-prev' || link.id === 'webring-next') continue; try { var url = new URL(link.href, location.href); if (url.host !== location.host) continue; url.searchParams.set('w', w); link.href = url.toString(); } catch (e) {} } } })();
Mock Page
A self-contained test page. Open in browser, append ?w=500 to URL.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>wal.sh — mock</title> <style> :root { --bg: #111; --fg: #ccc; --accent: #6af; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--fg); max-width: 42em; margin: 0 auto; padding: 2em; } h1 { font-size: 1.4em; margin-bottom: 0.5em; } h1 a { color: var(--accent); text-decoration: none; } nav.site { margin-bottom: 2em; font-size: 0.9em; } nav.site a { color: var(--accent); text-decoration: none; margin-right: 1em; } section { margin-bottom: 2em; line-height: 1.6; } ul { padding-left: 1.5em; } li { margin-bottom: 0.3em; } a { color: var(--accent); } .meta { font-size: 0.75em; opacity: 0.5; margin-top: 3em; } /* webring structural */ .webring { display: flex; align-items: center; justify-content: center; gap: 1em; padding: 1em 0; margin-top: 2em; border-top: 1px solid #333; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; font-size: 0.85em; color: #999; } .webring a { color: #6af; text-decoration: none; } .webring a:hover { text-decoration: underline; } .webring a.wraparound { color: #f84; } .webring a.wraparound::after { content: ' \21BB'; font-size: 0.7em; opacity: 0.6; } #webring-status { font-size: 0.8em; opacity: 0.7; } .webring-info { cursor: help; opacity: 0.5; } .webring-info:hover, .webring-info:focus { opacity: 1; } </style> </head> <body> <h1><a href="/">wal.sh</a></h1> <nav class="site"> <a href="/">Home</a> <a href="/research/">Research</a> <a href="/events/">Events</a> <a href="/about.html">About</a> </nav> <section> <h2>current focus</h2> <ul> <li><a href="/research/2026-agent-isolation-freebsd-jails/">agent isolation with freebsd jails</a></li> <li><a href="/research/2026-elenctic-vibe-code-review/">elenctic vibe code review</a></li> <li><a href="/research/tla-plus-system-design/">tla+ system design</a></li> </ul> </section> <section> <h2>2025</h2> <ul> <li><a href="/research/ads-b/">ads-b flight tracking</a> — sdr receiver 0.8km from logan</li> <li><a href="/research/unix-v4/">unix v4</a> — booting 1973 unix on simh/freebsd</li> <li><a href="https://github.com/jwalsh/repolens">repolens</a> — repository analysis</li> </ul> </section> <p class="meta">build: 2026-02-07 12:02 | sha: 1ce88dd</p> <!-- ================================================ --> <!-- WEBRING: the only required addition to any page --> <!-- ================================================ --> <nav class="webring" data-wwn-position="1" data-wwn-show-status="true" data-wwn-rewrite-links="true" aria-label="Webring navigation"> <a id="webring-prev" rel="prev">← prev</a> <span id="webring-status"></span> <a id="webring-next" rel="next">next →</a> <span class="webring-info" tabindex="0" role="button" aria-label="What do these values mean?" title="n=sites in ring, p=this site's position, w=winding number (net revolutions)">ⓘ</span> </nav> <script> // Inline for mock page — production uses <script src="/scripts/webring.js" defer> (function () { var nav = document.querySelector('.webring[data-wwn-position]'); if (!nav) return; var params = new URLSearchParams(window.location.search); var w = parseInt(params.get('w'), 10) || 0; var PORT = location.port ? ':' + location.port : ''; var currentHost = location.hostname + PORT; var showStatus = nav.getAttribute('data-wwn-show-status') !== 'false'; var rewriteLinks = nav.getAttribute('data-wwn-rewrite-links') !== 'false'; render([currentHost], currentHost); fetch('/.well-known/webring.json') .then(function (r) { return r.ok ? r.json() : Promise.reject(); }) .then(function (manifest) { if (!manifest.ring || typeof manifest.ring !== 'string') return; if (!Array.isArray(manifest.members) || manifest.members.length === 0) return; var ring = manifest.members .filter(function (m) { return m && typeof m.domain === 'string'; }) .map(function (m) { return m.domain; }); if (ring.length > 0) render(ring, currentHost); }) .catch(function () {}); function render(ring, self) { var n = ring.length; var campaign = n > 1 ? 'literate-web' : self.replace(/[.:]/g, '-'); var utm = 'utm_source=ring&utm_medium=webring&utm_campaign=' + campaign; var attrP = parseInt(nav.getAttribute('data-wwn-position'), 10); var detectedIndex = ring.indexOf(self); var p = (Number.isInteger(attrP) && attrP >= 1 && attrP <= n) ? attrP : (detectedIndex !== -1) ? (detectedIndex + 1) : attrP; var status = document.getElementById('webring-status'); var prev = document.getElementById('webring-prev'); var next = document.getElementById('webring-next'); var scheme = location.protocol + '//'; if (!Number.isInteger(p) || p < 1 || p > n) { var entry = scheme + ring[0] + '/?w=' + w + '&' + utm; if (status && showStatus) status.textContent = 'p=' + (attrP || '?') + ' w=' + w + ' n=' + n + ' (not in ring)'; if (prev) { prev.href = entry; prev.textContent = '\u2190 enter ring'; } if (next) { next.href = entry; next.textContent = 'enter ring \u2192'; } return; } var prevIndex = (p - 2 + n) % n; var nextIndex = p % n; var prevW = w - (p === 1 ? 1 : 0); var nextW = w + (p % n === 0 ? 1 : 0); if (status && showStatus) status.textContent = 'p=' + p + ' w=' + w + ' n=' + n; if (prev) { prev.href = scheme + ring[prevIndex] + '/?w=' + prevW + '&' + utm; prev.classList.toggle('wraparound', p === 1); } if (next) { next.href = scheme + ring[nextIndex] + '/?w=' + nextW + '&' + utm; next.classList.toggle('wraparound', p === n); } if (!rewriteLinks) return; var links = document.querySelectorAll('a[href]'); for (var i = 0; i < links.length; i++) { var link = links[i]; if (link.id === 'webring-prev' || link.id === 'webring-next') continue; try { var url = new URL(link.href, location.href); if (url.host !== location.host) continue; url.searchParams.set('w', w); link.href = url.toString(); } catch (e) {} } } })(); </script> </body> </html>
State Machines
Squid: Visitor Link Classification
The "squid" is the visitor's cursor — the head of the curve being
wound around the ring. Each link on a page is either human (the
visitor clicked it intentionally) or collected/contaminated (it
carries ?w=N injected by the JS). The visitor can't tell which is
which. That's the joke.
Site: Position State (p)
Position is fixed per-site. It doesn't change during a session. What changes is which site the squid is on.
Ring: Winding State (w)
The core state machine. This is \(\pi_1(S^1) \cong \mathbb{Z}\) realized as a URL parameter.
Combined: Full Navigation Flow
The complete picture: squid arrives, JS resolves position, visitor navigates, w changes (or doesn't) on wraparound.
Degenerate Case: ord=1 (Production wal.sh)
At ord=1, the ring collapses. Both prev and next point to self. Every click is a wraparound. The state machine simplifies to:
Link Contamination Scope
Which links get which parameters. The key insight: w and UTM
have different scopes and different lifetimes.
Live State: wal.sh/?w=500
What the production site looks like with the current URL:
| Field | Value | Derivation |
|---|---|---|
w |
500 | Parsed from ?w=500 |
ord |
1 | RING = [wal.sh], manifest has no members |
k |
1 | data-wwn-position="1" |
| prev → | 499 | k=1 → wraparound, w-1 |
| next → | 501 | k%1=0 → wraparound, w+1 |
| ↻ | both | every click at ord=1 is a wraparound |
The visitor has completed 500 net forward revolutions around a ring of 1. Mathematically: the curve has wound 500 times around the single point. Experientially: someone clicked "next" a lot.
When ord grows to 2 (add a second site), the same w=500 means they've gone around 500 times but now each revolution takes 2 clicks. That's 1000 clicks minimum. The winding number scales inversely with ring size.
Manifest
What /.well-known/webring.json currently serves (ord=1):
{
"id": "wal",
"name": "wal.sh",
"ring": "literate-web"
}
What it would serve at ord=3:
{
"id": "wal",
"name": "wal.sh",
"ring": "literate-web",
"version": 1,
"members": [
{"domain": "wal.sh", "name": "Jason Walsh"},
{"domain": "termbox.org", "name": "Termbox"},
{"domain": "jwalsh.net", "name": "JWalsh.net"}
]
}
Position is array order. No position field. Reordering the
array reorders the ring. Trust is editorial: you added them.
#+endsrc
Changelog
| Date | Change |
|---|---|
| 2026-02-07 | Initial: HTML, CSS, JS, mock page, 6 Mermaid diagrams |
