Skip to content

fix(webapp): a wikilink is a real link, not a wiki: string (BEA-136) - #151

Open
ssowonny wants to merge 1 commit into
mainfrom
bea-136-ph-scan-bug-rendered-wikilinks-emit-a-wiki-pseudo-scheme
Open

fix(webapp): a wikilink is a real link, not a wiki: string (BEA-136)#151
ssowonny wants to merge 1 commit into
mainfrom
bea-136-ph-scan-bug-rendered-wikilinks-emit-a-wiki-pseudo-scheme

Conversation

@ssowonny

Copy link
Copy Markdown
Contributor

TL;DR

  • Copy-link, middle-click and open-in-new-tab on a [[wikilink]] handed you wiki:guide — a dead string. Plain clicks always worked, which is why nobody caught it.
  • The anchor now carries the real /<project-id>/guide.md URL, so every way of using a link works the way it looks like it should.
  • A wikilink pointing at nothing loses its href and renders greyed with a dotted underline, instead of pretending to be a link.
  • Free fix while the line was open: cmd-clicking a plain relative markdown link now opens a tab too, instead of hijacking the current one.
  • Known gap, deliberate: /s/<token> share pages still render dead wikilinks — the target isn't part of the share, so a real URL would hand an anonymous visitor a 404.

Wikilinks resolved at click time. The DOM kept the server's wiki: marker, and the click handler rescued only the one path that never reads the attribute:

flowchart TD
    A["one rendered anchor<br/>&lt;a href=&quot;wiki:guide&quot;&gt;"] --> B["left-click"]
    A --> C["copy link / middle-click / cmd-click / new tab"]
    B --> D["handler intercepts,<br/>opens guide.md<br/>WORKS"]
    C --> E["browser follows href<br/>wiki:guide<br/>DEAD"]
Loading

The fix moves the match one step earlier, into transformHTML — the pass that already rewrites this HTML before the mount (relative image srcs, external-link targets). Nothing patches the mounted dangerouslySetInnerHTML subtree, which is the failure CLAUDE.md calls out by name.

flowchart LR
    S["server<br/>markdown.go<br/>href=wiki:guide"] --> T["transformHTML NEW<br/>match flatFiles<br/>href=/&lt;pid&gt;/guide.md"]
    T --> D["mounted DOM<br/>copy, middle-click,<br/>new tab all real"]
    D --> H["click handler<br/>plain -&gt; openFile<br/>modified -&gt; browser"]
Loading

What changed

Where Change
src/util.ts New pure resolveWiki(target, files) — the matching rules lifted verbatim out of openWikilink (exact path, path + .md, basename, basename + .md, all case-insensitive, path before basename). Pure and DOM-free, so node --test covers the whole matrix without a browser.
src/components/FileView.tsx transformHTML gains files + projectId. A hit gets href=urlForPath(hit.path, projectId) and a data-wiki marker; a miss loses its href and gets class="wiki-missing" + a title. openWikilink is gone.
src/components/FileView.tsx handleLinkClick intercepts data-wiki anchors only, and returns early for modified / non-primary clicks — the same rule nav.ts:linkProps applies to every other link in the app.
src/apps/Browser.tsx Passes projectId={project?.id} down. Volume mode has none, so urlForPath yields /<path> there.
src/style.css .markdown a.wiki-missing — an anchor with no href loses link styling entirely, and a broken cross-reference silently reading as prose is worse than the dead URL it replaced.
e2e_serve_test.go The seeded index.md gains a dangling [[nowhere]], so the unresolved case has something to assert against. Content-only change to an existing op.

markdown.go is untouched. wiki: stays the marker the server leaves behind (RenderMarkdown is pure and has no file tree) — it just never survives to the mounted DOM.

Why data-wiki rather than "any root-absolute href": an author writing [x](/somewhere) in their own markdown keeps today's behavior instead of quietly becoming an SPA route, and the target path is read back directly instead of re-parsed out of a URL.

The one deviation from the reviewed plan

resolveWiki takes a structural { path, name }[] instead of importing Node from api/types. Same call sites, same behavior, one less import in the module the node test runner loads.

Before / after

The bug lives in the href attribute, so the screenshots only show the visible half — the new unresolved-wikilink treatment. The attribute itself is in the DOM read below them.

before (origin/main) after
before after

The same page's anchors, read out of the live DOM:

before:  [ {text: "guide",   href: "wiki:guide"},
           {text: "notes",   href: "notes/readme.md"} ]

after:   [ {text: "guide",   href: "/<pid>/guide.md"},
           {text: "notes",   href: "notes/readme.md"},
           {text: "nowhere", href: null, class: "wiki-missing"} ]

Verification

  • go test ./... — pass (full suite).
  • npm test — 79 pass, including the new resolveWiki matrix (path/basename precedence, implied .md, mixed case, miss returns undefined).
  • npm run e2e — 171 pass, 1 skipped (pre-existing). The existing wikilink navigates to the target file now asserts getAttribute("href") and that a plain click did not reload the document; a new spec covers the modified click staying put and [[nowhere]] rendering with no href and no wiki: anywhere in #content.
  • npm run build run and internal/webapp/static/ committed; check-dist.sh clean.
  • UI checked by driving the real hub (screenshots above are that session).

The architecture/webapp-frontend.md note on transformHTML gained a sentence describing the new pass. No types or relationships changed, so there's no architecture diff section.

Build session

cd $(git worktree list | grep bea-136 | awk '{print $1}') && claude --resume 283443ee-89d8-4f4e-9de0-67a725084139

(Only works on the machine that built this.)

[[guide]] rendered as href="wiki:guide" — a pseudo-scheme no browser can
resolve. The delegated click handler rescued a plain left-click, so the
feature looked fine until someone copied the link, middle-clicked it, or
opened it in a new tab and got a dead string.

Resolution moves from click time to transform time: transformHTML (the
pass that already rewrites this HTML before the mount) matches the target
against flatFiles and writes the real urlForPath() URL, plus a data-wiki
marker. A wikilink matching no file loses its href entirely and renders as
.wiki-missing, so no "wiki:" survives into the DOM either way.

The matching rules didn't change — they moved into a pure resolveWiki() in
util.ts, where node --test covers the whole matrix without a browser.

The consequence to get right is the click: real hrefs mean a plain click
must be intercepted (or it does a full document load) and every modified
click must be let through (or the fix buys nothing) — the same rule
nav.ts:linkProps applies everywhere else. The guard sits above both
branches, so cmd-clicking a relative markdown link now opens a tab too
instead of SPA-navigating the current one.

markdown.go is unchanged: wiki: stays the marker the server leaves behind
because RenderMarkdown has no file tree. /s/<token> share pages keep their
dead wikilinks by the spec's decision — the target isn't part of the share.

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

Development

Successfully merging this pull request may close these issues.

1 participant