@proveanything/smartlinks 1.6.7 → 1.7.1
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.
- package/dist/api/attestation.d.ts +22 -0
- package/dist/api/attestation.js +22 -0
- package/dist/api/attestations.d.ts +292 -0
- package/dist/api/attestations.js +405 -0
- package/dist/api/containers.d.ts +236 -0
- package/dist/api/containers.js +316 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/tags.d.ts +20 -1
- package/dist/api/tags.js +30 -0
- package/dist/docs/API_SUMMARY.md +711 -9
- package/dist/docs/app-manifest.md +430 -0
- package/dist/docs/attestations.md +498 -0
- package/dist/docs/container-tracking.md +437 -0
- package/dist/docs/deep-link-discovery.md +6 -6
- package/dist/docs/executor.md +554 -0
- package/dist/docs/interactions.md +291 -0
- package/dist/docs/manifests.md +200 -0
- package/dist/docs/mpa.md +135 -0
- package/dist/docs/overview.md +372 -0
- package/dist/index.d.ts +3 -0
- package/dist/openapi.yaml +3110 -1323
- package/dist/types/appManifest.d.ts +159 -2
- package/dist/types/attestation.d.ts +12 -0
- package/dist/types/attestations.d.ts +237 -0
- package/dist/types/attestations.js +11 -0
- package/dist/types/containers.d.ts +186 -0
- package/dist/types/containers.js +10 -0
- package/dist/types/tags.d.ts +47 -3
- package/docs/API_SUMMARY.md +711 -9
- package/docs/app-manifest.md +430 -0
- package/docs/attestations.md +498 -0
- package/docs/container-tracking.md +437 -0
- package/docs/deep-link-discovery.md +6 -6
- package/docs/executor.md +554 -0
- package/docs/interactions.md +291 -0
- package/docs/manifests.md +200 -0
- package/docs/mpa.md +135 -0
- package/docs/overview.md +372 -0
- package/openapi.yaml +3110 -1323
- package/package.json +1 -1
|
@@ -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 |
|
package/dist/docs/mpa.md
ADDED
|
@@ -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 |
|