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

Author: jwalsh

jwalsh@nexus

Last Updated: 2026-04-20 22:12:22

build: 2026-04-28 22:29 | sha: c759f7e