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/event package;
  • the same architecture can later power article-card, product-card, or search-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