Three files in this tree read the same fact about the not-found page and answer
it in opposite directions. The disagreement was found while working #30 and the
argument is written there in full, in the comment of 2026-08-28; it is not
repeated here. What is here is the state, the choice it forces, and the reason
that choice has nowhere else to live: none of #30's four done-when clauses is
about what this page says about its own address, so #30 closes with this
standing, and no open issue on this board names it.
gh issue list --repo Flowfin/site --state open --limit 300 --json number,title,body \
--jq '[.[] | select((.body+.title) | test("canonical")) | .number]'
[]
Run 2026-09-02, before this issue was opened.
The state
The frame states this page's address twice, and both statements name /404.html:
grep -n -E 'rel="canonical"|og:url' dist/404.html
9: <link rel="canonical" href="https://flowfin.dev/404.html" />
15: <meta property="og:url" content="https://flowfin.dev/404.html" />
The sitemap in the same package leaves the page out, and the package says why:
grep -c '404' dist/sitemap.xml
0
grep -n -A6 'The not-found page is the one page left out' internal/site/crawler.go
53:// The not-found page is the one page left out, and it is left out rather than
54-// forgotten. A sitemap is a list of addresses a crawler is invited to fetch and
55-// index, and that page is served in answer to addresses that are not its own, so
56-// listing it asks for an index entry that sends a reader to an error page under
57-// an address the site says it has. Every other produced page is listed, and
58-// anything that is not a page is not, because what a sitemap carries is the
59-// addresses a reader can be sent to.
The reason written there is not about sitemaps. What it names is a page served in
answer to addresses that are not its own, and that is the same fact a canonical
link answers in the other direction.
The three that disagree, each in its own words:
grep -n -A5 'Canonical is the one address' internal/site/site.go
62: // Canonical is the one address this page is meant to be read at, stated
63- // inside the document so that a page reachable at a second one does not
64- // compete with itself. It is put on by whatever writes the page rather
65- // than read out of the prose, because the address is a property of where
66- // the build puts the file.
67- Canonical string
grep -n -A3 'What makes it different from every other page' internal/site/notfound.go
12:// What makes it different from every other page is the address it is served at.
13-// Every other page is served at the one address it was written for. This one is
14-// served at whatever the reader asked for, at any depth, so a reference on it
15-// that a browser resolves against the current document points somewhere
grep -n -A3 'The not-found document is the one entry not chosen here' decisions/0008-the-url-shape.md
49:The not-found document is the one entry not chosen here. The host serves a file
50-of that name at the root in response to a request that matches nothing, and it is
51-never linked, so the name is fixed by the host rather than by this scheme. That
52-is a claim about the host rather than a measurement. The same holds for the files
and one line sets the value for every page the build writes, so this page takes
its address off the table in 0008 rather than off the paragraph beneath it:
grep -n 'p.Canonical = ' internal/site/head.go
150: p.Canonical = Origin + addressOf(produced)
All run 2026-09-02 against f51005f.
What is not claimed
That this costs a reader anything today. What a crawler does with the link
depends on the status the host returns with the body, nothing this repository
produces is served anywhere, and I have not measured it. The disagreement is
between three files in this tree and is readable without a host, which is why it
does not wait on the publish in #58.
Why it is a decision rather than a repair, and why it needs its own issue
The clause a repair would make false was settled on an issue closed as completed:
gh issue view 74 --repo Flowfin/site --json state,stateReason --jq '"\(.state) \(.stateReason)"'
CLOSED COMPLETED
gh issue view 74 --repo Flowfin/site --json body --jq .body | tr '\n' ' ' | tr -s ' ' \
| grep -o 'every produced page carries a canonical address equal to the address #68 gives it'
every produced page carries a canonical address equal to the address #68 gives it
Both run 2026-09-02. Every produced page includes this one, so a build that
leaves the address off it makes that clause false, and whether the clause was
ever meant to reach this page is exactly what the three files disagree about.
The three routes, one line each, with the argument for and against each in #30:
- the page states no address, and the frame states one only where there is one,
which is the rule the icon reference in the same template already follows;
- the page keeps the address and states that it is not to be indexed, which adds
a directive this site uses nowhere rather than removing a field;
- it stays as it is, and
0008 gains a sentence saying the not-found entry
carries an address after all, so its table and its paragraph stop disagreeing.
The first two are a few lines and a golden file once the route is chosen. What is
not a change to this tree is the choice, because the clause it moves belongs to a
question already answered elsewhere.
Done when
One of the three routes is taken and this tree carries it, and #74's clause is
reconciled with it rather than left to be read both ways: either the clause is
read as not reaching this page and that reading is written where a reader of the
clause will find it, or the route chosen keeps the clause true as written. After
it, internal/site/site.go, internal/site/notfound.go,
internal/site/crawler.go and decisions/0008-the-url-shape.md say one thing
about whether this page has an address of its own rather than two, and the
commands above return that one thing.
Three files in this tree read the same fact about the not-found page and answer
it in opposite directions. The disagreement was found while working #30 and the
argument is written there in full, in the comment of 2026-08-28; it is not
repeated here. What is here is the state, the choice it forces, and the reason
that choice has nowhere else to live: none of #30's four done-when clauses is
about what this page says about its own address, so #30 closes with this
standing, and no open issue on this board names it.
Run 2026-09-02, before this issue was opened.
The state
The frame states this page's address twice, and both statements name
/404.html:The sitemap in the same package leaves the page out, and the package says why:
The reason written there is not about sitemaps. What it names is a page served in
answer to addresses that are not its own, and that is the same fact a canonical
link answers in the other direction.
The three that disagree, each in its own words:
and one line sets the value for every page the build writes, so this page takes
its address off the table in
0008rather than off the paragraph beneath it:All run 2026-09-02 against
f51005f.What is not claimed
That this costs a reader anything today. What a crawler does with the link
depends on the status the host returns with the body, nothing this repository
produces is served anywhere, and I have not measured it. The disagreement is
between three files in this tree and is readable without a host, which is why it
does not wait on the publish in #58.
Why it is a decision rather than a repair, and why it needs its own issue
The clause a repair would make false was settled on an issue closed as completed:
Both run 2026-09-02. Every produced page includes this one, so a build that
leaves the address off it makes that clause false, and whether the clause was
ever meant to reach this page is exactly what the three files disagree about.
The three routes, one line each, with the argument for and against each in #30:
which is the rule the icon reference in the same template already follows;
a directive this site uses nowhere rather than removing a field;
0008gains a sentence saying the not-found entrycarries an address after all, so its table and its paragraph stop disagreeing.
The first two are a few lines and a golden file once the route is chosen. What is
not a change to this tree is the choice, because the clause it moves belongs to a
question already answered elsewhere.
Done when
One of the three routes is taken and this tree carries it, and #74's clause is
reconciled with it rather than left to be read both ways: either the clause is
read as not reaching this page and that reading is written where a reader of the
clause will find it, or the route chosen keeps the clause true as written. After
it,
internal/site/site.go,internal/site/notfound.go,internal/site/crawler.goanddecisions/0008-the-url-shape.mdsay one thingabout whether this page has an address of its own rather than two, and the
commands above return that one thing.