Getting Started

    Multi-Page App Architecture

    Vite multi-page builds producing separate bundles for public and admin interfaces.

    Multi-Page App (MPA) Architecture

    SmartLinks apps use Vite's multi-page build to produce separate bundles for public and admin interfaces. This keeps the public bundle lean even as admin features grow.


    Why Multi-Page?

    Entry PointHTML filePurposeBundle target
    Public Portalindex.htmlEnd-user interfaceLean, mobile-optimised
    Admin Consoleadmin.htmlConfiguration & managementCan grow large

    Because AdminApp.tsx and its heavy dependencies are never imported into the public entry point, the public bundle stays small regardless of how complex the admin UI becomes:

    ScenarioPublic bundleAdmin bundle
    App template (baseline)~150 KB~150 KB
    Rich admin (future)~150 KB~500 KB+

    Entry Points

    FileLoadsRoutes
    index.htmlsrc/PublicApp.tsxPublic routes only/#/, /#/__dev (dev only)
    admin.htmlsrc/AdminApp.tsxAdmin routes only/#/, /#/settings, …

    Both use HashRouter for iframe compatibility — the hash keeps routing client-side so the server always serves the same HTML file regardless of the route.

    Development Helper: /#/__dev

    The /#/__dev route is a hidden helper page available in dev mode only. It provides:

    • URL parameter testing and context injection
    • Quick navigation to public / admin views
    • Widget preview at all three sizes
    • Context documentation inline

    It is lazy-loaded and stripped from production builds.


    Build Pipeline

    The build runs five sequential steps, each appending to dist/ — no step wipes the output directory:

    vite build
      && vite build --config vite.config.widget.ts
      && vite build --config vite.config.container.ts
      && vite build --config vite.config.executor.ts
      && node scripts/hash-bundles.mjs
    
    StepConfig / ScriptGate env varOutput
    1vite.config.tsAlways runsindex.html, admin.html, assets/*
    2vite.config.widget.tsVITE_ENABLE_WIDGETS=truewidgets.umd.js, widgets.es.js, widgets.css
    3vite.config.container.tsVITE_ENABLE_CONTAINERS=truecontainers.umd.js, containers.es.js, containers.css
    4vite.config.executor.tsVITE_ENABLE_EXECUTOR!=falseexecutor.umd.js, executor.es.js
    5scripts/hash-bundles.mjsAlways runsRenames bundles with content hashes; patches dist/app.manifest.json

    Steps 2–4 produce a harmless stub file when their gate env var is not set. Step 5 detects and skips stub files automatically.

    Convenience scripts:

    npm run build              # Full pipeline
    npm run build:widgets      # Widget build only (step 2)
    npm run build:containers   # Container build only (step 3)
    npm run build:executor     # Executor build only (step 4)
    

    Content-Hashed Bundles

    Widget, container, and executor bundles are renamed with an 8-character content hash after the build (e.g., widgets.umd.jswidgets-a3b4c5d6.umd.js). The post-build script (scripts/hash-bundles.mjs) patches dist/app.manifest.json with the hashed filenames.

    This enables aggressive CDN caching:

    • The manifest is served with no cache / short TTL — it always reflects current filenames
    • The bundles use permanent caching (e.g., Cache-Control: max-age=31536000, immutable) — any content change produces a new hash and a new URL

    The source public/app.manifest.json keeps template names (e.g., widgets.umd.js). Only the built copy in dist/ gets the hashed filenames patched in.


    Build Output

    dist/
    ├── index.html                      ← Public portal entry
    ├── admin.html                      ← Admin console entry
    ├── app.manifest.json               ← Patched with hashed bundle filenames
    ├── assets/
    │   ├── index-[hash].js             ← Public bundle (lean)
    │   ├── admin-[hash].js             ← Admin bundle (can be large)
    │   └── shared-[hash].js            ← Shared chunks (UI components, etc.)
    ├── widgets-[hash].umd.js           ← Widget bundle (UMD)
    ├── widgets-[hash].es.js            ← Widget bundle (ESM)
    ├── widgets-[hash].css              ← Widget styles (only if custom CSS exists)
    ├── containers-[hash].umd.js        ← Container bundle (UMD)
    ├── containers-[hash].es.js         ← Container bundle (ESM)
    ├── containers-[hash].css           ← Container styles (only if custom CSS exists)
    ├── executor-[hash].umd.js          ← Executor bundle (UMD)
    └── executor-[hash].es.js           ← Executor bundle (ESM)
    

    Widget and container CSS files are only present when the bundle ships custom styles. Most apps set "css": null in the manifest because they rely entirely on Tailwind/shadcn from the parent. See the AI-Native App Manifests guide for the CSS null warning.


    Platform Integration

    The parent SmartLinks platform embeds apps via iframe:

    ContextURL pattern
    Portal (public)https://app.example.com/#/?collectionId=...&appId=...
    Admin Consolehttps://app.example.com/admin.html#/?collectionId=...&appId=...

    Context parameters (collectionId, appId, productId, proofId) are passed as URL query params and read via the iframe responder. See the iframe Responder guide for the full context injection API.


    Related Guides

    GuideWhat it covers
    WidgetsWidget bundle: components, props, settings
    ContainersContainer bundle: full-app embeds
    Executor ModelExecutor bundle: SEO, LLM content, config mutations
    AI-Native App ManifestsHow manifests wire all bundles together for AI discovery
    iframe ResponderReading context params inside the iframe