Skip to content

Produce the page a host serves when an address matches nothing [#30] - #156

Merged
iderex merged 1 commit into
mainfrom
the-not-found-page
Aug 13, 2026
Merged

Produce the page a host serves when an address matches nothing [#30]#156
iderex merged 1 commit into
mainfrom
the-not-found-page

Conversation

@iderex

@iderex iderex commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The build produced no not-found document, so nothing in this repository answered
a request for an address the site does not have. What a reader gets instead is
the host's own page, on this project's own name:

curl -sS -o /dev/null -w '%{http_code}\n' https://flowfin.dev/an-address-this-site-does-not-have/
404
curl -sS https://flowfin.dev/an-address-this-site-does-not-have/ | grep -o -i -E '<title>[^<]*</title>'
<title>Page not found &middot; GitHub Pages</title>

Run 2026-08-13. That page names another party, says nothing about this project
and offers no way back into it.

What this does

The build writes 404.html at the output root, which is the one place a static
host looks, out of prose in content/not-found.txt and through the same frame
every other page goes through. So it looks like the rest of the site and gains
the properties the frame carries rather than a copy of them.

The file carries the address it sends a reader to, in a block the reader refuses
a file without. A not-found page with no way onward is where a reader leaves this
site, and it renders exactly like a finished one, so the way onward is a refusable
part of the file rather than a paragraph somebody remembers. The reader also
refuses a block that reads as a link and is not one: brackets left off, empty
brackets, an address with nothing to read, and a keyword one letter off, which is
the mistake that turns the block into a paragraph and loses only the link.

One invariant row is added, and it is about the one thing that makes this page
different from every other one. It is served in answer to whatever was asked
for, at any depth, so a reference a browser resolves against the current document
points somewhere different on every request. The row refuses a reference in any
produced page that is not absolute from the site root, which is what
decisions/0008-the-url-shape.md already says every reference in a produced page
is. The address of this page, and the fact that its name is fixed by the host
rather than chosen here, are that record's as well, and this change cites it
rather than arguing it a second time.

The page count in decisions/0003-generator-or-hand-written.md already counts
this page, so there is nothing to correct there:

grep -n 'not-found page' decisions/0003-generator-or-hand-written.md
10:privacy page, a legal notice and a not-found page. Eighteen pages, of which

Closes

Nothing. #30 stays open, and the reason is in the issue: one clause of its
done-when asks that requesting a path that does not exist on the served site
returns this page, and nothing this repository produces is served anywhere yet.

What was run

go run . ci
gate: 6 legs, in order: format, vet, test, build, links, invariants
  needs-network was not asked for. Asking costs a request to the public name from whatever machine runs it, and a verdict that moves when somebody else's service does rather than when this tree changes. Ask with: go run ./harness/needs-network
  format: ok, 39 file(s)
  vet: ok
  test: ok, 19 test file(s)
  build: ok, 4 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  invariants: ok, 28 rule(s) decided, 1 owed and not decided
6 of 6 legs ran. None was skipped.

needs-network was not asked for and is named above by the run itself. No test
was skipped, and none of this needs a display or elevation.

What the build writes:

go run . build
version 0.1.0
read templates/page.html.tmpl
read content/index.txt
read data/design-tokens.json (206 value(s))
wrote dist/index.html (1268 bytes)
wrote dist/privacy/index.html (3478 bytes, 2 checked, 3 promised, 2 residual)
wrote dist/404.html (1338 bytes, 1 way(s) onward)
wrote dist/.well-known/security.txt (379 bytes)
no assets/ in the tree, so nothing was copied verbatim
4 file(s) written into dist

The row refuses the mistake it exists for, against this tree rather than against
a fixture. With the way onward written as ../, which is correct from the page
it was typed on:

go run . invariants
  page-references-everything-from-the-site-root: REFUSED, 1 violation(s)
    it refuses a produced page carrying a reference that a browser resolves against the document it sits on rather than against the site root, an empty one included
    dist/404.html: line 17 href="../" is resolved against the document it sits on, and every reference in a produced page is absolute from the site root

And with the row taken back out of the table, the same tree passes everything,
which is what says this row is the thing that decides it:

go run . ci
  format: ok, 39 file(s)
  vet: ok
  test: ok, 19 test file(s)
  build: ok, 4 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  invariants: ok, 27 rule(s) decided, 1 owed and not decided
6 of 6 legs ran. None was skipped.

The link leg is green there on purpose rather than by luck: it resolves a
relative reference against the page it was written on and finds the file it lands
on, so a reference that is broken only for a reader at another depth is invisible
to it.

The reader's own guard, deleted and run:

go test ./internal/site/ -count=1
--- FAIL: TestNotFoundRefusesAFileWithNoWayOnward (0.00s)
    notfound_test.go:104: the file was accepted, and it carries the mistake this case is about:
        This page is not here

        The address you asked for is not one this site answers at.

Both were restored before the commit, and the run at the top of this section is
the tree as it is being pushed.

Against the lines of the budget in decisions/0005-the-speed-budget.md that can
be read off the output:

wc -c dist/404.html
1338 dist/404.html
grep -c -i -E '<style|<script|@font-face|url\(' dist/404.html ; echo "exit=$?"
0
exit=1
grep -o -E '(href|src)="[^"]*"' dist/404.html
href="#content"
href="/"
href="https://github.com/Flowfin/site/blob/main/NOTICE.md"

Every command in this section run 2026-08-13 at f333efd. 1338 bytes of markup
against a limit of 20 KB, no inlined stylesheet against a limit of 12 KB, no
script and no web font. The third reference is a link a reader may follow rather
than something the page fetches, which is the distinction the origin row already
draws, and the run above is green on that row.

The means

Go, the template package already in the toolchain, and a plain text file in
content/, which is what every other page here is made of. It adds no language,
no dependency and no runtime. The page property it introduces is decided by the
gate that decides the others, from the same population, so the rule is refusable
by a machine rather than described in a document. The guard in the reader is
shown to bite above by deleting it, and the row is shown to bite by writing the
mistake into the tree and then by taking the row out and watching the same tree
pass.

Who read it

Nobody else. This change carries no second reader, and the evidence above stands
in place of one.

A static host answers a request that matches nothing with one file at a fixed
name, and where the repository provides none the reader is shown the host's own
page on this project's domain. That is what a reader gets today: a missing
address under the published name answers 404 with a title of "Page not found
&middot; GitHub Pages", which says nothing about this project and offers no way
back into it.

So the build writes 404.html, through the same frame every other page goes
through, out of prose in content/. The file carries the address it sends a reader
to, in a block the reader refuses a file without, because a not-found page with
no way onward is where a reader leaves the site and it renders exactly like a
finished one.

The new row is about the one thing that makes this page different from every
other. It is served in answer to whatever was asked for, at any depth, so a
reference a browser resolves against the current document points somewhere
different on every request. The row refuses a reference in any produced page that
is not absolute from the site root, which is what record 0008 already says every
reference is, and nothing in the tree decided it until now: with the row taken
out, a not-found page linking to "../" passes the whole gate, the link leg
included, because that leg resolves a relative reference and finds the file it
lands on.

The address of the page and the fact that its name is fixed by the host rather
than chosen here are record 0008's, and this change cites it rather than
restating the argument.

Signed-off-by: Nils Lehnen <[email protected]>
@iderex iderex added the enhancement New feature or request label Aug 13, 2026
@iderex iderex self-assigned this Aug 13, 2026
@iderex iderex added this to the The pages milestone Aug 13, 2026
@iderex
iderex merged commit d027582 into main Aug 13, 2026
17 checks passed
@iderex
iderex deleted the the-not-found-page branch August 13, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant