One Nav, Two Stacks: A Microfrontend Between Magento and Laravel Without Replatforming
A working reference implementation on two production-grade stacks (Magento 2.4 + Laravel 11), with the host integration shape shown below and a server-rendered nav skeleton shipped on day one - not retrofitted after GSC panic.
TL;DR
Mid-market ecommerce rarely lives on one stack. The industry answer - "replatform everything onto one stack" - is a $100-500k, 6-12 month project most of them cannot afford.
I shipped a smaller answer on a real client stack: a 15-20 kb Preact microfrontend that mounts into both Magento 2.4 and Laravel 11 via a manifest. This is not a Module Federation hello-world - it is two real host integrations, around 120 lines of PHP on Magento and around 90 lines on Laravel, with one pnpm build and both sites updating in under a minute.
The opinionated part: microfrontends failed as a product architecture but work as a repair strategy. Greenfield product teams drown in coordination cost. Repair across inherited stacks is a different problem - and the pattern solves it cleanly, if you get the SEO contract right before shipping.
The proof point is deliberately concrete: a before/after crawler diff on identical URLs and user-agents. Not a modelled SEO score, not a Lighthouse proxy, but raw HTML facts - bytes, anchor counts, and whether the navigation exists in initial markup for non-rendering crawlers.
The problem nobody names out loud
A mid-market ecommerce group with multiple brands, accrued over years:
- A Magento 2.4 storefront - catalogue, cart, checkout.
- A Laravel 11 marketing site - brand story, awards programme, editorial.
- A handful of single-purpose SPAs on top.
Each stack has its own header and footer. When marketing adds a top-level category, it ships in one stack in a week and in the other in three. When design changes the logo, it takes two sprints to roll out across everything.
The cost is not engineering hours. The cost is that the brand is visibly inconsistent to customers, the teams know it, and every cross-team sync about the nav takes an hour.
Why "just consolidate on one stack" is not the answer
The standard advice is a monorepo or a headless rewrite. Both are correct on paper and wrong in the field.
Monorepos assume teams that want to converge. Inherited teams - Magento folks who have been on that stack for seven years, a Laravel team that came with an acquisition - do not want to converge. They have skill investment, release cadences, and on-call rotations built around their stack. A monorepo migration is a political project before it is an engineering one, and most mid-market companies cannot push one through.
Headless replatforming is the same project in a different wrapper. Twelve-month runway, executive buy-in, and a new front end that has to outpace the rate at which the old ones break the business.
A shared nav microfrontend does not compete with monorepo architecturally. It competes with doing nothing - which is what the organisation was going to do for the next two years anyway.
Why repair is different from design
Spotify publicly rolled back its extreme squad autonomy. The failure mode is always the same: teams own product-level vertical slices, those slices compose into one surface, coordination cost explodes, UX inconsistency becomes a feature of the architecture.
That is a real lesson. It does not mean no microfrontend is ever right.
Repair is a different problem than design. You are not building the surface - the surface already exists, in two incompatible implementations. You are installing the narrowest possible shared layer that brings them back into visual alignment. The nav is exactly that narrow: no business logic, no routing, no data dependencies beyond a flat link tree.
Everything the microfrontend critique flags - duplicate runtime, fragmented UX ownership, coordination overhead - either does not apply to a 15 kb shell (runtime is negligible) or applies less than the status quo (UX is already fragmented; we are reducing coordination by centralising the decision).
Shell architecture: 15-20 kb, one build, one file
A Preact tree built with Vite's library mode into one IIFE script and one CSS file, both with content-hashed filenames. A manifest.json maps logical names to hashed URLs.
- Preact over React - ~10 kb gzipped vs ~45 kb. Non-negotiable at a 15-20 kb budget.
- IIFE over ES modules - works in Magento's RequireJS environment without extra config, and in any
<script>tag on any stack. cssCodeSplit: false- one file, one request, no FOUC.- Tailwind with a prefix - scoped classes, no collision with host CSS.
- Content-hashed URLs via manifest - immutable caching. Hosts read the manifest at render time and emit
<link href="/nav/shared-nav.abc123.css">.
pnpm build takes ~8 seconds. Hosts pick up new hashes within their cache TTL. One bugfix lands on both sites in ~1 minute.
Host integration: Magento 2.4
Around 120 lines of new PHP, three files:
Acme\Theme\Model\SharedNavManifest(~85 lines) - HTTP-fetches the manifest with Magento's cache backend, falls back to a non-hashedshared-nav.iife.json fetch failure so the nav never disappears, only loses cache-busting.Acme\Theme\ViewModel\SharedNavAssets(~26 lines) - the ViewModel that phtml templates talk to. CSS goes through a ViewModel rather than static<css>layout XML because the URL has a hash in it.Acme\Theme\etc\frontend\di.xml(~7 lines) - wires the manifest URL through deploy config.
Two phtml partials - header and footer - emit the mount divs and asset tags. Included from default.xml, so every page type inherits the shared nav.
Host integration: Laravel 11
Around 90 lines. Smaller because the service container carries more weight.
App\Services\SharedNavManifest(~65 lines) - HTTP-fetches the manifest, caches viaCache::remember('shared_nav.manifest', 60, ...), logs and falls back to the unhashed bundle on fetch failure.config/services.php- three lines exposingservices.shared_nav.manifest_urlas env-driven config.- Two Blade layouts - public and a secondary layout for older marketing pages - emit
<link>and<script>tags from the manifest service.
The 60-second cache TTL controls how fast a pnpm build propagates - aggressive enough for release cadence, conservative enough that manifest fetches are one request per minute per worker.
Representative code shape (abridged)
The full production classes are client code, so I am not publishing them verbatim here. But the integration should not stay abstract either. This is the shape of the two host adapters - abridged to show the contract rather than every guardrail and framework detail.
A note on the manifest keys: Vite indexes manifest.json by entry source path and asset name - src/main.tsx and style.css in our build - not by the output filename. The host lookups use those keys; the unhashed filenames (shared-nav.iife.js, shared-nav.css) are only used as fallbacks when the manifest fetch fails.
Magento 2.4 - manifest service shape:
class SharedNavManifest
{
public function getJsUrl(): string
{
return '/nav/' . ($this->manifest()['src/main.tsx']['file'] ?? $this->fallbackJs);
}
public function getCssUrl(): string
{
return '/nav/' . ($this->manifest()['style.css']['file'] ?? $this->fallbackCss);
}
// SSR fallback: fetched once from the shell, cached in Magento's cache
// backend, and inlined into the mount div at render time.
public function getHeaderHtml(): string
{
return $this->snapshotHtml('header.html');
}
public function getFooterHtml(): string
{
return $this->snapshotHtml('footer.html');
}
private function manifest(): array
{
// 1) read cached manifest
// 2) on miss, fetch remote manifest URL
// 3) cache parsed JSON
// 4) on failure, log and fall back to unhashed asset names
}
private function snapshotHtml(string $key): string
{
// 1) read cached snapshot for $key
// 2) on miss, fetch rendered HTML from the shell (e.g. /nav/header.html)
// 3) cache body with a short TTL
// 4) on failure, return '' so the shell still hydrates later
}
}
Laravel 11 - manifest service shape:
class SharedNavManifest
{
public function manifest(): array
{
return Cache::remember('shared_nav.manifest', 60, function () {
// GET config('services.shared_nav.manifest_url'), parse JSON.
// On failure, log and return [] so fallback filenames kick in.
});
}
public function jsUrl(): string
{
return '/nav/' . ($this->manifest()['src/main.tsx']['file'] ?? 'shared-nav.iife.js');
}
public function cssUrl(): string
{
return '/nav/' . ($this->manifest()['style.css']['file'] ?? 'shared-nav.css');
}
public function headerHtml(): string
{
return $this->snapshotHtml('header.html');
}
public function footerHtml(): string
{
return $this->snapshotHtml('footer.html');
}
private function snapshotHtml(string $key): string
{
return Cache::remember("shared_nav.snapshot:{$key}", 60, function () use ($key) {
// fetch rendered HTML from the shell (e.g. /nav/header.html)
// return '' on failure so the shell still hydrates later
});
}
}
That is why the line counts matter. The host code is small enough to be reviewable, boring enough to be supportable, and explicit enough that another PHP team can own it without learning a front-end platform first.
The host <-> shell contract
The two sides agree on a tiny surface:
Host provides:
<link rel="stylesheet" href="{{ $nav->cssUrl() }}">
<div id="sa-header" style="min-height: 80px;">{!! $nav->headerHtml() !!}</div>
<!-- page body -->
<div id="sa-footer">{!! $nav->footerHtml() !!}</div>
<script defer src="{{ $nav->jsUrl() }}"></script>
Shell provides:
- A
nav-fallback.htmlemitted at build time, split into header and footer snippets the host inlines into the mount divs (the SSR fallback). - Client-side mount into
#sa-headerand#sa-footerthat replaces the SSR snapshot with the interactive tree (dropdowns, mobile menu, state). - One CSS file, one JS file, no global pollution (IIFE scope).
- No knowledge of Magento or Laravel. No runtime config, no feature flags.
Everything else - routing, authentication, cart state, checkout - stays on the host. The nav does not know the host exists. The host does not know the nav is Preact. That is the whole integration.
The min-height: 80px on the header mount is anti-CLS insurance - the slot reserves its space before hydration, so Core Web Vitals do not punish the deferred render.
The SEO question, answered honestly
This is the part every microfrontend post skips or hand-waves. I will not.
Also, this section is intentionally based on observable crawler facts, not modelled SEO metrics. I am not claiming a ranking uplift from a synthetic score. I am showing what a crawler can and cannot see in the initial HTML before and after the fallback ships.
Without a fallback, initial HTML is two empty divs:
<div id="sa-header" style="min-height: 80px;"></div>
<div id="sa-footer"></div>
Googlebot renders JavaScript (eventually) and sees the nav - with a delay measured in days. But GPTBot, ClaudeBot, and PerplexityBot do not render JavaScript. They see the empty divs. As far as AI search is concerned, the site has no nav.
I measured this before shipping the SSR fallback. Three pages, five user-agents, identical curl invocations. Same URLs, same crawl method, same parsing rule - only the fallback changed.
Before SSR fallback:
| Metric | Homepage | /about | /portfolio |
|---|---|---|---|
| Bytes | 35,050 | 35,050 | 97,521 |
<a href> total |
12 | 12 | 12 |
| Anchors from nav | 0 | 0 | 0 |
Twelve anchors per page, none of them structural. Every page - no matter how deep - exposed the same twelve inline body links to a non-rendering crawler. Sitemap.xml covered URL discovery, but not the four things nav does beyond discovery:
- Link equity - a multi-level nav is hundreds of internal links per page pointing at categories. Without it, category pages lose authority.
- Crawl budget - Googlebot prioritises pages by incoming-link density. Sitemap-only pages get crawled less often.
- Topic hierarchy - sitemap is flat. Nav signals semantic structure ("Shop -> Men -> Shoes").
- AI assistant context - ChatGPT and Perplexity build mental models from HTML, often ignoring sitemaps. Without nav in HTML, AI knows your URLs but not your structure.
The three-level mitigation ladder:
<noscript>fallback with critical links inside the mount div (hours of work).- SSR skeleton - Vite emits a
nav-fallback.htmlat build time; hosts inline it into the mount divs before hydration replaces it (a day or two). - Full SSR service - a Node process renders each nav request server-side (a week, plus a new production dependency).
Level 2 is the sweet spot for an ecommerce group this size. We shipped it before the first production release. Same curl invocations, four days later:
After SSR fallback:
| Metric | Homepage | /about | /portfolio |
|---|---|---|---|
| Bytes | 98,881 | 98,881 | 161,348 |
<a href> total |
112 | 112 | 112 |
Anchors from nav (#sa-header) |
31 | 31 | 31 |
Anchors from footer (#sa-footer) |
69 | 69 | 69 |
All five user-agents received byte-identical HTML (the only per-request variance is the Laravel CSRF meta token). The nav and footer tree are in initial HTML - 100 additional anchors per page, constant across every page, visible to every crawler that can parse HTML.
That matters methodologically. A crawler can disagree with my interpretation of the SEO impact, but it cannot disagree with 35,050 -> 98,881 bytes or 12 -> 112 anchors under the same crawl conditions. This is a reusable audit method, not a one-off anecdote.
The gap closed on release day. No retroactive GSC panic, no "we measured a drop and here's how we fixed it" narrative. The honest framing is "we knew the risk, we closed it before shipping".
What this article proves today - and what it does not yet
This article proves three things:
- The integration pattern is real on two production-grade PHP stacks.
- The SEO risk is real if the shell ships with empty mount points only.
- A Level 2 fallback closes that crawler-visibility gap on day one.
What it does not prove yet is a 90-day business outcome story. I do not have a "three months later, here are CrUX and GSC deltas" chart in this draft, because that would require waiting for the post-release window to mature. I would rather publish the implementation pattern and the crawler evidence honestly than pretend I have impact numbers I do not have yet.
That makes this a build-and-ship case study, not a finished growth narrative. When post-launch search-console and field-performance data are mature enough to be worth showing, they belong in a follow-up article.
What this pattern does not solve
Not overselling: the shared nav is the minimum viable shared surface - that is its strength and its ceiling.
- Primary page content still diverges. Magento renders products; Laravel renders marketing copy.
- Shared checkout - not solved. Checkout lives in Magento; marketing links into it via cookies on a common parent domain.
- Shared authentication - not solved. Cookies, redirects, OAuth handshakes - all host-specific.
- Shared search - could be built on top of the shell, but we did not. Search UX is coupled to Magento-native catalogue data.
A shared nav is not a distributed-front-end strategy. It is a band-aid across a healed fracture. If you need a distributed front end, you need a different architecture.
When this pattern fits
Short checklist. If you check fewer than three boxes, do something else.
- You have two or more existing stacks with established teams you cannot realistically move.
- There is no budget or appetite for a full front-end unification right now.
- The primary pain is UX inconsistency, not performance or architectural debt.
- Nobody on the executive side is willing to own a "unified portal" programme.
- You need to be AI-agent ready - which means the nav must be in initial HTML, not only after JS runs.
If all five apply, the pattern pays for itself in weeks, not quarters.
What's next
The same shell is about to land on two more stacks in the same group - a greenfield Magento storefront rewrite and a full Laravel marketing rewrite. Both will consume the existing manifest.json unchanged. Zero additional shell work, the same integration footprint per host. That is the portability proof.
If the pattern looks like it might fit your stack, the interesting conversation is not "how do I build a shell" - Vite's library mode docs will get you there in an afternoon. The interesting conversation is the SEO contract and shipping Level 2 on day one instead of retrofitting it after Google Search Console punishes you.
For a mid-market team, that is usually the real decision framework:
- Level 1:
<noscript>links when the risk window is small and the nav is shallow. - Level 2: build-time SSR fallback when you need full crawler-visible structure without adding a Node service.
- Level 3: full SSR service when the nav is dynamic enough that static fallback HTML becomes a maintenance problem.
That is where outside perspective usually helps, and where I spend most of my consulting time.