feat(examples): add micro-frontend example with Hub + multi-framework sub-apps#296
Open
feat(examples): add micro-frontend example with Hub + multi-framework sub-apps#296
Conversation
… sub-apps - Add ssr-micro-shared for sharing @esmx/router across all sub-apps - Add ssr-micro-html with native HTML + TypeScript - Add ssr-micro-vue2 with Vue 2.7 + Composition API - Add ssr-micro-vue3 with Vue 3.5 + SSR - Add ssr-micro-react with React 18 + Hooks - Add ssr-micro-hub to aggregate all sub-app routes - Configure modules.exports for framework library sharing (vue, react, react-dom) - Configure server-only exports for vue-server-renderer and @vue/server-renderer - Add README.md and README.en.md for each package - Remove outdated ssr-demo-html and ssr-demo-preact-htm examples
37060e3 to
0070dfb
Compare
added 24 commits
May 7, 2026 20:53
… page refresh loops
… cards - Add consistent styling system with CSS animations - Redesign home page with modern cards, icons, and feature section - Update all sub-apps with consistent card-based layout - Add fade-in animations for page transitions - Use container element pattern for clean unmounting - Improve visual hierarchy and responsive design
…igation - Create useLayout() composable in ssr-micro-shared - Add consistent sidebar with navigation across all micro-apps - Integrate layout into Vue2, Vue3, React, HTML, and Hub home page - Remove global layout styles from entry.server.ts
…t flicker on app switch
- Use fixed DOM IDs (esmx-sidebar, esmx-layout-footer) across all apps - Only update active state and rebind events when DOM already exists - Avoid DOM recreation during micro-app transitions
- Layout is now a class instance with mount/unmount methods - useLayout() acts as a composable factory for Vue2/Vue3 setup usage - Maintains shared DOM behavior to prevent flicker
…removing global animations - Remove global fadeIn animation from entry.server.ts - Return empty string in Layout.header if sidebar DOM already exists - Prevent v-html from replacing existing sidebar on each app switch
- Vue3: Move app creation to factory function, use v-once in template - React: Simplify app structure, add biome-ignore for safe HTML - HTML: Move Layout creation to mount function - Hub: Simplify home.ts Layout usage
…container element
…HomeApp) - HtmlApp: class-based HTML micro-app with render/mount/unmount methods - HomeApp: class-based Hub home page with render/mount/unmount methods - Both follow Vue pattern: instance created in factory function
- Move SSR-specific deps (ssr-micro-shared, @esmx/router) to devDependencies - Fix React app: useMemo for Layout, proper hook dependencies - Fix Vue2 entry comments to match other sub-apps - Remove unused private member in HtmlApp - Add biome override for dangerouslySetInnerHTML in micro-app
Move vue-server-renderer and @vue/server-renderer imports into renderToString() to prevent client-side module resolution errors. These modules are server-only (client: false) and should not be statically imported in client bundles.
…hrefs router.resolveLink() generates absolute URLs based on the router's base URL. During static build (postBuild), the base URL is hardcoded to localhost, which causes all sidebar links to point to http://localhost:3000/... in production deployments. This breaks SPA navigation and causes unexpected jumps to localhost. Fix: Use relative paths (e.g., /html) directly for href attributes. The actual SPA navigation is handled by event delegation calling router.push(), which correctly resolves paths against the current base.
Add RouterOptions.resolveLink option to allow transforming the result
of resolveLink() before it's returned. This enables use cases like
removing the domain from href attributes for static site generation.
Usage:
const router = new Router({
base: new URL(base),
resolveLink(link) {
link.attributes.href = link.route.url.href
.slice(link.route.url.origin.length) || '/';
return link;
}
});
Also update micro-app example to use resolveLink option instead of
hardcoding relative paths in hrefs.
Cloudflare Pages provides CF_PAGES_URL env var during build. Use it to generate correct base URL with /ssr-micro-hub/ path prefix so that resolveLink generates correct relative hrefs like /ssr-micro-hub/html instead of /html.
94bc7e4 to
d8f5f50
Compare
vue-server-renderer collects component CSS via context.renderStyles, but the context was never passed. Use a WeakMap utility (setSsrStyles/ getSsrStyles) keyed by Router instance to pass collected CSS from the Vue2 micro-app to the hub's entry.server, injecting it into <head>.
Vue2's $mount(el, true) checks data-server-rendered on el itself. The wrapper div pushed the attribute one level deeper to el.firstElementChild, causing Vue2 to skip hydration and do a full re-render, which caused page style flickering. Vue3 and React hydration starts from container.firstChild, so the wrapper is still needed for them.
Vue2 hydration does a strict byte-for-byte comparison of v-html domProps.innerHTML vs the browser-normalized elm.innerHTML (line 6930 of vue.runtime.common.dev.js). Multiline style attribute values from template literals survive in SSR but get collapsed by the browser, causing the comparison to fail and hydration to bail. Add normalizeHtml() helper that compresses whitespace inside style attribute values so the generated HTML matches browser output.
…ue2 app Extract the sidebar into a header.vue component using Vue2 scoped styles and useLink() for navigation. Rewrite app.vue content area with scoped CSS. This eliminates the v-html hydration byte-for-byte comparison problem entirely and provides proper CSS scoping via data-v-* attributes.
Vue2's custom header component was the wrong direction. All four micro-apps should use the shared Layout for header/footer with the normalizeHtml fix already in layout.ts. Revert Vue2 app.vue and Vue3/React back to using Layout + v-html/dangerouslySetInnerHTML.
- Remove unused ssr/getCurrentInstance/useSSRContext variables from Vue2/Vue3 app.vue - Remove unnecessary nextTick wrappers around layout.mount() - Hoist React App component (was defined 3x identically in create-app.tsx) - Deduplicate Vue3 onMount/onHydration (both call this.app.mount) - Extract SIDEBAR_WIDTH constant (260px) to shared layout.ts - Remove unnecessary Promise.resolve() wrapper in react/create-app.tsx
Add inline onclick handler to anchor tags to preventDefault() before the browser processes the href on mobile touch events. Without this, mobile browsers navigate via href before the delegated click event fires, causing full page refreshes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace direct router.context.head assignments with setRouterHead/getRouterHead utilities in ssr-micro-shared. Uses WeakMap<Router, Unhead> for storage, consistent with the existing ssr-styles pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add responsive mobile sidebar with hamburger menu and overlay - Fix SPA navigation by adding onclick preventDefault to nav links - Ensure cross-app switching is seamless without full page reload Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace .filter((value) => value.file) with if (value.file) inside the map callback. The filter was incorrectly excluding pkg-type exports (like vue-server-renderer) which have file: undefined, causing them to not be externalized and triggering SWC minifier errors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…new app removal When switching micro-apps, Vue/React unmount() removes its own DOM. The subsequent firstElementChild?.remove() then targets the NEW app instead of the old one, causing the newly mounted app to disappear. Fix by capturing the old element reference before calling unmount().
Non-hydration branch was creating intermediate div causing old+new app DOM to coexist during route switches. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
25475b3 to
1d19f7d
Compare
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add nav item hover effect (rgba blue background) - Add focus-visible ring for hamburger and close buttons - Add mobile sidebar close button (×) with event handler - Mobile sidebar width uses min(260px, 80vw) - Reduce mobile content padding to 16px via #esmx-main - Unify nav icons: 🏠 → Hm (all text-based) - Add card hover effect (shadow + border color) on home page - Unify badge size to 56×56px / 14px radius across all apps - Add id=esmx-main to main content areas for CSS targeting
…y, type scale - Localize logo as inline SVG data URI (layout + home page) - Add favicon as inline SVG data URI via Layout header - Reduce desktop sidebar brand font from 1.5rem to 1.25rem - Fix type scale: tag badge 12px → 0.75rem - Add role=img + aria-label to all badge elements for accessibility
- Sidebar nav: Esmx/HTML5/Vue/React SVG icons - Detail page badges: framework SVGs on gradient backgrounds - Home page cards: framework SVG icons - SVG logos encoded as inline data URIs or markup strings
…tokens - Define CSS custom properties for all colors (bg, text, border, etc.) - Add @media (prefers-color-scheme: dark) overrides - Add transition animations to nav items and cards - Add card hover scale effect (transform: scale(1.01)) - Add subtle radial gradient to main content background - Replace all hardcoded colors with var(--esmx-*) tokens - Update hub entry.server.ts body styles to use CSS variables
c2dfcec to
ceae750
Compare
toggleSidebar now uses direct style manipulation as fallback for inline style conflicts. Mobile open state CSS uses !important to ensure transform and width overrides win over inline styles.
ceae750 to
8cbc5c2
Compare
added 8 commits
May 10, 2026 15:21
…flicts When switching micro-apps, the new app's Layout.mount() runs before the old app's DOM is removed. With hardcoded global IDs (esmx-menu-btn, esmx-sidebar, etc.), document.getElementById finds the OLD element, binding events that get destroyed moments later. Prefix all Layout element IDs with this.appId so each instance has unique DOM targets, independent of mount order.
Layout now assigns the main content id dynamically in mount() by finding headerEl.nextElementSibling. App files no longer need to set any esmx-main id — the Layout manages it entirely. All sidebar/menu/overlay IDs now use this.appId prefix to avoid conflicts when switching micro-apps.
- Remove headerId/footerId — Layout no longer exposes IDs externally - All DOM queries use this.appId-scoped IDs (sidebar, menu-btn, etc.) - Mount/unmount only touch elements created by Layout itself - Remove CSS rules targeting external elements (#s-main, body) - App files no longer reference any Layout IDs
… API Layout now scopes all internal DOM IDs (sidebar, menu-btn, overlay, close-btn) with this.appId prefix to avoid conflicts when switching micro-apps. Public API (headerId, footerId) preserved — these are the contract between Layout and framework for positioning header/footer content. mount() finds sidebar via scoped ID instead of headerId container, making it resilient to DOM ordering during app transitions.
- vue: 3.5.23 → 3.5.34 - @vue/server-renderer: 3.5.23 → 3.5.34
- unhead: ^2.1.0 → ^3.1.0 - @unhead/vue: ^2.1.0 → ^3.1.0 - @unhead/react: ^2.1.15 (kept, 3.x requires React ≥19) - vue-tsc: ^3.0.5 → ^3.2.8 - tsc-alias: ^1.8.16 → ^1.8.17 - @types/node: ^24.0.0 → ^24.12.3 - typescript: 5.8.3 → 6.0.3 - vue: ^3.5.23 → ^3.5.34 - @vue/server-renderer: ^3.5.23 → ^3.5.34 - react/react-dom/@types: ^18.0.0 → ^18.3.1
- react/react-dom: ^18.3.1 → ^19.0.6 - @types/react: ^18.3.1 → ^19.2.14 - @types/react-dom: ^18.3.1 → ^19.2.3 - @unhead/react kept at 2.1.15 (3.x needs react>=19.2.4 not yet stable)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BREAKING CHANGE: Redesign the router mounting API (
root→appId) and addfirst-class SSR hydration support. Replace all legacy examples with a unified
micro-frontend architecture demo.
Core API Changes
rootreplaced byappIdRouterOptions.root(string | HTMLElement) →appId(string)document.getElementById(appId)renderToString()output in<div id="${appId}">'app'SSR hydration support
RouterMicroAppOptionsaddshydration(el)callback<div id="${appId}" data-ssr>...</div>data-ssr, callshydration()instead ofmount(), then removes the attributeresolveLinkhookRouterOptions.resolveLinkoption for customrouter-linktransformationDev-only SSR validation
renderToString()output is validated to contain exactly one root elementExample Projects
Removed 15+ legacy demos (
router-demo-*,ssr-demo-*,ssr-vue2-host/remote).Added
examples/micro-app/micro-frontend suite:ssr-micro-hubssr-micro-htmlssr-micro-vue2ssr-micro-vue3ssr-micro-reactssr-micro-sharedNotable Fixes
v-htmlhydration mismatch caused by inline style whitespace normalizationrenderToStringsodata-server-renderedattaches correctly