Skip to content

Canonical Deep Link

Verified against the code · 19d408be · 2026-08-25

All code-checkable claims match current code: canonicalNodeShareUrl/copyShareLink (frontend/app.js:4481-4497) build the link from the UUID extracted via uriToId (app.js:6326-6329) + BASE_URL, never the raw @id; notifications.py:66-67 builds {base}/node/{uuid} from req.node_id; export_service.canonical_web_uris (export_service.py:62-77) stamps the primary origin on every hasDomain edge regardless of per-domain publicURI, exactly as described; resolveDomainBaseForNode/nodeToTreeIndex (app.js:2768-2777) and the non_local_node/proxyRoutes 404 fallback in loadNodeDetail (app.js:4846-4864, 6352-6365) are present and match the described consumer-side resolution.

A node deep link — the URL you share from the app, receive by email, or follow from an export to open one specific node — is always the clean, domain-agnostic form:

{PRIMARY_ORIGIN}/node/{uuid}

wrapped for the single-page app as a hash link:

{PRIMARY_ORIGIN}/#{urlencode({PRIMARY_ORIGIN}/node/{uuid})}

It carries no domain and no transport. It is never the proxy form {origin}/api/proxy/{domain}/node/{uuid}. The owning domain is not part of a node's address — the consumer discovers it at open time.

Why one form

The format drifted into two shapes. The node-share email built the clean form from the node UUID, while the in-app copy-share-link copied the node's @id, and the FME webURI was the domain's publicURI. For a secondary-domain node both of the latter are the proxy form (because a secondary's publicURI is the /api/proxy/{domain} routing path), so the same node produced two different links — and a proxy-form link breaks the moment the baked-in domain is wrong. A node's identity must not encode how it was fetched (Contract 02.01: identity, not transport). The clean form is the canonical one because it is the simplest, and because the backend already resolves a bare UUID to its domain.

Producers (all emit the clean form)

  • In-app share buttoncopyShareLink builds the link from the node UUID + BASE_URL via canonicalNodeShareUrl(), never from the node @id.
  • Node-share emailnotifications.py builds {base}/node/{uuid} from the UUID.
  • FME webURI (consumed by GraphAnalyzer) — export_service.canonical_web_uris() stamps the canonical primary origin on every hasDomain edge, so GA's webURI + "/node/" + id yields the clean form.

Consumer (resolves the domain)

restoreFromHash / navigateToUri resolve the owning domain from the loaded trees (resolveDomainBaseForNodenodeToTreeIndexallTrees[idx].domainBase). For a node not in any loaded tree, loadNodeDetail follows the backend's structured non_local_node 404 (which lists proxyRoutes) to the owning secondary domain. The clean link therefore resolves for any domain without the domain ever appearing in the URL.

Authority

EIDOS Explorer defines this contract. Adjacent systems — GraphAnalyzer, DocuGraph, any FME consumer — conform: they consume whatever canonical webURI EIDOS emits and must not reintroduce a domain- or transport-coupled link. See the Domain Routing Key invariant for the matching rule on the fetch side (route by domain identity, never the emitted host).