Zum Inhalt springen

Wem gehört die URL?

In einem Architektur-Lab besaß ein Angular-Remote eine technisch einfache Route:

/details

Eigenständig gestartet war sie unter einer ebenso einfachen URL erreichbar:

https://remote.example/details

Innerhalb der Produktshell wurde dasselbe Remote jedoch unter einem zusätzlichen Produktbereich eingebunden:

https://product.example/tasks/details

Das Remote sollte weiterhin selbst definieren, was details bedeutet und welche Ansicht dort aktiviert wird. Gleichzeitig musste die Produktshell eine konsistente Produkt-URL erzeugen. Genau an dieser Grenze entstand ein unscheinbares Routingproblem.

Die Navigation im Remote sah zunächst so aus:

router.navigate(['details']);

Im Standalone-Modus konnte daraus wie erwartet werden:

/create → /details

Innerhalb der Produktshell wurde derselbe Befehl jedoch vom Router-Root aus aufgelöst:

/tasks/create → /details

Das Ergebnis war technisch nachvollziehbar, aber fachlich falsch. Erwartet war:

/tasks/create → /tasks/details

Eine naheliegende Gegenmaßnahme hätte darin bestanden, den Host-Präfix direkt in das Remote einzubauen:

router.navigate(['/tasks', 'details']);

Damit wäre das konkrete Problem innerhalb der Produktshell gelöst gewesen. Gleichzeitig hätte das Remote im Standalone-Modus eine Route erzeugt, die dort nicht existiert:

/tasks/details

Die funktionierende Lösung bestand nicht darin, einen der beiden vollständigen Pfade zum einzig richtigen Pfad zu erklären. Stattdessen wurde die Navigation relativ zum Mount-Kontext des Remotes ausgeführt:

router.navigate(['details'], {
relativeTo: remoteShellRoute,
});

Der Navigationsauftrag blieb in beiden Betriebsarten identisch. Nur der Kontext, in dem er aufgelöst wurde, unterschied sich:

Standalone:
/create → /details
Im Host:
/tasks/create → /tasks/details

Das Beispiel wirkt zunächst wie eine Besonderheit des Angular Routers. Tatsächlich verweist es auf eine allgemeinere Architekturfrage:

Wie können Host und Remote dieselbe fachliche Navigation unterstützen, obwohl sie unter unterschiedlichen äußeren URLs betrieben werden?

Die Antwort beginnt nicht bei navigate, basename oder pushState. Sie beginnt bei der Verantwortung für die URL.

Eine Browser-URL erfüllt in einer zusammengesetzten Anwendung mehrere Aufgaben gleichzeitig:

Browser-URL
├── adressiert das Produkt
├── bestimmt den Einstiegspunkt
├── wählt einen Produktbereich
├── adressiert möglicherweise fachlichen Zustand
├── erzeugt Browser-History
└── soll nach Reload rekonstruierbar sein

Sie ist externer Link, Bookmark, Authentifizierungsziel, Einstieg in einen Produktbereich, History-Eintrag und beobachtbarer Ausschnitt des Produktzustands. In einer Microfrontend-Architektur ist sie zusätzlich ein Vertrag zwischen Produktshell und Remote.

Deshalb greift die Frage zu kurz, welcher Router oder welche Komponente die URL technisch verändert. Entscheidend ist, welcher Teil des Systems die Bedeutung eines URL-Abschnitts verantwortet.

Route Ownership ist keine Frage danach, welche Komponente eine Router-API aufruft. Sie ist die Frage, welcher Teil des Systems für welchen Abschnitt der kanonischen Produkt-URL verantwortlich ist.

Der technische Router kann die URL schreiben, ohne ihre vollständige fachliche Bedeutung zu besitzen. Ebenso kann ein Remote die Bedeutung seines Routenraums besitzen, obwohl die kanonische URL von einem Router der Produktshell erzeugt wird.

Navigation ist ein Intent. Die URL ist beobachtbarer Zustand.

Eine Navigation beschreibt zunächst, welches Ziel erreicht werden soll. Sobald dieses Ziel in die Browser-URL und die Browser-History geschrieben wurde, wird es zu einem Zustand, der später erneut interpretiert werden muss. Damit entstehen Anforderungen, die über den ursprünglichen Klick hinausgehen: direkter Aufruf, Reload, Login-Redirect, Remount und Browser-Zurück.

Ein gut funktionierendes Ownership-Modell trennt den äußeren Produktbereich vom relativen Routenraum des Remotes.

Beispielsweise kann die Produktshell ein Remote unter /tasks einbinden. Innerhalb dieses Bereichs definiert das Remote einen eigenen Routenbaum:

Host
└── /tasks
Remote
└── projects
├── list
├── details
└── completed

Daraus entstehen abhängig von der Betriebsart unterschiedliche vollständige URLs.

Standalone:

/projects
/projects/details
/projects/completed

Innerhalb der Produktshell:

/tasks/projects
/tasks/projects/details
/tasks/projects/completed

Der Host besitzt in diesem Modell den Produkt-Origin, den äußeren Mount-Präfix, die Auswahl des Remotes und die Zusammensetzung der kanonischen Produkt-URL. Er muss wissen, unter welchem Produktpfad ein Remote erreichbar ist.

Das bedeutet nicht, dass der Host jede interne Route fachlich verstehen muss. Für das Mounting kann es genügen, den Produktbereich /tasks dem zuständigen Remote zuzuordnen. Was projects/details innerhalb dieses Bereichs bedeutet, bleibt Verantwortung des Remotes.

Der Host besitzt den äußeren Produktpfad. Das Remote besitzt seinen relativen Routenraum.

Dieses Modell ist besonders naheliegend, wenn Host und Remote denselben Router oder kompatible Mount-Mechanismen verwenden. Es ist jedoch keine universelle Vorgabe. Auch delegierte Routerbereiche oder Navigation Ports können dieselbe Trennung herstellen.

Das Remote definiert seine relativen Unterpfade und deren fachliche Bedeutung. Es entscheidet beispielsweise, ob eine Projektübersicht unter projects, eine Detailansicht unter projects/details oder ein Archiv unter projects/completed erreichbar ist.

Damit besitzt es:

  • die Zuordnung seiner Ansichten zu relativen Routen,
  • die Bedeutung der Unterpfade,
  • die interne Navigation innerhalb des Produktbereichs,
  • die fachliche Rekonstruktion der adressierten Ansichten.

Was das Remote nicht besitzen muss, ist der äußere Mount-Präfix.

Der Host muss wissen, unter welchem Produktpfad ein Remote erreichbar ist. Das Remote muss nicht wissen, unter welchem äußeren Präfix es gerade gemountet wurde.

Diese Trennung schützt beide Seiten. Die Produktshell kann einen Produktbereich umbenennen oder an einer anderen Stelle mounten, ohne die interne Navigation des Remotes neu zu entwerfen. Das Remote kann eigenständig betrieben werden, ohne eine künstliche Host-Struktur nachbilden zu müssen.

Technische Pfad-Ownership und fachliche Zustands-Ownership sind dabei nicht dasselbe. Der Host kann /tasks besitzen und trotzdem nicht wissen, welche Daten eine Route wie projects/details benötigt. Umgekehrt kann das Remote die fachliche Bedeutung von details besitzen, ohne die vollständige Produkt-URL festzulegen.

Warum hartcodierte Host-Präfixe die Unabhängigkeit zerstören

Abschnitt betitelt „Warum hartcodierte Host-Präfixe die Unabhängigkeit zerstören“

Dasselbe Muster zeigte sich im Architektur-Lab später bei einem weiteren Remote. Dort wurden zunächst relative Router-Kommandos verwendet:

['projects', 'detail'];

Im Zuge der Host-Integration wurden daraus absolute Produktpfade:

['/tasks', 'projects', 'detail'];

Innerhalb der Produktshell wirkte die Änderung zunächst stabil. Die Navigation erreichte wieder den erwarteten Produktbereich. Erst der eigenständige Betrieb zeigte die neue Abhängigkeit.

Das Remote erzeugte nun auch im Standalone-Betrieb:

/tasks/projects/detail

Diese Route existierte dort nicht. Das Remote kannte plötzlich nicht mehr nur seine fachlichen Unterpfade, sondern auch den Namen und die URL-Struktur seines Hosts.

Damit veränderte sich mehr als ein Router-Kommando:

  • Der Host-Präfix wurde Teil der Remote-Implementierung.
  • Eine Änderung des Mount-Pfads erforderte eine Änderung im Remote.
  • Standalone- und Host-Modus verwendeten nicht mehr dieselbe interne Navigation.
  • Der Remote-Test prüfte nicht länger denselben Routingvertrag wie die Host-Integration.
  • Der äußere Produktaufbau sickerte in die lokale Fachlichkeit.

Ein absoluter Pfad kann einen konkreten Integrationsfehler beheben und gleichzeitig die architektonische Unabhängigkeit beschädigen.

Der Fehler liegt nicht darin, dass absolute URLs grundsätzlich ungeeignet wären. Ein Produkt benötigt kanonische absolute URLs. Problematisch wird es, wenn ein fachlich eigenständiger Produktbereich den äußeren Composition-Pfad seines aktuellen Hosts als eigene Konstante behandelt.

Der Host-Pfad ist Konfiguration der Composition, nicht Fachwissen des Remotes.

Ein hartcodierter Host-Präfix macht ein Remote nicht integriert, sondern abhängig.

Die Alternative zu absoluten Host-Pfaden lautet nicht einfach: „Immer relativ navigieren.“

Ein Befehl wie

router.navigate(['details']);

ist nur dann eindeutig, wenn feststeht, relativ zu welcher aktivierten Route er aufgelöst wird.

Ohne einen definierten Kontext kann der Router den Befehl am Root einhängen:

Router-Root
└── details

Erwartet wäre jedoch möglicherweise:

Remote-Mount
└── tasks
└── details

Im Angular-Fall wurde deshalb die aktivierte Mount-Route des Remotes als technischer Kontext verwendet:

router.navigate(['details'], {
relativeTo: remoteShellRoute,
});

Der zugrunde liegende Vertrag lässt sich vereinfacht so ausdrücken:

interface RemoteMountContext {
baseRoute: ActivatedRoute;
}

Das Interface ist kein allgemeiner Microfrontend-Standard. ActivatedRoute ist eine konkrete Angular-Abhängigkeit und damit an die gemeinsame Angular-Laufzeit gebunden. Das Architekturprinzip dahinter ist jedoch frameworkneutral:

Das Remote benötigt keinen hartcodierten Host-Pfad. Es benötigt einen verlässlichen Kontext, innerhalb dessen seine relativen Routen gelten.

React Router kann dafür einen basename oder einen delegierten Routerbereich verwenden. Andere Integrationen können Base Paths, Mount Props oder eine Navigation-Abstraktion bereitstellen. Entscheidend ist nicht der Name der API, sondern die explizite Zuordnung des relativen Routenraums.

Relative Navigation ist ohne definierten Mount-Kontext nicht automatisch unabhängig. Sie ist lediglich unvollständig spezifiziert.

Die Grafik vergleicht die Auflösung derselben relativen Remote-Route im Standalone-Modus und innerhalb einer Produktshell. Standalone wird die Route details zu /details. Im Host wird der Präfix /tasks mit details zu /tasks/details kombiniert. Eine fehlerhafte Nebenvariante zeigt, dass ein im Remote hartcodierter Präfix /tasks den Standalone-Betrieb abhängig macht.

Abschnitt betitelt „Navigation Intent ist nicht Router-Synchronisation“

Im Architektur-Lab wurde die Navigation nicht direkt aus jeder Komponente heraus ausgeführt. Stattdessen äußerten Komponenten kleine semantische Navigationsabsichten:

openList
openDetails
openEdit
openProjectDetail

Ein solcher Intent beschreibt, was die Anwendung anzeigen soll. Er enthält weder den Host-Präfix noch konkrete Angular Router Commands. Er muss keine ActivatedRoute kennen und sagt nichts über pushState, React Router oder Browser-History-APIs aus.

Der technische Adapter übersetzte den Intent anschließend in einen Router-Auftrag:

{
commands: ['details'],
relativeTo: remoteShellRoute,
replaceUrl: false
}

Ein Navigation Intent gehört zur fachlichen oder präsentationsnahen Bedeutung. Ein Router-Auftrag gehört zum technischen Adapter.

Im konkreten Fall lief diese Übersetzung über einen Angular-/NgRx-Signal-Eventkanal innerhalb derselben Angular-Anwendung. Es wurden keine window-Events, CustomEvent-Nachrichten oder postMessage-Aufrufe eingesetzt. Der Kanal war auch kein frameworkneutraler Vertrag zwischen unabhängigen Laufzeiten.

Er transportierte letztlich einen einzelnen Auftrag an denselben Angular Router.

Das Event synchronisierte keine Router. Es übersetzte einen Navigation Intent in einen relativen Router-Auftrag.

Diese Abgrenzung ist wichtig. Ein Event wird nicht dadurch zu einem stabilen Microfrontend-Vertrag, dass es global benannt ist. Entscheidend ist, welche Verantwortung es transportiert.

Ein kleiner produktweiter Intent kann beispielsweise so aussehen:

interface ProductNavigationIntent {
target: ProductLocation;
history?: 'push' | 'replace';
}

Er beschreibt ein Ziel und überlässt die konkrete URL dem zuständigen Adapter. Er verteilt keinen Router-Zustand, transportiert keinen fremden fachlichen Zustand und verlangt nicht, dass mehrere Stores oder Router miteinander abgeglichen werden.

Problematisch wäre dagegen eine Kette wie diese:

Router A ändert URL
→ globales Event
→ Router B gleicht Zustand ab
→ Router B ändert URL
→ weiteres Event

Das wäre keine Navigation über eine klare Grenze, sondern Router-Synchronisation.

Navigation Intent
└── Eine Komponente bittet den URL-Owner um Navigation.
Router-Synchronisation
└── Mehrere Router versuchen, denselben Zustand gegenseitig abzugleichen.

Navigation darf als Intent über eine Grenze gehen. Die kanonische Browser-History sollte trotzdem einen klaren mutierenden Owner besitzen.

In der homogenen Angular-Integration des Architektur-Labs verwendete der Host einen Angular Router. Die Angular-Remotes exportierten relative Route-Arrays, die vom Host unter Produktpräfixen eingehängt wurden.

Das Ergebnis ließ sich vereinfacht so darstellen:

Browser-URL
Angular Router
├── /todos
│ └── relative Todos-Routen
├── /tasks
│ └── relative Tasks-Routen
└── /members-directory
└── relative Members-Routen

Alle Beteiligten verwendeten dieselbe Browser-History. Das Remote konnte eine Navigation anfordern, der gemeinsame Router erzeugte die kanonische URL und derselbe Router verarbeitete später Browser-Zurück.

Damit war die mutierende Ownership eindeutig:

Ein Router
└── eine Browser-History

Das bedeutet nicht, dass die Remotes keine Route Ownership besitzen. Sie besitzen weiterhin ihre relativen Routenbäume und deren fachliche Bedeutung. Der gemeinsame Router ist lediglich der technische Mechanismus, der diese Bäume in einen kanonischen URL-Raum integriert.

Gerade diese Unterscheidung verhindert einen unnötigen Shell-Monolithen. Der Host muss nicht sämtliche internen Routen fachlich modellieren. Er stellt den Mount-Kontext bereit und setzt die relativen Routenbäume zusammen.

Komplexer wird die Situation, wenn innerhalb eines Produktbereichs ein zweiter Router startet.

Im Architektur-Lab existierte dafür ein React Activity Stream. Der Angular Host Router beobachtete einen Bereich wie:

/activity-stream/...

Innerhalb dieses Bereichs verwendete das React-Remote einen eigenen BrowserRouter:

<BrowserRouter basename="/activity-stream">{/* Remote-Routen */}</BrowserRouter>

Damit beobachteten zwei Router denselben URL-Raum:

Browser-History
├── Angular Host Router
└── React BrowserRouter

Ein basename hilft dem React Router, seine relativen Pfade unterhalb des Produktpräfixes zu interpretieren. Es beantwortet jedoch noch nicht vollständig die Ownership-Frage:

Wer darf die kanonische Browser-URL verändern, und wer darf sie nur interpretieren?

Verschachtelte Router sind nicht automatisch falsch. Mehrere Modelle können funktionieren.

Der Host erkennt lediglich den äußeren Präfix:

Host
└── erkennt /activity-stream

Das React-Remote besitzt alles darunter:

React-Remote
└── besitzt den relativen URL-Bereich

In diesem Modell darf das Remote die History innerhalb seines delegierten Bereichs verändern. Der Vertrag muss dafür Mounting, Unmounting, direkte Einstiege und popstate-Verhalten eindeutig regeln.

Das React-Remote äußert nur semantische Navigationsziele. Ein Adapter der Produktshell erzeugt daraus die Browser-URL.

React-Remote
└── äußert Navigation Intent
Host
└── verändert Browser-URL

Dieses Modell reduziert konkurrierende History-Zugriffe, koppelt das Remote aber stärker an den bereitgestellten Navigation Port.

Beide Router verändern und beobachten dieselbe Browser-History:

Angular Router
+
React Router
└── verändern und beobachten dieselbe History

Dieses Modell kann eine Zeit lang funktionieren. Riskant wird es, sobald pushState, replaceState, popstate, Mounting und Unmounting nicht mehr dieselbe Vorstellung vom aktiven URL-Bereich besitzen.

Mehrere Router sind beherrschbar. Mehrere unklare Besitzer derselben History sind es deutlich weniger.

Die Grafik vergleicht einen gemeinsamen Router mit zwei verschachtelten Routern. Links besitzt ein Router eine Browser-History und integriert Host-Präfix sowie relative Remote-Routen. Rechts beobachten Angular Router und React Router dieselbe Browser-History. Dort steht als offene Ownership-Frage, wer pushState auslösen darf.

Die Frage nach dem mutierenden Owner lässt sich nicht für jede Architektur gleich beantworten.

Bei einem gemeinsamen Router liegt die technische Verantwortung meist ohnehin bei diesem Router. Bei einem delegierten URL-Bereich kann ein Remote die Browser-History innerhalb seiner Grenze selbst verändern. Über eine Frameworkgrenze hinweg kann ein Navigation Port sinnvoller sein.

Wichtig ist, dass ein konkretes Modell gewählt wird.

Ein Remote darf innerhalb seines delegierten Routenraums technisch navigieren. Bei Navigation zu einem anderen Produktbereich sollte es dagegen nicht zwangsläufig dessen URL-Struktur hartcodieren.

Problematisch wäre beispielsweise:

navigate('/members-directory/details/42');

Das aufrufende Remote kennt nun den Namen, den Mount-Pfad und die interne Struktur eines fremden Produktbereichs.

Ein stabilerer produktweiter Intent könnte lauten:

navigation.openProductArea({
area: 'members',
target: 'detail',
id: '42',
});

Der Plattformadapter erzeugt daraus die kanonische Produkt-URL.

Daraus darf allerdings kein zentraler fachlicher Megarouter entstehen, der sämtliche internen Routen aller Remotes katalogisiert. Der Vertrag sollte nur jene Navigation abbilden, die tatsächlich über eine Ownership-Grenze führt.

Innerhalb seines delegierten Routenraums darf ein Remote technisch navigieren. Über Produktgrenzen hinweg sollte es ein Ziel beschreiben, nicht die fremde URL-Struktur besitzen.

Route Ownership endet außerdem nicht am Pfadsegment. Auch weitere Bestandteile der URL und der History benötigen eine definierte Verantwortung:

  • Pfad,
  • Query-Parameter,
  • Fragment,
  • History-Modus push oder replace,
  • optionaler Navigation State.

Wer besitzt Filterparameter? Bleiben sie bei interner Navigation erhalten? Gehört ein Fragment zum Remote oder zur Shell? Soll ein technischer Redirect den aktuellen History-Eintrag ersetzen? Werden Parameter zwischen Standalone- und Host-Modus korrekt abgebildet?

Route Ownership endet nicht am Pfadsegment. Auch Query, Fragment und History-Semantik benötigen eine definierte Verantwortung.

Eine ausgelöste Navigation und Browser-Zurück sind zwei unterschiedliche Vorgänge.

Bei einer ausgelösten Navigation entsteht ein neuer Zustand:

Benutzer klickt auf „Details“
→ Anwendung äußert Navigation Intent
→ URL-Owner erzeugt History-Eintrag

Bei Browser-Zurück aktiviert der Browser dagegen einen bereits vorhandenen Eintrag:

Benutzer betätigt Zurück
→ Browser aktiviert bestehenden History-Eintrag
→ Router interpretiert neue URL

Navigation erzeugt History. Browser-Zurück interpretiert bereits vorhandene History.

Bei einem gemeinsamen Angular Router verarbeitet dieser Router das popstate-Signal, erkennt die URL und aktiviert den passenden Routenbaum erneut. Dafür sollte nicht erst ein fachlicher Navigation Intent rekonstruiert werden müssen.

Wer jede beobachtete URL-Änderung erneut als Navigation Event veröffentlicht, riskiert doppelte History-Einträge oder Rückkopplungsschleifen. Der Router interpretiert dann einen bestehenden Eintrag, woraufhin die Anwendung denselben Zustand noch einmal als neue Navigation erzeugt.

Browser-Zurück ist deshalb kein Navigation Intent. Es ist eine externe Zustandsänderung, die der URL-Owner und die beteiligten Routenbäume interpretieren müssen.

Eine Route rekonstruiert nicht automatisch den View-Zustand

Abschnitt betitelt „Eine Route rekonstruiert nicht automatisch den View-Zustand“

Im Architektur-Lab ließ sich diese Grenze an einem typischen Ablauf beobachten:

  1. Der Host öffnet /todos/board.
  2. Der Benutzer wählt ein Todo.
  3. Der lokale Store speichert die Auswahl.
  4. Die Anwendung navigiert nach /todos/details.
  5. Der Benutzer wechselt zu einem anderen Remote.
  6. Das Todos-Remote wird deaktiviert.
  7. Der Benutzer betätigt Browser-Zurück.
  8. /todos/details wird erneut aktiviert.
  9. Der neu erzeugte Store kennt das zuvor ausgewählte Todo nicht mehr.

Der Router hatte seine Aufgabe erfüllt:

URL wiederhergestellt
Route erkannt
Remote gemountet

Die fachliche Detailansicht war dennoch nicht rekonstruierbar. Die Route adressierte lediglich „Details“, nicht aber das konkrete Todo. Die notwendige Auswahl hatte ausschließlich im inzwischen verworfenen Store existiert.

Daraus ergibt sich eine wichtige Abstufung:

Dokument ist ladbar
Route ist auflösbar
Ansicht ist rekonstruierbar
fachlicher Zustand ist wiederhergestellt

Eine wiederhergestellte Route ist nicht dasselbe wie ein wiederhergestellter View-Zustand.

Die Browser-History kann nur den Zustand wiederherstellen, der in der URL oder über einen daraus ableitbaren Vertrag adressierbar ist.

Die Verantwortung dafür liegt nicht automatisch beim Host. Das Remote besitzt die fachliche Bedeutung seiner Route und muss entscheiden, welche Referenzen für eine Rekonstruktion benötigt werden. Der Host kann den Bereich aktivieren, aber er sollte nicht die lokale Entity-Auswahl des Remotes erraten müssen.

Bei einem direkten Browseraufruf wie

https://product.example/todos/details

müssen mehrere Ebenen funktionieren:

  1. DNS und Reverse Proxy wählen den richtigen Dienst.
  2. Der Webserver liefert das Einstiegspunkt-Dokument.
  3. Die Shell erkennt den Remote-Präfix.
  4. Das Remote erkennt seinen relativen Unterpfad.
  5. Benötigte fachliche Daten werden rekonstruiert.
  6. Authentifizierungs-Redirects erhalten das ursprüngliche Ziel.

Ein typischer Webserver-Fallback kann sicherstellen, dass auch bei einem unbekannten Dateipfad das SPA-Dokument ausgeliefert wird:

try_files $uri $uri/ /index.html;

Damit ist bewiesen, dass der Browser ein Einstiegspunkt-Dokument erhält. Mehr nicht.

Der Fallback beweist weder ein erfolgreiches Remote-Mounting noch die Auflösung des relativen Pfads. Er sagt nichts über Berechtigungen, fachliche Daten oder die Wiederherstellung einer ausgewählten Entity aus. Auch ein Login-Redirect kann das Ziel verlieren, obwohl der Webserver vorher korrekt index.html ausgeliefert hat.

Ein Server-Fallback macht eine URL ladbar, aber noch nicht fachlich rekonstruierbar.

Eine auflösbare Route ist noch kein belastbarer Deep Link. Eine URL wird erst dann rekonstruierbar, wenn sie den Zustand adressiert, den die Anwendung nach Reload, Remount oder Browser-Zurück benötigt.

Die Grafik zeigt fünf aufeinander aufbauende Ebenen eines Deep Links: Das Dokument wird geladen, die Shell erkennt das Remote, das Remote löst die Route auf, eine fachliche Referenz ist vorhanden und die Ansicht wird vollständig rekonstruiert. Ein seitlicher Abbruch zeigt die gültige Route /todos/details, bei der das ausgewählte Todo fehlt.

Eine Route wie

/todos/details

kann funktionieren, wenn die konkrete Auswahl aus einem anderen stabilen Zustand rekonstruierbar ist. Innerhalb eines ununterbrochenen Arbeitsablaufs kann ein lokaler Store dafür ausreichen.

Nach einem Reload, einem Remount oder einem direkten externen Link fehlt diese Auswahl jedoch möglicherweise. Eine rekonstruierbare Alternative könnte deshalb lauten:

/todos/42

oder:

/todos/details/42

Daraus folgt nicht, dass jede Auswahl und jeder UI-Zustand in die URL geschrieben werden muss.

Ein belastbarer Deep Link soll typischerweise geteilt oder gebookmarkt werden können. Er soll nach Reload, Login-Redirect oder erneutem Mounting funktionieren. Dafür benötigt er eine stabile fachliche Referenz oder einen anderen reproduzierbaren Vertrag.

Ein transienter View-Zustand entsteht dagegen möglicherweise nur innerhalb eines laufenden Ablaufs. Er muss nicht bookmarkfähig sein, darf bei Reload verworfen werden und muss nicht künstlich zu einem öffentlichen Produktzustand erklärt werden.

Nicht jede Ansicht braucht eine eigene rekonstruierbare URL. Aber jede URL sollte ehrlich darüber sein, welchen Zustand sie tatsächlich wiederherstellen kann.

Eine Route ohne Entity-ID ist daher nicht automatisch fehlerhaft. Problematisch wird sie erst, wenn sie wie ein belastbarer Deep Link behandelt wird, obwohl ihre fachliche Voraussetzung ausschließlich in flüchtigem Speicher existiert.

Standalone- und Host-URLs dürfen unterschiedlich sein

Abschnitt betitelt „Standalone- und Host-URLs dürfen unterschiedlich sein“

Die beiden Betriebsarten eines Remotes besitzen unterschiedliche Composition-Kontexte.

Standalone kann eine fachliche Route unter folgender URL erreichbar sein:

https://tasks.example/projects/details

Im Produkt kann dieselbe Route so aussehen:

https://product.example/tasks/projects/details

Origin und äußerer Präfix unterscheiden sich. Die fachliche Route projects/details bleibt dennoch dieselbe.

Das ist kein Fehler. Ein Remote muss nicht in jedem Betriebsmodus dieselbe vollständige Browser-URL besitzen. Entscheidend ist, dass das Mapping explizit und überprüfbar ist.

Dafür sollten mindestens folgende Eigenschaften gelten:

  • Der relative Routenraum bleibt stabil.
  • Der Mount-Kontext ist explizit.
  • Interne Navigation verwendet keine hartcodierten Host-Präfixe.
  • Authentifizierungs-Redirects kennen die jeweilige kanonische URL.
  • Dokument-Fallbacks funktionieren pro Origin.
  • Tests prüfen sowohl den Standalone- als auch den Host-Betrieb.

Unabhängige Ausführbarkeit bedeutet nicht identische äußere URLs. Sie bedeutet, dass die fachliche Route in jedem Kontext eindeutig aufgelöst werden kann.

Der Standalone-Modus darf dabei ein eigenes Mapping bereitstellen. Ein Mini-Host oder eine lokale Shell kann denselben relativen Routenraum unter einem anderen äußeren Präfix aktivieren. Entscheidend ist, dass dieser Kontext nicht heimlich in die Remote-Fachlichkeit einfließt.

Die Verantwortlichkeiten lassen sich als vereinfachte Matrix darstellen:

VerantwortungHostRemote
Produkt-Origin im Host-Betriebbesitztverwendet
Äußerer Mount-Präfixbesitztdarf ihn nicht hartcodieren
Relativer Unterpfadmountetdefiniert
Fachliche Bedeutung der Routekennt sie nur soweit nötigbesitzt
Produktweite Navigationkoordiniertäußert Intent
Interne Remote-Navigationstellt Kontext bereitbesitzt
Browser-Historystellt einen klaren Owner oder delegierten Bereich sicherfordert Änderung an oder besitzt den delegierten Bereich
Deep-Link-Datenaktiviert den Produktbereichrekonstruiert den fachlichen Zustand
Server-Fallbackverantwortet den Host-Originverantwortet den Standalone-Origin

Diese Aufteilung ist kein unveränderliches Gesetz. Sie macht jedoch sichtbar, wo unterschiedliche Verantwortungen häufig miteinander verwechselt werden.

Der Host kann den äußeren Pfad besitzen, ohne die fachliche Bedeutung jeder Unterroute zu kontrollieren. Das Remote kann seinen Routenraum besitzen, ohne den vollständigen Produktpfad zu kennen. Ein gemeinsamer Router kann die History verändern, ohne automatisch Eigentümer des fachlichen URL-Zustands zu sein.

Technische Pfad-Ownership und fachliche Zustands-Ownership sind nicht dasselbe.

Je nach Integration können unterschiedliche Modelle tragfähig sein:

  • Gemeinsamer Router: Der Host mountet relative Route-Arrays; ein Router besitzt die Browser-History.
  • Delegierter Routerbereich: Der Host erkennt den äußeren Präfix; das Remote besitzt den URL-Raum darunter.
  • Navigation Port: Das Remote beschreibt ein semantisches Ziel; ein Plattformadapter erzeugt die kanonische URL.
  • Getrennte Standalone- und Host-Mappings: Der fachliche Zielraum bleibt stabil, während der Composition-Kontext unterschiedliche äußere URLs erzeugt.

Keines dieser Modelle ist pauschal richtig. Entscheidend ist, ob die Ownership eindeutig bleibt, direkte Einstiege und Browser-Zurück funktionieren, der eigenständige Betrieb realistisch testbar ist und der benötigte fachliche Zustand rekonstruiert werden kann.

Die Wahl des Modells ist weniger wichtig als die Klarheit seines Vertrags.

Das Routingproblem aus dem Architektur-Lab ließ sich technisch mit relativeTo lösen. Seine architektonische Bedeutung lag jedoch nicht in dieser einzelnen Router-Option, sondern in der Trennung zweier Verantwortungen.

Der Host konnte /tasks als äußeren Produktbereich besitzen. Das Remote konnte details als eigene relative Route definieren. Der Mount-Kontext verband beide Teile, ohne den Host-Präfix in die Remote-Implementierung zu schreiben.

Ein hartcodierter Host-Präfix löst ein lokales Routingproblem und erzeugt eine strukturelle Abhängigkeit.

Navigation über eine Ownership-Grenze kann als Intent formuliert werden. Die konkrete URL erzeugt anschließend der zuständige Adapter, der gemeinsame Router oder ein klar delegierter Routerbereich. Mehrere Router bleiben möglich, benötigen aber einen eindeutigen mutierenden History-Owner oder explizit delegierte URL-Bereiche.

Damit endet die Verantwortung noch nicht. Eine Route kann gültig sein, obwohl der fachliche Zustand fehlt. Ein Server-Fallback kann das SPA-Dokument ausliefern, obwohl die benötigte Entity nicht rekonstruierbar ist.

Eine wiederhergestellte Route ist nicht dasselbe wie ein wiederhergestellter fachlicher Zustand.

Standalone- und Host-Modus dürfen unterschiedliche äußere URLs besitzen. Entscheidend ist ein explizites Mapping zwischen Composition-Kontext und relativem Routenraum.

Ein stabiler Routingvertrag beantwortet deshalb mehr als die Frage, wie eine Komponente von create nach details gelangt. Er beantwortet, wer den äußeren Produktpfad besitzt, wer den relativen Unterpfad interpretiert, wer History-Einträge erzeugt und welcher fachliche Zustand nach Reload, Remount oder Browser-Zurück wiederhergestellt werden kann.

Die URL gehört nicht dem Router, der sie gerade verändert. Sie gehört dem Produktvertrag, der sie wiederherstellbar macht.