Inspiration
Link rot is quietly erasing the web. Studies of scholarly papers, Wikipedia and Stack Overflow keep finding that a large share of cited links are dead or now point somewhere else. Whole eras — GeoCities, Google Wave, Yahoo Answers, Google+, AltaVista, the NCSA Mosaic home page — live only inside the Internet Archive, where the pages are slow, broken on phones and hard to cite. Kiroween's Resurrection category asked us to bring dead technology back to life, so I built a ritual for raising dead links.
What it does
Paste any URL (or click a tombstone in the Graveyard) and hold a séance in three steps:
- Check for a pulse – the server requests the URL and pronounces it alive or dead, with a cause of death: 404, DNS vanished, timeout, or body-snatched — the page still answers but has been redirected to something else (GeoCities → Yahoo).
- Dig up the remains – the Wayback Machine CDX API returns every monthly capture, drawn as a "haunting timeline" by year. Séance pre-selects a golden-era snapshot (e.g. GeoCities 2007, AltaVista 1999).
- Hold the séance – the archived HTML is fetched raw, stripped of scripts, iframes, trackers, event handlers and inline styles, and rebuilt as a clean, readable, mobile-friendly page with archived images rewired. Gemini writes a first-person spirit message, a one-line epitaph and three search queries to find where the content lives today. If Gemini is unavailable, a deterministic extractive summarizer takes over, so it always works.
The Graveyard has six one-click demos — GeoCities, Google Wave, Yahoo Answers, Google+, AltaVista and NCSA Mosaic — each chosen because it has real HTTP 200 captures in the archive and a pre-picked golden-era snapshot.
Then you can download the reborn page as one standalone HTML file (with a provenance footer: original URL, capture date, archive link), copy a citation, or open the original capture.
The haunted UI is part of the function: tombstone verdicts, an Ouija-planchette loader while the archive is queried, fog, candlelight and a moon — all disabled under prefers-reduced-motion, with WCAG AA contrast.
How we built it (how Kiro was used)
Everything was built with the Kiro CLI (v2.37, Linux arm64) in a headless sandbox; the full .kiro/ directory is in the repo.
- Steering first. Before any code, Kiro wrote four steering docs:
product.md(purpose, users, playful-spooky-but-clear voice),tech.md(Node + Express + node-html-parser only, no build step, ES modules,node:test),structure.md, andsecurity.md. The security doc usesinclusion: fileMatchonserver.js|src/**/*.js, so SSRF and sanitization rules are injected only when server code is in play — keeping context lean for UI work. This made the biggest difference: Kiro never once reached for a framework, a build tool or an extra dependency. - Spec-driven development. Kiro produced
.kiro/specs/seance/requirements.md(user stories with EARS acceptance criteria),design.md(data flow, JSON API contracts, error handling and correctness properties), andtasks.md— 15 ordered tasks, each tied to requirement IDs. When a production bug appeared (a dead domain was being rejected as "unsafe"), I went back to the spec first: Kiro updated requirements/design, added task T16, then implemented and tested it. T17, an IPv6 parsing bug that only showed up on Render, was logged in the same spec; I fixed that one by hand because the Kiro free tier was throttling at the time. - Custom agent + hooks. I had Kiro author a custom CLI agent,
.kiro/agents/seance-builder.json, whose prompt is "take the next unchecked task, implement only that, test, tick it". Its hooks automate the loop:agentSpawnprintsgit statusand the next unchecked task so every session starts oriented;postToolUseonfs_writerunsnpm testand feeds the tail back to the model after every file write;stopruns a secret scan for Google API keys. Kiro's first attempt used the wrong hook schema;kiro-cli agent validatecaught it and Kiro fixed its own config. - IDE hooks for teammates.
.kiro/hooks/also has Kiro IDE hooks: test-on-save, a security review onsrc/**edits checked againstsecurity.md, a "spooky copy check" onpublic/**(voice, contrast, reduced motion), and a manual "sync tasks" hook that reconcilestasks.mdwith the code. - MCP.
.kiro/settings/mcp.jsonregisters thefetchMCP server for inspecting live Wayback Machine responses. Honestly, in this headless build the agent mostly used its own shell and the spec's API contracts instead, so MCP played a small role. - Property-based testing. The design's correctness properties became hand-rolled property tests (no extra deps): random private, loopback, link-local and metadata IPs (IPv4 and IPv6) must always be blocked; random public URLs must pass; sanitized HTML must never contain
<script>,on*=orjavascript:; the fallback summarizer always returns ≤2 sentences, one epitaph and exactly 3 queries. PBT caught a real bug: Kiro's first IPv6 check mis-parsedfc00::/7. 135 tests pass. - Spec vs vibe. I used vibe-style prompts only for the "costume" pass (fog, tombstones, planchette). Everything with security or data contracts went through the spec, which is why the API, sanitizer and SSRF guard never regressed while the UI was being repainted.
Stack: Node.js 22, Express, node-html-parser, Wayback Machine CDX + id_ raw snapshots, Gemini API (REST, key sent in a header, never logged), vanilla HTML/CSS/JS, Render free tier.
Challenges we ran into
- Body-snatched pages. Most famous dead sites don't 404 — they 301 to a homepage that returns 200. Séance had to treat a redirect to a different host as a death ("possessed").
- Dead domains vs SSRF safety. The first SSRF guard rejected any URL whose DNS lookup failed — which blocked exactly the domains Séance exists to resurrect. The fix split syntax validation (archive routes, which never touch the target host) from fetch validation (pulse), with DNS failure reported as a cause of death.
- Archived HTML is hostile. 1990s table soup, broken markup, tracking scripts and missing images. Sanitize → extract → re-render, plus image rewiring to
web.archive.org/web/{ts}id_/…, gave a consistent page. - IPv6 parsing on a real host (T17). On Render, the resolver returned IPv6 addresses ending in
::(e.g. forwww.google.com), which the SSRF guard's expansion mis-parsed as malformed, so Google/YouTube URLs were rejected. Logged as task T17 in the spec, then fixed: expansion rewritten aroundsplit('::')with hex-group validation, plus the IPv4-mapped hex form (::ffff:7f00:1), with regression tests. A related fix stopped a plainwww↔ apex redirect from being reported as a body-snatching. - Headless Kiro. The sandbox has no desktop, so sign-in used Kiro's device-code flow with an AWS Builder ID, and all work ran through
kiro-cli chat --agent … --no-interactivewith hooks providing the feedback I'd normally get from the IDE. A free-tier throttle cut one run short;--resumepicked it up where it left off.
Accomplishments that we're proud of
- A real tool for a real problem that anyone can use in a few seconds, with output you can save and cite.
- A complete
.kiro/story: steering → spec → custom agent with hooks → property-based tests → spec-first bug fix. - Works with or without AI; never executes archived scripts; SSRF-safe.
What we learned
Scoped steering (fileMatch) is the cheapest way to make an agent careful in one area without slowing it down elsewhere. Hooks that run tests after every write turn an agent from "writes code" into "writes code that passes". And a spec is the place to fix a bug, not just the code.
What's next for Séance
- Batch mode: paste a document or bookmarks file and resurrect every dead link in it, with a report.
- A browser extension that offers a séance whenever you hit a 404.
- "Find the living": follow redirects and search results to suggest where moved content lives now.
- Save reborn pages to the Internet Archive's Save Page Now when the content still exists elsewhere.
License: MIT (see LICENSE in the repo). Demo video: https://youtu.be/CcU-MdIMzPc
Built With
- css3
- express.js
- gemini-api
- html5
- internet-archive
- javascript
- kiro
- kiro-cli
- node-html-parser
- node.js
- render
- wayback-machine