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.
stateDiagram-v2 state "Page Load" as PL state "Human Link" as HL state "Contaminated Link" as CL [*] --> PL : GET /?w=500 PL --> HL : visitor reads page,\nclicks an <a> they chose PL --> CL : JS rewrote this <a>\nto carry ?w=500 state HL { state "Ring Transition" as RT state "Internal Browse" as IB state "External Exit" as EE [*] --> RT : clicked prev/next\n(webring nav) [*] --> IB : clicked /research/\n(same origin) [*] --> EE : clicked github.com\n(different host) } state CL { state "w Preserved" as WP state "w + UTM" as WU state "Clean" as CK [*] --> WU : prev/next links\n?w=501&utm_source=ring [*] --> WP : internal links\n/about?w=500 [*] --> CK : external links\n(untouched) } HL --> CL : from visitor's POV\nthese are indistinguishable CL --> HL : the ?w in the URL\nlooks like any tracking param note right of CL Contamination scope: - webring prev/next: w + UTM - internal same-origin: w only - external: clean (no injection) The ambiguity IS the feature. wal.sh/projects?w=500 is either topology or marketing. Both. Neither. end note
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.
stateDiagram-v2 state "p resolution" as PR [*] --> PR : JS loads state PR { state "Read data-wwn-position" as ATTR state "indexOf(self) in RING" as IDX state "Orphan (p > n)" as ORPH [*] --> ATTR : nav element exists ATTR --> IDX : attribute absent or invalid ATTR --> Resolved : 1 ≤ attrP ≤ n IDX --> Resolved : found in array IDX --> ORPH : not found } state "Resolved: k=N" as Resolved state "Orphan Mode" as OrphanMode PR --> Resolved PR --> OrphanMode state Resolved { state "k=1 (boundary)" as P1 state "1 < k < ord (interior)" as PM state "k=ord (boundary)" as PN [*] --> P1 : position 1 [*] --> PM : position 2..ord-1 [*] --> PN : position ord } state OrphanMode { [*] --> EnterRing : both links → RING[0]\nw preserved } note right of P1 k=1: prev is WRAPAROUND (w-1) next is normal end note note right of PN k=ord: next is WRAPAROUND (w+1) prev is normal end note note right of PM interior: neither link changes w. quiet topology. end note
Ring: Winding State (w)
The core state machine. This is \(\pi_1(S^1) \cong \mathbb{Z}\) realized as a URL parameter.
stateDiagram-v2 state "w ∈ ℤ" as W [*] --> W : parse ?w from URL\n(default: 0) state W { state "w = current" as WK WK --> WK : next (k < ord)\nw unchanged WK --> WK : prev (k > 1)\nw unchanged state "Wraparound ↻" as WRAP WK --> WRAP : next at k=ord\nOR prev at k=1 state WRAP { state "Forward wrap" as FW state "Backward wrap" as BW [*] --> FW : k=ord, clicked next [*] --> BW : k=1, clicked prev } FW --> WK : w → w+1\nk → 1 BW --> WK : w → w-1\nk → ord } note right of W The ONLY two transitions that change w. Everything else is identity. At ord=1: EVERY click is a wraparound. Both next and prev trigger WRAP. w reduces to signed click counter. At ord=7: 6 out of 7 clicks are identity. Only the boundary crossing matters. The check: k % ord === 0 (next) k === 1 (prev) end note
Combined: Full Navigation Flow
The complete picture: squid arrives, JS resolves position, visitor navigates, w changes (or doesn't) on wraparound.
stateDiagram-v2 state "Arrival" as A state "JS Init" as JSI state "Browsing" as BR state "Ring Hop" as RH state "New Page" as NP [*] --> A : GET /page?w=500&utm_source=ring A --> JSI : parse ?w=500 state JSI { state "Phase 1: Sync ord=1" as P1 state "Phase 2: Async fetch" as P2 state "Manifest OK" as MOK state "Manifest fail" as MF [*] --> P1 : render immediately P1 --> P2 : fetch /.well-known/webring.json P2 --> MOK : 200 + valid P2 --> MF : 404 / error MOK --> Upgraded : re-render with real ring MF --> BaseCase : ord=1 holds } state "Nav Ready" as NR JSI --> NR NR --> BR : visitor browses page state BR { state "Read content" as RC state "Click internal link" as CI state "Click external link" as CE state "Click prev/next" as CN [*] --> RC RC --> CI : /research/?w=500 RC --> CE : github.com (clean) RC --> CN : webring nav } CI --> BR : same page, w preserved\nno ring transition CE --> [*] : leaves site, w lost CN --> RH : ring transition state RH { state "Check boundary" as CB state if_wrap <<choice>> [*] --> CB CB --> if_wrap if_wrap --> NoWrap : k not at boundary if_wrap --> Wrap : k=1 (prev) or k=ord (next) state "w unchanged" as NoWrap state "w ± 1" as Wrap } RH --> NP : navigate to ring[prevIndex] or ring[nextIndex] NP --> A : new page loads at new site\n?w=500 or ?w=501 or ?w=499 note right of RH This loop IS the winding. Each full revolution (ord clicks same direction) increments |w| by exactly 1. end note
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:
stateDiagram-v2 state "wal.sh" as SITE [*] --> SITE : /?w=500 SITE --> SITE : next → /?w=501 ↻ SITE --> SITE : prev → /?w=499 ↻ SITE --> SITE : /about?w=500 (internal, no change) note right of SITE ord=1, k=1 prev: w-1 (always k=1, always wraparound) next: w+1 (always k%1=0, always wraparound) w is a signed click counter. The topology is correct. The experience is a self-loop. ← prev ord=1 k=1 w=500 next → ↻ ↻ Both links orange. Both links are complete revolutions. end note
Link Contamination Scope
Which links get which parameters. The key insight: w and UTM
have different scopes and different lifetimes.
graph TD subgraph "Page: wal.sh/?w=500&utm_source=ring" A["Internal: /research/"] -->|"JS rewrites"| A2["/research/?w=500"] B["Internal: /about.html"] -->|"JS rewrites"| B2["/about.html?w=500"] C["Prev: ← prev"] -->|"JS sets href"| C2["wal.sh/?w=499&utm_source=ring ↻"] D["Next: next →"] -->|"JS sets href"| D2["wal.sh/?w=501&utm_source=ring ↻"] E["External: github.com/jwalsh"] -->|"untouched"| E2["github.com/jwalsh"] end style A2 fill:#335,color:#6af style B2 fill:#335,color:#6af style C2 fill:#533,color:#f84 style D2 fill:#533,color:#f84 style E2 fill:#222,color:#999 subgraph Legend L1["Blue: w only (internal)"] L2["Orange: w + UTM (ring hop + wraparound)"] L3["Grey: clean (external)"] end style L1 fill:#335,color:#6af style L2 fill:#533,color:#f84 style L3 fill:#222,color:#999
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 |
