Back to blog

Million Dollar Homepage: 21 Years Later, What a 2005 Page Teaches About Link Rot

Hello HaWkers, on August 26, 2026 the Million Dollar Homepage turned 21 years old online. A 21 year old student named Alex Tew, from Cricklade, England, put up a page on August 26, 2005 with a grid of 1000 by 1000 pixels and sold each pixel for $1, in minimum blocks of 10 by 10 for $100. In January 2006 he auctioned the last 1,000 pixels on eBay: the auction opened on January 1, closed on January 11 with a bid of $38,100 and pushed the gross total to $1,037,100.

The part that matters for people who write code is not the money. As I write this article, milliondollarhomepage.com answered HTTP 200. The page is there, at the same URL, 21 years later. And the links it sold, those died. How many of your 2019 URLs still answer today? And how many of the links you cited in your last ten articles still lead somewhere?

The page that sold a million pixels

The model was ridiculously simple and that is exactly why it worked. Tew needed money for college, built a page with a giant 1000 by 1000 <img>, an image map on top of it and sold ad space by the pixel. Each advertiser sent the artwork for the block and the destination URL. In five months, from August 2005 to January 2006, the whole thing closed at $1,037,100 gross.

Technically, what lives there is the bare minimum:

  • Static HTML served directly.
  • One large image with a <map> and hundreds of <area> elements.
  • No database in front of it, no framework, no build step.

That choice was not visionary, it was just what you could do in 2005. But that is exactly why the page crossed three decades of stack churn without needing maintenance. There is no dependency to update, no runtime to migrate, no Node version to bump. If you enjoy this kind of archaeology, I already wrote about how to recreate that aesthetic in the article on retro style in web design with CSS and JavaScript.

The paradox: the page lives, its links died

Here is the twist. The container survived, the content it pointed to did not.

In 2014, when the page was nine years old, an analysis cited by the Guardian and by Gizmodo found 22% of the links dead, the equivalent of 221,900 pixels. Of those, 23,200 pixels were born broken, because the advertiser never delivered the destination URL. In other words: around 20% of the links died over eight years. In 2017, the estimate recorded on Wikipedia was already approximately 40% of the links affected by link rot.

Notice what happened. Alex Tew did his part: he kept the URL alive for two decades. The ones who broke the deal were the hundreds of companies that bought pixels, changed domains, got acquired, swapped the entire site or simply vanished. The durability of your page does not depend only on you. It depends on everyone you cite.

Link rot is not an anecdote, it is statistics

The Pew Research Center published the report "When Online Content Disappears" on May 17, 2024, and the numbers are brutal:

  • 38% of the pages that existed in 2013 were no longer accessible in October 2023.
  • 25% of all pages that existed at some point between 2013 and 2023 were already gone.
  • 8% of the pages that existed in 2023 were already gone that same year. Decay starts fast.
  • 23% of news pages have at least one broken link.
  • 21% of government website pages do too.
  • 54% of Wikipedia pages have at least one dead link in the references section.

And on social media the shelf life is even shorter: following a sample of tweets for three months, nearly 1 in 5 stopped being publicly visible. In 60% of the cases the account went private, was suspended or deleted; in the other 40% the author deleted just that post. For tweets in Turkish or Arabic, more than 40% vanished within three months.

Translating that to your blog: if you have been publishing for five years, a relevant chunk of your sources is already fiction. The text still claims something with a link that no longer proves anything.

Cool URIs don't change: the 1998 rule that still holds

In 1998, Tim Berners-Lee wrote a short document called "Cool URIs don't change", hosted at w3.org/Provider/Style/URI. The thesis fits in one line: once you create a URI, it is your obligation to keep it working forever; if the document moves, the old URL becomes a redirect.

While writing this article, I tested that address. It answered HTTP 200. Twenty-eight years at the same URL, practicing what it preaches.

The part most teams ignore is that a URL is not an implementation detail, it is a public contract. The classic mistakes:

  • Putting the technology in the URL: /article.php, /posts.aspx. The technology changes, the URL stays stuck.
  • Putting a date or a status in the URL: /2024/new/product. Next year none of that is true.
  • Restructuring the site and letting the 404 handle it. It does not handle it: you lose the external link, the ranking and the citation.

Practical rule: short URL, no extension, no state, and every old path becomes a permanent 301.

Auditing the links on your own site

Enough theory. The first step is knowing how many links in your content are already dead. You can do that with plain Node, without installing anything:

// scripts/check-links.mjs
// Scans the content markdown and tests each external link.
import { readdir, readFile } from 'node:fs/promises'
import { join } from 'node:path'

const CONTENT_DIR = 'content/blog'
const CONCURRENCY = 8
const TIMEOUT_MS = 10000

async function collectLinks(dir) {
  const files = await readdir(dir, { withFileTypes: true })
  const found = new Map() // url -> files where it appears

  for (const file of files) {
    if (!file.isFile() || !file.name.endsWith('.md')) continue

    const raw = await readFile(join(dir, file.name), 'utf8')
    // Captures only markdown links to http/https
    const matches = raw.matchAll(/\]\((https?:\/\/[^)\s]+)\)/g)

    for (const [, url] of matches) {
      if (!found.has(url)) found.set(url, [])
      found.get(url).push(file.name)
    }
  }

  return found
}

async function probe(url) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)

  try {
    // HEAD is cheaper, but many servers answer 405
    let res = await fetch(url, { method: 'HEAD', redirect: 'follow', signal: controller.signal })
    if (res.status === 405 || res.status === 501) {
      res = await fetch(url, { method: 'GET', redirect: 'follow', signal: controller.signal })
    }
    return { url, status: res.status, ok: res.ok }
  } catch (error) {
    return { url, status: 0, ok: false, error: error.name }
  } finally {
    clearTimeout(timer)
  }
}

const links = await collectLinks(CONTENT_DIR)
const queue = [...links.keys()]
const broken = []

// Manual concurrency pool: does not take down someone else's server or yours
await Promise.all(
  Array.from({ length: CONCURRENCY }, async () => {
    while (queue.length) {
      const url = queue.shift()
      const result = await probe(url)
      if (!result.ok) broken.push({ ...result, files: links.get(url) })
    }
  })
)

console.log(`Unique links checked: ${links.size}`)
console.log(`Broken: ${broken.length}`)
for (const item of broken) {
  console.log(`${item.status || item.error}\t${item.url}\t${item.files.slice(0, 3).join(', ')}`)
}

process.exit(broken.length > 0 ? 1 : 0)

Run this once against your old content. The result usually hurts.

Putting the check in CI every week

A manual audit is the one you run once and never again. Schedule it:

# .github/workflows/link-check.yml
name: link-check

on:
  schedule:
    # Every Monday at 06:00 UTC
    - cron: '0 6 * * 1'
  workflow_dispatch:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      # Fails the job when a dead link shows up, and the alert arrives by email
      - run: node scripts/check-links.mjs

Redirect instead of deleting

When you change a slug, the external link that pointed to the old one does not change with it. The only honest way out is the permanent redirect. In a Nuxt project, that lives in routeRules:

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 301: permanent. Google transfers the authority from the old link.
    '/blog/old-post': { redirect: { to: '/blog/new-post', statusCode: 301 } },

    // An entire section that moved
    '/articles/**': { redirect: { to: '/blog/**', statusCode: 301 } },
  },
})

Three details that make a difference:

  1. 301, not 302. The 302 is temporary and tells the search engine to keep indexing the old URL. If the change is final, use 301.
  2. Do not chain redirects. A -> B -> C works in the browser and wastes crawl budget. Point A -> C directly.
  3. Never redirect everything to the home page. For a search engine, redirecting content that vanished to the home page is treated as a disguised 404. If there is no equivalent destination, return an honest 410.

Saving what you cite before it disappears

You control your own URLs. You do not control the URLs you cite. The defense is archiving the source the moment you write, and not on the day it breaks.

// scripts/archive-sources.mjs
// Sends each source to the Wayback Machine and stores the snapshot.
const SAVE_ENDPOINT = 'https://web.archive.org/save/'
const AVAILABILITY = 'https://archive.org/wayback/available?url='

export async function ensureArchived(url) {
  // 1. Does a snapshot already exist?
  const check = await fetch(`${AVAILABILITY}${encodeURIComponent(url)}`)
  const data = await check.json()
  const snapshot = data?.archived_snapshots?.closest

  if (snapshot?.available) {
    return { url, archived: snapshot.url, created: false }
  }

  // 2. It does not exist: request the archiving now
  const saved = await fetch(`${SAVE_ENDPOINT}${url}`, { method: 'GET', redirect: 'follow' })

  if (!saved.ok) {
    throw new Error(`Failed to archive ${url}: HTTP ${saved.status}`)
  }

  return { url, archived: saved.url, created: true }
}

With that, when the source dies, your article keeps proving what it claims. For the most important citations, it is worth linking directly to the snapshot and leaving the original as a secondary reference.

A checklist for pages that cross a decade

What the Million Dollar Homepage got right by accident, you can do on purpose:

  • Static output whenever possible. HTML that does not depend on a runtime does not break when the runtime is discontinued. A statically generated site survives a host change with an rsync.
  • Fewer dependencies. Every package in package.json is a chance that tomorrow's build will not run. The 2005 page has zero.
  • No content locked inside a third party API. If your article text only exists inside a SaaS CMS, your archive is in the hands of that company's pricing plan.
  • URLs with no technology and no date. /blog/name-of-the-subject survives any migration.
  • Assets on your own domain. An image hosted on a free third party service is link rot with an expiration date.
  • Correct sitemap and lastmod. It helps the search engine notice what is still alive.
  • Plain text backup of the content, versioned in Git. Markdown in a repository is the most durable format available today: plain text, readable without any tool.

None of this is exotic. It is the opposite: it is choosing the boring and the simple over the impressive and the fragile.

What this changes for anyone publishing in 2026

There is a nice irony here. In 2005, publishing a page that lasts was the accidental default, because the stack was poor. In 2026, with all the tooling we have, publishing something that lasts became a deliberate decision, one that demands discipline against the temptation to add one more layer.

And the pressure has grown. With search engines and AI assistants summarizing content instead of sending clicks, the citation that remains is the link. When that link dies, the evidence that your work existed first disappears too. Keeping a URL alive stopped being an SEO best practice and became authorship preservation.

The Million Dollar Homepage is not a viral marketing success story, or not only that. It is proof that the hardest thing on the web is not publishing. It is staying published. Alex Tew did his part for 21 years: run the audit script on your content today and find out how many years you have already lost.

Let's go! 🦅

📚 Want to Keep Up With What Is Coming?

This article covered link rot, permanent URLs and how to audit your own content, but the ecosystem changes every week and not everything turns into an article here.

On X I share what I am testing, the behind the scenes of my projects and the news that shows up before it becomes a post.

Follow Me There

👉 Follow @jeffbruchado on X

💡 Daily content about development, career and the tools I actually use

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments