Skip to content

Resolve canonicals whose URL last segment differs from the resource id (cross-namespace canonicals)#1331

Open
KyleOps wants to merge 2 commits into
HL7:masterfrom
KyleOps:feat/canonical-url-redirects
Open

Resolve canonicals whose URL last segment differs from the resource id (cross-namespace canonicals)#1331
KyleOps wants to merge 2 commits into
HL7:masterfrom
KyleOps:feat/canonical-url-redirects

Conversation

@KyleOps

@KyleOps KyleOps commented Jun 19, 2026

Copy link
Copy Markdown

Problem

A FHIR resource can legitimately have a canonical URL whose last path segment differs from its id (and therefore from its rendered filename). When it does, the published canonical does not resolve — it 404s and falls through to the IG landing/history page.

Real-world case: AU Base external-terminology extensions. By convention these are authored as:

  • id = au-v3-ActEncounterCode-extended → rendered file ValueSet-au-v3-ActEncounterCode-extended.html
  • url (canonical) = http://terminology.hl7.org.au/ValueSet/v3-ActEncounterCode-extended (no au-)

FHIR does not require id and url to match, and this difference is intentional (the AU terminology authors confirmed the convention: id gets the au- prefix; the canonical uses <hl7-au-namespace>/<external-id>-extended). 13 resources in AU Base 6.x follow it.

Dereferencing the canonical resolves to …/ValueSet-v3-ActEncounterCode-extended.html, but the publisher only ever wrote …/ValueSet-**au-**v3-ActEncounterCode-extended.html, so → 404.

Root cause

spec.internals already records the canonical → rendered-file mapping, e.g.:

"http://terminology.hl7.org.au/ValueSet/v3-ActEncounterCode-extended" : "ValueSet-au-v3-ActEncounterCode-extended.html"

But IGReleaseRedirectionBuilder.buildCloudRedirections() only emits a stub when the path key is IG-canonical-relative; cross-namespace canonicals come through parseSpecDetails as full-URL keys and are dropped by the if (!s.contains(":")) guard. So no redirect is generated for them, and resolution targets a <Type>-<tail>.html file that doesn't exist.

Fix

For a cross-namespace canonical (full-URL key) whose last segment differs from the rendered file, emit a flat alias <Type>-<tail>.html that redirects to the actual rendered <Type>-<id>.html (the mapping is already in spec.internals). Guards:

  • no-op when <Type>-<tail> already equals the rendered id (the common case — no alias, no overhead);
  • only when the real rendered page exists in that folder;
  • never overwrites a real rendered page (only an existing redirect stub).

Generated in each published version folder, so versioned canonicals (…|6.0.0) resolve too.

Scope: the cloud/static redirect path (buildCloudRedirections). Builds on #1327 (static-HTML redirect template + wiring CLOUD into -go-publish); the net-new change here is one method, createCanonicalUrlAlias.

Testing

Built the jar and ran -go-publish (server type cloud) on AU Base 6.x, deployed the output to a GitHub Pages preview:

  • Before (prod today): https://hl7.org.au/fhir/ValueSet-v3-ActEncounterCode-extended.htmlContent Not Found.
  • After (preview): the alias ValueSet-v3-ActEncounterCode-extended.html is generated, returns 200, and redirects to ValueSet-au-v3-ActEncounterCode-extended.html (the real resource page).
  • All 13 affected canonicals now resolve (e.g. CodeSystem-v2-0203, CodeSystem-v3-ActCode, ValueSet-jurisdiction-extended, ValueSet-v3-ServiceDeliveryLocationRoleType-extended, …).
  • No regression: a normal canonical where id == tail (e.g. ValueSet-accession-number-type) is unchanged — it renders directly, no alias emitted.

The full cross-host chain (terminology.hl7.org.au/...hl7.org.au/fhir/... → alias) is completed by the existing edge/host rewrite; this change supplies the file that rewrite lands on.

Live before/after

⚠️ The aliases this PR generates are absolute (as described above). GitHub Pages can't perform the production host rewrite and would bounce an absolute production URL off-site, so on this preview the alias files were manually rewritten to relative purely to make the redirect clickable within Pages. The preview demonstrates that the alias is generated and points to the correct resource; it does not reflect a change in redirect style — the PR remains absolute.

Design note

Aliases use absolute redirect targets, consistent with the existing canonical/version redirect stubs (and with FHIR canonicals being absolute global identifiers). Making preview builds host-agnostic (so absolute links don't point at production) is a separate, broader concern (a preview/-base override) and is intentionally out of scope here.

Affected resources (AU Base 6.x)

CodeSystems: au-v2-0203, au-v2-0360, au-v2-0443, au-v3-ActCode, au-location-physical-type, au-location-type.
ValueSets: au-v2-0203-extended, au-v2-0360-extended, au-v2-0443-extended, au-v3-ActEncounterCode-extended, au-v3-ServiceDeliveryLocationRoleType-extended, au-jurisdiction-extended, au-location-physical-type-extended.

(Genuinely-external code systems like mims-externalhttp://www.mims.com.au/codes are correctly left alone — their canonical points to an outside authority and shouldn't resolve to the IG.)


Note: this branch is based on #1327, so the diff includes that PR's commit until it merges; the net-new commit here is the createCanonicalUrlAlias change.

KyleOps added 2 commits June 17, 2026 20:26
…o-publish

ServerType.CLOUD is intended for static hosts (S3, GitHub Pages, etc.) that
cannot execute server-side redirect scripts, but two defects made it
produce non-functional output:

1. HTML_TEMPLATE was a verbatim copy of PHP_TEMPLATE, so createHtmlRedirect()
   wrote PHP into index.html. Replaced it with a static HTML redirect
   (meta refresh + window.location.replace + canonical link), with JSON/XML
   links for machine clients. Content negotiation is not possible without a
   server runtime, so browsers go to the human-readable page.

2. PublicationProcess.updatePublishBox (the -go-publish path) had no CLOUD
   branch, so cloud sites fell through to buildApacheRedirections() and got
   PHP. Added the missing CLOUD -> buildCloudRedirections() case, matching
   IGReleaseUpdater.
…-namespace)

A resource can legitimately have a canonical whose last path segment differs from
its id/filename — e.g. AU external-terminology extensions, where the id is
'au-<x>-extended' (file ValueSet-au-<x>-extended.html) but the canonical is
http://terminology.hl7.org.au/ValueSet/<x>-extended (no 'au-'). FHIR does not
require id and url to match.

Today the cloud redirect builder skips these (the spec.internals 'paths' entry has
a full-URL key, dropped by the !contains(":") guard), so dereferencing the
published canonical 404s: resolution targets <Type>-<tail>.html, which doesn't
exist (the file is <Type>-<id>.html).

Emit a flat static-HTML alias <Type>-<tail>.html -> the rendered <Type>-<id>.html
for such canonicals (the mapping is already in spec.internals). No-op when tail==id
(the common case) and never overwrites a real rendered page.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant