Skip to content

docs: fix 118 dead mirrored links, adopt stranded pages, self-syncing changelog - #130

Merged
adnaan merged 6 commits into
mainfrom
feat/m2-ia-repairs
Aug 1, 2026
Merged

docs: fix 118 dead mirrored links, adopt stranded pages, self-syncing changelog#130
adnaan merged 6 commits into
mainfrom
feat/m2-ia-repairs

Conversation

@adnaan

@adnaan adnaan commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

M2 of the docs overhaul (M1 was #129). Structural rot rather than feature adoption — but one item is a correctness bug, not tidiness.

Closes #45. Closes #77.

118 dead links in mirrored pages

Upstream links its siblings relatively. cmd/sync rewrote only absolute GitHub URLs, so every relative link shipped broken — and could not be fixed in content/, because the next sync overwrites it. The fix belongs in the mirror step.

RewriteRelative resolves each target against the page's own source_path directory, which handles ./, ../ and ../../ uniformly, then either maps the result to a mirrored page's site URL or — when the file is real upstream but not mirrored (ROADMAP.md, docs/proposals/*) — rewrites to its GitHub URL at the synced ref, so the reader still reaches it. Fragments preserved; directory targets use /tree/.

The first pass matched only dot-prefixed targets and left 63 bare sibling links dead — more than the 55 it fixed. [Controller Pattern](controller-pattern.md) is upstream's commoner spelling and mirrors to /reference/controller-pattern.md, a URL ending in .md that tinkerdown does not serve. The second commit generalises to any ](...) target, excluding anchors, site-absolute paths, protocol-relative URLs and anything with a scheme.

Deliberately conservative in three places, each pinned by a test: only the ](...) form is matched (upstream prose contains bare relative paths that are not links); fenced code blocks are skipped (a relative path inside an example is part of the example); and it never matches across repos, because "upstream meant a file in its own repo that does not exist" is indistinguishable from "upstream meant the other repo's file", so guessing would invent links rather than fix them.

Verified by running the real sync at v0.22.0 / v0.2.0 / v0.20.0: relative links in synced pages went 118 → 0, the diff contains nothing but link edits, and every site-URL target resolves to a real page.

Five stranded pages

tinkerdown serves only nav-registered pages, so all five returned 303 to the home page — and four were linked from pages that are in the nav, including the Recipes hub. Readers following those links were silently bounced to the site root.

Adopted four: formnovalidate (it has a live mounted app and an embed), ephemeral-components, the progressive-complexity reference (not a duplicate of the guide — 170 lines of lookup tables against 777 of prose, and the guide links to it), and cli/ai-assistants.

Removed one: recipes/apps/todos.md, a stale mirror of the pre-consolidation livetemplate/examples README whose Quick Start says cd todos, a path that does not exist here. The nav's "Todos" already points at the docs-native deep dive. Its one real inbound link now does too.

Every markdown file under content/ is now either nav-registered or explicitly sidebar: false.

The test that should have caught all of it

TestSidebarWalk accepted any 303, with a comment about /cli/cli/. But an unregistered page also 303s — to the site root — and the walk followed it to the home page's 200. That is how /guides/ephemeral-components sat in this very list while being unreachable. It now refuses to follow redirects and accepts a 303 only when it points at the same path's trailing-slash variant.

Turning that on immediately failed three URLs that had been passing, which exposed a second live bug: <dir>/index.md pages are served only with a trailing slash, and nine content links used the bare form — including the Recipes hub's own "Todos" link and the Learn spine's pointer to Counter deeper. All nine fixed.

Changelog, provenance, catalog

changelog.md promised "The Phase 3 sync action will keep each section in step with its source on every release". No such entry was ever added, so it froze at livetemplate v0.8.23 while the library shipped v0.22.0 — 1568 hand-pasted lines. sync maps one source to one page and cannot concatenate four, so it is split per repo: /changelog/{livetemplate,client,cli} are real sync entries reading v0.22.0 / v0.20.0 / v0.2.0, and /changelog is an index explaining the server/client pinning.

Six recipes/apps/* pages still declared source_repo: livetemplate/examples years after consolidation. Nothing syncs them, so it was fiction — and it drives "Edit this page", sending contributors to a repo where their edit would be lost.

The Recipes hub's Apps list named four apps out of nine.

#45 — login mountPath

The recipe threaded its mount path through the handler constructor onto a controller field purely to build an absolute redirect target. ctx.Redirect("", …) has made that unnecessary since livetemplate#443: the framework emits a relative "reload self" reference and the browser resolves it against the un-stripped URL. Verified against the pinned v0.22.0 rather than assumed. TestLogin_E2E_SubpathMount — the issue's own acceptance criterion — passes.

#77 — upload-modes in CI

Gated behind two env vars since it was written, so it never ran anywhere: four upload destinations and their WebSocket-disabled fallbacks had no automated coverage. CI now fetches the client bundle from npm at livetemplate.ClientVersion — the exact version {{lvtClientScriptURL}} serves — via a new cmd/clientversion helper, so the job also guards the server/client pairing. Hardcoding a version would re-rot on the next bump, which is how the client README advertised 0.1.0 for nineteen minor versions. The whole recipe was run locally end-to-end before being written into the workflow.

sweep also took a base URL and then visited the sitemap's <loc> entries verbatim — which carry the canonical production base, so a local sweep screenshotted the deployed site. Now rehosted onto the supplied base.

Verification

  • go test ./e2e/... against a local stack: ok, 140s
  • Serialized go test -p 1 ./cmd/... ./examples/...: green, including login and patterns
  • tinkerdown validate content/: 98/98
  • Local sweep resolves 152 URLs, all on 127.0.0.1, none on fly.dev

Three links still 404 after rewriting; they are pre-existing upstream authoring bugs this surfaces rather than causes, fixed in livetemplate/livetemplate#530 and livetemplate/lvt (docs/fix-cross-repo-links).

🤖 Generated with Claude Code

https://claude.ai/code/session_01QzC2djPjPHkNJgPzFpMX7v

adnaan and others added 6 commits August 1, 2026 15:51
Every mirrored page carried links that 404 on the docs site. Upstream links
its siblings relatively — ../references/api-reference.md from
docs/guides/foo.md — which is correct in that repo and meaningless once
mirrored, because the site has no such path. The existing rewriter only
mapped absolute GitHub URLs, so 55 relative links across 16 synced pages
shipped broken, and could not be fixed in content/ because the next sync
overwrites it. The fix belongs in the mirror step.

RewriteRelative resolves each target against the page's own source_path
directory, which handles ./, ../ and ../../ uniformly, then either maps the
result to a mirrored page's site URL or — when the file is real upstream but
not mirrored (ROADMAP.md, docs/proposals/*, docs/design/*) — rewrites to its
GitHub URL at the synced ref, so the reader still reaches it. Fragments are
preserved; directory targets use /tree/ rather than /blob/.

Deliberately conservative in three places. It matches only the ](...) form,
since upstream prose contains bare relative paths that are not links. It
skips fenced code blocks, because a relative path inside an example is part
of the example — this is the regression the existing blunt ReplaceAll would
have caused, and it is pinned by a test. And it never matches across repos:
two repos can hold the same source_path, and "upstream meant a file in its
own repo that does not exist" is indistinguishable from "upstream meant the
other repo's file", so guessing would invent links rather than fix them.

Kept as a separate pass rather than folded into Rewrite: the two have
different matching rules, so the existing fence-preservation guarantees stay
intact and independently tested.

Verified by running the real sync at v0.22.0 / v0.2.0 / v0.20.0: relative
links in synced pages went 55 -> 0, the diff contains nothing but link
edits, all 16 site-URL targets resolve to real pages, and 9 of 12 GitHub
fallbacks return 200.

The other 3 are pre-existing upstream authoring bugs this surfaces rather
than causes — they were equally dead before, just silently:
  - guides/OBSERVABILITY.md links ../internal/observe/, but that package is
    at the repo root, so it is one ../ short.
  - the lvt CLI guide links ../references/{api-reference,template-support-matrix}.md,
    which exist in livetemplate, not lvt.
Filed for upstream rather than papered over here.

Also fixes 3 dead links in docs-native pages left from the examples-repo
mirror era (counter.md pointing at ../../docs/CONFIGURATION.md, chat.md at
../../README.md and a design doc); their front matter still claims
livetemplate/examples, which M2-P3 reconciles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzC2djPjPHkNJgPzFpMX7v
Five pages existed on disk and in no sidebar. That understates it: tinkerdown
only serves nav-registered pages, so all five returned 303 to the home page,
and four of them were linked from pages that ARE in the nav — the Recipes
hub, the Progressive Complexity guide, Standard HTML Reactivity, the Update
Flow. Readers following those links were silently bounced to the site root.

Adopted four:
  - recipes/formnovalidate.md -> Recipes. The sharpest case: it has a live
    mounted app (cmd/site mounts /apps/draft-form/ and the no-js variant),
    an embed, and inbound links from five pages including the Recipes hub.
  - guides/ephemeral-components.md -> Concepts, linked from the Progressive
    Complexity guide.
  - reference/progressive-complexity.md -> Reference. Not a duplicate of the
    guide despite the name — 170 lines of lookup tables against the guide's
    777 lines of prose, and the guide links to it explicitly.
  - cli/ai-assistants.md -> Ecosystem. The only one with no reader-facing
    inbound links, but it is real ecosystem content, not a stray.

Removed one. recipes/apps/todos.md was a stale mirror of the pre-consolidation
livetemplate/examples todos/README.md — generic Features/Quick Start/Testing
content whose Quick Start says `cd todos`, a path that does not exist in this
repo. The nav's "Todos" already points at recipes/todos/index.md, the
docs-native deep dive covering the same app properly (auth scoping, why
components live outside lvt:"persist", where the recipe stops). Keeping both
would put two pages titled "Todos" in front of the reader, which is the
problem this phase exists to fix. Its one real inbound link, from the Update
Flow page, now points at the deep dive.

No nav landing pages moved, so e2e/docs_ia_test.go's section->page contract
is unchanged; it passes as-is along with breadcrumb and staging.

Every markdown file under content/ is now either nav-registered or explicitly
sidebar: false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzC2djPjPHkNJgPzFpMX7v
… the walk

Four fixes that all trace back to pages the site claimed to maintain and
did not.

changelog.md said "The Phase 3 sync action will keep each section in step
with its source on every release". No such entry was ever added to
source-of-truth.yaml, so the page never synced and froze at livetemplate
v0.8.23 while the library shipped v0.22.0 — 1568 hand-pasted lines, wrong
for over a year of releases. cmd/sync maps one source file to one page and
cannot concatenate four, so the page is split per repo:
/changelog/{livetemplate,client,cli} are now real sync entries, and
/changelog is a short index explaining how the server and client versions
are pinned to each other. They currently read v0.22.0 / v0.20.0 / v0.2.0 and
will stay current without anyone touching them.

TestSidebarWalk accepted any 303 with a comment about /cli -> /cli/. But
tinkerdown serves only nav-registered pages and bounces everything else to
the site root with a 303, and the walk followed it to the home page's 200 —
so a stranded page was indistinguishable from a healthy one. That is how
/guides/ephemeral-components sat in this very list while being unreachable.
The walk now refuses to follow redirects and accepts a 303 only when it
points at the same path's trailing-slash variant.

Turning that on immediately failed three URLs that had been passing:
/recipes/counter, /recipes/todos and /recipes/progressive-enhancement are
<dir>/index.md pages that tinkerdown serves ONLY with a trailing slash, and
the bare form redirects to the site root. Nine content links used the bare
form — including the Recipes hub's own "Todos" link and the Learn spine's
pointer to Counter deeper — so readers following them landed on the home
page. All nine fixed, and the walk's own list corrected.

Six recipes/apps pages still declared source_repo: livetemplate/examples
with a source_commit from that repo, years after the consolidation made them
docs-native. Nothing syncs them, so the frontmatter was pure fiction — and it
drives the "Edit this page" link, sending contributors to a repo where their
edit would be lost. Corrected to this repo, and the index page's prose
claiming the pages are mirrored corrected with it.

Also fills the Recipes hub's Apps list, which named four apps out of nine —
login, seat-picker, shared-notepad, file-tree and upload-modes were absent
despite all being in the sidebar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzC2djPjPHkNJgPzFpMX7v
Closes #45.

The recipe threaded its own mount path through the handler constructor and
onto a controller field purely to build an absolute POST-Redirect-GET target,
because http.StripPrefix removes the mount before the handler sees the URL
and an absolute redirect cannot be reconstructed from the request.

livetemplate has not needed that since #443: ctx.Redirect("", …) emits a
relative "reload self" reference RAW in the Location header, and the *browser*
resolves it against the un-stripped request URI. Verified against the pinned
v0.22.0 rather than assumed — relativeSelfReference splits the stripped path
and returns "./<last-segment>", which resolves back to the app under a
trailing-slash mount pattern. Both mounts here use one ("/apps/login/" in
cmd/site, MOUNT_PATH normalised to a trailing slash in the standalone cmd), so
the parent-path caveat in the Redirect godoc does not apply.

Removes the Handler(mountPath, …) parameter, the AuthController.mountPath
field and its 12-line explanation, and updates both call sites. The recipe's
own prose described the workaround as a necessity; it now teaches the relative
form and why the trailing-slash mount matters.

TestLogin_E2E_SubpathMount — the acceptance criterion named in the issue —
passes: a real browser logs in under a subpath mount and lands back at the
mount rather than the domain root, and logout returns there too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzC2djPjPHkNJgPzFpMX7v
Closes #77.

sweep took a base URL, fetched <base>/sitemap.xml — and then visited the
<loc> entries verbatim. The sitemap is generated with the site's canonical
absolute base URL, so every entry points at production regardless of which
host served it. `make sweep SWEEP_URL=http://127.0.0.1:8084` therefore
screenshotted livetemplate.fly.dev and reported on content the caller had
not changed, which is exactly what it did during the M1 verification pass.
Sitemap URLs are now rehosted onto the supplied base, keeping only the path.
Confirmed: a local sweep now resolves 152 URLs, all on 127.0.0.1, none on
fly.dev.

The upload-modes browser e2e has been gated behind LVT_UPLOAD_MODES_E2E +
LVT_LOCAL_CLIENT since it was written, so it never ran anywhere — the four
upload destinations and their WebSocket-disabled fallbacks had no automated
coverage at all. It needs a real client bundle on disk because upload mode is
a client-side transport decision that server config cannot exercise.

CI now fetches that bundle from npm at livetemplate.ClientVersion — the exact
version {{lvtClientScriptURL}} serves — via a new cmd/clientversion helper.
Pinning it that way makes the job guard the server/client pairing too: a Go
dependency bump moving ClientVersion onto a client that breaks uploads fails
here instead of in production. Hardcoding a version in the workflow would
re-rot on the next bump, which is how the client README advertised 0.1.0 for
nineteen minor versions.

The recipe was verified end to end locally rather than authored blind: npm
pack at 0.20.0, extract package/dist/livetemplate-client.browser.js, run the
suite against it — all four modes pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzC2djPjPHkNJgPzFpMX7v
The first pass matched only dot-prefixed targets and left 63 bare sibling
links dead — more than the 55 it fixed. Upstream writes same-directory
references without a prefix ([Controller Pattern](controller-pattern.md)),
which is the commoner spelling, and mirrored verbatim it resolves to
/reference/controller-pattern.md — a URL ending in .md that tinkerdown does
not serve, so it 303s the reader to the site root.

Generalised: match any ](...) target and decide with isRelativeRef, which
excludes in-page anchors, site-absolute paths, protocol-relative URLs and
anything carrying a scheme (http:, https:, mailto:, tel:), checking for ':'
before the first '/' so a path containing a colon is not mistaken for one.
Everything else resolves through the same source_path-relative logic as
before, so a bare sibling that IS mirrored becomes its site URL and one that
is not — docs/guides/ARCHITECTURE.md, which does not exist upstream either —
becomes a GitHub URL instead of a dead site path.

Relative links in mirrored pages are now 0 of both spellings, verified by
re-running the real sync against all three source repos. The diff remains
links-only and every site-URL target resolves to a real page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzC2djPjPHkNJgPzFpMX7v
@adnaan
adnaan merged commit 46282a2 into main Aug 1, 2026
4 checks passed
@adnaan
adnaan deleted the feat/m2-ia-repairs branch August 1, 2026 17:00
adnaan added a commit to livetemplate/lvt that referenced this pull request Aug 1, 2026
The CLI guide's "For more information" list linked ../references/api-reference.md
and ../references/template-support-matrix.md. Neither path exists here — lvt has
no docs/references/ directory at all. Both documents describe the core library
and live in livetemplate/livetemplate, so a relative path was never going to
reach them.

Pointed at their rendered home on the docs site instead: stable across repo
layout changes, and where someone reading a CLI guide actually wants to land.

Surfaced by the docs site's sync tool, which now resolves upstream-relative
links when mirroring (livetemplate/docs#130) and turned these into visible 404s
rather than silently broken paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adnaan added a commit that referenced this pull request Aug 2, 2026
… broke (#132)

Fixes a bug introduced in #130 that corrupted five published pages.

RewriteRelative skipped fenced code blocks but not inline code spans, and Go
generic call syntax inside one — `AssertPureState[T](t)` — contains the exact
](...) shape a markdown link does. The rewriter resolved (t) as a relative path,
so documented code rendered on the live site as a GitHub URL, in
reference/session.md (three places), reference/limitations.md and
guides/ephemeral-components.md. The #130 plan flagged this exact risk; the guard
written for it covered only the fenced case, which is not the one that occurs
in this corpus.

rewriteOutsideCodeSpans splits a line on backticks and rewrites only the
even-indexed prose segments. A line with an odd number of backticks cannot have
its spans paired — the parity is wrong from the stray one onward, so code would
land on an "outside" index and be rewritten anyway — and is left entirely alone.
That costs a genuine link on a malformed line staying as upstream wrote it,
which is recoverable; mangling documented code is not.

Three regression tests, all failing against the previous implementation: an
inline span alongside a real link, multiple spans interleaved with links, and
the unbalanced-backtick case. The last one caught a wrong claim in the first
version of the safety comment, which is why the explicit count check exists
rather than relying on parity.

Re-syncs at v0.23.0, which repairs the five pages and carries the release's own
content — the Async/Pending guide rewrite, the Validate reference and the lvt-el
overlay section from livetemplate#530. Supersedes #131, closed unmerged because
it carried the same content through the unfixed rewriter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant