Best viewed on desktop Mobile Friendly tutorial coming soon!

The Idea

⚠️ If the preview looks stale — reload it
This lesson runs two apps in the preview, and it doesn't always refresh on its own. After you Solve a step, switch lessons, or if the notice's × button stops working, hit reload on the preview — or reload the whole page. Everything also works if you download the lesson and run it locally.

A big app is rarely built, shipped, or owned by one team. Micro-frontends split a site into independent pieces — each with its own codebase, release schedule, and owner — that come together in the browser. They’re the front-end cousin of microservices.

There are two ways to bring a remote piece into a host page:

  • Module federation ships code: the host downloads the remote’s JavaScript and runs it itself, so the host owns the render.
  • App federation — Marko’s approach — ships rendered HTML: the remote app renders its own piece (fetching its own data, running its own logic) and the host simply embeds the result. The remote owns the render; the host just knows a URL.

Marko does this with the <micro-frame> tag. It behaves like an <iframe> — “show me what lives at this URL” — but instead of an isolated frame, the fetched HTML is streamed straight into the page. This embedding of one document’s output inside another is called transclusion:

Host app localhost:3000 <micro-frame> remote's HTML, embedded here Remote app localhost:3001 /fragment renders its own HTML 1. fetch 2. HTML back

This workspace has two separate apps — look at the file tree: a host/ app on port 3000 and a remote/ app on port 3001, each with its own package.json. They can even be on different Marko versions (this host is on Marko 5 so it can use the @micro-frame/marko tags; the remote is a plain Marko 6 app). Each keeps its own Marko runtime, kept apart by a runtimeId (set in the remote’s vite.config), so the two don’t clash once the remote’s HTML lands inside the host page.

The remote notice is a real, interactive component — try dismissing it with the ×. Its JavaScript is served by the remote, but the browser can’t reach the remote’s port directly, so the host forwards those requests through itself (the /remote-assets/ route in host/). That’s the point of app federation: the remote is a fully independent app — its own render, data, and interactivity — and the host just embeds the result and relays what the browser needs. Open both previews: the host page already embeds the remote’s notice with <micro-frame>, and the remote serves that same notice on its own at /fragment. Unlike an iframe, the embedded notice is just part of the host’s HTML — no box, no separate scrollbar, and it’s visible to search engines and screen readers.

Seeing the runtime isolation

How do two Marko runtimes share one page without stepping on each other? The runtimeId. The remote is built with runtimeId: 'mr', so every marker and every piece of hydration data it emits is namespaced with mr — no other app’s runtime uses that key, so nothing collides.

Open the Remote app preview (port 3001), then use your browser’s View Source to see the real bytes. Alongside the notice’s HTML you’ll find Marko’s component-boundary comments and a small hydration script, all carrying the mr id — roughly like this (simplified):

<!--mr ...--> <!-- component boundary, namespaced by runtimeId -->
<div ...>…the notice…</div>
<script>$mr_C = window.$mr_C || []; /* the remote's hydration registry, keyed by "mr" */
$mr_C.push(/* scope + state for the × button */)</script>

When the host embeds this, its own runtime lives under a different id, so the two sets of markers and registries never overlap. That’s what lets you drop as many remotes as you like onto one page — each stays sandboxed by its own runtimeId.

In the next lesson you’ll wire the embed up yourself.

Powered by WebContainers Built with TutorialKit Built with ❤️ by defunkt-dev
Files
Preparing Environment
  • Installing the host app
  • Installing the remote app
  • Building the remote, then starting both apps