Vyriy MFE Profile Card Starter
A microfrontend starter should be small enough to understand in one sitting, but realistic enough to teach the contracts that matter in production.
For the Vyriy CLI mfe / openmfe preset, a profile-card example is a good default. It is more useful than a toy hello-card, but much simpler than a product-card that would require catalog APIs, prices, availability, currencies, and business-specific data.
The goal of this starter is to show a calm architecture for microfrontends:
Attributes are input.
Events are output.
Component is UI.
Prerender is HTML.
Semantic is meaning.
Manifest is contract.
Static assets are deployable files.
This structure can later evolve into a real widget for vyriy.dev, such as a documentation search widget backed by a local index, embeddings, or an API-side OpenAI integration.
Why profile-card?
The generated component can be used like this:
<vyriy-profile-card name="Ada Lovelace" role="Mathematician" avatar-url="/avatar.svg"></vyriy-profile-card>
It demonstrates the key OpenMFE ideas without requiring a database or remote service:
- attributes are simple and visible;
- prerendered HTML is easy to inspect;
- semantic output can be valid JSON-LD using
Person; - manifest attributes, events, functions, icon, and screenshots remain meaningful;
- outbound browser communication can use the existing
@vyriy/eventpackage; - the same architecture can later power
article-card,product-card, orsearch-widget.
External contract idea
The public microfrontend surface can be exposed as:
GET /demo
GET /prerender
GET /semantic
GET /manifest.yml
GET /js/profile-card.js
GET /css/profile-card.css
GET /icon.svg
GET /avatar.svg
GET /screenshots/default.svg
GET /screenshots/compact.svg
The important part is not whether all these URLs come from one server or several servers. The important part is that the contract stays stable.
In development, these endpoints can be split across api, ui, and static. In production, ui and static can be merged into one Docker static server or one S3 + CDN origin, while api can run as Docker, Fargate, or AWS Lambda.
Development architecture
The development setup should keep boundaries explicit:
api -> OpenMFE contract endpoints + host demo
ui -> frontend bundle + pure UI demo
static -> icons, screenshots, avatar, public assets
A practical local setup:
api -> http://localhost:3000
ui -> http://localhost:3001
static -> http://localhost:3002
For UI development, webpack-dev-server is a good fit because it can serve the UI demo, rebuild the custom element bundle, and expose generated JS/CSS during development.
API:
GET http://localhost:3000/demo
GET http://localhost:3000/prerender
GET http://localhost:3000/semantic
GET http://localhost:3000/manifest.yml
UI:
GET http://localhost:3001/demo
GET http://localhost:3001/js/profile-card.js
GET http://localhost:3001/css/profile-card.css
STATIC:
GET http://localhost:3002/icon.svg
GET http://localhost:3002/avatar.svg
GET http://localhost:3002/screenshots/default.svg
Production architecture
Option 1: API + CDN
This is the preferred production shape:
API runtime:
/demo
/prerender
/semantic
/manifest.yml
CDN:
/js/profile-card.js
/css/profile-card.css
/icon.svg
/avatar.svg
/screenshots/default.svg
The API can be deployed as Docker, Fargate, or Lambda. The UI and static files can be deployed to S3 + CloudFront or served by a static Docker image.
Option 2: all-in-one Docker
For simple deployments, everything can be served from one container:
/demo
/prerender
/semantic
/manifest.yml
/js/profile-card.js
/css/profile-card.css
/icon.svg
/avatar.svg
/screenshots/default.svg
This is simple, but it gives less caching flexibility than separating API from static assets.
Option 3: static-only demo
For a fully static demo, the project can generate:
/demo.html
/prerender.html
/semantic.json
/manifest.yml
/js/profile-card.js
/css/profile-card.css
/icon.svg
/screenshots/default.svg
This is useful for documentation, but real OpenMFE-style integration is better demonstrated with live endpoints.
Recommended repository structure
my-mfe/
packages/
profile-card-contract/
src/
attributes.ts
config.ts
createManifest.ts
createSemantic.ts
events.ts
normalizeAttributes.ts
types.ts
index.ts
workspaces/
api/
src/
routes/
demo.ts
manifest.ts
prerender.ts
semantic.ts
env.ts
index.ts
ui/
src/
ProfileCard.ts
defineProfileCard.ts
events.ts
renderProfileCard.ts
enhanceProfileCard.ts
styles.scss
index.ts
public/
demo.html
static/
public/
icon.svg
avatar.svg
screenshots/
default.svg
compact.svg
.env.example
package.json
README.md
This follows the usual Vyriy preference:
contract separately
api separately
ui separately
static separately
The profile-card-contract package is optional for the smallest starter, but it is useful because both api and ui need the same attribute names, defaults, event names, event schemas, and manifest metadata.
Environment variables
The preset should separate public URLs from runtime internals.
MFE_TAG=vyriy-profile-card
MFE_NAME=Vyriy Profile Card
MFE_VERSION=0.1.0
API_PUBLIC_URL=http://localhost:3000
UI_PUBLIC_URL=http://localhost:3001
STATIC_PUBLIC_URL=http://localhost:3002
API_PORT=3000
UI_PORT=3001
STATIC_PORT=3002
The API uses UI_PUBLIC_URL to render scripts and styles into /demo:
<link rel="stylesheet" href="${UI_PUBLIC_URL}/css/profile-card.css" />
<script type="module" src="${UI_PUBLIC_URL}/js/profile-card.js"></script>
The API uses STATIC_PUBLIC_URL to generate manifest URLs:
icon: 'http://localhost:3002/icon.svg'
screenshots:
- url: 'http://localhost:3002/screenshots/default.svg'
The UI may use API_PUBLIC_URL only for its local demo links or examples.
Shared MFE config
The manifest should not be hardcoded as a static YAML string. It should be generated from a typed config.
export const profileCardConfig = {
tag: 'vyriy-profile-card',
name: 'Vyriy Profile Card',
version: '0.1.0',
description: 'A small OpenMFE-oriented profile card starter.',
paths: {
frontend: '/js/profile-card.js',
css: '/css/profile-card.css',
prerender: '/prerender',
semantic: '/semantic',
manifest: '/manifest.yml',
icon: '/icon.svg',
avatar: '/avatar.svg',
screenshots: {
default: '/screenshots/default.svg',
compact: '/screenshots/compact.svg',
},
},
} as const;
Then the API can produce deployment-specific absolute URLs:
const manifest = createManifest({
config: profileCardConfig,
apiPublicUrl: env.API_PUBLIC_URL,
uiPublicUrl: env.UI_PUBLIC_URL,
staticPublicUrl: env.STATIC_PUBLIC_URL,
});
Attributes
export const profileCardAttributes = {
name: {
required: true,
description: 'Profile display name.',
schema: {
type: 'string',
minLength: 1,
},
},
role: {
required: false,
description: 'Short profile role or title.',
default: 'Developer',
schema: {
type: 'string',
},
},
'avatar-url': {
required: false,
description: 'Optional avatar image URL.',
default: '/avatar.svg',
schema: {
type: 'string',
format: 'uri-reference',
},
},
} as const;
A simple normalized runtime type:
export type ProfileCardAttributes = {
name: string;
role: string;
'avatar-url': string;
};
Events
Vyriy already has a small event helper package for microfrontends: @vyriy/event.
The starter should use it instead of hand-writing new CustomEvent(...) everywhere. This keeps event names validated, makes analytics payloads consistent, and teaches the host integration contract from the first generated example.
The package exposes two useful groups:
custom events -> validated MFE-owned events
analytics events -> standard openmfe.analytics events
For profile-card, the starter can emit:
vyriy-profile-card.select
openmfe.analytics
The local event contract can live in packages/profile-card-contract/src/events.ts:
export const profileCardSelectEventName = 'vyriy-profile-card.select' as const;
export type ProfileCardSelectDetail = {
name: string;
role: string;
avatarUrl: string | null;
};
export const profileCardEvents = [
{
name: profileCardSelectEventName,
description: 'Emitted when the user selects the profile card.',
schema: {
type: 'object',
required: ['name'],
properties: {
name: {
type: 'string',
description: 'Selected profile display name.',
},
role: {
type: 'string',
description: 'Selected profile role.',
},
avatarUrl: {
type: ['string', 'null'],
description: 'Selected profile avatar URL.',
},
},
},
},
{
name: 'openmfe.analytics',
description: 'Standard OpenMFE analytics event emitted for profile card interactions.',
schema: {
type: 'object',
required: [
'name',
'origin',
'id',
'variant',
'action',
'category',
'data',
],
properties: {
name: { type: 'string' },
origin: { type: 'string' },
id: { type: ['string', 'null'] },
variant: { type: ['string', 'null'] },
action: { type: 'string' },
category: { type: ['string', 'null'] },
data: { type: 'object' },
},
},
},
] as const;
This gives the generated MFE a clean rule:
attributes in
events out
API routes
GET /demo
This is the host demo. It shows the full integration story:
SSR/SSG HTML
+ prerendered markup
+ CSS from UI
+ JS from UI
+ browser enhancement
Example response:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vyriy Profile Card MFE Demo</title>
<link rel="stylesheet" href="http://localhost:3001/css/profile-card.css" />
</head>
<body>
<main>
<h1>Vyriy Profile Card MFE Demo</h1>
<vyriy-profile-card name="Ada Lovelace" role="Mathematician" avatar-url="http://localhost:3002/avatar.svg">
<article class="vyriy-profile-card" data-openmfe="vyriy-profile-card">
<img class="vyriy-profile-card__avatar" src="http://localhost:3002/avatar.svg" alt="Ada Lovelace" />
<div class="vyriy-profile-card__body">
<h2 class="vyriy-profile-card__name">Ada Lovelace</h2>
<p class="vyriy-profile-card__role">Mathematician</p>
</div>
</article>
</vyriy-profile-card>
</main>
<script type="module" src="http://localhost:3001/js/profile-card.js"></script>
<script type="module">
const card = document.querySelector('vyriy-profile-card');
card?.addEventListener('vyriy-profile-card.select', (event) => {
console.log('Profile selected:', event.detail);
});
card?.addEventListener('openmfe.analytics', (event) => {
console.log('Analytics event:', event.detail);
});
</script>
</body>
</html>
The host demo is API-owned because it demonstrates how a host can combine prerendered HTML with UI assets and listen to outbound MFE events.
GET /prerender
The endpoint should accept query parameters matching the frontend attributes.
GET /prerender?name=Ada%20Lovelace&role=Mathematician&avatar-url=/avatar.svg
Example response:
<article class="vyriy-profile-card" data-openmfe="vyriy-profile-card">
<img class="vyriy-profile-card__avatar" src="/avatar.svg" alt="Ada Lovelace" />
<div class="vyriy-profile-card__body">
<h2 class="vyriy-profile-card__name">Ada Lovelace</h2>
<p class="vyriy-profile-card__role">Mathematician</p>
</div>
</article>
Recommended response headers:
{
'content-type': 'text/html; charset=utf-8',
'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',
'access-control-allow-origin': '*',
'x-content-type-options': 'nosniff',
}
GET /semantic
The semantic endpoint is not UI. It returns machine-readable meaning for the same configuration.
GET /semantic?name=Ada%20Lovelace&role=Mathematician&avatar-url=/avatar.svg
Example response:
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Ada Lovelace",
"jobTitle": "Mathematician",
"image": "/avatar.svg"
}
Recommended response headers:
{
'content-type': 'application/ld+json; charset=utf-8',
'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',
'access-control-allow-origin': '*',
'x-content-type-options': 'nosniff',
}
GET /manifest.yml
The manifest is the public contract of the MFE. It describes where the frontend lives, how to call the prerender and semantic endpoints, which attributes are accepted, which events can be emitted, and which assets describe the component.
Example response:
version: '1.1'
name: 'Vyriy Profile Card'
source: 'http://localhost:3001/js/profile-card.js'
tag: 'vyriy-profile-card'
url:
frontend: 'http://localhost:3001/js/profile-card.js'
prerender: 'http://localhost:3000/prerender'
semantic: 'http://localhost:3000/semantic'
publisher:
name: 'Vyriy'
email: 'hello@example.com'
icon: 'http://localhost:3002/icon.svg'
description: 'A small OpenMFE-oriented profile card starter for Vyriy CLI.'
documentation: 'https://vyriy.dev/'
attributes:
- name: 'name'
description: 'Profile display name.'
required: true
schema:
type: 'string'
minLength: 1
- name: 'role'
description: 'Short profile role or title.'
required: false
schema:
type: 'string'
default: 'Developer'
- name: 'avatar-url'
description: 'Optional avatar image URL.'
required: false
schema:
type: 'string'
format: 'uri-reference'
default: '/avatar.svg'
events:
- name: 'vyriy-profile-card.select'
description: 'Emitted when the user selects the profile card.'
schema:
type: 'object'
required:
- name
properties:
name:
type: 'string'
description: 'Selected profile display name.'
role:
type: 'string'
description: 'Selected profile role.'
avatarUrl:
type:
- 'string'
- 'null'
description: 'Selected profile avatar URL.'
- name: 'openmfe.analytics'
description: 'Standard OpenMFE analytics event emitted for profile card interactions.'
schema:
type: 'object'
required:
- name
- origin
- id
- variant
- action
- category
- data
properties:
name:
type: 'string'
description: 'Analytics event name.'
origin:
type: 'string'
description: 'Microfrontend tag name.'
id:
type:
- 'string'
- 'null'
description: 'Component instance id or null.'
variant:
type:
- 'string'
- 'null'
description: 'Experiment variant or null.'
action:
type: 'string'
description: 'User action.'
category:
type:
- 'string'
- 'null'
description: 'Analytics category or null.'
data:
type: 'object'
description: 'Custom analytics payload.'
functions:
- name: 'refresh'
description: 'Refreshes the current profile card rendering.'
async: true
parameters: []
return:
description: 'Refresh result.'
schema:
type: 'object'
properties:
ok:
type: 'boolean'
description: 'Whether refresh completed successfully.'
semantic:
type: 'object'
required:
- '@context'
- '@type'
- 'name'
properties:
'@context':
type: 'string'
description: 'JSON-LD context URL.'
'@type':
type: 'string'
description: 'Schema.org entity type.'
name:
type: 'string'
description: 'Profile display name.'
jobTitle:
type: 'string'
description: 'Profile role or title.'
image:
type: 'string'
description: 'Profile avatar URL.'
screenshots:
- url: 'http://localhost:3002/screenshots/default.svg'
description: 'Default profile card rendering.'
- url: 'http://localhost:3002/screenshots/compact.svg'
description: 'Compact profile card rendering.'
examples:
- description: 'Default profile card.'
attributes:
name: 'Ada Lovelace'
role: 'Mathematician'
avatar-url: 'http://localhost:3002/avatar.svg'
repository: 'https://github.com/evheniy/vyriy'
Recommended response headers:
{
'content-type': 'application/yaml; charset=utf-8',
'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',
'access-control-allow-origin': '*',
'x-content-type-options': 'nosniff',
}
If a hash is available, add etag:
{
etag: '"manifest-hash"',
}
Do not use immutable for a stable /manifest.yml URL. The manifest is a contract and may change between deploys. Use immutable only for versioned or content-hashed assets.
UI workspace
The UI workspace owns the browser component, styles, and pure UI demo.
workspaces/ui/
src/
ProfileCard.ts
defineProfileCard.ts
events.ts
renderProfileCard.ts
enhanceProfileCard.ts
styles.scss
index.ts
public/
demo.html
events.ts
The UI workspace can wrap @vyriy/event behind local helper functions. This keeps the component small and keeps event names in one place.
import { dispatchAnalyticsEvent, dispatchCustomEvent } from '@vyriy/event';
import type { ProfileCardSelectDetail } from '@app/profile-card-contract';
import { profileCardSelectEventName } from '@app/profile-card-contract';
export const dispatchProfileCardSelectEvent = (target: HTMLElement, detail: ProfileCardSelectDetail) => {
dispatchCustomEvent(target, profileCardSelectEventName, detail);
};
export const dispatchProfileCardAnalyticsEvent = (target: HTMLElement, detail: ProfileCardSelectDetail) => {
dispatchAnalyticsEvent(target, {
name: 'profile_card_select',
action: 'select profile card',
category: 'profile',
data: detail,
});
};
The generated package should include @vyriy/event as a dependency:
{
"dependencies": {
"@vyriy/event": "^0.1.0"
}
}
ProfileCard.ts
A minimal custom element can start simple:
import { dispatchProfileCardAnalyticsEvent, dispatchProfileCardSelectEvent } from './events.js';
import { renderProfileCard } from './renderProfileCard.js';
export class ProfileCard extends HTMLElement {
static get observedAttributes() {
return ['name', 'role', 'avatar-url'];
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
if (this.isConnected) {
this.render();
}
}
private render() {
const name = this.getAttribute('name') ?? '';
const role = this.getAttribute('role') ?? 'Developer';
const avatarUrl = this.getAttribute('avatar-url') ?? '/avatar.svg';
this.innerHTML = renderProfileCard({
name,
role,
'avatar-url': avatarUrl,
});
this.querySelector('.vyriy-profile-card')?.addEventListener('click', () => {
const detail = {
name,
role,
avatarUrl,
};
dispatchProfileCardSelectEvent(this, detail);
dispatchProfileCardAnalyticsEvent(this, detail);
});
}
}
The component does not know anything about the host application. It only emits DOM events.
defineProfileCard.ts
import { ProfileCard } from './ProfileCard.js';
export const defineProfileCard = () => {
if (!customElements.get('vyriy-profile-card')) {
customElements.define('vyriy-profile-card', ProfileCard);
}
};
index.ts
import './styles.scss';
import { defineProfileCard } from './defineProfileCard.js';
export { ProfileCard } from './ProfileCard.js';
export { defineProfileCard } from './defineProfileCard.js';
defineProfileCard();
UI demo
The UI demo is intentionally different from the API demo.
It shows only client-side rendering:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Profile Card UI Demo</title>
</head>
<body>
<main>
<h1>Profile Card UI Demo</h1>
<vyriy-profile-card
name="Ada Lovelace"
role="Mathematician"
avatar-url="http://localhost:3002/avatar.svg"
></vyriy-profile-card>
</main>
<script type="module" src="/js/profile-card.js"></script>
<script type="module">
const card = document.querySelector('vyriy-profile-card');
card?.addEventListener('vyriy-profile-card.select', (event) => {
console.log('UI demo event:', event.detail);
});
</script>
</body>
</html>
The API demo shows host integration. The UI demo shows frontend development.
Enhancement vs hydration
For Web Components, it is often more accurate to say enhancement instead of React-style hydration.
The API can return stable prerendered HTML. The browser bundle then upgrades the custom element and attaches behavior.
For the starter, it is acceptable if the custom element re-renders its content after loading. But the README should describe the intended model clearly:
The example uses progressive enhancement.
The prerender endpoint returns stable HTML.
The frontend bundle upgrades the component in the browser.
A future version can optimize this by detecting existing markup and only attaching events instead of replacing the DOM.
Static workspace
The static workspace owns public assets:
workspaces/static/public/
icon.svg
avatar.svg
screenshots/
default.svg
compact.svg
icon.svg
<svg
width="256"
height="256"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="title description"
>
<title id="title">Vyriy Profile Card icon</title>
<desc id="description">A calm rounded square icon with a profile card symbol.</desc>
<rect width="256" height="256" rx="56" fill="#111827" />
<rect x="56" y="48" width="144" height="160" rx="24" fill="#F9FAFB" />
<circle cx="128" cy="104" r="36" fill="#9CA3AF" />
<rect x="84" y="156" width="88" height="14" rx="7" fill="#111827" />
<rect x="96" y="180" width="64" height="10" rx="5" fill="#6B7280" />
<circle cx="184" cy="184" r="20" fill="#10B981" />
<path
d="M175 184L181 190L194 176"
stroke="white"
stroke-width="6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Screenshot example
For the starter, screenshots can be SVG files. They are easy to commit, review, and edit. Later, production widgets can generate .webp screenshots with Playwright.
<svg
width="1280"
height="720"
viewBox="0 0 1280 720"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="title description"
>
<title id="title">Default profile card screenshot</title>
<desc id="description">A profile card microfrontend rendered in a desktop host page.</desc>
<rect width="1280" height="720" fill="#F3F4F6" />
<rect x="80" y="72" width="1120" height="576" rx="32" fill="white" />
<rect x="128" y="120" width="360" height="480" rx="28" fill="#F9FAFB" stroke="#E5E7EB" />
<circle cx="308" cy="244" r="72" fill="#9CA3AF" />
<rect x="188" y="368" width="240" height="28" rx="14" fill="#111827" />
<rect x="224" y="420" width="168" height="18" rx="9" fill="#6B7280" />
<rect x="232" y="492" width="152" height="44" rx="22" fill="#10B981" />
<rect x="560" y="152" width="420" height="40" rx="20" fill="#111827" />
<rect x="560" y="232" width="520" height="20" rx="10" fill="#6B7280" />
<rect x="560" y="272" width="460" height="20" rx="10" fill="#9CA3AF" />
<rect x="560" y="312" width="360" height="20" rx="10" fill="#D1D5DB" />
<rect x="560" y="420" width="520" height="96" rx="24" fill="#F9FAFB" stroke="#E5E7EB" />
</svg>
Recommended headers for SVG assets:
{
'content-type': 'image/svg+xml; charset=utf-8',
'cache-control': 'public, max-age=31536000, immutable',
'access-control-allow-origin': '*',
'x-content-type-options': 'nosniff',
}
For .webp screenshots:
{
'content-type': 'image/webp',
'cache-control': 'public, max-age=31536000, immutable',
'access-control-allow-origin': '*',
'x-content-type-options': 'nosniff',
}
Generated scripts
A generated project could expose scripts like this:
{
"scripts": {
"start": "run-p start:*",
"start:api": "yarn workspace @app/api start",
"start:ui": "yarn workspace @app/ui start",
"start:static": "yarn workspace @app/static start",
"build": "run-s build:*",
"build:api": "yarn workspace @app/api build",
"build:ui": "yarn workspace @app/ui build",
"build:static": "yarn workspace @app/static build",
"lint": "run-s lint:*",
"test": "run-s test:*",
"check": "run-s lint build test"
}
}
For the UI workspace, webpack-dev-server can serve /demo, /js/profile-card.js, and /css/profile-card.css during development.
The generated MFE preset should also install @vyriy/event in the UI workspace or root workspace dependencies, depending on the final monorepo layout.
CLI preset implementation notes
The mfe / openmfe preset can generate this in stages:
1. create base monorepo files
2. create shared profile-card contract
3. create API routes
4. create UI custom element
5. create static assets
6. wire dev scripts
7. run yarn install
8. run lint, test, and build checks
A useful generated README should explain the contract with one compact block:
Attributes are input.
Events are output.
Prerender is HTML.
Semantic is meaning.
Manifest is contract.
Static assets are cacheable.
The event section should mention that outbound communication is implemented with @vyriy/event:
vyriy-profile-card.select -> component-owned event
openmfe.analytics -> standard analytics event
This makes the starter small, but not fake. It demonstrates how a host application can render, enhance, inspect, and observe a microfrontend without importing its internals.
How this evolves into search-widget
The starter should not begin with OpenAI or search. It should first teach the contract.
A future search-widget can keep the same architecture:
GET /demo
GET /prerender
GET /semantic
GET /manifest.yml
GET /search?q=router
GET /suggest?q=rou
GET /js/search-widget.js
GET /css/search-widget.css
The browser component should never call OpenAI directly. API keys stay on the backend:
browser MFE
-> /search?q=...
-> API
-> local index / embeddings / OpenAI API
-> normalized results
The starter therefore prepares the right boundaries:
UI knows API_PUBLIC_URL.
API knows secrets.
Manifest exposes public contract.
Static assets are cacheable.
Preset implementation notes
For the first Vyriy CLI version, keep the generated MFE small:
preset: mfe
component tag: vyriy-profile-card
example type: profile card
semantic type: Person
Later, the CLI can offer examples:
MFE example:
1. profile-card
2. article-card
3. search-widget
The profile-card starter should include enough structure to become real, but not so much that the first generated project feels heavy.
Summary
The proposed starter has three clean development surfaces:
api -> contract endpoints and host demo
ui -> frontend bundle and UI demo
static -> public assets
It has a clear production path:
api -> Docker, Fargate, or Lambda
ui -> Docker static server or S3 + CDN
static -> usually merged with UI CDN
It teaches the important OpenMFE-oriented contracts:
/prerender -> HTML
/semantic -> JSON-LD
/manifest.yml -> formal public contract
/js/*.js -> browser implementation
/css/*.css -> styles
/icon.svg -> identity
/screenshots -> visual documentation
This makes profile-card a good calm starter: simple enough for a generated example, but structured enough to become the foundation for real widgets on vyriy.dev.
References
- OpenMFE specification: https://openmfe.org/architecture/specification/
- OpenMFE performance notes on prerendering: https://openmfe.org/development/performance/
- OpenMFE development and manifest notes: https://openmfe.org/development/microfrontend-deepdive/
- OpenMFE devtools and manifest validator: https://openmfe.org/development/openmfe-devtools/