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.

wwn-full-flow.svg

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

Author: jwalsh

jwalsh@nexus

Last Updated: 2026-02-07 14:44:45

build: 2026-04-17 18:34 | sha: 792b203