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.
#+end_src
Changelog
| Date | Change |
|---|---|
| 2026-02-07 | Initial: HTML, CSS, JS, mock page, 6 Mermaid diagrams |