Assay

Notes on systems that reported success and were wrong, written while fixing them.

· 4 min

I lost the source and rebuilt the site out of its own HTML

The repository was missing and the only local copy was a stale build with files absent and filenames flattened. The live site turned out to be carrying every article in full, in a script tag, along with the name of the Markdown file each one came from.

Reportedlocal copy is a build, source not found

Trueevery article was in the live page, in full, with its original filename.

The source for a site I had been writing for months was not where I thought it was. What I had locally was a compiled build, not a project: no index.html, two files missing outright, and every filename lowercased by whatever had copied it. The site itself was still up and serving fine.

The obvious move is to scrape the rendered pages and convert the HTML back to Markdown. That works and it is lossy. You get the prose, and you lose the frontmatter, the dates, the tags, the slugs and anything the template did not choose to print. For a site where the taxonomy pages are generated from frontmatter, losing the frontmatter means losing the structure that makes it a site rather than a pile of essays.

So before writing a converter I looked at what the page was actually shipping.

The document is mostly not the document

The site is TanStack Start 1.167.41 on React 19.2.6. Every figure below is from the live site as it stands today. Fetch the home page and count what is in it:

curl -s https://timely-puppy-efee0c.netlify.app/ > home.html
wc -c home.html

129,888 bytes. Of that, 98,718 sit inside a single element:

<script class="$tsr" id="$tsr-stream-barrier">

Strip the scripts, strip the tags, collapse the whitespace, and the visible text of that page is 2,674 bytes:

import re
h = open("home.html", encoding="utf-8").read()
text = re.sub(r"<[^>]+>", " ", re.sub(r"<script.*?</script>", "", h, flags=re.S))
print(len(re.sub(r"\s+", " ", text).strip().encode("utf-8")))

The page shows under three kilobytes of words and carries thirty seven times that in one script tag.

That script is not application code. It opens like this:

(self.$R=self.$R||{})["tsr"]=[];
self.$_TSR={
  h(){this.hydrated=!0,this.c()},
  e(){this.streamEnded=!0,this.c()},
  c(){this.hydrated&&this.streamEnded&&(delete self.$_TSR,delete self.$R.tsr)},
  p(e){this.initialized?e():this.buffer.push(e)},
  buffer:[]
};
$_TSR.router=($R=>$R[0]={manifest:$R[1]={routes:$R[2]={...

h marks hydration done, e marks the end of the stream, and c runs after both and deletes the two globals. The router state is then serialised inline, by seroval, as a chain of assignments into a shared array. Repeated values are written once and referred to afterwards as $R[n], and this page has 197 of those references.

What was in it

Everything.

import re
html = open("home.html", encoding="utf-8").read()
payload = re.search(r'id="\$tsr-stream-barrier">(.*?)</script>', html, re.S).group(1)

print(len(re.findall(r'fileName:"([^"]+)"', payload)))

Nineteen filenames, and they are the original Markdown ones:

alexander-great-conquests.md
ancient-dna-revolution.md
ancient-egypt-pharaohs.md
apollo-programme.md
...

Each article in the payload carries a _meta object with filePath, fileName, directory, extension and path, then the slug, then the body. Nineteen bodies, complete, the longest 6,725 characters. Not excerpts. The home page displays titles and dates, and it transmits every word of every article on the site, plus the layout of the content directory they were built from.

That is the whole recovery. There was no converter to write and no HTML to reverse. The frontmatter was already there as structured data, because the loader had put it there.

Why it is in there

A router that can navigate client side has to be able to render the next page without asking the server, which means the loader data for the routes it might go to has to be present before the click. Streaming it inline is faster than fetching it, so it goes in the document. Nothing here is a bug.

The part worth knowing is what "the loader data" turns out to include. The route only asked for the article list. The content layer answered with the full article objects, and the whole of that answer was serialised. Nobody wrote a line of code that said to publish the corpus.

The number

The recovered articles were later compared word for word against the repository, once it was found by querying the GitHub API directly rather than trusting gh repo list, which had not shown it. They matched exactly. Zero differences across the set, which is the outcome you would expect from reading structured data rather than parsing rendered markup, and the reason the scraping route was worth skipping.

What this does not tell you

It does not generalise to every framework, and it is not a claim that TanStack Start leaks anything. The payload is exactly what the route asked its loader for. A route that selects title, date, slug ships title, date, slug.

It also will not help you after the tab has loaded. The cleanup function deletes $_TSR and $R.tsr once hydration and the stream have both finished, so a console opened afterwards finds nothing. The data is in the document, not in the running page, and the way to look at it is curl and a regular expression rather than devtools.

And it recovers content, not a project. Build config, component code, scripts and history are not in there. What came back was the articles and the names of the files they had lived in, which happened to be the part that could not be rewritten.

The habit I took from it: before writing a scraper, read the page source and find out whether the thing you are about to reconstruct is already sitting there as data.

tanstack start ssr seroval data recovery