Problem
rw's canonical page identity is (sectionRef, subpath) — it's what listPages() returns, what PageMeta exposes, and what comments are keyed by. But every read API on the napi surface is keyed by full site path: renderPage(path), renderSearchDocument(path), getPageMarkdown(path).
So any JS consumer holding a page identity has to turn it into a path before it can read the page. Today that means re-deriving the section's scope path (listSections() → find section → join) — i.e. reimplementing, in TypeScript, a resolver rw already owns.
Because rw already has the resolver:
// crates/rw-site/src/site.rs
pub fn page_path_for(&self, section_ref: &str, subpath: &str) -> Option<String>
It's the exact inverse of SiteState::section_location (the function list_pages() uses to produce the identity), it reloads internally so cold sites resolve correctly, and rw-comments already uses it for precisely this purpose — mapping a sectionRef#subpath key to a page (crates/rw-comments/src/creation.rs).
It just isn't exposed through napi.
Proposal
Expose it:
RwSite.pagePathFor(sectionRef: string, subpath: string): Promise<string | null>
Purely additive — no existing signature or type changes. It wraps Site::page_path_for directly; None → null.
Why the primitive rather than re-keying a method
The alternative is re-keying the read methods to take (sectionRef, subpath). That solves it one method at a time and churns public signatures. Exposing the resolver once makes every path-keyed API — renderPage, renderSearchDocument, getPageMarkdown — addressable by page identity, and keeps rw the single owner of identity→path resolution instead of having each embedding host grow its own copy in another language.
Cost is one extra napi call per read (pagePathFor then getPageMarkdown), both an atomic snapshot load plus a lookup — negligible next to rendering.
Note page_path_for returns Option, collapsing "no such section ref" and "no such page" into one null. That's fine for the caller I have in mind (a 404 either way), but worth stating.
What it unblocks
rwdocs/backstage-plugins#102 — the Backstage search collator emits documents that can't be resolved back to the page they describe (location strips the section scope prefix and isn't round-trippable). The fix is for the collator to emit rw's own page identity (siteRef + sectionRef + subpath), and for the rw-backend plugin to serve a page's Markdown from that identity:
GET /site/:ns/:kind/:name/markdown?sectionRef=…&subpath=…
→ site.pagePathFor(sectionRef, subpath) // null → 404
→ site.getPageMarkdown(path)
With pagePathFor, that route is a thin composition. Without it, the plugin has to carry a TypeScript reimplementation of page_path_for that must track rw's section semantics.
Together with getPageMarkdown (#648) this makes an MCP docs-retrieval tool straightforward: search hit → identity → Markdown, with no path arithmetic anywhere in the consumer.
Happy to send the PR if the shape looks right.
Problem
rw's canonical page identity is
(sectionRef, subpath)— it's whatlistPages()returns, whatPageMetaexposes, and what comments are keyed by. But every read API on the napi surface is keyed by full site path:renderPage(path),renderSearchDocument(path),getPageMarkdown(path).So any JS consumer holding a page identity has to turn it into a path before it can read the page. Today that means re-deriving the section's scope path (
listSections()→ find section → join) — i.e. reimplementing, in TypeScript, a resolver rw already owns.Because rw already has the resolver:
It's the exact inverse of
SiteState::section_location(the functionlist_pages()uses to produce the identity), it reloads internally so cold sites resolve correctly, and rw-comments already uses it for precisely this purpose — mapping asectionRef#subpathkey to a page (crates/rw-comments/src/creation.rs).It just isn't exposed through napi.
Proposal
Expose it:
Purely additive — no existing signature or type changes. It wraps
Site::page_path_fordirectly;None→null.Why the primitive rather than re-keying a method
The alternative is re-keying the read methods to take
(sectionRef, subpath). That solves it one method at a time and churns public signatures. Exposing the resolver once makes every path-keyed API —renderPage,renderSearchDocument,getPageMarkdown— addressable by page identity, and keeps rw the single owner of identity→path resolution instead of having each embedding host grow its own copy in another language.Cost is one extra napi call per read (
pagePathForthengetPageMarkdown), both an atomic snapshot load plus a lookup — negligible next to rendering.Note
page_path_forreturnsOption, collapsing "no such section ref" and "no such page" into onenull. That's fine for the caller I have in mind (a 404 either way), but worth stating.What it unblocks
rwdocs/backstage-plugins#102 — the Backstage search collator emits documents that can't be resolved back to the page they describe (
locationstrips the section scope prefix and isn't round-trippable). The fix is for the collator to emit rw's own page identity (siteRef+sectionRef+subpath), and for the rw-backend plugin to serve a page's Markdown from that identity:With
pagePathFor, that route is a thin composition. Without it, the plugin has to carry a TypeScript reimplementation ofpage_path_forthat must track rw's section semantics.Together with
getPageMarkdown(#648) this makes an MCP docs-retrieval tool straightforward: search hit → identity → Markdown, with no path arithmetic anywhere in the consumer.Happy to send the PR if the shape looks right.