@proveanything/smartlinks 1.6.6 → 1.7.0
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 +701 -7
- 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 +189 -215
- package/dist/docs/executor.md +554 -0
- package/dist/index.d.ts +3 -0
- package/dist/openapi.yaml +3110 -1323
- package/dist/types/appManifest.d.ts +152 -0
- 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 +701 -7
- 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 +189 -215
- package/docs/executor.md +554 -0
- package/openapi.yaml +3110 -1323
- package/package.json +1 -1
|
@@ -26,113 +26,112 @@ Complete guide to registering and discovering navigable states in SmartLinks app
|
|
|
26
26
|
|
|
27
27
|
## Overview
|
|
28
28
|
|
|
29
|
-
SmartLinks apps
|
|
29
|
+
SmartLinks apps expose **deep-linkable states** — specific views, pages, or configurations that the portal, AI orchestrators, or other apps can navigate to directly without loading the app first.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Deep-linkable states come from **two sources** depending on their nature:
|
|
32
|
+
|
|
33
|
+
| Source | What goes here | When it changes |
|
|
34
|
+
|--------|---------------|-----------------|
|
|
35
|
+
| **App manifest** (`app.manifest.json`) | Fixed routes built into the app | Only when the app itself is updated |
|
|
36
|
+
| **App config** (`appConfig.linkable`) | Content-driven entries that vary by collection | When admins create, remove, or rename content |
|
|
37
|
+
|
|
38
|
+
Consumers merge both sources to get the full set of navigable states for an app.
|
|
32
39
|
|
|
33
40
|
```text
|
|
34
|
-
|
|
35
|
-
│
|
|
36
|
-
│
|
|
37
|
-
│
|
|
38
|
-
│
|
|
39
|
-
│
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
42
|
+
│ App Definition (build time) │
|
|
43
|
+
│ app.manifest.json → "linkable": [ │
|
|
44
|
+
│ { "title": "Gallery", "path": "/gallery" }, │
|
|
45
|
+
│ { "title": "Settings", "path": "/settings" } │
|
|
46
|
+
│ ] │
|
|
47
|
+
└──────────────────────────────────────┬──────────────────────────────────┘
|
|
48
|
+
│ static (always present)
|
|
49
|
+
▼
|
|
50
|
+
┌────────────────────────┐
|
|
51
|
+
│ merged linkable[] │◄──── dynamic (per collection)
|
|
52
|
+
└────────────────────────┘
|
|
53
|
+
▲
|
|
54
|
+
┌──────────────────────────────────────┴──────────────────────────────────┐
|
|
55
|
+
│ App Config (runtime, per collection) │
|
|
56
|
+
│ appConfig.linkable = [ │
|
|
57
|
+
│ { "title": "About Us", "params": { "pageId": "about-us" } }, │
|
|
58
|
+
│ { "title": "FAQ", "params": { "pageId": "faq" } } │
|
|
59
|
+
│ ] │
|
|
60
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
50
61
|
```
|
|
51
62
|
|
|
52
63
|
### Key Benefits
|
|
53
64
|
|
|
54
65
|
- ✅ **Discoverable** — consumers don't need to know the app's internal routing
|
|
55
|
-
- ✅ **
|
|
56
|
-
- ✅ **Dynamic** —
|
|
66
|
+
- ✅ **No first-run initialisation** — static routes are available from the moment the app is installed
|
|
67
|
+
- ✅ **Dynamic** — content-driven entries update automatically when data changes
|
|
57
68
|
- ✅ **AI-friendly** — machine-readable titles make states naturally addressable by LLMs
|
|
58
|
-
- ✅ **No extra endpoints** — built on top of the existing `appConfiguration` API
|
|
69
|
+
- ✅ **No extra endpoints** — built on top of the existing `appConfiguration` API and the app manifest
|
|
59
70
|
|
|
60
71
|
---
|
|
61
72
|
|
|
62
|
-
##
|
|
73
|
+
## Two Sources of Truth
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
### Static Links — App Manifest
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
import { appConfiguration } from '@proveanything/smartlinks';
|
|
77
|
+
Static links are routes that are **built into the app itself** — they exist regardless of what content admins have created. They belong in `app.manifest.json` as a top-level `linkable` array, as a peer to `widgets` and `containers`:
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
]
|
|
79
|
-
},
|
|
80
|
-
admin: true
|
|
81
|
-
});
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"linkable": [
|
|
82
|
+
{ "title": "Gallery", "path": "/gallery" },
|
|
83
|
+
{ "title": "Settings", "path": "/settings" },
|
|
84
|
+
{ "title": "Advanced Settings", "path": "/settings", "params": { "tab": "advanced" } }
|
|
85
|
+
],
|
|
86
|
+
"setup": { }
|
|
87
|
+
}
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const config = await appConfiguration.getConfig({ collectionId, appId });
|
|
90
|
-
const linkable = config?.linkable ?? [];
|
|
90
|
+
**Use this for:**
|
|
91
|
+
- Named sections or tabs that are always present in the app
|
|
92
|
+
- Admin-accessible pages that exist at fixed routes
|
|
93
|
+
- Any navigable state whose existence doesn't depend on content
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// { title: 'About Us', params: { pageId: 'about-us' } },
|
|
95
|
-
// { title: 'FAQ', params: { pageId: 'faq' } },
|
|
96
|
-
// ]
|
|
97
|
-
```
|
|
95
|
+
**Why manifest, not config?**
|
|
96
|
+
These routes are part of the app's *definition*, not its *runtime state*. Putting them in the manifest means they're immediately available the moment the app is installed — no first-run initialisation, no sync step, no risk of them being missing if the app has never been launched.
|
|
98
97
|
|
|
99
98
|
---
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Each app **MAY** store a `linkable` array at the top level of its `appConfig`. This array describes all the deep-linkable states the app currently exposes.
|
|
100
|
+
### Dynamic Links — App Config
|
|
104
101
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
The `linkable` key is **reserved** at the top level of `appConfig` for this purpose. Do not use it for anything else.
|
|
102
|
+
Dynamic links are entries **generated from content** — CMS pages, product guides, FAQ items, or anything else that admins create and manage. These are stored at `appConfig.linkable` and updated whenever the content changes:
|
|
108
103
|
|
|
109
104
|
```typescript
|
|
110
|
-
|
|
105
|
+
import { appConfiguration } from '@proveanything/smartlinks';
|
|
106
|
+
|
|
111
107
|
await appConfiguration.setConfig({
|
|
112
108
|
collectionId,
|
|
113
109
|
appId,
|
|
114
110
|
config: {
|
|
115
111
|
// ... other app config ...
|
|
116
|
-
linkable: [
|
|
112
|
+
linkable: [
|
|
113
|
+
{ title: 'About Us', params: { pageId: 'about-us' } },
|
|
114
|
+
{ title: 'Care Guide', params: { pageId: 'care-guide' } },
|
|
115
|
+
{ title: 'FAQ', params: { pageId: 'faq' } },
|
|
116
|
+
]
|
|
117
117
|
},
|
|
118
118
|
admin: true
|
|
119
119
|
});
|
|
120
|
-
|
|
121
|
-
// Reading
|
|
122
|
-
const config = await appConfiguration.getConfig({ collectionId, appId });
|
|
123
|
-
const linkable: DeepLinkEntry[] = config?.linkable ?? [];
|
|
124
120
|
```
|
|
125
121
|
|
|
126
|
-
|
|
122
|
+
**Use this for:**
|
|
123
|
+
- Pages or content entries created by admins that should be directly navigable
|
|
124
|
+
- Any set of linkable states that varies between collections or changes over time
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
The `linkable` key is **reserved** at the top level of `appConfig` for this purpose.
|
|
127
|
+
|
|
128
|
+
> **`appConfig.linkable` is for dynamic content only.** Static routes belong in the manifest. Don't write fixed routes to `appConfig` on first install just to make them discoverable — that's the problem this split is designed to avoid.
|
|
130
129
|
|
|
131
130
|
---
|
|
132
131
|
|
|
133
132
|
## Schema: DeepLinkEntry
|
|
134
133
|
|
|
135
|
-
|
|
134
|
+
Both sources use the same entry shape:
|
|
136
135
|
|
|
137
136
|
```typescript
|
|
138
137
|
interface DeepLinkEntry {
|
|
@@ -159,65 +158,63 @@ interface DeepLinkEntry {
|
|
|
159
158
|
}
|
|
160
159
|
```
|
|
161
160
|
|
|
162
|
-
### Field Notes
|
|
163
|
-
|
|
164
161
|
| Field | Required | Notes |
|
|
165
162
|
|-------|----------|-------|
|
|
166
163
|
| `title` | ✅ | Displayed in menus and offered to AI agents. Should be concise and human-readable. |
|
|
167
164
|
| `path` | ❌ | Hash route within the app. Defaults to `/` if omitted. |
|
|
168
165
|
| `params` | ❌ | App-specific query params only. Platform params are injected automatically. |
|
|
169
166
|
|
|
170
|
-
> **Tip:**
|
|
167
|
+
> **Tip:** Different entries can share the same `path` if they differ by `params` — for example, two entries pointing at `/settings` with different `tab` params.
|
|
171
168
|
|
|
172
169
|
---
|
|
173
170
|
|
|
174
|
-
##
|
|
175
|
-
|
|
176
|
-
### 1. Data-Driven Entries (Dynamic)
|
|
171
|
+
## Quick Start
|
|
177
172
|
|
|
178
|
-
|
|
173
|
+
**An app with both static routes and dynamic content pages:**
|
|
179
174
|
|
|
175
|
+
`app.manifest.json` — declare static routes once, at build time:
|
|
180
176
|
```json
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
]
|
|
177
|
+
{
|
|
178
|
+
"linkable": [
|
|
179
|
+
{ "title": "Gallery", "path": "/gallery" },
|
|
180
|
+
{ "title": "Settings", "path": "/settings" }
|
|
181
|
+
]
|
|
182
|
+
}
|
|
186
183
|
```
|
|
187
184
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
App code — sync dynamic content entries whenever pages change:
|
|
186
|
+
```typescript
|
|
187
|
+
import { appConfiguration } from '@proveanything/smartlinks';
|
|
191
188
|
|
|
192
|
-
|
|
189
|
+
async function syncDynamicLinks(collectionId: string, appId: string): Promise<void> {
|
|
190
|
+
// Always fetch fresh data — never rely on cache
|
|
191
|
+
const pages = await myApi.getPublishedPages({ forceRefresh: true });
|
|
192
|
+
const entries = pages
|
|
193
|
+
.filter(p => p.deepLinkable)
|
|
194
|
+
.map(p => ({ title: p.title, params: { pageId: p.slug } }));
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
const config = await appConfiguration.getConfig({ collectionId, appId, admin: true }) ?? {};
|
|
197
|
+
await appConfiguration.setConfig({
|
|
198
|
+
collectionId, appId,
|
|
199
|
+
config: { ...config, linkable: entries },
|
|
200
|
+
admin: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
200
203
|
```
|
|
201
204
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
### 3. Mixed
|
|
205
|
+
Consumer — merge both sources:
|
|
206
|
+
```typescript
|
|
207
|
+
// staticLinks come from the app's manifest (loaded by the platform when the app was registered)
|
|
208
|
+
const staticLinks: DeepLinkEntry[] = appManifest?.linkable ?? [];
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
// dynamicLinks come from appConfig (per-collection, fetched at runtime)
|
|
211
|
+
const config = await appConfiguration.getConfig({ collectionId, appId });
|
|
212
|
+
const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
|
|
209
213
|
|
|
210
|
-
|
|
211
|
-
[
|
|
212
|
-
{ "title": "Gallery", "path": "/gallery" },
|
|
213
|
-
{ "title": "About Us", "params": { "pageId": "about-us" } },
|
|
214
|
-
{ "title": "FAQ", "params": { "pageId": "faq" } },
|
|
215
|
-
{ "title": "Settings", "path": "/settings" }
|
|
216
|
-
]
|
|
214
|
+
// Full set — static (structural) first, then dynamic (content)
|
|
215
|
+
const allLinks = [...staticLinks, ...dynamicLinks];
|
|
217
216
|
```
|
|
218
217
|
|
|
219
|
-
**Ordering:** Entries are displayed in array order. Place the most commonly navigated states first.
|
|
220
|
-
|
|
221
218
|
---
|
|
222
219
|
|
|
223
220
|
## URL Resolution
|
|
@@ -254,16 +251,17 @@ The platform automatically injects the following — **never include these in `p
|
|
|
254
251
|
|
|
255
252
|
---
|
|
256
253
|
|
|
257
|
-
## Syncing
|
|
254
|
+
## Syncing Dynamic Links
|
|
255
|
+
|
|
256
|
+
This section only applies to **dynamic links stored in `appConfig.linkable`**. Static links declared in the manifest require no syncing.
|
|
258
257
|
|
|
259
258
|
### When to Sync
|
|
260
259
|
|
|
261
|
-
Apps **MUST** update
|
|
260
|
+
Apps **MUST** update `appConfig.linkable` whenever the set of dynamic navigable states changes:
|
|
262
261
|
|
|
263
262
|
- A page is created, deleted, published, or unpublished
|
|
264
263
|
- A page's `deepLinkable` flag is toggled
|
|
265
264
|
- A page title changes
|
|
266
|
-
- App routes are added or removed
|
|
267
265
|
|
|
268
266
|
### How to Sync
|
|
269
267
|
|
|
@@ -273,9 +271,9 @@ The sync pattern is a **full replace** — read current config, overwrite `linka
|
|
|
273
271
|
import { appConfiguration } from '@proveanything/smartlinks';
|
|
274
272
|
|
|
275
273
|
async function syncLinkable(collectionId: string, appId: string): Promise<void> {
|
|
276
|
-
// 1. Fetch the current set of
|
|
277
|
-
// Always fetch fresh — never rely on a local cache
|
|
278
|
-
const entries = await
|
|
274
|
+
// 1. Fetch the current set of dynamic entries from your data source.
|
|
275
|
+
// Always fetch fresh — never rely on a local cache.
|
|
276
|
+
const entries = await fetchDynamicLinkEntries(); // app-specific
|
|
279
277
|
|
|
280
278
|
// 2. Read the existing appConfig to preserve other keys
|
|
281
279
|
let config: Record<string, unknown> = {};
|
|
@@ -289,7 +287,7 @@ async function syncLinkable(collectionId: string, appId: string): Promise<void>
|
|
|
289
287
|
// Config may not exist yet on first run — that's fine
|
|
290
288
|
}
|
|
291
289
|
|
|
292
|
-
// 3.
|
|
290
|
+
// 3. Overwrite only the linkable key and save
|
|
293
291
|
await appConfiguration.setConfig({
|
|
294
292
|
collectionId,
|
|
295
293
|
appId,
|
|
@@ -299,42 +297,13 @@ async function syncLinkable(collectionId: string, appId: string): Promise<void>
|
|
|
299
297
|
}
|
|
300
298
|
```
|
|
301
299
|
|
|
302
|
-
### What `fetchLinkableEntries` looks like
|
|
303
|
-
|
|
304
|
-
This function is app-specific. A typical CMS app might look like:
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
|
|
308
|
-
// Fetch your pages/content from the server (not from cache)
|
|
309
|
-
const pages = await myApi.getPublishedPages({ forceRefresh: true });
|
|
310
|
-
|
|
311
|
-
return pages
|
|
312
|
-
.filter(page => page.deepLinkable)
|
|
313
|
-
.map(page => ({
|
|
314
|
-
title: page.title,
|
|
315
|
-
params: { pageId: page.slug },
|
|
316
|
-
}));
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
A route-based app simply returns a static array:
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
|
|
324
|
-
return [
|
|
325
|
-
{ title: 'Gallery', path: '/gallery' },
|
|
326
|
-
{ title: 'Settings', path: '/settings' },
|
|
327
|
-
];
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
300
|
### Idempotency
|
|
332
301
|
|
|
333
302
|
The sync is always a full replace of the `linkable` array. Running it multiple times with the same data produces the same result — no duplicates, no orphans.
|
|
334
303
|
|
|
335
|
-
### Clearing
|
|
304
|
+
### Clearing Dynamic Links
|
|
336
305
|
|
|
337
|
-
To remove all
|
|
306
|
+
To remove all dynamic entries (e.g., all content was unpublished):
|
|
338
307
|
|
|
339
308
|
```typescript
|
|
340
309
|
await appConfiguration.setConfig({
|
|
@@ -345,13 +314,48 @@ await appConfiguration.setConfig({
|
|
|
345
314
|
});
|
|
346
315
|
```
|
|
347
316
|
|
|
317
|
+
Note that clearing `appConfig.linkable` only removes dynamic entries. Static links declared in the manifest are unaffected.
|
|
318
|
+
|
|
348
319
|
---
|
|
349
320
|
|
|
350
321
|
## Consumer Patterns
|
|
351
322
|
|
|
323
|
+
### Merging Both Sources
|
|
324
|
+
|
|
325
|
+
Consumers must always merge static (manifest) and dynamic (appConfig) entries. The platform provides the manifest when the app is registered; the consumer fetches the config at runtime:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { appConfiguration } from '@proveanything/smartlinks';
|
|
329
|
+
|
|
330
|
+
interface ResolvedLink extends DeepLinkEntry {
|
|
331
|
+
appId: string;
|
|
332
|
+
source: 'manifest' | 'config';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function getAppLinks(
|
|
336
|
+
collectionId: string,
|
|
337
|
+
appId: string,
|
|
338
|
+
appManifest: { linkable?: DeepLinkEntry[] }
|
|
339
|
+
): Promise<ResolvedLink[]> {
|
|
340
|
+
// Static entries from the manifest — always present, no network call needed
|
|
341
|
+
const staticLinks = (appManifest.linkable ?? []).map(e => ({
|
|
342
|
+
...e, appId, source: 'manifest' as const
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
// Dynamic entries from appConfig — vary per collection
|
|
346
|
+
const config = await appConfiguration.getConfig({ collectionId, appId });
|
|
347
|
+
const dynamicLinks = (config?.linkable ?? []).map(e => ({
|
|
348
|
+
...e, appId, source: 'config' as const
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
// Static first (fixed structure), then dynamic (content entries)
|
|
352
|
+
return [...staticLinks, ...dynamicLinks];
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
352
356
|
### Portal Navigation Menu
|
|
353
357
|
|
|
354
|
-
A portal shell
|
|
358
|
+
A portal shell aggregates links from all installed apps to build a unified navigation menu:
|
|
355
359
|
|
|
356
360
|
```typescript
|
|
357
361
|
import { appConfiguration } from '@proveanything/smartlinks';
|
|
@@ -363,15 +367,22 @@ interface NavLink {
|
|
|
363
367
|
params?: Record<string, string>;
|
|
364
368
|
}
|
|
365
369
|
|
|
366
|
-
async function buildNavMenu(
|
|
370
|
+
async function buildNavMenu(
|
|
371
|
+
collectionId: string,
|
|
372
|
+
apps: Array<{ appId: string; manifest: { linkable?: DeepLinkEntry[] } }>
|
|
373
|
+
): Promise<NavLink[]> {
|
|
367
374
|
const links: NavLink[] = [];
|
|
368
375
|
|
|
369
|
-
for (const appId of
|
|
376
|
+
for (const { appId, manifest } of apps) {
|
|
377
|
+
// Static links from manifest
|
|
378
|
+
const staticLinks = manifest.linkable ?? [];
|
|
379
|
+
|
|
380
|
+
// Dynamic links from appConfig
|
|
370
381
|
const config = await appConfiguration.getConfig({ collectionId, appId });
|
|
371
|
-
const
|
|
382
|
+
const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
|
|
372
383
|
|
|
373
384
|
links.push(
|
|
374
|
-
...
|
|
385
|
+
...[...staticLinks, ...dynamicLinks].map(entry => ({ ...entry, appId }))
|
|
375
386
|
);
|
|
376
387
|
}
|
|
377
388
|
|
|
@@ -381,19 +392,22 @@ async function buildNavMenu(collectionId: string, appIds: string[]): Promise<Nav
|
|
|
381
392
|
|
|
382
393
|
### AI Orchestration
|
|
383
394
|
|
|
384
|
-
An AI agent
|
|
395
|
+
An AI agent discovers all navigable states and offers them to the user:
|
|
385
396
|
|
|
386
397
|
```typescript
|
|
387
398
|
import { appConfiguration } from '@proveanything/smartlinks';
|
|
388
399
|
|
|
389
|
-
async function getNavigationOptions(
|
|
400
|
+
async function getNavigationOptions(
|
|
401
|
+
collectionId: string,
|
|
402
|
+
appId: string,
|
|
403
|
+
appManifest: { linkable?: DeepLinkEntry[] }
|
|
404
|
+
): Promise<string[]> {
|
|
405
|
+
const staticLinks = appManifest.linkable ?? [];
|
|
390
406
|
const config = await appConfiguration.getConfig({ collectionId, appId });
|
|
391
|
-
const
|
|
407
|
+
const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
|
|
392
408
|
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
// Which would you prefer?"
|
|
396
|
-
return linkable.map(entry => entry.title);
|
|
409
|
+
// "I can navigate you to: Gallery, Settings, About Us, or FAQ."
|
|
410
|
+
return [...staticLinks, ...dynamicLinks].map(entry => entry.title);
|
|
397
411
|
}
|
|
398
412
|
```
|
|
399
413
|
|
|
@@ -401,16 +415,15 @@ The `title` field is intentionally human-readable so it can be injected directly
|
|
|
401
415
|
|
|
402
416
|
### Cross-App Navigation
|
|
403
417
|
|
|
404
|
-
An app
|
|
418
|
+
An app constructs a `NavigationRequest` using the target app's merged links:
|
|
405
419
|
|
|
406
420
|
```typescript
|
|
407
|
-
//
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
});
|
|
421
|
+
// Search static links first, then dynamic
|
|
422
|
+
const staticEntry = targetManifest?.linkable?.find(e => e.title === 'Gallery');
|
|
423
|
+
const config = await appConfiguration.getConfig({ collectionId, appId: 'target-app-id' });
|
|
424
|
+
const dynamicEntry = config?.linkable?.find(e => e.title === 'About Us');
|
|
412
425
|
|
|
413
|
-
const entry =
|
|
426
|
+
const entry = staticEntry ?? dynamicEntry;
|
|
414
427
|
|
|
415
428
|
if (entry) {
|
|
416
429
|
onNavigate({
|
|
@@ -423,51 +436,12 @@ if (entry) {
|
|
|
423
436
|
|
|
424
437
|
---
|
|
425
438
|
|
|
426
|
-
## Manifest Declaration
|
|
427
|
-
|
|
428
|
-
Apps that support deep linking **SHOULD** declare the `linkable` key in their `app.admin.json` config schema. This signals to platform tooling that the key is auto-managed and prevents accidental manual edits:
|
|
429
|
-
|
|
430
|
-
```json
|
|
431
|
-
{
|
|
432
|
-
"setup": {
|
|
433
|
-
"configSchema": {
|
|
434
|
-
"properties": {
|
|
435
|
-
"linkable": {
|
|
436
|
-
"type": "array",
|
|
437
|
-
"description": "Auto-managed deep link registry. Do not edit manually.",
|
|
438
|
-
"readOnly": true,
|
|
439
|
-
"items": {
|
|
440
|
-
"type": "object",
|
|
441
|
-
"required": ["title"],
|
|
442
|
-
"properties": {
|
|
443
|
-
"title": { "type": "string", "description": "Human-readable label" },
|
|
444
|
-
"path": { "type": "string", "description": "Hash route path, e.g. '/gallery'" },
|
|
445
|
-
"params": {
|
|
446
|
-
"type": "object",
|
|
447
|
-
"description": "App-specific query params. Do not include platform context params.",
|
|
448
|
-
"additionalProperties": { "type": "string" }
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
> **Note:** The `readOnly: true` flag is a hint to the admin UI to hide or disable this field in manual config editors.
|
|
460
|
-
|
|
461
|
-
---
|
|
462
|
-
|
|
463
439
|
## TypeScript Types
|
|
464
440
|
|
|
465
|
-
Copy these types into your app, or import them if they become part of the SDK's public API:
|
|
466
|
-
|
|
467
441
|
```typescript
|
|
468
442
|
/**
|
|
469
443
|
* A single navigable state exposed by a SmartLinks app.
|
|
470
|
-
*
|
|
444
|
+
* Used in both app.manifest.json `linkable` (static) and appConfig `linkable` (dynamic).
|
|
471
445
|
*/
|
|
472
446
|
export interface DeepLinkEntry {
|
|
473
447
|
/** Human-readable label shown in menus and offered to AI agents */
|
|
@@ -485,10 +459,7 @@ export interface DeepLinkEntry {
|
|
|
485
459
|
params?: Record<string, string>;
|
|
486
460
|
}
|
|
487
461
|
|
|
488
|
-
/**
|
|
489
|
-
* The shape of the `linkable` key in appConfig.
|
|
490
|
-
* Alias for convenience.
|
|
491
|
-
*/
|
|
462
|
+
/** Convenience alias for an array of DeepLinkEntry */
|
|
492
463
|
export type DeepLinkRegistry = DeepLinkEntry[];
|
|
493
464
|
```
|
|
494
465
|
|
|
@@ -498,17 +469,20 @@ export type DeepLinkRegistry = DeepLinkEntry[];
|
|
|
498
469
|
|
|
499
470
|
### Rules
|
|
500
471
|
|
|
501
|
-
1.
|
|
502
|
-
2.
|
|
503
|
-
3. **`
|
|
504
|
-
4. **
|
|
505
|
-
5.
|
|
472
|
+
1. **Static routes belong in the manifest** — if a route exists regardless of content, declare it in `app.manifest.json`. Do not write it to `appConfig` on first run.
|
|
473
|
+
2. **`appConfig.linkable` is for dynamic content only** — it should contain entries generated from, and varying with, the app's data.
|
|
474
|
+
3. **`linkable` is reserved** — do not use this key for other purposes in either the manifest or `appConfig`.
|
|
475
|
+
4. **No platform context params in entries** — `collectionId`, `appId`, `productId`, `proofId`, `lang`, `theme` are injected by the platform automatically.
|
|
476
|
+
5. **`title` is required** — every entry must have a human-readable label.
|
|
477
|
+
6. **Consumers must merge both sources** — never assume all links come from `appConfig` alone.
|
|
478
|
+
7. **Sync from server** — when updating `appConfig.linkable`, always fetch the latest content state; never rely on a local cache.
|
|
479
|
+
8. **Full replace** — always overwrite the entire `appConfig.linkable` array; never diff or patch individual entries.
|
|
506
480
|
|
|
507
481
|
### Best Practices
|
|
508
482
|
|
|
509
483
|
- **Keep titles short and scannable** — they appear in navigation menus and AI prompts. Prefer "About Us" over "Our Company Story & Mission".
|
|
510
|
-
- **
|
|
511
|
-
- **Sync eagerly** — trigger a sync immediately after any content change rather than on a schedule. The operation is cheap and idempotent.
|
|
512
|
-
- **Handle missing
|
|
513
|
-
- **Don't duplicate the default route** — if your app has only one view,
|
|
514
|
-
- **Test URL resolution** — confirm that your entries produce the correct URLs
|
|
484
|
+
- **Static first, dynamic second** — when rendering merged links, show static (structural) entries before dynamic (content) entries. This provides a consistent layout even while content is loading.
|
|
485
|
+
- **Sync dynamic links eagerly** — trigger a sync immediately after any content change rather than on a schedule. The operation is cheap and idempotent.
|
|
486
|
+
- **Handle missing entries gracefully** — older apps won't have manifest `linkable` entries or `appConfig.linkable`. Always use `?? []` on both sources.
|
|
487
|
+
- **Don't duplicate the default route** — if your app has only one view, neither source needs a `linkable` entry for it. The app can always be opened to its default state without a deep link.
|
|
488
|
+
- **Test URL resolution** — confirm that your entries produce the correct URLs. Check that params are correctly appended and that platform context params are not doubled up.
|