@proveanything/smartlinks 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ # Interactions: Event Tracking & Analytics
2
+
3
+ The `interactions` namespace is a **critical pattern** for tracking user engagement. Many apps rely heavily on it for logging events that can trigger journeys, feed analytics dashboards, and drive aggregated results such as vote counts.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ Interactions have two distinct layers:
10
+
11
+ | Layer | Purpose |
12
+ |-------|---------|
13
+ | **Interaction Types** | Definitions stored in the database — configure an interaction's ID, permissions, and display metadata once per collection |
14
+ | **Interaction Events** | Individual event records logged each time a user performs that interaction |
15
+
16
+ ```text
17
+ ┌──────────────────────────────────────────────────────────────────┐
18
+ │ Your App │
19
+ │ │
20
+ │ 1. Create type once: interactions.create(collectionId, { │
21
+ │ id: 'vote', permissions: { uniquePerUser: true } }) │
22
+ │ │
23
+ │ 2. Log events: interactions.appendEvent(collectionId, { │
24
+ │ interactionId: 'vote', outcome: 'option-a', userId }) │
25
+ │ │
26
+ │ 3. Read results: interactions.countsByOutcome(collectionId, │
27
+ │ { interactionId: 'vote' }) │
28
+ │ → [{ outcome: 'option-a', count: 42 }, ...] │
29
+ └──────────────────────────────────────────────────────────────────┘
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Common Use Cases
35
+
36
+ | Use Case | `interactionId` example | `outcome` example |
37
+ |----------|-------------------------|-------------------|
38
+ | Competition entry | `competition-entry` | `"entered"` |
39
+ | Voting / polling | `vote` | `"option-a"` |
40
+ | Mailing list signup | `newsletter-signup` | `"subscribed"` |
41
+ | Warranty registration | `warranty-registration` | `"activated"` |
42
+ | Product scan / view | `product-view` | `"scanned"` |
43
+ | Form submission | `registration-form` | `"submitted"` |
44
+
45
+ ---
46
+
47
+ ## Interaction Types (Definitions)
48
+
49
+ Interaction types are defined once per collection and control permissions, display metadata, and uniqueness constraints.
50
+
51
+ ### Create a Type
52
+
53
+ ```typescript
54
+ await SL.interactions.create(collectionId, {
55
+ id: 'vote',
56
+ appId: 'my-app',
57
+ permissions: {
58
+ allowPublicSubmit: true,
59
+ uniquePerUser: true,
60
+ startAt: '2026-06-01T00:00:00Z',
61
+ endAt: '2026-06-30T23:59:59Z',
62
+ allowPublicSummary: true,
63
+ },
64
+ data: {
65
+ display: {
66
+ title: 'Vote',
67
+ description: 'Cast your vote for the competition.',
68
+ },
69
+ },
70
+ });
71
+ ```
72
+
73
+ ### Update / Delete a Type
74
+
75
+ ```typescript
76
+ // Update permissions or display
77
+ await SL.interactions.update(collectionId, 'vote', {
78
+ permissions: { endAt: '2026-07-15T23:59:59Z' },
79
+ });
80
+
81
+ // Delete the definition (does not delete existing events)
82
+ await SL.interactions.remove(collectionId, 'vote');
83
+ ```
84
+
85
+ ### List / Get Types
86
+
87
+ ```typescript
88
+ // Admin: list all types for an app
89
+ const { items } = await SL.interactions.list(collectionId, { appId: 'my-app' });
90
+
91
+ // Admin: get a single type
92
+ const type = await SL.interactions.get(collectionId, 'vote');
93
+
94
+ // Public: list available types (respects permissions)
95
+ const { items } = await SL.interactions.publicList(collectionId, { appId: 'my-app' });
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Logging Events
101
+
102
+ ### Admin Event Append
103
+
104
+ Use on the server side or in admin flows. Requires `userId` **or** `contactId`.
105
+
106
+ ```typescript
107
+ await SL.interactions.appendEvent(collectionId, {
108
+ appId: 'my-app',
109
+ interactionId: 'vote',
110
+ outcome: 'option-a', // The result / choice — used by countsByOutcome()
111
+ userId: 'user_abc123', // One of userId or contactId is required
112
+ productId: 'prod_xyz', // Optional — scope to a specific product
113
+ scope: 'round-1', // Optional — custom segmentation string
114
+ metadata: { source: 'mobile', region: 'UK' },
115
+ });
116
+ ```
117
+
118
+ ### Public Event Submit
119
+
120
+ Use in client-side app code. Same body shape as `appendEvent` but hits the public endpoint (respects interaction permissions like `allowPublicSubmit`, `requireAuth`).
121
+
122
+ ```typescript
123
+ await SL.interactions.submitPublicEvent(collectionId, {
124
+ appId: 'my-app',
125
+ interactionId: 'competition-entry',
126
+ outcome: 'entered',
127
+ userId: currentUser.id,
128
+ metadata: { answer: 'Paris' },
129
+ });
130
+ ```
131
+
132
+ ### Update an Existing Event
133
+
134
+ ```typescript
135
+ await SL.interactions.updateEvent(collectionId, {
136
+ eventId: 'evt_abc123', // Required — the event to update
137
+ interactionId: 'vote',
138
+ userId: 'user_abc123',
139
+ outcome: 'option-b', // Override the outcome
140
+ status: 'deleted', // Soft-delete the event
141
+ });
142
+ ```
143
+
144
+ ### Event Body Fields
145
+
146
+ | Field | Type | Required | Description |
147
+ |-------|------|----------|-------------|
148
+ | `interactionId` | string | ✅ | Which interaction type this event belongs to |
149
+ | `userId` or `contactId` | string | ✅ (one of) | The actor. `appendEvent` / `updateEvent` require one of these |
150
+ | `appId` | string | ❌ | Scopes the event to your app |
151
+ | `outcome` | string | ❌ | The result or choice — what `countsByOutcome()` aggregates |
152
+ | `scope` | string | ❌ | Custom segmentation (e.g., `"round-1"`, `"region-uk"`) |
153
+ | `productId` | string | ❌ | Scope to a product |
154
+ | `proofId` | string | ❌ | Scope to a proof |
155
+ | `broadcastId` | string | ❌ | Links the event to a broadcast campaign |
156
+ | `journeyId` | string | ❌ | Links the event to a journey run |
157
+ | `metadata` | object | ❌ | Arbitrary extra data stored with the event |
158
+ | `timestamp` | string | ❌ | ISO datetime override (defaults to server time) |
159
+ | `source` | string | ❌ | Free-text source tag (e.g., `"mobile"`, `"email-link"`) |
160
+
161
+ ---
162
+
163
+ ## Reading Results
164
+
165
+ ### Counts by Outcome (Aggregations)
166
+
167
+ The primary analytics function — returns how many times each outcome was recorded:
168
+
169
+ ```typescript
170
+ // Admin (full access, deduplication options)
171
+ const results = await SL.interactions.countsByOutcome(collectionId, {
172
+ appId: 'my-app',
173
+ interactionId: 'vote',
174
+ scope: 'round-1', // Optional — filter by scope
175
+ from: '2026-06-01', // Optional — date range
176
+ to: '2026-06-30',
177
+ dedupeLatest: true, // Count only the latest event per user (for re-votes)
178
+ });
179
+ // Returns: [{ outcome: 'option-a', count: 42 }, { outcome: 'option-b', count: 37 }]
180
+
181
+ // Public (respects allowPublicSummary permission)
182
+ const results = await SL.interactions.publicCountsByOutcome(
183
+ collectionId,
184
+ { appId: 'my-app', interactionId: 'vote' },
185
+ authToken // Optional — pass if user is authenticated
186
+ );
187
+ ```
188
+
189
+ ### Query Event History
190
+
191
+ Flexible admin query for raw interaction events:
192
+
193
+ ```typescript
194
+ const events = await SL.interactions.query(collectionId, {
195
+ appId: 'my-app',
196
+ interactionId: 'vote',
197
+ userId: 'user_abc123', // Filter by user
198
+ outcome: 'option-a', // Filter by outcome
199
+ from: '2026-06-01T00:00Z',
200
+ to: '2026-06-30T23:59Z',
201
+ limit: 100,
202
+ order: 'DESC',
203
+ latestPerEventId: true, // Deduplicate: one row per interactionId per user
204
+ include: ['interaction'], // Embed the interaction type definition in each row
205
+ });
206
+ ```
207
+
208
+ ### Public: User's Own History
209
+
210
+ Lets authenticated users see their own events:
211
+
212
+ ```typescript
213
+ const myEvents = await SL.interactions.publicMyInteractions(
214
+ collectionId,
215
+ { appId: 'my-app', interactionId: 'vote' },
216
+ authToken
217
+ );
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Permissions Reference
223
+
224
+ Set on the interaction type definition via `permissions`:
225
+
226
+ | Permission | Type | Description |
227
+ |------------|------|-------------|
228
+ | `enabled` | boolean | Master on/off switch for submissions (default: enabled) |
229
+ | `allowPublicSubmit` | boolean | Allow unauthenticated / public submissions |
230
+ | `allowAnonymousSubmit` | boolean | Allow submissions without any session |
231
+ | `requireAuth` | boolean | Block submissions unless user is authenticated |
232
+ | `allowedOrigins` | string[] | Restrict to specific site domains (substring match) |
233
+ | `startAt` | string (ISO) | Earliest time submissions are accepted |
234
+ | `endAt` | string (ISO) | Latest time submissions are accepted |
235
+ | `uniquePerUser` | boolean | Prevent duplicate submissions per user |
236
+ | `uniquePerUserWindowSeconds` | number | Time window for uniqueness (e.g., `86400` = 1 day) |
237
+ | `uniqueOutcome` | string | Outcome tag to check for duplicates (e.g., `"submitted"`) |
238
+ | `allowPublicSummary` | boolean | Show counts/aggregates to unauthenticated users |
239
+ | `allowAuthenticatedSummary` | boolean | Show counts/aggregates to authenticated users |
240
+ | `allowOwnRead` | boolean | Let users read their own event history via public API |
241
+
242
+ ---
243
+
244
+ ## Integration with Journeys
245
+
246
+ Interactions are the primary bridge between user actions and automated workflows:
247
+
248
+ ```text
249
+ User submits interaction event
250
+
251
+ Platform emits event to Journey trigger
252
+
253
+ Journey step runs: send confirmation email, update CRM, award points, etc.
254
+ ```
255
+
256
+ When defining a journey trigger, reference the `interactionId` that should fire it. The interaction `outcome` and `metadata` are available as variables in journey steps.
257
+
258
+ ---
259
+
260
+ ## TypeScript Types
261
+
262
+ ```typescript
263
+ import type {
264
+ AppendInteractionBody, // Event body for appendEvent / submitPublicEvent
265
+ UpdateInteractionBody, // Event body for updateEvent
266
+ InteractionEventRow, // Raw event record returned by query()
267
+ OutcomeCount, // { outcome: string | null; count: number }
268
+ InteractionPermissions, // Full permissions config shape
269
+ InteractionTypeRecord, // Definition record from create() / get()
270
+ InteractionTypeList, // { items, limit, offset }
271
+ CreateInteractionTypeBody, // Body for create()
272
+ UpdateInteractionTypeBody, // Body for update()
273
+ AdminInteractionsQueryRequest, // query() filter options
274
+ AdminInteractionsCountsByOutcomeRequest,
275
+ PublicInteractionsCountsByOutcomeRequest,
276
+ PublicInteractionsByUserRequest,
277
+ } from '@proveanything/smartlinks';
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Best Practices
283
+
284
+ - Use descriptive `interactionId` values: `warranty-registration`, `competition-entry`, `newsletter-vote`
285
+ - Use `outcome` to capture the choice — it's the key field that `countsByOutcome()` aggregates on
286
+ - Include `metadata` for richer analytics and debugging (`source`, `device`, `region`, etc.)
287
+ - Use `uniquePerUser: true` for actions that should only happen once (votes, registrations)
288
+ - Set `startAt`/`endAt` on the type definition — don't enforce time limits in app code
289
+ - Use `scope` to segment a single interaction type across multiple rounds, regions, or variants
290
+ - Prefer `submitPublicEvent` in client-side widget code; use `appendEvent` in server-side / admin flows
291
+ - Keep `getSEO()` and `getLLMContent()` calls separate from interaction submission paths
@@ -0,0 +1,200 @@
1
+ # AI-Native App Manifests
2
+
3
+ SmartLinks apps are designed to be **AI-discoverable, AI-configurable, and AI-importable**. Every app ships with a structured manifest and a prose guide that allow AI systems to set up, configure, and populate apps without custom integration code.
4
+
5
+ ---
6
+
7
+ ## The Split Manifest
8
+
9
+ The manifest is split into two files so the public portal never loads admin-only configuration data.
10
+
11
+ ```text
12
+ app.manifest.json ← lean, always loaded (widget render, SEO, executor discovery)
13
+ app.admin.json ← loaded on-demand (setup wizards, import, AI config flows)
14
+ ```
15
+
16
+ ### `app.manifest.json` — always loaded
17
+
18
+ | Section | Purpose | AI Consumer |
19
+ |---------|---------|-------------|
20
+ | `meta` | App identity, version, appId, SEO priority | All workflows |
21
+ | `admin` | Pointer to `app.admin.json` | Admin orchestrators |
22
+ | `widgets` | Bundle files + component definitions + settings schemas | Widget Builder |
23
+ | `containers` | Bundle files + component definitions | Container Loader |
24
+ | `executor` | Bundle files, factory name, exports list | Server / AI |
25
+ | `linkable` | Static deep-link routes | Portal menus / AI nav |
26
+
27
+ ```json
28
+ {
29
+ "meta": { "name": "My App", "appId": "my-app", "version": "1.0.0" },
30
+ "admin": "app.admin.json",
31
+ "widgets": {
32
+ "files": {
33
+ "js": { "umd": "dist/widgets.umd.js", "esm": "dist/widgets.es.js" },
34
+ "css": null
35
+ },
36
+ "components": [
37
+ {
38
+ "name": "MyWidget",
39
+ "description": "Compact summary card.",
40
+ "sizes": ["compact", "standard", "large"],
41
+ "settings": { ... }
42
+ }
43
+ ]
44
+ },
45
+ "containers": {
46
+ "files": {
47
+ "js": { "umd": "dist/containers.umd.js", "esm": "dist/containers.es.js" },
48
+ "css": null
49
+ },
50
+ "components": [{ "name": "PublicContainer", "description": "Full app view." }]
51
+ },
52
+ "executor": {
53
+ "files": { "js": { "umd": "dist/executor.umd.js", "esm": "dist/executor.es.js" } },
54
+ "factory": "createMyAppExecutor",
55
+ "exports": ["createMyAppExecutor", "getSEO", "getLLMContent"],
56
+ "description": "Programmatic configuration and SEO API for My App."
57
+ },
58
+ "linkable": [
59
+ { "title": "Home", "path": "/" },
60
+ { "title": "Gallery", "path": "/gallery" }
61
+ ]
62
+ }
63
+ ```
64
+
65
+ > ⚠️ **`css` is `null` by default.** Most widgets and containers use Tailwind/shadcn classes inherited from the parent and produce **no CSS output file**. Set `"css": null` in the manifest. Only set it to a filename if your widget/container ships a custom CSS file that actually exists in `dist/`. The parent portal checks this value before injecting a `<link>` tag — a non-null value pointing to a missing file will cause a 404.
66
+
67
+ ---
68
+
69
+ ### `app.admin.json` — loaded on-demand
70
+
71
+ | Section | Purpose | AI Consumer |
72
+ |---------|---------|-------------|
73
+ | `aiGuide` | Pointer to `ai-guide.md` prose instructions | All AI workflows |
74
+ | `setup` | Setup wizard: questions, config schema, save instructions | Setup Wizard |
75
+ | `import` | Bulk data import: field definitions, CSV shape, API calls | Data Importer |
76
+ | `tunable` | Runtime-adjustable settings | AI Optimizer |
77
+ | `metrics` | Tracked interactions and KPIs | Analytics Advisor |
78
+
79
+ The admin orchestrator fetches the manifest, reads the `admin` pointer, and fetches `app.admin.json` only when needed — never on public page loads.
80
+
81
+ See the [App Configuration Files reference](app-manifest.md) for the full field-by-field schema for both files.
82
+
83
+ ---
84
+
85
+ ## Widget Settings Schema
86
+
87
+ Each widget component in `app.manifest.json` should include a `settings` object using **JSON Schema** to describe its configurable props. This enables schema-form libraries and AI orchestrators to auto-generate configuration UIs without per-widget code.
88
+
89
+ ```json
90
+ "components": [
91
+ {
92
+ "name": "MyWidget",
93
+ "description": "What this widget does",
94
+ "sizes": ["compact", "standard", "large"],
95
+ "settings": {
96
+ "type": "object",
97
+ "properties": {
98
+ "displayMode": {
99
+ "type": "string",
100
+ "title": "Display Mode",
101
+ "description": "How the widget renders",
102
+ "enum": ["compact", "standard", "large"],
103
+ "enumLabels": {
104
+ "compact": "Icons only",
105
+ "standard": "With names",
106
+ "large": "Full cards"
107
+ },
108
+ "default": "standard",
109
+ "order": 1
110
+ },
111
+ "showImage": {
112
+ "type": "boolean",
113
+ "title": "Show Product Image",
114
+ "default": true,
115
+ "order": 2
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ]
121
+ ```
122
+
123
+ | Field | Purpose |
124
+ |-------|---------|
125
+ | `type`, `enum` | Standard JSON Schema for validation |
126
+ | `title` | Human-readable label for form rendering |
127
+ | `description` | Help text shown alongside the field |
128
+ | `enumLabels` | Friendly display names for enum values (`{ value → label }`) |
129
+ | `default` | Pre-selected value when no configuration exists |
130
+ | `order` | Field display order in rendered forms (lower number = higher position) |
131
+
132
+ The `settings` schema serves dual purposes: AI orchestrators use it to understand what a widget accepts and generate configuration conversationally; schema-form renderers (e.g., in admin consoles) use it to auto-generate settings UIs.
133
+
134
+ ---
135
+
136
+ ## The AI Guide (`ai-guide.md`)
137
+
138
+ A companion Markdown file deployed alongside the manifest provides **natural-language instructions** for AI orchestrators. The admin config references it via `"aiGuide": "ai-guide.md"` — orchestrators resolve and fetch it relative to the manifest URL.
139
+
140
+ While the manifest is structured data, the AI guide provides prose context, nuance, and step-by-step instructions that help an LLM drive setup wizards, imports, and troubleshooting flows conversationally.
141
+
142
+ Use the [AI Guide Template](ai-guide-template.md) as your starting point.
143
+
144
+ ---
145
+
146
+ ## Three AI Workflows
147
+
148
+ ### 1. Widget Builder
149
+
150
+ An AI fetches `app.manifest.json`, reads `widgets.components[]` including the `settings` JSON Schema for each widget, and can:
151
+ - Generate React code to embed the widget with correct props
152
+ - Auto-render a configuration UI from the settings schema
153
+ - Understand valid size hints and required vs optional props
154
+
155
+ ### 2. Setup Wizard
156
+
157
+ An AI reads `setup` from `app.admin.json` to drive a conversational configuration flow:
158
+
159
+ 1. Walk the user through each `setup.questions[]` entry
160
+ 2. Validate answers against `setup.configSchema` (JSON Schema)
161
+ 3. Optionally use `setup.contentHints` to auto-generate content via `SL.ai.chat.completions`
162
+ 4. Save using the `setup.saveWith` instructions (method, scope, admin flag)
163
+
164
+ The AI guide (`ai-guide.md`) provides prose instructions on how to handle each question, what sensible defaults look like, and what to do if the user is unsure.
165
+
166
+ ### 3. Data Importer
167
+
168
+ An AI reads `import` from `app.admin.json` to:
169
+
170
+ 1. Generate a CSV template from `import.fields[]` (with types, required flags, and examples)
171
+ 2. Normalise user-provided data against field types
172
+ 3. Call `import.saveWith.method` for each row
173
+
174
+ For multi-app imports, fields from multiple manifests can be merged into a single CSV template.
175
+
176
+ ---
177
+
178
+ ## Maintaining the Manifest
179
+
180
+ When you change your app's configuration shape, update all three files together:
181
+
182
+ | File | What to update |
183
+ |------|---------------|
184
+ | `app.manifest.json` | `widgets.components[].settings`, `containers`, `executor.exports`, `linkable` |
185
+ | `app.admin.json` | `setup.questions`, `setup.configSchema`, `import.fields`, `tunable.fields` |
186
+ | `ai-guide.md` | Prose instructions, validation rules, examples, and any new question guidance |
187
+
188
+ Keeping all three in sync ensures AI orchestrators, setup wizards, and data importers all work from consistent, accurate information.
189
+
190
+ ---
191
+
192
+ ## Related Guides
193
+
194
+ | Guide | What it covers |
195
+ |-------|---------------|
196
+ | [App Configuration Files](app-manifest.md) | Full field-by-field reference for both JSON files |
197
+ | [Executor Model](executor.md) | Building `executor.umd.js` — SEO, LLM content, config mutations |
198
+ | [Deep Link Discovery](deep-link-discovery.md) | `linkable` — static and dynamic navigable states |
199
+ | [AI Guide Template](ai-guide-template.md) | Starter template for `ai-guide.md` |
200
+ | [AI & Chat Completions](ai.md) | `SL.ai` — used in `contentHints` auto-generation |
@@ -0,0 +1,135 @@
1
+ # Multi-Page App (MPA) Architecture
2
+
3
+ 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.
4
+
5
+ ---
6
+
7
+ ## Why Multi-Page?
8
+
9
+ | Entry Point | HTML file | Purpose | Bundle target |
10
+ |-------------|-----------|---------|---------------|
11
+ | Public Portal | `index.html` | End-user interface | Lean, mobile-optimised |
12
+ | Admin Console | `admin.html` | Configuration & management | Can grow large |
13
+
14
+ 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:
15
+
16
+ | Scenario | Public bundle | Admin bundle |
17
+ |----------|--------------|-------------|
18
+ | App template (baseline) | ~150 KB | ~150 KB |
19
+ | Rich admin (future) | ~150 KB | ~500 KB+ |
20
+
21
+ ---
22
+
23
+ ## Entry Points
24
+
25
+ | File | Loads | Routes |
26
+ |------|-------|--------|
27
+ | `index.html` → `src/PublicApp.tsx` | Public routes only | `/#/`, `/#/__dev` (dev only) |
28
+ | `admin.html` → `src/AdminApp.tsx` | Admin routes only | `/#/`, `/#/settings`, … |
29
+
30
+ 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.
31
+
32
+ ### Development Helper: `/#/__dev`
33
+
34
+ The `/#/__dev` route is a hidden helper page available **in dev mode only**. It provides:
35
+ - URL parameter testing and context injection
36
+ - Quick navigation to public / admin views
37
+ - Widget preview at all three sizes
38
+ - Context documentation inline
39
+
40
+ It is lazy-loaded and **stripped from production builds**.
41
+
42
+ ---
43
+
44
+ ## Build Pipeline
45
+
46
+ The build runs **five sequential steps**, each appending to `dist/` — no step wipes the output directory:
47
+
48
+ ```
49
+ vite build
50
+ && vite build --config vite.config.widget.ts
51
+ && vite build --config vite.config.container.ts
52
+ && vite build --config vite.config.executor.ts
53
+ && node scripts/hash-bundles.mjs
54
+ ```
55
+
56
+ | Step | Config / Script | Gate env var | Output |
57
+ |------|----------------|-------------|--------|
58
+ | 1 | `vite.config.ts` | Always runs | `index.html`, `admin.html`, `assets/*` |
59
+ | 2 | `vite.config.widget.ts` | `VITE_ENABLE_WIDGETS=true` | `widgets.umd.js`, `widgets.es.js`, `widgets.css` |
60
+ | 3 | `vite.config.container.ts` | `VITE_ENABLE_CONTAINERS=true` | `containers.umd.js`, `containers.es.js`, `containers.css` |
61
+ | 4 | `vite.config.executor.ts` | `VITE_ENABLE_EXECUTOR!=false` | `executor.umd.js`, `executor.es.js` |
62
+ | 5 | `scripts/hash-bundles.mjs` | Always runs | Renames bundles with content hashes; patches `dist/app.manifest.json` |
63
+
64
+ Steps 2–4 produce a harmless stub file when their gate env var is not set. Step 5 detects and skips stub files automatically.
65
+
66
+ **Convenience scripts:**
67
+
68
+ ```bash
69
+ npm run build # Full pipeline
70
+ npm run build:widgets # Widget build only (step 2)
71
+ npm run build:containers # Container build only (step 3)
72
+ npm run build:executor # Executor build only (step 4)
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Content-Hashed Bundles
78
+
79
+ Widget, container, and executor bundles are renamed with an 8-character content hash after the build (e.g., `widgets.umd.js` → `widgets-a3b4c5d6.umd.js`). The post-build script (`scripts/hash-bundles.mjs`) patches `dist/app.manifest.json` with the hashed filenames.
80
+
81
+ This enables aggressive CDN caching:
82
+ - The **manifest** is served with no cache / short TTL — it always reflects current filenames
83
+ - The **bundles** use permanent caching (e.g., `Cache-Control: max-age=31536000, immutable`) — any content change produces a new hash and a new URL
84
+
85
+ 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.
86
+
87
+ ---
88
+
89
+ ## Build Output
90
+
91
+ ```
92
+ dist/
93
+ ├── index.html ← Public portal entry
94
+ ├── admin.html ← Admin console entry
95
+ ├── app.manifest.json ← Patched with hashed bundle filenames
96
+ ├── assets/
97
+ │ ├── index-[hash].js ← Public bundle (lean)
98
+ │ ├── admin-[hash].js ← Admin bundle (can be large)
99
+ │ └── shared-[hash].js ← Shared chunks (UI components, etc.)
100
+ ├── widgets-[hash].umd.js ← Widget bundle (UMD)
101
+ ├── widgets-[hash].es.js ← Widget bundle (ESM)
102
+ ├── widgets-[hash].css ← Widget styles (only if custom CSS exists)
103
+ ├── containers-[hash].umd.js ← Container bundle (UMD)
104
+ ├── containers-[hash].es.js ← Container bundle (ESM)
105
+ ├── containers-[hash].css ← Container styles (only if custom CSS exists)
106
+ ├── executor-[hash].umd.js ← Executor bundle (UMD)
107
+ └── executor-[hash].es.js ← Executor bundle (ESM)
108
+ ```
109
+
110
+ > 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](manifests.md) guide for the CSS null warning.
111
+
112
+ ---
113
+
114
+ ## Platform Integration
115
+
116
+ The parent SmartLinks platform embeds apps via iframe:
117
+
118
+ | Context | URL pattern |
119
+ |---------|------------|
120
+ | Portal (public) | `https://app.example.com/#/?collectionId=...&appId=...` |
121
+ | Admin Console | `https://app.example.com/admin.html#/?collectionId=...&appId=...` |
122
+
123
+ Context parameters (`collectionId`, `appId`, `productId`, `proofId`) are passed as URL query params and read via the iframe responder. See the [iframe Responder guide](iframe-responder.md) for the full context injection API.
124
+
125
+ ---
126
+
127
+ ## Related Guides
128
+
129
+ | Guide | What it covers |
130
+ |-------|---------------|
131
+ | [Widgets](widgets.md) | Widget bundle: components, props, settings |
132
+ | [Containers](containers.md) | Container bundle: full-app embeds |
133
+ | [Executor Model](executor.md) | Executor bundle: SEO, LLM content, config mutations |
134
+ | [AI-Native App Manifests](manifests.md) | How manifests wire all bundles together for AI discovery |
135
+ | [iframe Responder](iframe-responder.md) | Reading context params inside the iframe |