Skip to content

Resolve relative links from index.md against its own folder#651

Merged
srid merged 3 commits intomasterfrom
fix-index-relative-links
Apr 25, 2026
Merged

Resolve relative links from index.md against its own folder#651
srid merged 3 commits intomasterfrom
fix-index-relative-links

Conversation

@srid
Copy link
Copy Markdown
Owner

@srid srid commented Apr 24, 2026

Relative URLs inside <dir>/index.md now resolve against <dir>/, not its parent. Closes #608. Previously, Emanote canonicalized subfolder/index.md to route subfolder (dropping the trailing index slug), and then derived the link-resolution base by taking that route's parent — which landed one folder too high. So [foo](./foo.md) inside subfolder/index.md resolved to foo.md at the root instead of subfolder/foo.md.

The fix derives the base from the note's on-disk source filepath (takeDirectory) rather than its canonicalized route. A new noteResolveLinkBase :: Note -> Maybe (R 'Folder) lives in Emanote.Model.Note, and modelResolveLinkBase :: Model -> LMLRoute -> Maybe (R 'Folder) in Emanote.Model.Type wraps the model lookup so the four splice callsites (Url.urlResolvingSplice and three in Pandoc.Renderer.Embed) collapse to a single function call. Synthesized notes (placeholders, missing-note pages) lack an on-disk path and fall back to noteParent — they don't trigger the bug since they have no canonicalized source to drift from.

Five new unit tests in RelSpec cover top-level files, subfolder index files, deeply nested index files, sibling non-index files, and an end-to-end parseUnresolvedRelTarget resolution from subfolder/index.md. An additional e2e scenario runs in both live and static modes against a subfolder/index.md + subfolder/sibling.md fixture, asserting the article-body anchor's href resolves to subfolder/sibling.html. The selector is scoped to <article> because the sidebar nav surfaces a child link to the same target — and that path is auto-generated from the route hierarchy, so it stays correct even when the bug is live.

The post-implement Hickey review flagged the lookup-and-fallback algorithm as inlined identically at four splice sites. The second commit extracts modelResolveLinkBase to encode the rule once. Lowy review was clean.

srid added 2 commits April 24, 2026 19:36
#608)

Notes loaded from `<dir>/index.md` had their canonicalized route stripped
of the trailing `index` slug (so `subfolder/index.md` got route
`subfolder`). The route's parent — used as the base for resolving
relative URLs — was thus the wrong folder (the parent of `subfolder`,
not `subfolder/` itself), so `[foo](./foo.md)` inside
`subfolder/index.md` resolved to `foo.md` instead of `subfolder/foo.md`.

Derive the base from the note's on-disk source filepath rather than from
its canonicalized route. Adds `noteResolveLinkBase` and the pure helper
`resolveLinkBaseFromFilePath` in `Emanote.Model.Note`; updates the four
callsites in `Rel.noteRels`, `Url.urlResolvingSplice`, and the three
embed splices in `Embed`.
The post-implement Hickey review flagged that the algorithm "look up the
note for `r`; if found, use `noteResolveLinkBase`, otherwise fall back to
`R.routeParent r`" was inlined identically at four splice callsites (one
in `Url`, three in `Embed`). The rule lived only as a social contract
between sites — a future change would require updating all four.

Extract `modelResolveLinkBase :: ModelT f -> LMLRoute -> Maybe (R 'Folder)`
in `Emanote.Model.Type` so the four callsites collapse to a single
function call.
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 24, 2026

Hickey/Lowy Analysis

# Lens Finding Disposition
1 Hickey Four-site duplication of "lookup note; if found use noteResolveLinkBase, else fall back to routeParent" Fixed in this PR
2 Lowy (no findings)

Hickey rationale

The unenforced rule "when resolving links for a note identified by noteRoute, look up the note; if found, use noteResolveLinkBase; otherwise fall back to R.withLmlRoute R.routeParent noteRoute" was encoded identically at four locations: three splices in Pandoc.Renderer.Embed (embedBlockWikiLinkResolvingSplice, embedBlockRegularLinkResolvingSplice, embedInlineWikiLinkResolvingSplice) and one in Pandoc.Renderer.Url (urlResolvingSplice). The rule lived only as a social contract — a future change to the algorithm would require updating four sites, and missing one would produce a silent divergence. Extracted to modelResolveLinkBase :: ModelT f -> LMLRoute -> Maybe (R 'Folder) in Emanote.Model.Type so each callsite reduces to a single function call. (Commit af887c47.)

Lowy rationale

Volatility is correctly encapsulated. noteResolveLinkBase isolates the volatile decision — "derive link-base from on-disk path when available, fall back to route hierarchy otherwise" — behind a stable interface. That decision is an observed axis of change (the bug itself), not speculative. Placement in Note.hs is correct: the function is a pure computation over Note attributes, and putting it in Rel.hs would conflate link-base derivation (an input property) with link resolution. The fallback for synthesized notes (no _noteSource) is sound — synthesized notes are not the class of notes the bug affected, and noteParent is the only signal available. Reuse across four independent splice callsites is the pattern expected of a volatility boundary.

@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 24, 2026

/do results

Step Status Duration Verification
sync 0s git fetch ok; forge=github; noGit=false
research 5m 28s Located bug at Rel.hs:74 + Url.hs:32 + Embed.hs:34/51/62. Fix: derive parent from _noteSource filepath instead of canonicalized route.
branch 4s On branch fix-index-relative-links from origin/master.
implement 7m 36s Added noteResolveLinkBase + resolveLinkBaseFromFilePath in Note.hs; updated 4 callsites; 4 unit tests in RelSpec.hs all pass.
check 5s cabal build all: up to date, no errors.
docs 12s Existing docs accurate; no update needed.
fmt 25s pre-commit: cabal-fmt, fourmolu, hlint, nixpkgs-fmt all pass.
commit 21s Commit feec272 pushed.
hickey+lowy 6m 23s Hickey: 1 finding (4-site dedup) → fixed in af887c4. Lowy: no findings.
police 3m 59s Rules clean (7/7); fact-check clean; 1 deferred efficiency concern.
test 10s 28/28 tests pass.
create-pr 41s Draft PR #651 with hickey/lowy comment.
ci 4m 27s All 3 CI checks green on HEAD af887c4.
Total 31m 47s

Slowest step: implement (7m 36s)

Optimization suggestions

  • implement dominated: research and implement together took 13 minutes. Splitting the bug fix from the test scaffolding into separate iterations (write the failing test, then write the smallest possible fix) might shave time, but for a bug-fix-with-helper-extraction this is roughly the floor.
  • hickey+lowy (6m 23s): ran sequentially rather than in parallel. Future runs should emit both Agent tool calls in a single response — easily halves this step.
  • ci (4m 27s): includes Playwright browser cache hit + e2e tests. For followup pushes on this branch, --from ci-only would skip everything before CI.
  • police (3m 59s): three lenses ran serially against the diff. /simplify already runs its three lenses in parallel; the rules+fact-check passes are bounded by main-context work. Acceptable.

Add a `subfolder/index.md` + `subfolder/sibling.md` to the e2e fixture
notebook, plus a smoke scenario that opens `/subfolder.html` and
asserts the article-body link to `./sibling.md` resolves to a href
containing `subfolder/sibling`. The selector is scoped to <article>
because the sidebar nav surfaces a child link to the same target — and
that path is auto-generated from the route hierarchy, so it stays
correct even when the bug is live. Only the article anchor exercises
the relative-URL resolver we're testing.

Also note the fix in CHANGELOG.md under 1.6.0.0 → Bug fixes.
@srid srid marked this pull request as ready for review April 25, 2026 00:37
@srid srid merged commit 1b2cb22 into master Apr 25, 2026
4 checks passed
@srid srid deleted the fix-index-relative-links branch April 25, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relative paths in an folder/index.md file

1 participant