@proveanything/smartlinks 1.8.7 → 1.8.9
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/docs/API_SUMMARY.md +1 -1
- package/dist/docs/building-react-components.md +319 -0
- package/dist/docs/containers.md +184 -0
- package/dist/docs/overview.md +1 -0
- package/dist/docs/widgets.md +98 -0
- package/docs/API_SUMMARY.md +1 -1
- package/docs/building-react-components.md +319 -0
- package/docs/containers.md +184 -0
- package/docs/overview.md +1 -0
- package/docs/widgets.md +98 -0
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Building React Components for SmartLinks
|
|
2
|
+
|
|
3
|
+
> **Read this first** before implementing widgets or containers for the SmartLinks platform.
|
|
4
|
+
|
|
5
|
+
This guide covers the fundamental concepts you need to understand to build React components that work correctly in the SmartLinks ecosystem. For implementation details, see [widgets.md](./widgets.md) and [containers.md](./containers.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Dual-Mode Rendering](#dual-mode-rendering)
|
|
12
|
+
2. [The Router Contract](#the-router-contract)
|
|
13
|
+
3. [The useAppContext Pattern](#the-useappcontext-pattern)
|
|
14
|
+
4. [Common Pitfalls](#common-pitfalls)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Dual-Mode Rendering
|
|
19
|
+
|
|
20
|
+
Every SmartLinks component can run in **two modes**. The platform decides which mode to use based on configuration — your app doesn't choose.
|
|
21
|
+
|
|
22
|
+
| Mode | How It Works | When Used |
|
|
23
|
+
|------|-------------|-----------|
|
|
24
|
+
| **Direct Component** | Your component runs directly in the parent's React context | Default - better performance, shared context |
|
|
25
|
+
| **Iframe** | Your app runs inside an iframe with its own URL | Fallback - full isolation when needed |
|
|
26
|
+
|
|
27
|
+
**Key insight:** You write your component code once, and it must work correctly in **both** modes.
|
|
28
|
+
|
|
29
|
+
### What Changes Between Modes?
|
|
30
|
+
|
|
31
|
+
| Aspect | Direct Component Mode | Iframe Mode |
|
|
32
|
+
|--------|----------------------|-------------|
|
|
33
|
+
| **Context source** | Props passed from parent | URL search parameters |
|
|
34
|
+
| **Router** | Parent provides `MemoryRouter` | Your app provides `HashRouter` |
|
|
35
|
+
| **Styling** | Inherits parent's CSS scope | Fully isolated CSS |
|
|
36
|
+
| **SDK instance** | Shared with parent via props | Own instance from `window.SL` |
|
|
37
|
+
| **Communication** | Direct callbacks (props) | `postMessage` |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The Router Contract
|
|
42
|
+
|
|
43
|
+
This is the **most important rule** to avoid runtime errors:
|
|
44
|
+
|
|
45
|
+
### ❌ The Critical Rule
|
|
46
|
+
|
|
47
|
+
> **Your exported component (`PublicContainer` or `PublicComponent`) must NOT be wrapped in any `<Router>` component.**
|
|
48
|
+
|
|
49
|
+
If you wrap your export in `<MemoryRouter>`, `<HashRouter>`, or `<BrowserRouter>`, React Router will throw:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Error: You cannot render a <Router> inside another <Router>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### ✅ Where Routing Goes
|
|
56
|
+
|
|
57
|
+
**For Widgets (no routing needed):**
|
|
58
|
+
```tsx
|
|
59
|
+
// src/exports/PublicComponent.tsx
|
|
60
|
+
export const PublicComponent = (props) => {
|
|
61
|
+
return <WidgetContent />; // No router needed
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**For Containers (with internal navigation):**
|
|
66
|
+
```tsx
|
|
67
|
+
// src/exports/PublicContainer.tsx
|
|
68
|
+
import { Routes, Route } from 'react-router-dom';
|
|
69
|
+
|
|
70
|
+
export const PublicContainer = (props) => {
|
|
71
|
+
return (
|
|
72
|
+
<Routes> {/* ✅ Routes are fine */}
|
|
73
|
+
<Route path="/" element={<Home />} />
|
|
74
|
+
<Route path="/detail/:id" element={<Detail />} />
|
|
75
|
+
</Routes>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**For Iframe Entry Point:**
|
|
81
|
+
```tsx
|
|
82
|
+
// src/App.tsx (only used in iframe mode)
|
|
83
|
+
import { HashRouter, Routes, Route } from 'react-router-dom';
|
|
84
|
+
|
|
85
|
+
function App() {
|
|
86
|
+
return (
|
|
87
|
+
<HashRouter> {/* ✅ HashRouter only in iframe entry point */}
|
|
88
|
+
<Routes>
|
|
89
|
+
<Route path="/" element={<Home />} />
|
|
90
|
+
</Routes>
|
|
91
|
+
</HashRouter>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Why This Matters
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Direct Component Mode:
|
|
100
|
+
Portal App
|
|
101
|
+
└─ MemoryRouter ← Parent provides this
|
|
102
|
+
└─ Your Component
|
|
103
|
+
└─ [If you add another Router here, it breaks]
|
|
104
|
+
|
|
105
|
+
Iframe Mode:
|
|
106
|
+
<iframe>
|
|
107
|
+
└─ Your App.tsx
|
|
108
|
+
└─ HashRouter ← You provide this
|
|
109
|
+
└─ Your Component
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## The useAppContext Pattern
|
|
115
|
+
|
|
116
|
+
To access context (collectionId, productId, etc.) in a way that works in **both** modes, use this abstraction:
|
|
117
|
+
|
|
118
|
+
### The Hook
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// src/hooks/useAppContext.ts
|
|
122
|
+
import { useContext, createContext, useMemo } from 'react';
|
|
123
|
+
import { useSearchParams } from 'react-router-dom';
|
|
124
|
+
|
|
125
|
+
export interface AppContextValue {
|
|
126
|
+
collectionId: string;
|
|
127
|
+
appId: string;
|
|
128
|
+
productId?: string;
|
|
129
|
+
proofId?: string;
|
|
130
|
+
pageId?: string;
|
|
131
|
+
lang?: string;
|
|
132
|
+
user?: { id: string; email: string; name?: string };
|
|
133
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
134
|
+
onNavigate?: (request: any) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const AppContext = createContext<AppContextValue | null>(null);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns app context regardless of rendering mode.
|
|
141
|
+
* - Direct component mode: reads from React Context (props)
|
|
142
|
+
* - Iframe mode: reads from URL search params
|
|
143
|
+
*/
|
|
144
|
+
export function useAppContext(): AppContextValue {
|
|
145
|
+
const ctx = useContext(AppContext);
|
|
146
|
+
|
|
147
|
+
// Direct-component mode
|
|
148
|
+
if (ctx) return ctx;
|
|
149
|
+
|
|
150
|
+
// Iframe mode — read from URL
|
|
151
|
+
const [searchParams] = useSearchParams();
|
|
152
|
+
const SL = (window as any).SL ?? require('@proveanything/smartlinks');
|
|
153
|
+
|
|
154
|
+
return useMemo(() => ({
|
|
155
|
+
collectionId: searchParams.get('collectionId') ?? '',
|
|
156
|
+
appId: searchParams.get('appId') ?? '',
|
|
157
|
+
productId: searchParams.get('productId') ?? undefined,
|
|
158
|
+
proofId: searchParams.get('proofId') ?? undefined,
|
|
159
|
+
pageId: searchParams.get('pageId') ?? undefined,
|
|
160
|
+
lang: searchParams.get('lang') ?? undefined,
|
|
161
|
+
SL,
|
|
162
|
+
}), [searchParams, SL]);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Wire It Up in Your Export
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// src/exports/PublicContainer.tsx
|
|
170
|
+
import { AppContext } from '@/hooks/useAppContext';
|
|
171
|
+
|
|
172
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
173
|
+
return (
|
|
174
|
+
<AppContext.Provider value={props}>
|
|
175
|
+
<YourComponentTree />
|
|
176
|
+
</AppContext.Provider>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Use It in Your Components
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// Anywhere in your component tree
|
|
185
|
+
import { useAppContext } from '@/hooks/useAppContext';
|
|
186
|
+
|
|
187
|
+
function MyComponent() {
|
|
188
|
+
const { collectionId, productId, SL } = useAppContext();
|
|
189
|
+
// This works in both direct-component and iframe modes!
|
|
190
|
+
|
|
191
|
+
return <div>Collection: {collectionId}</div>;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Why Not Just useSearchParams()?
|
|
196
|
+
|
|
197
|
+
In **direct-component mode**, there are no URL search params — context comes as props. If you use `useSearchParams()` directly, it will return empty values. The `useAppContext()` pattern handles both cases automatically.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Common Pitfalls
|
|
202
|
+
|
|
203
|
+
### ❌ "You cannot render a `<Router>` inside another `<Router>`"
|
|
204
|
+
|
|
205
|
+
**Cause:** Your exported component wraps itself in a Router.
|
|
206
|
+
|
|
207
|
+
**Fix:** Remove the Router wrapper from `PublicContainer.tsx` or `PublicComponent.tsx`. Only your iframe entry point (`App.tsx`) should have a Router.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### ❌ "Invalid hook call" / Minified React error #321
|
|
212
|
+
|
|
213
|
+
**Cause:** Your bundle includes its own copy of React instead of using the parent's shared instance.
|
|
214
|
+
|
|
215
|
+
**Fix:** Ensure your build configuration externalizes React, ReactDOM, and react/jsx-runtime:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// vite.config.container.ts or vite.config.widget.ts
|
|
219
|
+
export default defineConfig({
|
|
220
|
+
build: {
|
|
221
|
+
rollupOptions: {
|
|
222
|
+
external: [
|
|
223
|
+
'react',
|
|
224
|
+
'react-dom',
|
|
225
|
+
'react/jsx-runtime',
|
|
226
|
+
'react-router-dom',
|
|
227
|
+
'@proveanything/smartlinks',
|
|
228
|
+
// ... other shared dependencies
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### ❌ Context values are undefined in direct-component mode
|
|
238
|
+
|
|
239
|
+
**Cause:** Using `useSearchParams()` or reading from `window.location` instead of using the `useAppContext()` pattern.
|
|
240
|
+
|
|
241
|
+
**Fix:** Implement the `useAppContext()` hook as shown above and use it throughout your component tree.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### ❌ Navigation doesn't work
|
|
246
|
+
|
|
247
|
+
**Cause:** Using `window.location` or `window.parent.postMessage` for navigation.
|
|
248
|
+
|
|
249
|
+
**Fix:** Use the `onNavigate` callback from props/context with structured `NavigationRequest` objects. See [widgets.md](./widgets.md#cross-app-navigation) for details.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### ❌ Styles leak between apps
|
|
254
|
+
|
|
255
|
+
**Cause:** Direct components share the parent's CSS scope — there's no iframe isolation.
|
|
256
|
+
|
|
257
|
+
**Fix:** Use CSS Modules, scoped class prefixes, or Tailwind with a unique prefix. Avoid global CSS selectors in your bundle.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### ❌ CORS errors when loading bundles
|
|
262
|
+
|
|
263
|
+
**Cause:** Your widget/container JavaScript or CSS is served from a different origin without CORS headers.
|
|
264
|
+
|
|
265
|
+
**Fix:** Configure CORS headers on your hosting, or use inline bundle source via the SDK's `getWidgets()` API.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Quick Diagnostic Checklist
|
|
270
|
+
|
|
271
|
+
| Issue | Check |
|
|
272
|
+
|-------|-------|
|
|
273
|
+
| Router error | Remove all `<Router>` wrappers from your exported component |
|
|
274
|
+
| Invalid hook call | Verify React is externalized in your build config |
|
|
275
|
+
| Missing context | Use `useAppContext()` instead of `useSearchParams()` |
|
|
276
|
+
| Duplicate React | Check `window.React === yourBundle.React` in browser console |
|
|
277
|
+
| CSS conflicts | Use scoped CSS (modules/prefixes), avoid global selectors |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## File Structure Reference
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
my-app/
|
|
285
|
+
├── src/
|
|
286
|
+
│ ├── App.tsx ← Iframe entry (has HashRouter)
|
|
287
|
+
│ ├── exports/
|
|
288
|
+
│ │ ├── PublicContainer.tsx ← Container export (no Router!)
|
|
289
|
+
│ │ └── PublicComponent.tsx ← Widget export (no Router!)
|
|
290
|
+
│ ├── hooks/
|
|
291
|
+
│ │ └── useAppContext.ts ← Dual-mode abstraction
|
|
292
|
+
│ ├── pages/
|
|
293
|
+
│ │ ├── Home.tsx ← Shared components
|
|
294
|
+
│ │ └── Detail.tsx
|
|
295
|
+
│ └── main.tsx ← Iframe bootstrap
|
|
296
|
+
├── vite.config.ts ← Iframe build
|
|
297
|
+
├── vite.config.container.ts ← Container bundle
|
|
298
|
+
└── vite.config.widget.ts ← Widget bundle
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Next Steps
|
|
304
|
+
|
|
305
|
+
Now that you understand the core concepts, see the implementation guides:
|
|
306
|
+
|
|
307
|
+
- **[widgets.md](./widgets.md)** — Build lightweight preview components
|
|
308
|
+
- **[containers.md](./containers.md)** — Build full-page embedded experiences
|
|
309
|
+
- **[mpa.md](./mpa.md)** — Multi-page app architecture (optional)
|
|
310
|
+
- **[iframe-responder.md](./iframe-responder.md)** — Parent-side iframe integration
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Related Documentation
|
|
315
|
+
|
|
316
|
+
- [theme.system.md](./theme.system.md) — Theming and CSS variables
|
|
317
|
+
- [ai.md](./ai.md) — AI assistant integration
|
|
318
|
+
- [deep-link-discovery.md](./deep-link-discovery.md) — Deep linking patterns
|
|
319
|
+
- [manifests.md](./manifests.md) — App manifest configuration
|
package/dist/docs/containers.md
CHANGED
|
@@ -26,6 +26,190 @@ Imagine a homepage displaying 10 app widgets. If containers were bundled with wi
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## Dual-Mode Rendering
|
|
30
|
+
|
|
31
|
+
Containers can run in **two modes**, and the same container code must work in both:
|
|
32
|
+
|
|
33
|
+
| Mode | How It Works | Who Provides the Router? |
|
|
34
|
+
|------|-------------|--------------------------|
|
|
35
|
+
| **Direct Component** | Container runs directly in the parent's React context | **The framework** — wraps your component in `MemoryRouter` |
|
|
36
|
+
| **Iframe** | Container runs inside an iframe with its own URL | **Your app** — you manage your own `HashRouter` |
|
|
37
|
+
|
|
38
|
+
### The Critical Rule: No Router Wrappers in Your Export
|
|
39
|
+
|
|
40
|
+
> **❌ Your exported `PublicContainer` must NOT be wrapped in a `<Router>`, `<MemoryRouter>`, `<HashRouter>`, or `<BrowserRouter>`.**
|
|
41
|
+
|
|
42
|
+
**Why?** In direct-component mode, the framework already wraps your container in a `MemoryRouter`. If your component includes its own Router, React Router will throw: _"You cannot render a `<Router>` inside another `<Router>`"_.
|
|
43
|
+
|
|
44
|
+
**The routing architecture:**
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Direct Component Mode:
|
|
48
|
+
Portal Shell (HashRouter)
|
|
49
|
+
└─ ContentOrchestrator
|
|
50
|
+
└─ MemoryRouter ← framework provides this
|
|
51
|
+
└─ <YourContainer /> ← only contains <Routes>, no <Router>
|
|
52
|
+
└─ <Route path="/" element={<Home />} />
|
|
53
|
+
└─ <Route path="/detail/:id" element={<Detail />} />
|
|
54
|
+
|
|
55
|
+
Iframe Mode:
|
|
56
|
+
<iframe src="your-app-url">
|
|
57
|
+
└─ <HashRouter> ← your App.tsx provides this
|
|
58
|
+
└─ <Routes>
|
|
59
|
+
└─ <Route path="/" element={<Home />} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Where routing goes:**
|
|
63
|
+
- ✅ Your **iframe entry point** (`App.tsx` / `main.tsx`) should use `HashRouter`
|
|
64
|
+
- ✅ Your **exported container** (`PublicContainer.tsx`) should include `<Routes>` and `<Route>` elements
|
|
65
|
+
- ❌ Your **exported container** should NOT include any `<Router>` wrapper
|
|
66
|
+
|
|
67
|
+
### The Router Contract
|
|
68
|
+
|
|
69
|
+
In direct-component mode, the framework's `MemoryRouter` gives you full React Router capabilities:
|
|
70
|
+
|
|
71
|
+
- ✅ `useNavigate()` works — navigates within your container's routes
|
|
72
|
+
- ✅ `useLocation()` works — returns current location in the MemoryRouter
|
|
73
|
+
- ✅ `useParams()` works — reads params from your route definitions
|
|
74
|
+
- ✅ `<Routes>` / `<Route>` work — define your internal navigation
|
|
75
|
+
- ✅ `useSearchParams()` works — manages search params within the MemoryRouter
|
|
76
|
+
- ⚠️ `window.location` does **NOT** reflect your container's route — it reflects the portal's URL
|
|
77
|
+
|
|
78
|
+
**Example: Correct Container Structure**
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// ❌ WRONG — has Router wrapper
|
|
82
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
83
|
+
return (
|
|
84
|
+
<MemoryRouter> {/* ❌ Don't do this! */}
|
|
85
|
+
<Routes>
|
|
86
|
+
<Route path="/" element={<Home />} />
|
|
87
|
+
</Routes>
|
|
88
|
+
</MemoryRouter>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ✅ CORRECT — no Router wrapper
|
|
93
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
94
|
+
return (
|
|
95
|
+
<AppContext.Provider value={props}>
|
|
96
|
+
<Routes> {/* ✅ Routes are fine, just no Router */}
|
|
97
|
+
<Route path="/" element={<Home />} />
|
|
98
|
+
<Route path="/detail/:id" element={<Detail />} />
|
|
99
|
+
</Routes>
|
|
100
|
+
</AppContext.Provider>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### The `useAppContext()` Pattern
|
|
106
|
+
|
|
107
|
+
To write containers that work identically in both modes, use this abstraction pattern:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// src/hooks/useAppContext.ts
|
|
111
|
+
import { useContext, createContext, useMemo } from 'react';
|
|
112
|
+
import { useSearchParams } from 'react-router-dom';
|
|
113
|
+
|
|
114
|
+
export interface AppContextValue {
|
|
115
|
+
collectionId: string;
|
|
116
|
+
appId: string;
|
|
117
|
+
productId?: string;
|
|
118
|
+
proofId?: string;
|
|
119
|
+
pageId?: string;
|
|
120
|
+
initialPath?: string;
|
|
121
|
+
lang?: string;
|
|
122
|
+
user?: { id: string; email: string; name?: string };
|
|
123
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
124
|
+
onNavigate?: (request: any) => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const AppContext = createContext<AppContextValue | null>(null);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns app context regardless of rendering mode.
|
|
131
|
+
* - Direct component mode: reads from AppContext (props)
|
|
132
|
+
* - Iframe mode: reads from URL search params
|
|
133
|
+
*/
|
|
134
|
+
export function useAppContext(): AppContextValue {
|
|
135
|
+
const ctx = useContext(AppContext);
|
|
136
|
+
|
|
137
|
+
// If context exists, we're in direct-component mode
|
|
138
|
+
if (ctx) return ctx;
|
|
139
|
+
|
|
140
|
+
// Otherwise, we're in iframe mode — read from URL params
|
|
141
|
+
const [searchParams] = useSearchParams();
|
|
142
|
+
const SL = (window as any).SL ?? require('@proveanything/smartlinks');
|
|
143
|
+
|
|
144
|
+
return useMemo(() => ({
|
|
145
|
+
collectionId: searchParams.get('collectionId') ?? '',
|
|
146
|
+
appId: searchParams.get('appId') ?? '',
|
|
147
|
+
productId: searchParams.get('productId') ?? undefined,
|
|
148
|
+
proofId: searchParams.get('proofId') ?? undefined,
|
|
149
|
+
pageId: searchParams.get('pageId') ?? undefined,
|
|
150
|
+
lang: searchParams.get('lang') ?? undefined,
|
|
151
|
+
SL,
|
|
152
|
+
}), [searchParams, SL]);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Usage in your container:**
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// src/exports/PublicContainer.tsx
|
|
160
|
+
import { Routes, Route } from 'react-router-dom';
|
|
161
|
+
import { AppContext } from '@/hooks/useAppContext';
|
|
162
|
+
import { Home } from '@/pages/Home';
|
|
163
|
+
import { Detail } from '@/pages/Detail';
|
|
164
|
+
|
|
165
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
166
|
+
return (
|
|
167
|
+
<AppContext.Provider value={props}>
|
|
168
|
+
<Routes>
|
|
169
|
+
<Route path="/" element={<Home />} />
|
|
170
|
+
<Route path="/detail/:id" element={<Detail />} />
|
|
171
|
+
</Routes>
|
|
172
|
+
</AppContext.Provider>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/pages/Home.tsx
|
|
177
|
+
import { useAppContext } from '@/hooks/useAppContext';
|
|
178
|
+
import { useNavigate } from 'react-router-dom';
|
|
179
|
+
|
|
180
|
+
export function Home() {
|
|
181
|
+
const { collectionId, productId, SL } = useAppContext();
|
|
182
|
+
const navigate = useNavigate();
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div>
|
|
186
|
+
<h1>Collection: {collectionId}</h1>
|
|
187
|
+
<button onClick={() => navigate('/detail/123')}>
|
|
188
|
+
View Detail
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Deep Linking
|
|
196
|
+
|
|
197
|
+
The framework sets the `MemoryRouter`'s initial route based on the `pageId` or `initialPath` prop. For example, if the portal navigates to your container with `pageId: 'settings'`, your router will start at `/settings`.
|
|
198
|
+
|
|
199
|
+
To advertise deep-linkable pages, declare them in your `app.manifest.json`:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"linkable": [
|
|
204
|
+
{ "title": "Home", "path": "/" },
|
|
205
|
+
{ "title": "Settings", "path": "/settings" },
|
|
206
|
+
{ "title": "Item Detail", "path": "/item/:itemId" }
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
29
213
|
## Container Props
|
|
30
214
|
|
|
31
215
|
Container props extend the standard `SmartLinksWidgetProps` with an additional `className` prop:
|
package/dist/docs/overview.md
CHANGED
|
@@ -45,6 +45,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
45
45
|
| Topic | File | When to Use |
|
|
46
46
|
|-------|------|-------------|
|
|
47
47
|
| **API Reference** | `docs/API_SUMMARY.md` | Complete SDK function reference, types, error handling |
|
|
48
|
+
| **Building React Components** | `docs/building-react-components.md` | **READ THIS FIRST** — Dual-mode rendering, router rules, useAppContext pattern |
|
|
48
49
|
| **Multi-Page Architecture** | `docs/mpa.md` | Build pipeline, entry points, multi-page setup, content hashing |
|
|
49
50
|
| **AI & Chat** | `docs/ai.md` | Chat completions, RAG, streaming, tool calling, voice, podcasts, TTS |
|
|
50
51
|
| **Analytics** | `docs/analytics.md` | Fire-and-forget page/click/tag analytics plus admin dashboard queries |
|
package/dist/docs/widgets.md
CHANGED
|
@@ -30,6 +30,104 @@ Widgets are self-contained React components that:
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## Dual-Mode Rendering
|
|
34
|
+
|
|
35
|
+
Widgets can run in **two modes**, and the same widget code must work in both:
|
|
36
|
+
|
|
37
|
+
| Mode | How It Works | When Used |
|
|
38
|
+
|------|-------------|-----------|
|
|
39
|
+
| **Direct Component** | Widget runs directly in the parent's React context | Default - when loaded as ESM/UMD in the parent app |
|
|
40
|
+
| **Iframe** | Widget runs inside an iframe with its own URL | Fallback or when full isolation is needed |
|
|
41
|
+
|
|
42
|
+
### The Critical Rule: No Router Wrappers
|
|
43
|
+
|
|
44
|
+
> **❌ Your widget component must NOT be wrapped in a `<Router>`, `<MemoryRouter>`, `<HashRouter>`, or `<BrowserRouter>`.**
|
|
45
|
+
|
|
46
|
+
**Why?** In direct component mode, the parent application already provides routing context. If your widget includes its own Router, React Router will throw: _"You cannot render a `<Router>` inside another `<Router>`"_.
|
|
47
|
+
|
|
48
|
+
**Where routing goes:**
|
|
49
|
+
- ✅ Your **iframe entry point** (`App.tsx` / `main.tsx`) should use `HashRouter`
|
|
50
|
+
- ❌ Your **exported widget** (`PublicComponent.tsx`) should NOT include any Router
|
|
51
|
+
|
|
52
|
+
Widgets are typically single-view components and don't need internal routing. If you need multi-page navigation, use a container instead.
|
|
53
|
+
|
|
54
|
+
### The `useAppContext()` Pattern
|
|
55
|
+
|
|
56
|
+
To write widgets that work identically in both modes, use this abstraction pattern:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// src/hooks/useAppContext.ts
|
|
60
|
+
import { useContext, createContext, useMemo } from 'react';
|
|
61
|
+
import { useSearchParams } from 'react-router-dom';
|
|
62
|
+
|
|
63
|
+
export interface AppContextValue {
|
|
64
|
+
collectionId: string;
|
|
65
|
+
appId: string;
|
|
66
|
+
productId?: string;
|
|
67
|
+
proofId?: string;
|
|
68
|
+
pageId?: string;
|
|
69
|
+
lang?: string;
|
|
70
|
+
user?: { id: string; email: string; name?: string };
|
|
71
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
72
|
+
onNavigate?: (request: any) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const AppContext = createContext<AppContextValue | null>(null);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns app context regardless of rendering mode.
|
|
79
|
+
* - Direct component mode: reads from AppContext (props)
|
|
80
|
+
* - Iframe mode: reads from URL search params
|
|
81
|
+
*/
|
|
82
|
+
export function useAppContext(): AppContextValue {
|
|
83
|
+
const ctx = useContext(AppContext);
|
|
84
|
+
|
|
85
|
+
// If context exists, we're in direct-component mode
|
|
86
|
+
if (ctx) return ctx;
|
|
87
|
+
|
|
88
|
+
// Otherwise, we're in iframe mode — read from URL params
|
|
89
|
+
const [searchParams] = useSearchParams();
|
|
90
|
+
const SL = (window as any).SL ?? require('@proveanything/smartlinks');
|
|
91
|
+
|
|
92
|
+
return useMemo(() => ({
|
|
93
|
+
collectionId: searchParams.get('collectionId') ?? '',
|
|
94
|
+
appId: searchParams.get('appId') ?? '',
|
|
95
|
+
productId: searchParams.get('productId') ?? undefined,
|
|
96
|
+
proofId: searchParams.get('proofId') ?? undefined,
|
|
97
|
+
pageId: searchParams.get('pageId') ?? undefined,
|
|
98
|
+
lang: searchParams.get('lang') ?? undefined,
|
|
99
|
+
SL,
|
|
100
|
+
}), [searchParams, SL]);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Usage in your widget:**
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// src/exports/PublicComponent.tsx
|
|
108
|
+
import { AppContext } from '@/hooks/useAppContext';
|
|
109
|
+
import { WidgetContent } from '@/components/WidgetContent';
|
|
110
|
+
|
|
111
|
+
export const PublicComponent = (props: Record<string, any>) => {
|
|
112
|
+
return (
|
|
113
|
+
<AppContext.Provider value={props}>
|
|
114
|
+
<WidgetContent />
|
|
115
|
+
</AppContext.Provider>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/components/WidgetContent.tsx
|
|
120
|
+
import { useAppContext } from '@/hooks/useAppContext';
|
|
121
|
+
|
|
122
|
+
export function WidgetContent() {
|
|
123
|
+
const { collectionId, productId, SL } = useAppContext();
|
|
124
|
+
// Works in both direct-component and iframe modes!
|
|
125
|
+
return <div>Collection: {collectionId}</div>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
33
131
|
## Widget Props Interface
|
|
34
132
|
|
|
35
133
|
All widgets receive the `SmartLinksWidgetProps` interface:
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Building React Components for SmartLinks
|
|
2
|
+
|
|
3
|
+
> **Read this first** before implementing widgets or containers for the SmartLinks platform.
|
|
4
|
+
|
|
5
|
+
This guide covers the fundamental concepts you need to understand to build React components that work correctly in the SmartLinks ecosystem. For implementation details, see [widgets.md](./widgets.md) and [containers.md](./containers.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Dual-Mode Rendering](#dual-mode-rendering)
|
|
12
|
+
2. [The Router Contract](#the-router-contract)
|
|
13
|
+
3. [The useAppContext Pattern](#the-useappcontext-pattern)
|
|
14
|
+
4. [Common Pitfalls](#common-pitfalls)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Dual-Mode Rendering
|
|
19
|
+
|
|
20
|
+
Every SmartLinks component can run in **two modes**. The platform decides which mode to use based on configuration — your app doesn't choose.
|
|
21
|
+
|
|
22
|
+
| Mode | How It Works | When Used |
|
|
23
|
+
|------|-------------|-----------|
|
|
24
|
+
| **Direct Component** | Your component runs directly in the parent's React context | Default - better performance, shared context |
|
|
25
|
+
| **Iframe** | Your app runs inside an iframe with its own URL | Fallback - full isolation when needed |
|
|
26
|
+
|
|
27
|
+
**Key insight:** You write your component code once, and it must work correctly in **both** modes.
|
|
28
|
+
|
|
29
|
+
### What Changes Between Modes?
|
|
30
|
+
|
|
31
|
+
| Aspect | Direct Component Mode | Iframe Mode |
|
|
32
|
+
|--------|----------------------|-------------|
|
|
33
|
+
| **Context source** | Props passed from parent | URL search parameters |
|
|
34
|
+
| **Router** | Parent provides `MemoryRouter` | Your app provides `HashRouter` |
|
|
35
|
+
| **Styling** | Inherits parent's CSS scope | Fully isolated CSS |
|
|
36
|
+
| **SDK instance** | Shared with parent via props | Own instance from `window.SL` |
|
|
37
|
+
| **Communication** | Direct callbacks (props) | `postMessage` |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The Router Contract
|
|
42
|
+
|
|
43
|
+
This is the **most important rule** to avoid runtime errors:
|
|
44
|
+
|
|
45
|
+
### ❌ The Critical Rule
|
|
46
|
+
|
|
47
|
+
> **Your exported component (`PublicContainer` or `PublicComponent`) must NOT be wrapped in any `<Router>` component.**
|
|
48
|
+
|
|
49
|
+
If you wrap your export in `<MemoryRouter>`, `<HashRouter>`, or `<BrowserRouter>`, React Router will throw:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Error: You cannot render a <Router> inside another <Router>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### ✅ Where Routing Goes
|
|
56
|
+
|
|
57
|
+
**For Widgets (no routing needed):**
|
|
58
|
+
```tsx
|
|
59
|
+
// src/exports/PublicComponent.tsx
|
|
60
|
+
export const PublicComponent = (props) => {
|
|
61
|
+
return <WidgetContent />; // No router needed
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**For Containers (with internal navigation):**
|
|
66
|
+
```tsx
|
|
67
|
+
// src/exports/PublicContainer.tsx
|
|
68
|
+
import { Routes, Route } from 'react-router-dom';
|
|
69
|
+
|
|
70
|
+
export const PublicContainer = (props) => {
|
|
71
|
+
return (
|
|
72
|
+
<Routes> {/* ✅ Routes are fine */}
|
|
73
|
+
<Route path="/" element={<Home />} />
|
|
74
|
+
<Route path="/detail/:id" element={<Detail />} />
|
|
75
|
+
</Routes>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**For Iframe Entry Point:**
|
|
81
|
+
```tsx
|
|
82
|
+
// src/App.tsx (only used in iframe mode)
|
|
83
|
+
import { HashRouter, Routes, Route } from 'react-router-dom';
|
|
84
|
+
|
|
85
|
+
function App() {
|
|
86
|
+
return (
|
|
87
|
+
<HashRouter> {/* ✅ HashRouter only in iframe entry point */}
|
|
88
|
+
<Routes>
|
|
89
|
+
<Route path="/" element={<Home />} />
|
|
90
|
+
</Routes>
|
|
91
|
+
</HashRouter>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Why This Matters
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Direct Component Mode:
|
|
100
|
+
Portal App
|
|
101
|
+
└─ MemoryRouter ← Parent provides this
|
|
102
|
+
└─ Your Component
|
|
103
|
+
└─ [If you add another Router here, it breaks]
|
|
104
|
+
|
|
105
|
+
Iframe Mode:
|
|
106
|
+
<iframe>
|
|
107
|
+
└─ Your App.tsx
|
|
108
|
+
└─ HashRouter ← You provide this
|
|
109
|
+
└─ Your Component
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## The useAppContext Pattern
|
|
115
|
+
|
|
116
|
+
To access context (collectionId, productId, etc.) in a way that works in **both** modes, use this abstraction:
|
|
117
|
+
|
|
118
|
+
### The Hook
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// src/hooks/useAppContext.ts
|
|
122
|
+
import { useContext, createContext, useMemo } from 'react';
|
|
123
|
+
import { useSearchParams } from 'react-router-dom';
|
|
124
|
+
|
|
125
|
+
export interface AppContextValue {
|
|
126
|
+
collectionId: string;
|
|
127
|
+
appId: string;
|
|
128
|
+
productId?: string;
|
|
129
|
+
proofId?: string;
|
|
130
|
+
pageId?: string;
|
|
131
|
+
lang?: string;
|
|
132
|
+
user?: { id: string; email: string; name?: string };
|
|
133
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
134
|
+
onNavigate?: (request: any) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const AppContext = createContext<AppContextValue | null>(null);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns app context regardless of rendering mode.
|
|
141
|
+
* - Direct component mode: reads from React Context (props)
|
|
142
|
+
* - Iframe mode: reads from URL search params
|
|
143
|
+
*/
|
|
144
|
+
export function useAppContext(): AppContextValue {
|
|
145
|
+
const ctx = useContext(AppContext);
|
|
146
|
+
|
|
147
|
+
// Direct-component mode
|
|
148
|
+
if (ctx) return ctx;
|
|
149
|
+
|
|
150
|
+
// Iframe mode — read from URL
|
|
151
|
+
const [searchParams] = useSearchParams();
|
|
152
|
+
const SL = (window as any).SL ?? require('@proveanything/smartlinks');
|
|
153
|
+
|
|
154
|
+
return useMemo(() => ({
|
|
155
|
+
collectionId: searchParams.get('collectionId') ?? '',
|
|
156
|
+
appId: searchParams.get('appId') ?? '',
|
|
157
|
+
productId: searchParams.get('productId') ?? undefined,
|
|
158
|
+
proofId: searchParams.get('proofId') ?? undefined,
|
|
159
|
+
pageId: searchParams.get('pageId') ?? undefined,
|
|
160
|
+
lang: searchParams.get('lang') ?? undefined,
|
|
161
|
+
SL,
|
|
162
|
+
}), [searchParams, SL]);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Wire It Up in Your Export
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// src/exports/PublicContainer.tsx
|
|
170
|
+
import { AppContext } from '@/hooks/useAppContext';
|
|
171
|
+
|
|
172
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
173
|
+
return (
|
|
174
|
+
<AppContext.Provider value={props}>
|
|
175
|
+
<YourComponentTree />
|
|
176
|
+
</AppContext.Provider>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Use It in Your Components
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// Anywhere in your component tree
|
|
185
|
+
import { useAppContext } from '@/hooks/useAppContext';
|
|
186
|
+
|
|
187
|
+
function MyComponent() {
|
|
188
|
+
const { collectionId, productId, SL } = useAppContext();
|
|
189
|
+
// This works in both direct-component and iframe modes!
|
|
190
|
+
|
|
191
|
+
return <div>Collection: {collectionId}</div>;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Why Not Just useSearchParams()?
|
|
196
|
+
|
|
197
|
+
In **direct-component mode**, there are no URL search params — context comes as props. If you use `useSearchParams()` directly, it will return empty values. The `useAppContext()` pattern handles both cases automatically.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Common Pitfalls
|
|
202
|
+
|
|
203
|
+
### ❌ "You cannot render a `<Router>` inside another `<Router>`"
|
|
204
|
+
|
|
205
|
+
**Cause:** Your exported component wraps itself in a Router.
|
|
206
|
+
|
|
207
|
+
**Fix:** Remove the Router wrapper from `PublicContainer.tsx` or `PublicComponent.tsx`. Only your iframe entry point (`App.tsx`) should have a Router.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### ❌ "Invalid hook call" / Minified React error #321
|
|
212
|
+
|
|
213
|
+
**Cause:** Your bundle includes its own copy of React instead of using the parent's shared instance.
|
|
214
|
+
|
|
215
|
+
**Fix:** Ensure your build configuration externalizes React, ReactDOM, and react/jsx-runtime:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// vite.config.container.ts or vite.config.widget.ts
|
|
219
|
+
export default defineConfig({
|
|
220
|
+
build: {
|
|
221
|
+
rollupOptions: {
|
|
222
|
+
external: [
|
|
223
|
+
'react',
|
|
224
|
+
'react-dom',
|
|
225
|
+
'react/jsx-runtime',
|
|
226
|
+
'react-router-dom',
|
|
227
|
+
'@proveanything/smartlinks',
|
|
228
|
+
// ... other shared dependencies
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### ❌ Context values are undefined in direct-component mode
|
|
238
|
+
|
|
239
|
+
**Cause:** Using `useSearchParams()` or reading from `window.location` instead of using the `useAppContext()` pattern.
|
|
240
|
+
|
|
241
|
+
**Fix:** Implement the `useAppContext()` hook as shown above and use it throughout your component tree.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### ❌ Navigation doesn't work
|
|
246
|
+
|
|
247
|
+
**Cause:** Using `window.location` or `window.parent.postMessage` for navigation.
|
|
248
|
+
|
|
249
|
+
**Fix:** Use the `onNavigate` callback from props/context with structured `NavigationRequest` objects. See [widgets.md](./widgets.md#cross-app-navigation) for details.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### ❌ Styles leak between apps
|
|
254
|
+
|
|
255
|
+
**Cause:** Direct components share the parent's CSS scope — there's no iframe isolation.
|
|
256
|
+
|
|
257
|
+
**Fix:** Use CSS Modules, scoped class prefixes, or Tailwind with a unique prefix. Avoid global CSS selectors in your bundle.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### ❌ CORS errors when loading bundles
|
|
262
|
+
|
|
263
|
+
**Cause:** Your widget/container JavaScript or CSS is served from a different origin without CORS headers.
|
|
264
|
+
|
|
265
|
+
**Fix:** Configure CORS headers on your hosting, or use inline bundle source via the SDK's `getWidgets()` API.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Quick Diagnostic Checklist
|
|
270
|
+
|
|
271
|
+
| Issue | Check |
|
|
272
|
+
|-------|-------|
|
|
273
|
+
| Router error | Remove all `<Router>` wrappers from your exported component |
|
|
274
|
+
| Invalid hook call | Verify React is externalized in your build config |
|
|
275
|
+
| Missing context | Use `useAppContext()` instead of `useSearchParams()` |
|
|
276
|
+
| Duplicate React | Check `window.React === yourBundle.React` in browser console |
|
|
277
|
+
| CSS conflicts | Use scoped CSS (modules/prefixes), avoid global selectors |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## File Structure Reference
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
my-app/
|
|
285
|
+
├── src/
|
|
286
|
+
│ ├── App.tsx ← Iframe entry (has HashRouter)
|
|
287
|
+
│ ├── exports/
|
|
288
|
+
│ │ ├── PublicContainer.tsx ← Container export (no Router!)
|
|
289
|
+
│ │ └── PublicComponent.tsx ← Widget export (no Router!)
|
|
290
|
+
│ ├── hooks/
|
|
291
|
+
│ │ └── useAppContext.ts ← Dual-mode abstraction
|
|
292
|
+
│ ├── pages/
|
|
293
|
+
│ │ ├── Home.tsx ← Shared components
|
|
294
|
+
│ │ └── Detail.tsx
|
|
295
|
+
│ └── main.tsx ← Iframe bootstrap
|
|
296
|
+
├── vite.config.ts ← Iframe build
|
|
297
|
+
├── vite.config.container.ts ← Container bundle
|
|
298
|
+
└── vite.config.widget.ts ← Widget bundle
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Next Steps
|
|
304
|
+
|
|
305
|
+
Now that you understand the core concepts, see the implementation guides:
|
|
306
|
+
|
|
307
|
+
- **[widgets.md](./widgets.md)** — Build lightweight preview components
|
|
308
|
+
- **[containers.md](./containers.md)** — Build full-page embedded experiences
|
|
309
|
+
- **[mpa.md](./mpa.md)** — Multi-page app architecture (optional)
|
|
310
|
+
- **[iframe-responder.md](./iframe-responder.md)** — Parent-side iframe integration
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Related Documentation
|
|
315
|
+
|
|
316
|
+
- [theme.system.md](./theme.system.md) — Theming and CSS variables
|
|
317
|
+
- [ai.md](./ai.md) — AI assistant integration
|
|
318
|
+
- [deep-link-discovery.md](./deep-link-discovery.md) — Deep linking patterns
|
|
319
|
+
- [manifests.md](./manifests.md) — App manifest configuration
|
package/docs/containers.md
CHANGED
|
@@ -26,6 +26,190 @@ Imagine a homepage displaying 10 app widgets. If containers were bundled with wi
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## Dual-Mode Rendering
|
|
30
|
+
|
|
31
|
+
Containers can run in **two modes**, and the same container code must work in both:
|
|
32
|
+
|
|
33
|
+
| Mode | How It Works | Who Provides the Router? |
|
|
34
|
+
|------|-------------|--------------------------|
|
|
35
|
+
| **Direct Component** | Container runs directly in the parent's React context | **The framework** — wraps your component in `MemoryRouter` |
|
|
36
|
+
| **Iframe** | Container runs inside an iframe with its own URL | **Your app** — you manage your own `HashRouter` |
|
|
37
|
+
|
|
38
|
+
### The Critical Rule: No Router Wrappers in Your Export
|
|
39
|
+
|
|
40
|
+
> **❌ Your exported `PublicContainer` must NOT be wrapped in a `<Router>`, `<MemoryRouter>`, `<HashRouter>`, or `<BrowserRouter>`.**
|
|
41
|
+
|
|
42
|
+
**Why?** In direct-component mode, the framework already wraps your container in a `MemoryRouter`. If your component includes its own Router, React Router will throw: _"You cannot render a `<Router>` inside another `<Router>`"_.
|
|
43
|
+
|
|
44
|
+
**The routing architecture:**
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Direct Component Mode:
|
|
48
|
+
Portal Shell (HashRouter)
|
|
49
|
+
└─ ContentOrchestrator
|
|
50
|
+
└─ MemoryRouter ← framework provides this
|
|
51
|
+
└─ <YourContainer /> ← only contains <Routes>, no <Router>
|
|
52
|
+
└─ <Route path="/" element={<Home />} />
|
|
53
|
+
└─ <Route path="/detail/:id" element={<Detail />} />
|
|
54
|
+
|
|
55
|
+
Iframe Mode:
|
|
56
|
+
<iframe src="your-app-url">
|
|
57
|
+
└─ <HashRouter> ← your App.tsx provides this
|
|
58
|
+
└─ <Routes>
|
|
59
|
+
└─ <Route path="/" element={<Home />} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Where routing goes:**
|
|
63
|
+
- ✅ Your **iframe entry point** (`App.tsx` / `main.tsx`) should use `HashRouter`
|
|
64
|
+
- ✅ Your **exported container** (`PublicContainer.tsx`) should include `<Routes>` and `<Route>` elements
|
|
65
|
+
- ❌ Your **exported container** should NOT include any `<Router>` wrapper
|
|
66
|
+
|
|
67
|
+
### The Router Contract
|
|
68
|
+
|
|
69
|
+
In direct-component mode, the framework's `MemoryRouter` gives you full React Router capabilities:
|
|
70
|
+
|
|
71
|
+
- ✅ `useNavigate()` works — navigates within your container's routes
|
|
72
|
+
- ✅ `useLocation()` works — returns current location in the MemoryRouter
|
|
73
|
+
- ✅ `useParams()` works — reads params from your route definitions
|
|
74
|
+
- ✅ `<Routes>` / `<Route>` work — define your internal navigation
|
|
75
|
+
- ✅ `useSearchParams()` works — manages search params within the MemoryRouter
|
|
76
|
+
- ⚠️ `window.location` does **NOT** reflect your container's route — it reflects the portal's URL
|
|
77
|
+
|
|
78
|
+
**Example: Correct Container Structure**
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// ❌ WRONG — has Router wrapper
|
|
82
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
83
|
+
return (
|
|
84
|
+
<MemoryRouter> {/* ❌ Don't do this! */}
|
|
85
|
+
<Routes>
|
|
86
|
+
<Route path="/" element={<Home />} />
|
|
87
|
+
</Routes>
|
|
88
|
+
</MemoryRouter>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ✅ CORRECT — no Router wrapper
|
|
93
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
94
|
+
return (
|
|
95
|
+
<AppContext.Provider value={props}>
|
|
96
|
+
<Routes> {/* ✅ Routes are fine, just no Router */}
|
|
97
|
+
<Route path="/" element={<Home />} />
|
|
98
|
+
<Route path="/detail/:id" element={<Detail />} />
|
|
99
|
+
</Routes>
|
|
100
|
+
</AppContext.Provider>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### The `useAppContext()` Pattern
|
|
106
|
+
|
|
107
|
+
To write containers that work identically in both modes, use this abstraction pattern:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// src/hooks/useAppContext.ts
|
|
111
|
+
import { useContext, createContext, useMemo } from 'react';
|
|
112
|
+
import { useSearchParams } from 'react-router-dom';
|
|
113
|
+
|
|
114
|
+
export interface AppContextValue {
|
|
115
|
+
collectionId: string;
|
|
116
|
+
appId: string;
|
|
117
|
+
productId?: string;
|
|
118
|
+
proofId?: string;
|
|
119
|
+
pageId?: string;
|
|
120
|
+
initialPath?: string;
|
|
121
|
+
lang?: string;
|
|
122
|
+
user?: { id: string; email: string; name?: string };
|
|
123
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
124
|
+
onNavigate?: (request: any) => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const AppContext = createContext<AppContextValue | null>(null);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns app context regardless of rendering mode.
|
|
131
|
+
* - Direct component mode: reads from AppContext (props)
|
|
132
|
+
* - Iframe mode: reads from URL search params
|
|
133
|
+
*/
|
|
134
|
+
export function useAppContext(): AppContextValue {
|
|
135
|
+
const ctx = useContext(AppContext);
|
|
136
|
+
|
|
137
|
+
// If context exists, we're in direct-component mode
|
|
138
|
+
if (ctx) return ctx;
|
|
139
|
+
|
|
140
|
+
// Otherwise, we're in iframe mode — read from URL params
|
|
141
|
+
const [searchParams] = useSearchParams();
|
|
142
|
+
const SL = (window as any).SL ?? require('@proveanything/smartlinks');
|
|
143
|
+
|
|
144
|
+
return useMemo(() => ({
|
|
145
|
+
collectionId: searchParams.get('collectionId') ?? '',
|
|
146
|
+
appId: searchParams.get('appId') ?? '',
|
|
147
|
+
productId: searchParams.get('productId') ?? undefined,
|
|
148
|
+
proofId: searchParams.get('proofId') ?? undefined,
|
|
149
|
+
pageId: searchParams.get('pageId') ?? undefined,
|
|
150
|
+
lang: searchParams.get('lang') ?? undefined,
|
|
151
|
+
SL,
|
|
152
|
+
}), [searchParams, SL]);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Usage in your container:**
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// src/exports/PublicContainer.tsx
|
|
160
|
+
import { Routes, Route } from 'react-router-dom';
|
|
161
|
+
import { AppContext } from '@/hooks/useAppContext';
|
|
162
|
+
import { Home } from '@/pages/Home';
|
|
163
|
+
import { Detail } from '@/pages/Detail';
|
|
164
|
+
|
|
165
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
166
|
+
return (
|
|
167
|
+
<AppContext.Provider value={props}>
|
|
168
|
+
<Routes>
|
|
169
|
+
<Route path="/" element={<Home />} />
|
|
170
|
+
<Route path="/detail/:id" element={<Detail />} />
|
|
171
|
+
</Routes>
|
|
172
|
+
</AppContext.Provider>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/pages/Home.tsx
|
|
177
|
+
import { useAppContext } from '@/hooks/useAppContext';
|
|
178
|
+
import { useNavigate } from 'react-router-dom';
|
|
179
|
+
|
|
180
|
+
export function Home() {
|
|
181
|
+
const { collectionId, productId, SL } = useAppContext();
|
|
182
|
+
const navigate = useNavigate();
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div>
|
|
186
|
+
<h1>Collection: {collectionId}</h1>
|
|
187
|
+
<button onClick={() => navigate('/detail/123')}>
|
|
188
|
+
View Detail
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Deep Linking
|
|
196
|
+
|
|
197
|
+
The framework sets the `MemoryRouter`'s initial route based on the `pageId` or `initialPath` prop. For example, if the portal navigates to your container with `pageId: 'settings'`, your router will start at `/settings`.
|
|
198
|
+
|
|
199
|
+
To advertise deep-linkable pages, declare them in your `app.manifest.json`:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"linkable": [
|
|
204
|
+
{ "title": "Home", "path": "/" },
|
|
205
|
+
{ "title": "Settings", "path": "/settings" },
|
|
206
|
+
{ "title": "Item Detail", "path": "/item/:itemId" }
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
29
213
|
## Container Props
|
|
30
214
|
|
|
31
215
|
Container props extend the standard `SmartLinksWidgetProps` with an additional `className` prop:
|
package/docs/overview.md
CHANGED
|
@@ -45,6 +45,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
45
45
|
| Topic | File | When to Use |
|
|
46
46
|
|-------|------|-------------|
|
|
47
47
|
| **API Reference** | `docs/API_SUMMARY.md` | Complete SDK function reference, types, error handling |
|
|
48
|
+
| **Building React Components** | `docs/building-react-components.md` | **READ THIS FIRST** — Dual-mode rendering, router rules, useAppContext pattern |
|
|
48
49
|
| **Multi-Page Architecture** | `docs/mpa.md` | Build pipeline, entry points, multi-page setup, content hashing |
|
|
49
50
|
| **AI & Chat** | `docs/ai.md` | Chat completions, RAG, streaming, tool calling, voice, podcasts, TTS |
|
|
50
51
|
| **Analytics** | `docs/analytics.md` | Fire-and-forget page/click/tag analytics plus admin dashboard queries |
|
package/docs/widgets.md
CHANGED
|
@@ -30,6 +30,104 @@ Widgets are self-contained React components that:
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## Dual-Mode Rendering
|
|
34
|
+
|
|
35
|
+
Widgets can run in **two modes**, and the same widget code must work in both:
|
|
36
|
+
|
|
37
|
+
| Mode | How It Works | When Used |
|
|
38
|
+
|------|-------------|-----------|
|
|
39
|
+
| **Direct Component** | Widget runs directly in the parent's React context | Default - when loaded as ESM/UMD in the parent app |
|
|
40
|
+
| **Iframe** | Widget runs inside an iframe with its own URL | Fallback or when full isolation is needed |
|
|
41
|
+
|
|
42
|
+
### The Critical Rule: No Router Wrappers
|
|
43
|
+
|
|
44
|
+
> **❌ Your widget component must NOT be wrapped in a `<Router>`, `<MemoryRouter>`, `<HashRouter>`, or `<BrowserRouter>`.**
|
|
45
|
+
|
|
46
|
+
**Why?** In direct component mode, the parent application already provides routing context. If your widget includes its own Router, React Router will throw: _"You cannot render a `<Router>` inside another `<Router>`"_.
|
|
47
|
+
|
|
48
|
+
**Where routing goes:**
|
|
49
|
+
- ✅ Your **iframe entry point** (`App.tsx` / `main.tsx`) should use `HashRouter`
|
|
50
|
+
- ❌ Your **exported widget** (`PublicComponent.tsx`) should NOT include any Router
|
|
51
|
+
|
|
52
|
+
Widgets are typically single-view components and don't need internal routing. If you need multi-page navigation, use a container instead.
|
|
53
|
+
|
|
54
|
+
### The `useAppContext()` Pattern
|
|
55
|
+
|
|
56
|
+
To write widgets that work identically in both modes, use this abstraction pattern:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// src/hooks/useAppContext.ts
|
|
60
|
+
import { useContext, createContext, useMemo } from 'react';
|
|
61
|
+
import { useSearchParams } from 'react-router-dom';
|
|
62
|
+
|
|
63
|
+
export interface AppContextValue {
|
|
64
|
+
collectionId: string;
|
|
65
|
+
appId: string;
|
|
66
|
+
productId?: string;
|
|
67
|
+
proofId?: string;
|
|
68
|
+
pageId?: string;
|
|
69
|
+
lang?: string;
|
|
70
|
+
user?: { id: string; email: string; name?: string };
|
|
71
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
72
|
+
onNavigate?: (request: any) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const AppContext = createContext<AppContextValue | null>(null);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns app context regardless of rendering mode.
|
|
79
|
+
* - Direct component mode: reads from AppContext (props)
|
|
80
|
+
* - Iframe mode: reads from URL search params
|
|
81
|
+
*/
|
|
82
|
+
export function useAppContext(): AppContextValue {
|
|
83
|
+
const ctx = useContext(AppContext);
|
|
84
|
+
|
|
85
|
+
// If context exists, we're in direct-component mode
|
|
86
|
+
if (ctx) return ctx;
|
|
87
|
+
|
|
88
|
+
// Otherwise, we're in iframe mode — read from URL params
|
|
89
|
+
const [searchParams] = useSearchParams();
|
|
90
|
+
const SL = (window as any).SL ?? require('@proveanything/smartlinks');
|
|
91
|
+
|
|
92
|
+
return useMemo(() => ({
|
|
93
|
+
collectionId: searchParams.get('collectionId') ?? '',
|
|
94
|
+
appId: searchParams.get('appId') ?? '',
|
|
95
|
+
productId: searchParams.get('productId') ?? undefined,
|
|
96
|
+
proofId: searchParams.get('proofId') ?? undefined,
|
|
97
|
+
pageId: searchParams.get('pageId') ?? undefined,
|
|
98
|
+
lang: searchParams.get('lang') ?? undefined,
|
|
99
|
+
SL,
|
|
100
|
+
}), [searchParams, SL]);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Usage in your widget:**
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// src/exports/PublicComponent.tsx
|
|
108
|
+
import { AppContext } from '@/hooks/useAppContext';
|
|
109
|
+
import { WidgetContent } from '@/components/WidgetContent';
|
|
110
|
+
|
|
111
|
+
export const PublicComponent = (props: Record<string, any>) => {
|
|
112
|
+
return (
|
|
113
|
+
<AppContext.Provider value={props}>
|
|
114
|
+
<WidgetContent />
|
|
115
|
+
</AppContext.Provider>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/components/WidgetContent.tsx
|
|
120
|
+
import { useAppContext } from '@/hooks/useAppContext';
|
|
121
|
+
|
|
122
|
+
export function WidgetContent() {
|
|
123
|
+
const { collectionId, productId, SL } = useAppContext();
|
|
124
|
+
// Works in both direct-component and iframe modes!
|
|
125
|
+
return <div>Collection: {collectionId}</div>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
33
131
|
## Widget Props Interface
|
|
34
132
|
|
|
35
133
|
All widgets receive the `SmartLinksWidgetProps` interface:
|