daveisbest.org

Director’s commentary · Because I Could

Digging for Angelfire and GeoCities

Looking for a 1999 Angelfire site and a 2001 GeoCities page in the web's archives, what the game shows while they're missing, and the one real thing on them: the hit counter.

Two things I made as a teenager were on the web once and, as far as I know, aren't any more: my first PHP site, on Angelfire in 1999, and a GeoCities page from around 2001. Disk 5 and Disk 6 each have an old browser on a machine in the corner, and the plan was always to show the real pages in it. So I'll be plain about where that stands: I haven't found either of them yet. What you see in the game today are period-style placeholders written to stand in for them. This page is about how I'm looking, and what's built while I do.

Where the free homepages went

GeoCities is the famous one. Yahoo closed it in October 2009, and it didn't go quietly. The Internet Archive ran special crawls before the shutdown, Archive Team grabbed as much as it could and later released the lot as a torrent, and mirrors such as OoCities and ReoCities put large parts of it back online at the old paths. If a 2001 page survived anywhere, it's probably in one of those.

Angelfire is the harder case. It had been deleting inactive accounts for years, and this year it went dark altogether: member sites went offline early in 2026 and the service was gone by the spring. The live site is no help, so archived snapshots are the only likely source for a 1999 page. And a site nobody linked to is exactly the kind of site the crawlers missed.

The catch with all of them is that you need an address, and I don't remember either. By 2001, GeoCities addresses came in two shapes, the old neighbourhood-and-number kind and the newer member-name kind, so that's two searches before I've even started.

Searching without an address

The Wayback Machine has a public index, the CDX API, which lists every capture it holds under a URL prefix, with a timestamp, a status code and a type for each. It can filter that list with regular expressions, which is what you need when all you have is a hunch that your name was somewhere in the address.

https://web.archive.org/cdx/search/cdx
  ?url=angelfire.com/*
  &from=1999&to=2001
  &filter=original:.*dave.*
  &filter=mimetype:text/html
  &collapse=urlkey
  &fl=timestamp,original,statuscode
  &output=json
Not repo code: the shape of the query I'm running. A prefix, a date range, a filter on the original URL, and one row per distinct page.

Across a whole host that's slow and full of other people's pages, so the real work is narrowing it down: old emails with sign-up confirmations in them, friends' link pages that might have pointed at mine, anything with even part of an address. Once there's one hit, the capture's own links lead to the rest of the site.

If the pages do turn up, they won't be embedded from the archive. They'll be recreated as HTML inside the game's browser, faithfully, with one change: any clip art or animated GIFs I didn't make get swapped for originals, because they were never mine to republish.

What's in the browser today

The browser is an original design, and it knows three sites: the 1999 homepage, the 2001 page and my own college Flash site from Disk 6. Every address in its location bar is invented, on made-up hosts, so no real company's name ends up on screen. The TODO comments are where the real addresses go when I find them.

// TODO(David): the real Angelfire address, if you remember it (even part of it helps the Wayback Machine).
const AF = "http://members.pagebarn.net/davesplace/";
// TODO(David): the real GeoCities address (neighbourhood and number), if you remember it.
const GC = "http://www.netburbs.com/Cyberia/Terminal/4417/";
// TODO(David): where did your own college site live, and what was its address? It wasn't the college's, so it's on a free host for now.
const CO = "http://members.pagebarn.net/collegeonline/";

export const SITES: Record<SiteId, SiteDef> = {
  "angelfire-1999": {
    id: "angelfire-1999",
    era: 1999,
    home: "home",
    pages: {
      home: { id: "home", title: "~*~ Dave's Place ~*~", url: `${AF}index.php3` },
      about: { id: "about", title: "Dave's Place - All About Me", url: `${AF}about.php3` },
      links: { id: "links", title: "Dave's Place - Kool Links", url: `${AF}links.php3` },
      guestbook: { id: "guestbook", title: "Dave's Place - Guestbook", url: `${AF}guestbook.php3` },
    },
  },
minigames/retro-browser/logic.ts: three invented addresses, waiting for real ones.

The pages are written in each year's idiom. The 1999 one has a scrolling marquee, an under-construction sign, a guestbook and a What's New list; the 2001 one has a menu down the side and a version number it hadn't earned. Every line, date and link on them is a placeholder, and the source says so above each site. Links that leave the site all lead to Server not found, which was also realistic. The 1999 pages even take a little longer to load than the 2001 ones.

The one real thing: the hit counter

The odometer on the 1999 homepage is the only thing on either site that isn't pretend. It counts real visitors to daveisbest.org. Each browser is counted once: the first time the page appears, it posts to /api/hits and remembers that it has with a flag in local storage, and then it reads the count. If the network or the storage fails, it shows the last count that browser saw, or a row of zeros.

if (f && get(HIT_FLAG) !== "1" && !(opts.storage === undefined && postedThisSession)) {
  // Set the flag first, so a double-mounted effect can't count the same visit twice.
  set(HIT_FLAG, "1");
  if (opts.storage === undefined) postedThisSession = true;
  try {
    const res = await f(endpoint, { method: "POST" });
    if (!res.ok) del(HIT_FLAG);
  } catch {
    del(HIT_FLAG);
  }
}
minigames/retro-browser/hits.ts: count this browser once, then read.

On the server it's one Redis INCR, through Upstash's REST API in production and an in-memory map in development.

/** POST → { count } after counting this visit. The client only posts once per browser. */
export async function POST(req: Request) {
  const limited = limit(req, limiters.hits);
  if (limited) return limited;
  try {
    return ok({ count: await getStore().incr(KEY) });
  } catch (e) {
    console.error("[hits] write failed", e);
    return fail(503, "store-unavailable");
  }
}
app/api/hits/route.ts

The rate limit allows five hits per client every ten minutes. The client is a SHA-256 of the IP address plus a salt that changes every time the server restarts, so the address itself is never stored or logged, and a hash can't be linked to one from yesterday.

// A fresh salt per process: hashed keys can't be reversed or linked across restarts.
const SALT = randomBytes(16).toString("hex");

/** SHA-256 of the client IP plus a process salt. The IP itself is never stored or logged. */
export function clientKey(req: Request): string {
  const ip =
    req.headers.get("x-forwarded-for")?.split(",")[0].trim() || req.headers.get("x-real-ip")?.trim() || "unknown";
  return createHash("sha256").update(ip + SALT).digest("hex");
}
lib/server/ratelimit.ts

It counts browsers rather than people, and anyone who clears their storage gets counted twice. So did every hit counter in 1999.

What's next

Find the addresses. Until then the pages stay honest placeholders, and the narration doesn't pretend they're anything else.

There's also a question the archive will settle that my memory can't. I remember the Angelfire site as PHP, and the placeholder page jokes that PHP is like HTML but it can count. Whether a free account in 1999 would really have run PHP is something only the original pages can answer. If they turn up and it was plain HTML with a borrowed counter, the joke gets rewritten.