# Vyriy MFE Profile Card Starter

A calm OpenMFE-oriented starter architecture for Vyriy CLI presets.

Published: 2026-05-28
Tags: vyriy, calm-architecture, mfe, openmfe, widget, Profile Card
Source: https://vyriy.dev/blog/vyriy-mfe-profile-card-starter/

---

# 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:

```txt
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:

```html
<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:

```txt
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:

```txt
api     -> OpenMFE contract endpoints + host demo
ui      -> frontend bundle + pure UI demo
static  -> icons, screenshots, avatar, public assets
```

A practical local setup:

```txt
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.

```txt
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:

```txt
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:

```txt
/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:

```txt
/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

```txt
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:

```txt
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.

```env
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`:

```html
<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:

```yaml
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.

```ts
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:

```ts
const manifest = createManifest({
  config: profileCardConfig,
  apiPublicUrl: env.API_PUBLIC_URL,
  uiPublicUrl: env.UI_PUBLIC_URL,
  staticPublicUrl: env.STATIC_PUBLIC_URL,
});
```

## Attributes

```ts
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:

```ts
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:

```txt
custom events      -> validated MFE-owned events
analytics events   -> standard openmfe.analytics events
```

For `profile-card`, the starter can emit:

```txt
vyriy-profile-card.select
openmfe.analytics
```

The local event contract can live in `packages/profile-card-contract/src/events.ts`:

```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:

```txt
attributes in
events out
```

## API routes

### `GET /demo`

This is the host demo. It shows the full integration story:

```txt
SSR/SSG HTML
+ prerendered markup
+ CSS from UI
+ JS from UI
+ browser enhancement
```

Example response:

```html
<!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.

```txt
GET /prerender?name=Ada%20Lovelace&role=Mathematician&avatar-url=/avatar.svg
```

Example response:

```html
<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:

```ts
{
  '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.

```txt
GET /semantic?name=Ada%20Lovelace&role=Mathematician&avatar-url=/avatar.svg
```

Example response:

```json
{
  "@context": "https://schema.org",
  "@type": "Person",
  "name": "Ada Lovelace",
  "jobTitle": "Mathematician",
  "image": "/avatar.svg"
}
```

Recommended response headers:

```ts
{
  '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:

```yaml
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:

```ts
{
  '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`:

```ts
{
  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.

```txt
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.

```ts
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:

```json
{
  "dependencies": {
    "@vyriy/event": "^0.1.0"
  }
}
```

### `ProfileCard.ts`

A minimal custom element can start simple:

```ts
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`

```ts
import { ProfileCard } from './ProfileCard.js';

export const defineProfileCard = () => {
  if (!customElements.get('vyriy-profile-card')) {
    customElements.define('vyriy-profile-card', ProfileCard);
  }
};
```

### `index.ts`

```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:

```html
<!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:

```txt
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:

```txt
workspaces/static/public/
  icon.svg
  avatar.svg
  screenshots/
    default.svg
    compact.svg
```

### `icon.svg`

```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
<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:

```ts
{
  '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:

```ts
{
  '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:

```json
{
  "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:

```txt
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:

```txt
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`:

```txt
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:

```txt
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:

```txt
browser MFE
  -> /search?q=...
      -> API
          -> local index / embeddings / OpenAI API
          -> normalized results
```

The starter therefore prepares the right boundaries:

```txt
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:

```txt
preset: mfe
component tag: vyriy-profile-card
example type: profile card
semantic type: Person
```

Later, the CLI can offer examples:

```txt
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:

```txt
api     -> contract endpoints and host demo
ui      -> frontend bundle and UI demo
static  -> public assets
```

It has a clear production path:

```txt
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:

```txt
/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/
