Skip to content

Add host mode routing from realm.json#4709

Open
backspace wants to merge 22 commits into
mainfrom
routing-from-config-cs-10054
Open

Add host mode routing from realm.json#4709
backspace wants to merge 22 commits into
mainfrom
routing-from-config-cs-10054

Conversation

@backspace
Copy link
Copy Markdown
Contributor

@backspace backspace commented May 7, 2026

When a realm config has hostRoutingRules:

CleanShot 2026-05-19 at 14 31 45@2x

The published realm resolves custom routes:

CleanShot 2026-05-19 at 14 32 30@2x

backspace and others added 2 commits May 7, 2026 09:42
CS-10054: assert getHostRoutingMap returns [{ path, id }] pairs read
from the indexed RealmConfig card. The test fails today (method does
not exist) and pins the desired shape for the implementation pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Preview deployments

Host Test Results

    1 files  ±0      1 suites  ±0   1h 49m 15s ⏱️ + 1m 11s
2 711 tests ±0  2 696 ✅ ±0  15 💤 ±0  0 ❌ ±0 
2 730 runs  ±0  2 715 ✅ ±0  15 💤 ±0  0 ❌ ±0 

Results for commit e2ac7e4. ± Comparison against earlier commit abe813f.

Realm Server Test Results

    1 files  ±    0      1 suites  ±0   8m 27s ⏱️ -23s
1 451 tests ±    0  1 451 ✅ +    1  0 💤 ±0  0 ❌  - 1 
1 542 runs   - 1 542  1 542 ✅  - 1 540  0 💤 ±0  0 ❌  - 2 

Results for commit e2ac7e4. ± Comparison against earlier commit abe813f.

@backspace backspace marked this pull request as ready for review May 19, 2026 21:49
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e42fa0b0c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/realm-server/handlers/serve-index.ts
Comment thread packages/base/realm-config.gts Outdated
@field instance = contains(StringField, {
description:
'Card instance to render when the realm is navigated at the given path',
'Card URL to render at this path. Relative URLs are resolved against the realm root.',
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the way it was defined previously, the routing was defined via a relationship:

  {
    "data": {
      "type": "card",
      "attributes": {
        …
        "hostRoutingRules": [
          { "path": "/whitepaper" },
          { "path": "/docs" }
        ]
      },
      "relationships": {
        "hostRoutingRules.0.instance": {
          "links": { "self": "./white-paper" }
        },
        "hostRoutingRules.1.instance": {
          "links": { "self": "./docs" }
        }
      },
      …
    }
  }

IMO this new structure is much more readable:

  {
    "data": {
      "type": "card",
      "attributes": {
        …
        "hostRoutingRules": [
          { "path": "/whitepaper", "instance": "./white-paper" },
          { "path": "/docs",       "instance": "./docs" }
        ]
      },
      …
    }
  }

and we previously agreed that the default edit template for realm config should cover validation, so losing the typing of CardDef isn’t a dealbreaker

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds “host mode” routing based on hostRoutingRules stored in a realm’s realm.json (RealmConfig card), allowing published realms to serve custom bare paths (e.g. /whitepaper) that render a target card both server-side (SSR head/isolated/scoped CSS) and client-side (SPA hydration/navigation).

Changes:

  • Add Realm.getHostRoutingMap() to read routing rules from the indexed RealmConfig card and return a {path,id} map.
  • Update realm-server serve-index to (a) rewrite SSR fetch target based on a matched routing rule and (b) inject hostRoutingMap into the host environment meta tag per request.
  • Add unit + e2e coverage for routing behavior and adjust host app to resolve routed paths before building the default card URL.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/runtime-common/realm.ts Adds getHostRoutingMap() to read and validate routing rules from the indexed RealmConfig card.
packages/realm-server/handlers/serve-index.ts Applies routing rules for SSR rendering and injects hostRoutingMap into the environment meta tag; tweaks meta-tag rewrite regex.
packages/host/config/environment.js Adds default hostRoutingMap to the host config surface for realm-server injection.
packages/host/app/services/host-mode-service.ts Exposes hostRoutingMap from config and adds resolveRoutedPath() helper.
packages/host/app/routes/index.gts Uses routing resolution to pick the routed target card ID before default URL construction in host mode.
packages/base/realm-config.gts Changes routing rule instance field to a flat string (instead of a JSON:API relationship) so it indexes as attributes.
packages/realm-server/tests/realm-routing-test.ts Adds a QUnit test covering Realm.getHostRoutingMap() behavior.
packages/realm-server/tests/index.ts Registers the new routing test in the realm-server test suite.
packages/matrix/tests/host-mode.spec.ts Adds an end-to-end test that publishes routing rules and verifies bare-path resolution renders the target card in host mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6086 to +6093
try {
// resolveRRI accepts URL form, registered-prefix form
// (`@cardstack/foo/...`), `$REALM/...`, and relative
// references — and preserves whichever canonical form the
// input/realm are in. That's what we want here so the same-
// realm guard below stays robust to either addressing scheme.
id = resolveRRI(instance, realmRRI);
} catch {
Comment on lines +6096 to +6106
// Defensive same-realm guard. The project spec restricts routing
// rules to cards within the same realm, and CS-10052 enforces
// that in the UI — but the file is hand-editable, so the read
// path must filter too. Without this guard, a realm owner could
// point `instance` at a private realm's card and the serve-index
// cardURL rewrite would surface its prerendered HTML through
// their public realm's routed path, bypassing the private
// realm's permissions.
let idCanonical = unresolveCardReference(id);
if (!idCanonical.startsWith(realmCanonical)) {
this.#log.warn(
Comment on lines 327 to +345
let publicPermissions = await hasPublicPermissions(cardURL, routingDeps);

if (!publicPermissions) {
ctxt.body = injectHeadHTML(
indexHTML,
`<title>Boxel</title>\n${defaultIconLinks().join('\n')}`,
);
return;
}

// CS-10055: host routing rules in the realm config can map a bare path
// (e.g. /whitepaper) to a target card. When the requested path matches
// a rule, rewrite cardURL so the head/isolated/scoped CSS fetched
// below render the routed target. The same map is also injected as
// a <script> further down so the SPA can resolve the path post-hydration.
let routingMap: { path: string; id: string }[] = [];
let routedRealm = await findOrMountRealm(requestURL, routingDeps);
if (routedRealm) {
routingMap = await routedRealm.getHostRoutingMap();
Comment on lines +340 to +341
// below render the routed target. The same map is also injected as
// a <script> further down so the SPA can resolve the path post-hydration.
Comment on lines +127 to +133
// Returns the target card id if `path` matches a routing rule, else null.
// `path` is the path within the realm; a leading slash is added if absent
// so the index path is matchable as either '' or '/'.
resolveRoutedPath(path: string): string | null {
let normalized = path.startsWith('/') ? path : `/${path}`;
let rule = this.hostRoutingMap.find((r) => r.path === normalized);
return rule ? rule.id : null;
Comment on lines +7 to +9
// CS-10054 TDD: pin the desired behavior of Realm.getHostRoutingMap before
// the method exists. The fixture is a realm.json RealmConfig card with one
// routing rule mapping `/whitepaper` to a white-paper card in the same realm.
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.

2 participants