Domain-Aware Note URLs
Date: 2026-05-04
Status: Approved
Problem
Two places in the app return note URLs but neither accounts for custom domain routing:
- GraphQL
NoteViewhaspermalink(path only, e.g./my/note) but no fullurlfield. - MCP search results build
URL = env.PublicURL() + note.Permalink, always using the main domain even if the note is served on a custom domain.
NoteViews.ResolveURL exists but is an unimplemented stub returning only the path.
Goal: Both GraphQL and MCP return the full URL, preferring the first custom domain route if one exists, falling back to publicURL + permalink.
Shared Infrastructure (model layer)
Add a reverse route index to NoteViews:
// In NoteViews struct
customDomainRoutes map[int64]struct{ Host, Path string }
Populated in RegisterNoteRoutes: for each route with non-empty host, store the first one per note.PathID.
Implement ResolveFullURL (replacing the current stub):
func (nv *NoteViews) ResolveFullURL(note *NoteView, publicURL string) string {
if r, ok := nv.customDomainRoutes[note.PathID]; ok {
scheme := schemeFrom(publicURL) // parses scheme from publicURL
return scheme + "://" + r.Host + r.Path
}
return publicURL + note.Permalink
}
GraphQL
- Add
url: String! @goField(forceResolver: true)toNoteViewinschema.graphqls - Run
make gqlgento regenerate resolver stub - Add
NoteURL(*model.NoteView) stringto the GraphQL resolverEnvinterface - App implements:
return a.noteViews.ResolveFullURL(note, a.config.PublicURL) - Resolver:
return r.env.NoteURL(obj), nil
MCP
- Add
NoteURL(*model.NoteView) stringto MCPEnvinterface - Same app implementation as GraphQL
- Change
buildSearchPayload,buildSimilarPayload,searchResultItemFromNoteto acceptnoteURL func(*model.NoteView) stringinstead ofpublicURL string - Call site:
buildSearchPayload(query, results, env.NoteURL, env.LatestNoteChunks())
E2E Tests
Seed note
Add one .md file to the seedvault with:
---
route: customdomain.test/mcp-url-test
---
Check if the existing multidomain seedvault notes already provide a suitable note; if not, add a dedicated one (e.g. mcp-url-test.md).
e2e/note-url.spec.js (GraphQL)
- Query
notePaths { latestNoteView { url permalink } } - Assert: note with
route: customdomain.test/mcp-url-testreturnsurlstarting withhttp://customdomain.test/ - Assert: regular notes return
urlstarting withAPP_URL
e2e/mcp-url.spec.js (MCP)
tools/call→searchwith a query matching the custom-domain note- Assert: result
urlstarts withhttp://customdomain.test/ - Assert: result
urlfor a regular note starts withAPP_URL
Key Decisions
- First custom domain wins: if a note has multiple custom domain routes, the first one registered is used
- Scheme is derived from
publicURL(sohttp://in dev,https://in prod) - Both GraphQL and MCP use the same
NoteURLmethod onapp, backed by the sameResolveFullURLlogic NoteViews.ResolveFullURLis the single source of truth; the old stubResolveURLis replaced