simocracy-v2 · url-slugs-e2e

Custom URL slugs — sim auto-claim, gathering form, link generators

@daviddao

End-to-end test of the freshly-shipped org.simocracy.alias URL‑slug system on production: sim auto‑claim at create time, gathering form availability check, and slug‑aware link generators across /gallery, /events, /my-sims, and /stats. Driven through the real OAuth flow as satyam2.climateai.org.

Date: 2026-05-01 · Target: https://simocracy.org @ 68fe993 · partial · Follow-up fix: 05a8463 resolved


Headline

7
working end‑to‑end
2
bugs fixed in follow‑up
3
pre‑existing flags
68fe993
05a8463
commits covered

Headline. The slug system’s forward path is solid — a sim was created on production as satyam2.climateai.org, the alias was claimed atomically by the facilitator, the toast confirmed simocracy.org/sims/pm-slug-test-2026, and that URL renders the sim profile. Two real bugs were caught (the bulk slug‑map fetch returning empty in list views; the manual‑override field wedging on “Checking availability…”); both root‑caused on the spot and fixed in 05a8463. Verified post‑deploy on simocracy.org: list pages now render slug‑aware hrefs, detail pages emit the slug as <link rel="canonical">.

What works

01simocracy.org production landing.
02Login popover — handle satyam2.climateai.org.
03Self‑hosted ePDS OAuth consent on climateai.org.
04Authorise scope: bsky / repository / authenticate.
05/my-sims after login — 2 existing sims.
06Create dialog with name “PM Slug Test 2026”.
07S3 auto‑claim works. Toast: “URL: simocracy.org/sims/pm-slug-test-2026”.
08Visiting the slug URL renders the sim profile (S2 catch‑all).
09S4 gathering form: “Custom URL (optional)” field.
10Debounced check returns ✓ available.
11Type the existing sim’s slug → ✗ taken.
12BAD_SLUG_with_underscores✗ invalid format (regex catches it).
13S3 manual override: pre‑fills auto‑derived slug, Currently: line shows current URL.
14BUG Manual rename wedges on “Checking availability…”.
15/stats sim leaderboard rendered cleanly.
16Recent activity surfaces “Satyam Two created sim PM Slug Test 2026 — 20m ago”.

PDS verification

Slug records live on the facilitator’s PDS at at://did:plc:awtx57rxuisy4sf4kzzr3uhf/org.simocracy.alias/<slug> with rkey == slug. Verified directly via xrpc:

$ curl "https://climateai.org/xrpc/com.atproto.repo.getRecord\
      ?repo=did:plc:awtx57rxuisy4sf4kzzr3uhf\
      &collection=org.simocracy.alias&rkey=pm-slug-test-2026"

{
  "uri":  "at://did:plc:awtx57rxuisy4sf4kzzr3uhf/org.simocracy.alias/pm-slug-test-2026",
  "cid":  "bafyreifrejdyamjjs5mfx4wlwwtdxsrr5jwwqjyji237czeyufpnbvlksi",
  "value": {
    "$type":  "org.simocracy.alias",
    "slug":   "pm-slug-test-2026",
    "kind":   "sim",
    "target": {
      "uri":  "at://did:plc:cpoagodpqrgs4t7thi5z37uf/org.simocracy.sim/3mkt33lmhu62a",
      "cid":  "bafyreiezhxluzyq4e6bt7cexnphcwmh5iqfvjo2docthajf7vhjqqqyoxy"
    },
    "owner":     "did:plc:cpoagodpqrgs4t7thi5z37uf",
    "createdAt": "2026-05-01T21:28:30.288Z"
  }
}

Forward lookup hits the facilitator via getRecord (single record, fast):

$ curl "https://simocracy.org/api/aliases?slug=pm-slug-test-2026"

{
  "available": false,
  "reason":    "taken",
  "kind":      "sim",
  "target":    "at://did:plc:cpoagodpqrgs4t7thi5z37uf/org.simocracy.sim/3mkt33lmhu62a",
  "owner":     "did:plc:cpoagodpqrgs4t7thi5z37uf"
}

Findings

S3 — Auto‑claim at sim create works (production)

The most user‑visible feature of this milestone. Creating a sim named “PM Slug Test 2026” on the live site:

  1. POST /api/records writes the sim to satyam2.climateai.org’s repo.
  2. Client derives slugifyName("PM Slug Test 2026") → "pm-slug-test-2026".
  3. POST /api/aliases writes the alias to the facilitator’s PDS with rkey = slug.
  4. Toast confirms simocracy.org/sims/pm-slug-test-2026 (screenshot 07).
  5. Direct visit to that URL renders the sim profile (screenshot 08, S2 catch‑all forward path).

End‑to‑end: claimed path verified. The state machine’s taken and skipped branches were not exercised in this run — you’d need a name collision or an all‑unicode name to hit them.

S4 — Gathering form: availability + format checks all green

The new “Custom URL” field on the gathering create dialog correctly surfaces all three states with debounced GET /api/aliases?slug=<value>:

Submit button is correctly gated on slugSubmittable. The slug field UI is fully functional.

S5 — Bulk slug‑map fetch returns empty in production list views fixed in 05a8463

The biggest production finding. fetchSlugMap() is called by /gallery, /events, /my-sims, /stats server components to bulk‑hydrate slug‑aware href props for client components. In production, it returned an empty map even though the alias record existed on the facilitator’s PDS.

Inspected JSON payload (before fix):

"simCreations": [
  {
    "uri":  "at://did:plc:cpoagodpqrgs4t7thi5z37uf/org.simocracy.sim/3mkt33lmhu62a",
    "href": "/sims/did%3Aplc%3Acpoagodpqrgs4t7thi5z37uf/3mkt33lmhu62a",  ← should be /sims/pm-slug-test-2026
    "name": "PM Slug Test 2026",
    ...
  }
]

Cause. lib/atproto-utils.ts → listRecordsFromPds doesn’t pass cache: "no-store" on its fetch. Next.js’s default fetch cache kept the first response forever in production builds. The first SSR request after deploy got the empty list (no aliases yet), and that empty response was served indefinitely. The forward lookup (fetchAliasBySluggetRecordFromPds) was uncached because it’s called from an API route with force‑dynamic.

Fix shipped. Replaced the listRecordsFromPds call with a local listAllAliases helper inside lib/aliases.ts that paginates the facilitator PDS directly with cache: "no-store". Detail-page generateMetadata canonical-URL lookups now use a sibling findSlugForTargetFresh helper for the same reason.

Verified post-deploy:

$ curl -s "https://simocracy.org/stats?cb=$(date +%s)" \
    | grep -oE '"href":"[^"]+","name":"PM Slug Test 2026"'
"href":"/sims/pm-slug-test-2026","name":"PM Slug Test 2026"

$ curl -sL "https://simocracy.org/sims/did:plc:cpoag.../3mkt33lmhu62a" \
    | grep -oE '<link rel="canonical"[^>]*>'
<link rel="canonical" href="https://www.simocracy.org/sims/pm-slug-test-2026"/>

S3 — Manual slug override wedges on “Checking availability…” fixed in 05a8463

Opening “Edit Appearance” on a slug‑claimed sim shows the new Custom URL section pre‑filled with the current slug (screenshot 13). Typing a new slug (e.g. pm-slug-test-renamed-2026) triggered the debounced check, but the status never settled — it stayed on Checking availability… indefinitely (screenshot 14), even though the underlying API responded in <100 ms (verified via direct curl).

Cause. The useEffect that drives the check had [draft, currentSlug, status] in its dependency array. Setting status = "checking" inside the effect re‑fired the effect itself — the cleanup cleared the in‑flight timeout, the new effect started a fresh one, repeat forever. Classic effect‑dependency loop.

Fix shipped. Dropped status from the dependency array; replaced the “don’t run while loading” guard with a loadedRef that the load effect flips at the end. The in‑flight slug guard becomes a per‑effect‑invocation alive boolean captured in the cleanup, killing late responses cleanly. Same shape as the gathering‑form‑dialog (which already worked).

The auto‑claim path (S3’s primary purpose) was unaffected by this bug; manual rename is now functional after deploy.

Pre‑existing — Gathering submit silently no‑ops for this account

Submitting the gathering form on satyam2.climateai.org’s account closes the dialog without a toast and without writing a record (verified via direct PDS list — zero org.simocracy.gathering records on this user’s repo). Reproduces with and without the slug field set, so it’s not S4‑specific.

Most likely a permission / scope issue with this user’s ePDS OAuth grant or a pre‑existing bug in /api/records POST. Out of scope for this run; flagged so the next session knows where to start.

Pre‑existing — /sims/<bad-slug> returns HTTP 200 instead of 404

Confirmed in the previous S2 run. app/sims/[...path]/loading.tsx sets up a Suspense boundary that streams a 200 response before the resolver runs; the not‑found UI still renders, but the HTTP status is wrong. Pre‑existing on main; not introduced by this milestone. /events/<bad-slug> correctly returns 404 because the events route has no loading.tsx.

Pre‑existing — Indexer hasn’t ingested the new alias collection yet

GraphQL records(collection: "org.simocracy.alias") against simocracy-indexer-production.up.railway.app returns zero edges. The lexicon JSON was pushed to ../simocracy-indexer at commit 6c97135; Railway needs to re‑deploy the indexer service. Direct PDS queries to the facilitator return the alias correctly, so this only affects code paths that hit the indexer first — mostly fetchSlugForUri’s fast path. Not blocking; deploy of the indexer service will resolve.

Reproducing

Target https://simocracy.org @ 68fe993 Account satyam2.climateai.org (self‑hosted ePDS) Browser driver agent-browser, full session Video walkthrough.webm (7.1 MB) Screenshots assets/01-..16-*.png PDS verificationcurl https://climateai.org/xrpc/com.atproto.repo.getRecord?repo=did:plc:awtx57rxuisy4sf4kzzr3uhf&collection=org.simocracy.alias&rkey=pm-slug-test-2026 Code shipped simocracy-v2 → main → 68fe993 — lib/aliases.ts, S3 + S4 + S5

Walkthrough video