docs: fix 118 dead mirrored links, adopt stranded pages, self-syncing changelog - #130
Merged
Conversation
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
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>
This was referenced Aug 1, 2026
Closed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/syncrewrote only absolute GitHub URLs, so every relative link shipped broken — and could not be fixed incontent/, because the next sync overwrites it. The fix belongs in the mirror step.RewriteRelativeresolves each target against the page's ownsource_pathdirectory, 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.mdthat 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), andcli/ai-assistants.Removed one:
recipes/apps/todos.md, a stale mirror of the pre-consolidationlivetemplate/examplesREADME whose Quick Start sayscd 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 explicitlysidebar: false.The test that should have caught all of it
TestSidebarWalkaccepted 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-componentssat 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.mdpages 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.mdpromised "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/changelogis an index explaining the server/client pinning.Six
recipes/apps/*pages still declaredsource_repo: livetemplate/examplesyears 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
mountPathThe 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 newcmd/clientversionhelper, 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.sweepalso 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, 140sgo test -p 1 ./cmd/... ./examples/...: green, includingloginandpatternstinkerdown validate content/: 98/98Three 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