@proveanything/smartlinks 1.6.5 → 1.6.7

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.6.5 | Generated: 2026-03-03T14:01:39.215Z
3
+ Version: 1.6.7 | Generated: 2026-03-03T14:18:48.483Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -7,20 +7,18 @@ Complete guide to registering and discovering navigable states in SmartLinks app
7
7
  ## Table of Contents
8
8
 
9
9
  - [Overview](#overview)
10
- - [Quick Start](#quick-start)
11
- - [The `linkable` Registry](#the-linkable-registry)
10
+ - [Two Sources of Truth](#two-sources-of-truth)
11
+ - [Static Links — App Manifest](#static-links--app-manifest)
12
+ - [Dynamic Links — App Config](#dynamic-links--app-config)
12
13
  - [Schema: DeepLinkEntry](#schema-deeplinkentry)
13
- - [Entry Types](#entry-types)
14
- - [Data-Driven (Dynamic)](#1-data-driven-entries-dynamic)
15
- - [Route-Based (Static)](#2-route-based-entries-static)
16
- - [Mixed](#3-mixed)
14
+ - [Quick Start](#quick-start)
17
15
  - [URL Resolution](#url-resolution)
18
- - [Syncing the Registry](#syncing-the-registry)
16
+ - [Syncing Dynamic Links](#syncing-dynamic-links)
19
17
  - [Consumer Patterns](#consumer-patterns)
18
+ - [Merging Both Sources](#merging-both-sources)
20
19
  - [Portal Navigation Menu](#portal-navigation-menu)
21
20
  - [AI Orchestration](#ai-orchestration)
22
21
  - [Cross-App Navigation](#cross-app-navigation)
23
- - [Manifest Declaration](#manifest-declaration)
24
22
  - [TypeScript Types](#typescript-types)
25
23
  - [Rules & Best Practices](#rules--best-practices)
26
24
 
@@ -28,113 +26,112 @@ Complete guide to registering and discovering navigable states in SmartLinks app
28
26
 
29
27
  ## Overview
30
28
 
31
- SmartLinks apps can expose **deep-linkable states** — specific views, pages, or configurations that other apps, the portal, or AI orchestrators can navigate to directly without loading the app first.
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
+
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.admin.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 |
32
37
 
33
- This is done by publishing a `linkable` array in the app's `appConfig`. Any consumer (portal shell, AI agent, another app) can read this array to discover what navigable states the app currently offers.
38
+ Consumers merge both sources to get the full set of navigable states for an app.
34
39
 
35
40
  ```text
36
- ┌─────────────────────────────────────────────────────────────────────┐
37
- Your SmartLinks App
38
-
39
- ┌───────────────────────┐ writes ┌────────────────────────┐│
40
- │ Admin / Data Events │ ─────────────► appConfig.linkable ││
41
- └───────────────────────┘ └────────────────────────┘│
42
- └─────────────────────────────────────────────────────────────────────┘
43
-
44
- reads linkable[]
45
-
46
- ┌───────────────────────────────────┼───────────────────────┐
47
- ▼ ▼ ▼
48
- ┌─────────────────────┐ ┌────────────────────┐ ┌─────────────────────┐
49
- │ Portal Nav Menu │ │ AI Orchestrator │ │ Another App │
50
- (sidebar / drawer) │ │ ("Go to About Us")│ │ (cross-app nav) │
51
- └─────────────────────┘ └────────────────────┘ └─────────────────────┘
41
+ ┌─────────────────────────────────────────────────────────────────────────┐
42
+ App Definition (build time)
43
+ app.admin.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
+ └─────────────────────────────────────────────────────────────────────────┘
52
61
  ```
53
62
 
54
63
  ### Key Benefits
55
64
 
56
65
  - ✅ **Discoverable** — consumers don't need to know the app's internal routing
57
- - ✅ **Decoupled** — the app owns the registry; consumers just read it
58
- - ✅ **Dynamic** — the registry updates automatically when content changes
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
59
68
  - ✅ **AI-friendly** — machine-readable titles make states naturally addressable by LLMs
60
- - ✅ **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
61
70
 
62
71
  ---
63
72
 
64
- ## Quick Start
73
+ ## Two Sources of Truth
65
74
 
66
- **App sidepublish linkable states:**
75
+ ### Static Links App Manifest
67
76
 
68
- ```typescript
69
- 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.admin.json` as a top-level `linkable` array:
70
78
 
71
- await appConfiguration.setConfig({
72
- collectionId,
73
- appId,
74
- config: {
75
- // ... other app config ...
76
- linkable: [
77
- { title: 'Gallery', path: '/gallery' },
78
- { title: 'About Us', params: { pageId: 'about-us' } },
79
- { title: 'FAQ', params: { pageId: 'faq' } },
80
- ]
81
- },
82
- admin: true
83
- });
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
+ }
84
88
  ```
85
89
 
86
- **Consumer side — read linkable states:**
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
87
94
 
88
- ```typescript
89
- import { appConfiguration } from '@proveanything/smartlinks';
90
-
91
- const config = await appConfiguration.getConfig({ collectionId, appId });
92
- const linkable = config?.linkable ?? [];
93
-
94
- // linkable = [
95
- // { title: 'Gallery', path: '/gallery' },
96
- // { title: 'About Us', params: { pageId: 'about-us' } },
97
- // { title: 'FAQ', params: { pageId: 'faq' } },
98
- // ]
99
- ```
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.
100
97
 
101
98
  ---
102
99
 
103
- ## The `linkable` Registry
100
+ ### Dynamic Links — App Config
104
101
 
105
- 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.
106
-
107
- ### Storage Location
108
-
109
- 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:
110
103
 
111
104
  ```typescript
112
- // Writing
105
+ import { appConfiguration } from '@proveanything/smartlinks';
106
+
113
107
  await appConfiguration.setConfig({
114
108
  collectionId,
115
109
  appId,
116
110
  config: {
117
111
  // ... other app config ...
118
- linkable: [ /* DeepLinkEntry[] */ ]
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
+ ]
119
117
  },
120
118
  admin: true
121
119
  });
122
-
123
- // Reading
124
- const config = await appConfiguration.getConfig({ collectionId, appId });
125
- const linkable: DeepLinkEntry[] = config?.linkable ?? [];
126
120
  ```
127
121
 
128
- ### Admin vs Public Access
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
125
+
126
+ The `linkable` key is **reserved** at the top level of `appConfig` for this purpose.
129
127
 
130
- - **Writing** always requires `admin: true` only the app itself (running with admin credentials) should update its own registry.
131
- - **Reading** can be done publicly — portals and consumers typically read the registry without admin credentials.
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.
132
129
 
133
130
  ---
134
131
 
135
132
  ## Schema: DeepLinkEntry
136
133
 
137
- Each entry in the `linkable` array describes one navigable state:
134
+ Both sources use the same entry shape:
138
135
 
139
136
  ```typescript
140
137
  interface DeepLinkEntry {
@@ -161,65 +158,63 @@ interface DeepLinkEntry {
161
158
  }
162
159
  ```
163
160
 
164
- ### Field Notes
165
-
166
161
  | Field | Required | Notes |
167
162
  |-------|----------|-------|
168
163
  | `title` | ✅ | Displayed in menus and offered to AI agents. Should be concise and human-readable. |
169
164
  | `path` | ❌ | Hash route within the app. Defaults to `/` if omitted. |
170
165
  | `params` | ❌ | App-specific query params only. Platform params are injected automatically. |
171
166
 
172
- > **Tip:** An entry with only a `title` (no `path` or `params`) is valid it navigates to the app's default view. This is rarely useful unless the app's home screen is itself a meaningful destination distinct from other entries.
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.
173
168
 
174
169
  ---
175
170
 
176
- ## Entry Types
177
-
178
- ### 1. Data-Driven Entries (Dynamic)
171
+ ## Quick Start
179
172
 
180
- Generated from app data e.g., CMS pages, product guides, FAQ items. These are typically re-synced whenever the underlying data changes.
173
+ **An app with both static routes and dynamic content pages:**
181
174
 
175
+ `app.admin.json` — declare static routes once, at build time:
182
176
  ```json
183
- [
184
- { "title": "About Us", "params": { "pageId": "about-us" } },
185
- { "title": "Care Guide", "params": { "pageId": "care-guide" } },
186
- { "title": "FAQ", "params": { "pageId": "faq" } }
187
- ]
177
+ {
178
+ "linkable": [
179
+ { "title": "Gallery", "path": "/gallery" },
180
+ { "title": "Settings", "path": "/settings" }
181
+ ]
182
+ }
188
183
  ```
189
184
 
190
- **When to use:** The set of linkable states depends on admin-created content. The registry is rebuilt whenever pages are added, removed, published, or renamed.
191
-
192
- ### 2. Route-Based Entries (Static)
185
+ App code sync dynamic content entries whenever pages change:
186
+ ```typescript
187
+ import { appConfiguration } from '@proveanything/smartlinks';
193
188
 
194
- Fixed routes that are built into the app. Declared once and rarely change.
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 } }));
195
195
 
196
- ```json
197
- [
198
- { "title": "Gallery", "path": "/gallery" },
199
- { "title": "Settings", "path": "/settings" },
200
- { "title": "Advanced Settings", "path": "/settings", "params": { "tab": "advanced" } }
201
- ]
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
+ }
202
203
  ```
203
204
 
204
- **When to use:** The app has multiple built-in views (tabs, sections) that should always be directly navigable, regardless of content.
205
-
206
- > **Note:** Different entries can share the same `path` if they differ by `params` as in the Settings example above.
207
-
208
- ### 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 ?? [];
209
209
 
210
- Apps can freely combine both patterns. Static routes can appear alongside dynamic content entries:
210
+ // dynamicLinks come from appConfig (per-collection, fetched at runtime)
211
+ const config = await appConfiguration.getConfig({ collectionId, appId });
212
+ const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
211
213
 
212
- ```json
213
- [
214
- { "title": "Gallery", "path": "/gallery" },
215
- { "title": "About Us", "params": { "pageId": "about-us" } },
216
- { "title": "FAQ", "params": { "pageId": "faq" } },
217
- { "title": "Settings", "path": "/settings" }
218
- ]
214
+ // Full set — static (structural) first, then dynamic (content)
215
+ const allLinks = [...staticLinks, ...dynamicLinks];
219
216
  ```
220
217
 
221
- **Ordering:** Entries are displayed in array order. Place the most commonly navigated states first.
222
-
223
218
  ---
224
219
 
225
220
  ## URL Resolution
@@ -256,16 +251,17 @@ The platform automatically injects the following — **never include these in `p
256
251
 
257
252
  ---
258
253
 
259
- ## Syncing the Registry
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.
260
257
 
261
258
  ### When to Sync
262
259
 
263
- Apps **MUST** update the `linkable` array whenever the set of navigable states changes:
260
+ Apps **MUST** update `appConfig.linkable` whenever the set of dynamic navigable states changes:
264
261
 
265
262
  - A page is created, deleted, published, or unpublished
266
263
  - A page's `deepLinkable` flag is toggled
267
264
  - A page title changes
268
- - App routes are added or removed
269
265
 
270
266
  ### How to Sync
271
267
 
@@ -275,9 +271,9 @@ The sync pattern is a **full replace** — read current config, overwrite `linka
275
271
  import { appConfiguration } from '@proveanything/smartlinks';
276
272
 
277
273
  async function syncLinkable(collectionId: string, appId: string): Promise<void> {
278
- // 1. Fetch the current set of linkable states from your data source
279
- // Always fetch fresh — never rely on a local cache
280
- const entries = await fetchLinkableEntries(); // your app-specific logic
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
281
277
 
282
278
  // 2. Read the existing appConfig to preserve other keys
283
279
  let config: Record<string, unknown> = {};
@@ -291,7 +287,7 @@ async function syncLinkable(collectionId: string, appId: string): Promise<void>
291
287
  // Config may not exist yet on first run — that's fine
292
288
  }
293
289
 
294
- // 3. Merge and save
290
+ // 3. Overwrite only the linkable key and save
295
291
  await appConfiguration.setConfig({
296
292
  collectionId,
297
293
  appId,
@@ -301,42 +297,13 @@ async function syncLinkable(collectionId: string, appId: string): Promise<void>
301
297
  }
302
298
  ```
303
299
 
304
- ### What `fetchLinkableEntries` looks like
305
-
306
- This function is app-specific. A typical CMS app might look like:
307
-
308
- ```typescript
309
- async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
310
- // Fetch your pages/content from the server (not from cache)
311
- const pages = await myApi.getPublishedPages({ forceRefresh: true });
312
-
313
- return pages
314
- .filter(page => page.deepLinkable)
315
- .map(page => ({
316
- title: page.title,
317
- params: { pageId: page.slug },
318
- }));
319
- }
320
- ```
321
-
322
- A route-based app simply returns a static array:
323
-
324
- ```typescript
325
- async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
326
- return [
327
- { title: 'Gallery', path: '/gallery' },
328
- { title: 'Settings', path: '/settings' },
329
- ];
330
- }
331
- ```
332
-
333
300
  ### Idempotency
334
301
 
335
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.
336
303
 
337
- ### Clearing the Registry
304
+ ### Clearing Dynamic Links
338
305
 
339
- To remove all linkable states (e.g., when an app is deactivated):
306
+ To remove all dynamic entries (e.g., all content was unpublished):
340
307
 
341
308
  ```typescript
342
309
  await appConfiguration.setConfig({
@@ -347,13 +314,48 @@ await appConfiguration.setConfig({
347
314
  });
348
315
  ```
349
316
 
317
+ Note that clearing `appConfig.linkable` only removes dynamic entries. Static links declared in the manifest are unaffected.
318
+
350
319
  ---
351
320
 
352
321
  ## Consumer Patterns
353
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
+
354
356
  ### Portal Navigation Menu
355
357
 
356
- A portal shell can aggregate linkable states from all apps in a collection to build a unified navigation menu:
358
+ A portal shell aggregates links from all installed apps to build a unified navigation menu:
357
359
 
358
360
  ```typescript
359
361
  import { appConfiguration } from '@proveanything/smartlinks';
@@ -365,15 +367,22 @@ interface NavLink {
365
367
  params?: Record<string, string>;
366
368
  }
367
369
 
368
- async function buildNavMenu(collectionId: string, appIds: string[]): Promise<NavLink[]> {
370
+ async function buildNavMenu(
371
+ collectionId: string,
372
+ apps: Array<{ appId: string; manifest: { linkable?: DeepLinkEntry[] } }>
373
+ ): Promise<NavLink[]> {
369
374
  const links: NavLink[] = [];
370
375
 
371
- for (const appId of appIds) {
376
+ for (const { appId, manifest } of apps) {
377
+ // Static links from manifest
378
+ const staticLinks = manifest.linkable ?? [];
379
+
380
+ // Dynamic links from appConfig
372
381
  const config = await appConfiguration.getConfig({ collectionId, appId });
373
- const entries: DeepLinkEntry[] = config?.linkable ?? [];
382
+ const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
374
383
 
375
384
  links.push(
376
- ...entries.map(entry => ({ ...entry, appId }))
385
+ ...[...staticLinks, ...dynamicLinks].map(entry => ({ ...entry, appId }))
377
386
  );
378
387
  }
379
388
 
@@ -383,19 +392,22 @@ async function buildNavMenu(collectionId: string, appIds: string[]): Promise<Nav
383
392
 
384
393
  ### AI Orchestration
385
394
 
386
- An AI agent can discover what states an app exposes and offer navigation as part of a conversation:
395
+ An AI agent discovers all navigable states and offers them to the user:
387
396
 
388
397
  ```typescript
389
398
  import { appConfiguration } from '@proveanything/smartlinks';
390
399
 
391
- async function getNavigationOptions(collectionId: string, appId: string) {
400
+ async function getNavigationOptions(
401
+ collectionId: string,
402
+ appId: string,
403
+ appManifest: { linkable?: DeepLinkEntry[] }
404
+ ): Promise<string[]> {
405
+ const staticLinks = appManifest.linkable ?? [];
392
406
  const config = await appConfiguration.getConfig({ collectionId, appId });
393
- const linkable: DeepLinkEntry[] = config?.linkable ?? [];
407
+ const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
394
408
 
395
- // The AI now knows what's available:
396
- // "I can navigate you to: Gallery, About Us, or FAQ.
397
- // Which would you prefer?"
398
- 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);
399
411
  }
400
412
  ```
401
413
 
@@ -403,16 +415,15 @@ The `title` field is intentionally human-readable so it can be injected directly
403
415
 
404
416
  ### Cross-App Navigation
405
417
 
406
- An app can use another app's `linkable` registry to construct a `NavigationRequest` for the portal's `onNavigate` callback:
418
+ An app constructs a `NavigationRequest` using the target app's merged links:
407
419
 
408
420
  ```typescript
409
- // Inside a widget or container's onNavigate handler
410
- const targetConfig = await appConfiguration.getConfig({
411
- collectionId,
412
- appId: 'target-app-id',
413
- });
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');
414
425
 
415
- const entry = targetConfig?.linkable?.find(e => e.title === 'About Us');
426
+ const entry = staticEntry ?? dynamicEntry;
416
427
 
417
428
  if (entry) {
418
429
  onNavigate({
@@ -425,51 +436,12 @@ if (entry) {
425
436
 
426
437
  ---
427
438
 
428
- ## Manifest Declaration
429
-
430
- 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:
431
-
432
- ```json
433
- {
434
- "setup": {
435
- "configSchema": {
436
- "properties": {
437
- "linkable": {
438
- "type": "array",
439
- "description": "Auto-managed deep link registry. Do not edit manually.",
440
- "readOnly": true,
441
- "items": {
442
- "type": "object",
443
- "required": ["title"],
444
- "properties": {
445
- "title": { "type": "string", "description": "Human-readable label" },
446
- "path": { "type": "string", "description": "Hash route path, e.g. '/gallery'" },
447
- "params": {
448
- "type": "object",
449
- "description": "App-specific query params. Do not include platform context params.",
450
- "additionalProperties": { "type": "string" }
451
- }
452
- }
453
- }
454
- }
455
- }
456
- }
457
- }
458
- }
459
- ```
460
-
461
- > **Note:** The `readOnly: true` flag is a hint to the admin UI to hide or disable this field in manual config editors.
462
-
463
- ---
464
-
465
439
  ## TypeScript Types
466
440
 
467
- Copy these types into your app, or import them if they become part of the SDK's public API:
468
-
469
441
  ```typescript
470
442
  /**
471
443
  * A single navigable state exposed by a SmartLinks app.
472
- * Stored as an array at `appConfig.linkable`.
444
+ * Used in both app.admin.json `linkable` (static) and appConfig `linkable` (dynamic).
473
445
  */
474
446
  export interface DeepLinkEntry {
475
447
  /** Human-readable label shown in menus and offered to AI agents */
@@ -487,10 +459,7 @@ export interface DeepLinkEntry {
487
459
  params?: Record<string, string>;
488
460
  }
489
461
 
490
- /**
491
- * The shape of the `linkable` key in appConfig.
492
- * Alias for convenience.
493
- */
462
+ /** Convenience alias for an array of DeepLinkEntry */
494
463
  export type DeepLinkRegistry = DeepLinkEntry[];
495
464
  ```
496
465
 
@@ -500,17 +469,20 @@ export type DeepLinkRegistry = DeepLinkEntry[];
500
469
 
501
470
  ### Rules
502
471
 
503
- 1. **`linkable` is reserved** — do not use this key for other purposes in `appConfig`.
504
- 2. **No platform context params in entries** — `collectionId`, `appId`, `productId`, `proofId`, `lang`, `theme` are injected by the platform automatically.
505
- 3. **`title` is required** — every entry must have a human-readable label.
506
- 4. **Sync from server** always fetch fresh data when syncing; never read from a local cache or previously fetched state.
507
- 5. **Full replace** — always overwrite the entire `linkable` array; never attempt to diff or patch individual entries.
472
+ 1. **Static routes belong in the manifest** — if a route exists regardless of content, declare it in `app.admin.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.
508
480
 
509
481
  ### Best Practices
510
482
 
511
483
  - **Keep titles short and scannable** — they appear in navigation menus and AI prompts. Prefer "About Us" over "Our Company Story & Mission".
512
- - **Order entries by relevance** — the first few entries are most likely to be shown in compact menus; place the most important states first.
513
- - **Sync eagerly** — trigger a sync immediately after any content change rather than on a schedule. The operation is cheap and idempotent.
514
- - **Handle missing `linkable` gracefully** — older apps won't have a `linkable` key. Always use `config?.linkable ?? []` rather than `config.linkable`.
515
- - **Don't duplicate the default route** — if your app has only one view, an empty `linkable` array (or omitting it) is better than a single entry that just opens the app.
516
- - **Test URL resolution** — confirm that your entries produce the correct URLs in the platform before publishing. Check that params are correctly appended and context params are not doubled up.
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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.6.5 | Generated: 2026-03-03T14:01:39.215Z
3
+ Version: 1.6.7 | Generated: 2026-03-03T14:18:48.483Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -7,20 +7,18 @@ Complete guide to registering and discovering navigable states in SmartLinks app
7
7
  ## Table of Contents
8
8
 
9
9
  - [Overview](#overview)
10
- - [Quick Start](#quick-start)
11
- - [The `linkable` Registry](#the-linkable-registry)
10
+ - [Two Sources of Truth](#two-sources-of-truth)
11
+ - [Static Links — App Manifest](#static-links--app-manifest)
12
+ - [Dynamic Links — App Config](#dynamic-links--app-config)
12
13
  - [Schema: DeepLinkEntry](#schema-deeplinkentry)
13
- - [Entry Types](#entry-types)
14
- - [Data-Driven (Dynamic)](#1-data-driven-entries-dynamic)
15
- - [Route-Based (Static)](#2-route-based-entries-static)
16
- - [Mixed](#3-mixed)
14
+ - [Quick Start](#quick-start)
17
15
  - [URL Resolution](#url-resolution)
18
- - [Syncing the Registry](#syncing-the-registry)
16
+ - [Syncing Dynamic Links](#syncing-dynamic-links)
19
17
  - [Consumer Patterns](#consumer-patterns)
18
+ - [Merging Both Sources](#merging-both-sources)
20
19
  - [Portal Navigation Menu](#portal-navigation-menu)
21
20
  - [AI Orchestration](#ai-orchestration)
22
21
  - [Cross-App Navigation](#cross-app-navigation)
23
- - [Manifest Declaration](#manifest-declaration)
24
22
  - [TypeScript Types](#typescript-types)
25
23
  - [Rules & Best Practices](#rules--best-practices)
26
24
 
@@ -28,113 +26,112 @@ Complete guide to registering and discovering navigable states in SmartLinks app
28
26
 
29
27
  ## Overview
30
28
 
31
- SmartLinks apps can expose **deep-linkable states** — specific views, pages, or configurations that other apps, the portal, or AI orchestrators can navigate to directly without loading the app first.
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
+
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.admin.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 |
32
37
 
33
- This is done by publishing a `linkable` array in the app's `appConfig`. Any consumer (portal shell, AI agent, another app) can read this array to discover what navigable states the app currently offers.
38
+ Consumers merge both sources to get the full set of navigable states for an app.
34
39
 
35
40
  ```text
36
- ┌─────────────────────────────────────────────────────────────────────┐
37
- Your SmartLinks App
38
-
39
- ┌───────────────────────┐ writes ┌────────────────────────┐│
40
- │ Admin / Data Events │ ─────────────► appConfig.linkable ││
41
- └───────────────────────┘ └────────────────────────┘│
42
- └─────────────────────────────────────────────────────────────────────┘
43
-
44
- reads linkable[]
45
-
46
- ┌───────────────────────────────────┼───────────────────────┐
47
- ▼ ▼ ▼
48
- ┌─────────────────────┐ ┌────────────────────┐ ┌─────────────────────┐
49
- │ Portal Nav Menu │ │ AI Orchestrator │ │ Another App │
50
- (sidebar / drawer) │ │ ("Go to About Us")│ │ (cross-app nav) │
51
- └─────────────────────┘ └────────────────────┘ └─────────────────────┘
41
+ ┌─────────────────────────────────────────────────────────────────────────┐
42
+ App Definition (build time)
43
+ app.admin.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
+ └─────────────────────────────────────────────────────────────────────────┘
52
61
  ```
53
62
 
54
63
  ### Key Benefits
55
64
 
56
65
  - ✅ **Discoverable** — consumers don't need to know the app's internal routing
57
- - ✅ **Decoupled** — the app owns the registry; consumers just read it
58
- - ✅ **Dynamic** — the registry updates automatically when content changes
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
59
68
  - ✅ **AI-friendly** — machine-readable titles make states naturally addressable by LLMs
60
- - ✅ **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
61
70
 
62
71
  ---
63
72
 
64
- ## Quick Start
73
+ ## Two Sources of Truth
65
74
 
66
- **App sidepublish linkable states:**
75
+ ### Static Links App Manifest
67
76
 
68
- ```typescript
69
- 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.admin.json` as a top-level `linkable` array:
70
78
 
71
- await appConfiguration.setConfig({
72
- collectionId,
73
- appId,
74
- config: {
75
- // ... other app config ...
76
- linkable: [
77
- { title: 'Gallery', path: '/gallery' },
78
- { title: 'About Us', params: { pageId: 'about-us' } },
79
- { title: 'FAQ', params: { pageId: 'faq' } },
80
- ]
81
- },
82
- admin: true
83
- });
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
+ }
84
88
  ```
85
89
 
86
- **Consumer side — read linkable states:**
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
87
94
 
88
- ```typescript
89
- import { appConfiguration } from '@proveanything/smartlinks';
90
-
91
- const config = await appConfiguration.getConfig({ collectionId, appId });
92
- const linkable = config?.linkable ?? [];
93
-
94
- // linkable = [
95
- // { title: 'Gallery', path: '/gallery' },
96
- // { title: 'About Us', params: { pageId: 'about-us' } },
97
- // { title: 'FAQ', params: { pageId: 'faq' } },
98
- // ]
99
- ```
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.
100
97
 
101
98
  ---
102
99
 
103
- ## The `linkable` Registry
100
+ ### Dynamic Links — App Config
104
101
 
105
- 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.
106
-
107
- ### Storage Location
108
-
109
- 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:
110
103
 
111
104
  ```typescript
112
- // Writing
105
+ import { appConfiguration } from '@proveanything/smartlinks';
106
+
113
107
  await appConfiguration.setConfig({
114
108
  collectionId,
115
109
  appId,
116
110
  config: {
117
111
  // ... other app config ...
118
- linkable: [ /* DeepLinkEntry[] */ ]
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
+ ]
119
117
  },
120
118
  admin: true
121
119
  });
122
-
123
- // Reading
124
- const config = await appConfiguration.getConfig({ collectionId, appId });
125
- const linkable: DeepLinkEntry[] = config?.linkable ?? [];
126
120
  ```
127
121
 
128
- ### Admin vs Public Access
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
125
+
126
+ The `linkable` key is **reserved** at the top level of `appConfig` for this purpose.
129
127
 
130
- - **Writing** always requires `admin: true` only the app itself (running with admin credentials) should update its own registry.
131
- - **Reading** can be done publicly — portals and consumers typically read the registry without admin credentials.
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.
132
129
 
133
130
  ---
134
131
 
135
132
  ## Schema: DeepLinkEntry
136
133
 
137
- Each entry in the `linkable` array describes one navigable state:
134
+ Both sources use the same entry shape:
138
135
 
139
136
  ```typescript
140
137
  interface DeepLinkEntry {
@@ -161,65 +158,63 @@ interface DeepLinkEntry {
161
158
  }
162
159
  ```
163
160
 
164
- ### Field Notes
165
-
166
161
  | Field | Required | Notes |
167
162
  |-------|----------|-------|
168
163
  | `title` | ✅ | Displayed in menus and offered to AI agents. Should be concise and human-readable. |
169
164
  | `path` | ❌ | Hash route within the app. Defaults to `/` if omitted. |
170
165
  | `params` | ❌ | App-specific query params only. Platform params are injected automatically. |
171
166
 
172
- > **Tip:** An entry with only a `title` (no `path` or `params`) is valid it navigates to the app's default view. This is rarely useful unless the app's home screen is itself a meaningful destination distinct from other entries.
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.
173
168
 
174
169
  ---
175
170
 
176
- ## Entry Types
177
-
178
- ### 1. Data-Driven Entries (Dynamic)
171
+ ## Quick Start
179
172
 
180
- Generated from app data e.g., CMS pages, product guides, FAQ items. These are typically re-synced whenever the underlying data changes.
173
+ **An app with both static routes and dynamic content pages:**
181
174
 
175
+ `app.admin.json` — declare static routes once, at build time:
182
176
  ```json
183
- [
184
- { "title": "About Us", "params": { "pageId": "about-us" } },
185
- { "title": "Care Guide", "params": { "pageId": "care-guide" } },
186
- { "title": "FAQ", "params": { "pageId": "faq" } }
187
- ]
177
+ {
178
+ "linkable": [
179
+ { "title": "Gallery", "path": "/gallery" },
180
+ { "title": "Settings", "path": "/settings" }
181
+ ]
182
+ }
188
183
  ```
189
184
 
190
- **When to use:** The set of linkable states depends on admin-created content. The registry is rebuilt whenever pages are added, removed, published, or renamed.
191
-
192
- ### 2. Route-Based Entries (Static)
185
+ App code sync dynamic content entries whenever pages change:
186
+ ```typescript
187
+ import { appConfiguration } from '@proveanything/smartlinks';
193
188
 
194
- Fixed routes that are built into the app. Declared once and rarely change.
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 } }));
195
195
 
196
- ```json
197
- [
198
- { "title": "Gallery", "path": "/gallery" },
199
- { "title": "Settings", "path": "/settings" },
200
- { "title": "Advanced Settings", "path": "/settings", "params": { "tab": "advanced" } }
201
- ]
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
+ }
202
203
  ```
203
204
 
204
- **When to use:** The app has multiple built-in views (tabs, sections) that should always be directly navigable, regardless of content.
205
-
206
- > **Note:** Different entries can share the same `path` if they differ by `params` as in the Settings example above.
207
-
208
- ### 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 ?? [];
209
209
 
210
- Apps can freely combine both patterns. Static routes can appear alongside dynamic content entries:
210
+ // dynamicLinks come from appConfig (per-collection, fetched at runtime)
211
+ const config = await appConfiguration.getConfig({ collectionId, appId });
212
+ const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
211
213
 
212
- ```json
213
- [
214
- { "title": "Gallery", "path": "/gallery" },
215
- { "title": "About Us", "params": { "pageId": "about-us" } },
216
- { "title": "FAQ", "params": { "pageId": "faq" } },
217
- { "title": "Settings", "path": "/settings" }
218
- ]
214
+ // Full set — static (structural) first, then dynamic (content)
215
+ const allLinks = [...staticLinks, ...dynamicLinks];
219
216
  ```
220
217
 
221
- **Ordering:** Entries are displayed in array order. Place the most commonly navigated states first.
222
-
223
218
  ---
224
219
 
225
220
  ## URL Resolution
@@ -256,16 +251,17 @@ The platform automatically injects the following — **never include these in `p
256
251
 
257
252
  ---
258
253
 
259
- ## Syncing the Registry
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.
260
257
 
261
258
  ### When to Sync
262
259
 
263
- Apps **MUST** update the `linkable` array whenever the set of navigable states changes:
260
+ Apps **MUST** update `appConfig.linkable` whenever the set of dynamic navigable states changes:
264
261
 
265
262
  - A page is created, deleted, published, or unpublished
266
263
  - A page's `deepLinkable` flag is toggled
267
264
  - A page title changes
268
- - App routes are added or removed
269
265
 
270
266
  ### How to Sync
271
267
 
@@ -275,9 +271,9 @@ The sync pattern is a **full replace** — read current config, overwrite `linka
275
271
  import { appConfiguration } from '@proveanything/smartlinks';
276
272
 
277
273
  async function syncLinkable(collectionId: string, appId: string): Promise<void> {
278
- // 1. Fetch the current set of linkable states from your data source
279
- // Always fetch fresh — never rely on a local cache
280
- const entries = await fetchLinkableEntries(); // your app-specific logic
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
281
277
 
282
278
  // 2. Read the existing appConfig to preserve other keys
283
279
  let config: Record<string, unknown> = {};
@@ -291,7 +287,7 @@ async function syncLinkable(collectionId: string, appId: string): Promise<void>
291
287
  // Config may not exist yet on first run — that's fine
292
288
  }
293
289
 
294
- // 3. Merge and save
290
+ // 3. Overwrite only the linkable key and save
295
291
  await appConfiguration.setConfig({
296
292
  collectionId,
297
293
  appId,
@@ -301,42 +297,13 @@ async function syncLinkable(collectionId: string, appId: string): Promise<void>
301
297
  }
302
298
  ```
303
299
 
304
- ### What `fetchLinkableEntries` looks like
305
-
306
- This function is app-specific. A typical CMS app might look like:
307
-
308
- ```typescript
309
- async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
310
- // Fetch your pages/content from the server (not from cache)
311
- const pages = await myApi.getPublishedPages({ forceRefresh: true });
312
-
313
- return pages
314
- .filter(page => page.deepLinkable)
315
- .map(page => ({
316
- title: page.title,
317
- params: { pageId: page.slug },
318
- }));
319
- }
320
- ```
321
-
322
- A route-based app simply returns a static array:
323
-
324
- ```typescript
325
- async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
326
- return [
327
- { title: 'Gallery', path: '/gallery' },
328
- { title: 'Settings', path: '/settings' },
329
- ];
330
- }
331
- ```
332
-
333
300
  ### Idempotency
334
301
 
335
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.
336
303
 
337
- ### Clearing the Registry
304
+ ### Clearing Dynamic Links
338
305
 
339
- To remove all linkable states (e.g., when an app is deactivated):
306
+ To remove all dynamic entries (e.g., all content was unpublished):
340
307
 
341
308
  ```typescript
342
309
  await appConfiguration.setConfig({
@@ -347,13 +314,48 @@ await appConfiguration.setConfig({
347
314
  });
348
315
  ```
349
316
 
317
+ Note that clearing `appConfig.linkable` only removes dynamic entries. Static links declared in the manifest are unaffected.
318
+
350
319
  ---
351
320
 
352
321
  ## Consumer Patterns
353
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
+
354
356
  ### Portal Navigation Menu
355
357
 
356
- A portal shell can aggregate linkable states from all apps in a collection to build a unified navigation menu:
358
+ A portal shell aggregates links from all installed apps to build a unified navigation menu:
357
359
 
358
360
  ```typescript
359
361
  import { appConfiguration } from '@proveanything/smartlinks';
@@ -365,15 +367,22 @@ interface NavLink {
365
367
  params?: Record<string, string>;
366
368
  }
367
369
 
368
- async function buildNavMenu(collectionId: string, appIds: string[]): Promise<NavLink[]> {
370
+ async function buildNavMenu(
371
+ collectionId: string,
372
+ apps: Array<{ appId: string; manifest: { linkable?: DeepLinkEntry[] } }>
373
+ ): Promise<NavLink[]> {
369
374
  const links: NavLink[] = [];
370
375
 
371
- for (const appId of appIds) {
376
+ for (const { appId, manifest } of apps) {
377
+ // Static links from manifest
378
+ const staticLinks = manifest.linkable ?? [];
379
+
380
+ // Dynamic links from appConfig
372
381
  const config = await appConfiguration.getConfig({ collectionId, appId });
373
- const entries: DeepLinkEntry[] = config?.linkable ?? [];
382
+ const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
374
383
 
375
384
  links.push(
376
- ...entries.map(entry => ({ ...entry, appId }))
385
+ ...[...staticLinks, ...dynamicLinks].map(entry => ({ ...entry, appId }))
377
386
  );
378
387
  }
379
388
 
@@ -383,19 +392,22 @@ async function buildNavMenu(collectionId: string, appIds: string[]): Promise<Nav
383
392
 
384
393
  ### AI Orchestration
385
394
 
386
- An AI agent can discover what states an app exposes and offer navigation as part of a conversation:
395
+ An AI agent discovers all navigable states and offers them to the user:
387
396
 
388
397
  ```typescript
389
398
  import { appConfiguration } from '@proveanything/smartlinks';
390
399
 
391
- async function getNavigationOptions(collectionId: string, appId: string) {
400
+ async function getNavigationOptions(
401
+ collectionId: string,
402
+ appId: string,
403
+ appManifest: { linkable?: DeepLinkEntry[] }
404
+ ): Promise<string[]> {
405
+ const staticLinks = appManifest.linkable ?? [];
392
406
  const config = await appConfiguration.getConfig({ collectionId, appId });
393
- const linkable: DeepLinkEntry[] = config?.linkable ?? [];
407
+ const dynamicLinks: DeepLinkEntry[] = config?.linkable ?? [];
394
408
 
395
- // The AI now knows what's available:
396
- // "I can navigate you to: Gallery, About Us, or FAQ.
397
- // Which would you prefer?"
398
- 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);
399
411
  }
400
412
  ```
401
413
 
@@ -403,16 +415,15 @@ The `title` field is intentionally human-readable so it can be injected directly
403
415
 
404
416
  ### Cross-App Navigation
405
417
 
406
- An app can use another app's `linkable` registry to construct a `NavigationRequest` for the portal's `onNavigate` callback:
418
+ An app constructs a `NavigationRequest` using the target app's merged links:
407
419
 
408
420
  ```typescript
409
- // Inside a widget or container's onNavigate handler
410
- const targetConfig = await appConfiguration.getConfig({
411
- collectionId,
412
- appId: 'target-app-id',
413
- });
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');
414
425
 
415
- const entry = targetConfig?.linkable?.find(e => e.title === 'About Us');
426
+ const entry = staticEntry ?? dynamicEntry;
416
427
 
417
428
  if (entry) {
418
429
  onNavigate({
@@ -425,51 +436,12 @@ if (entry) {
425
436
 
426
437
  ---
427
438
 
428
- ## Manifest Declaration
429
-
430
- 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:
431
-
432
- ```json
433
- {
434
- "setup": {
435
- "configSchema": {
436
- "properties": {
437
- "linkable": {
438
- "type": "array",
439
- "description": "Auto-managed deep link registry. Do not edit manually.",
440
- "readOnly": true,
441
- "items": {
442
- "type": "object",
443
- "required": ["title"],
444
- "properties": {
445
- "title": { "type": "string", "description": "Human-readable label" },
446
- "path": { "type": "string", "description": "Hash route path, e.g. '/gallery'" },
447
- "params": {
448
- "type": "object",
449
- "description": "App-specific query params. Do not include platform context params.",
450
- "additionalProperties": { "type": "string" }
451
- }
452
- }
453
- }
454
- }
455
- }
456
- }
457
- }
458
- }
459
- ```
460
-
461
- > **Note:** The `readOnly: true` flag is a hint to the admin UI to hide or disable this field in manual config editors.
462
-
463
- ---
464
-
465
439
  ## TypeScript Types
466
440
 
467
- Copy these types into your app, or import them if they become part of the SDK's public API:
468
-
469
441
  ```typescript
470
442
  /**
471
443
  * A single navigable state exposed by a SmartLinks app.
472
- * Stored as an array at `appConfig.linkable`.
444
+ * Used in both app.admin.json `linkable` (static) and appConfig `linkable` (dynamic).
473
445
  */
474
446
  export interface DeepLinkEntry {
475
447
  /** Human-readable label shown in menus and offered to AI agents */
@@ -487,10 +459,7 @@ export interface DeepLinkEntry {
487
459
  params?: Record<string, string>;
488
460
  }
489
461
 
490
- /**
491
- * The shape of the `linkable` key in appConfig.
492
- * Alias for convenience.
493
- */
462
+ /** Convenience alias for an array of DeepLinkEntry */
494
463
  export type DeepLinkRegistry = DeepLinkEntry[];
495
464
  ```
496
465
 
@@ -500,17 +469,20 @@ export type DeepLinkRegistry = DeepLinkEntry[];
500
469
 
501
470
  ### Rules
502
471
 
503
- 1. **`linkable` is reserved** — do not use this key for other purposes in `appConfig`.
504
- 2. **No platform context params in entries** — `collectionId`, `appId`, `productId`, `proofId`, `lang`, `theme` are injected by the platform automatically.
505
- 3. **`title` is required** — every entry must have a human-readable label.
506
- 4. **Sync from server** always fetch fresh data when syncing; never read from a local cache or previously fetched state.
507
- 5. **Full replace** — always overwrite the entire `linkable` array; never attempt to diff or patch individual entries.
472
+ 1. **Static routes belong in the manifest** — if a route exists regardless of content, declare it in `app.admin.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.
508
480
 
509
481
  ### Best Practices
510
482
 
511
483
  - **Keep titles short and scannable** — they appear in navigation menus and AI prompts. Prefer "About Us" over "Our Company Story & Mission".
512
- - **Order entries by relevance** — the first few entries are most likely to be shown in compact menus; place the most important states first.
513
- - **Sync eagerly** — trigger a sync immediately after any content change rather than on a schedule. The operation is cheap and idempotent.
514
- - **Handle missing `linkable` gracefully** — older apps won't have a `linkable` key. Always use `config?.linkable ?? []` rather than `config.linkable`.
515
- - **Don't duplicate the default route** — if your app has only one view, an empty `linkable` array (or omitting it) is better than a single entry that just opens the app.
516
- - **Test URL resolution** — confirm that your entries produce the correct URLs in the platform before publishing. Check that params are correctly appended and context params are not doubled up.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",