@proveanything/smartlinks 1.8.7 → 1.8.10

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.8.7 | Generated: 2026-03-18T08:55:08.708Z
3
+ Version: 1.8.10 | Generated: 2026-03-19T15:24:09.009Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -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
@@ -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:
@@ -118,15 +302,15 @@ See `widgets.md` for the full `NavigationRequest` documentation and additional e
118
302
 
119
303
  ## Architecture
120
304
 
121
- Containers use **MemoryRouter** (not HashRouter) because the parent app owns the browser's URL bar. Context is passed via props rather than URL parameters. Each container gets its own `QueryClient` to avoid cache collisions with the parent app.
305
+ The framework wraps containers in a **MemoryRouter** (not HashRouter) because the parent app owns the browser's URL bar. Context is passed via props rather than URL parameters. Each container gets its own `QueryClient` to avoid cache collisions with the parent app.
122
306
 
123
307
  ```
124
308
  Parent App (owns URL bar, provides globals)
125
- └── <PublicContainer> Container component
126
- ├── QueryClientProvider (isolated)
127
- ├── MemoryRouter (internal routing)
128
- ├── LanguageProvider
129
- └── PublicPage (+ all sub-routes)
309
+ └── MemoryRouter Framework provides this
310
+ └── <PublicContainer> ← Container component (no Router!)
311
+ ├── QueryClientProvider (isolated)
312
+ ├── LanguageProvider
313
+ └── Routes/Route (internal navigation)
130
314
  ```
131
315
 
132
316
  ---
@@ -233,7 +417,7 @@ src/containers/
233
417
  1. Create your container component in `src/containers/MyContainer.tsx`
234
418
  2. Export it from `src/containers/index.ts`
235
419
  3. Add it to the `CONTAINER_MANIFEST` in `src/containers/index.ts`
236
- 4. Ensure it uses `MemoryRouter` (not HashRouter)
420
+ 4. Ensure it does NOT include a Router wrapper (framework provides `MemoryRouter`)
237
421
  5. Give it its own `QueryClient` to avoid cache collisions
238
422
 
239
423
  ---
@@ -258,7 +442,7 @@ src/containers/
258
442
  | -------------------------- | ---------------------------------------- | ---------------------------------------------------- |
259
443
  | Container doesn't render | Missing shared globals | Ensure all Shared Dependencies are on `window` |
260
444
  | Styles don't apply | Missing `containers.css` | Load the CSS file alongside the JS bundle |
261
- | Routing doesn't work | Using HashRouter instead of MemoryRouter | Containers must use MemoryRouter |
445
+ | Routing doesn't work | Container includes a Router wrapper | Remove all Router wrappers (framework provides MemoryRouter) |
262
446
  | Query cache conflicts | Sharing parent's QueryClient | Each container needs its own `QueryClient` instance |
263
447
  | `cva.cva` runtime error | Global set to lowercase `cva` | Use uppercase `CVA` for the global name |
264
448
  | Navigation does nothing | Using legacy string with `onNavigate` | Use structured `NavigationRequest` object instead |
@@ -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 |
@@ -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:
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.8.7 | Generated: 2026-03-18T08:55:08.708Z
3
+ Version: 1.8.10 | Generated: 2026-03-19T15:24:09.009Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -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
@@ -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:
@@ -118,15 +302,15 @@ See `widgets.md` for the full `NavigationRequest` documentation and additional e
118
302
 
119
303
  ## Architecture
120
304
 
121
- Containers use **MemoryRouter** (not HashRouter) because the parent app owns the browser's URL bar. Context is passed via props rather than URL parameters. Each container gets its own `QueryClient` to avoid cache collisions with the parent app.
305
+ The framework wraps containers in a **MemoryRouter** (not HashRouter) because the parent app owns the browser's URL bar. Context is passed via props rather than URL parameters. Each container gets its own `QueryClient` to avoid cache collisions with the parent app.
122
306
 
123
307
  ```
124
308
  Parent App (owns URL bar, provides globals)
125
- └── <PublicContainer> Container component
126
- ├── QueryClientProvider (isolated)
127
- ├── MemoryRouter (internal routing)
128
- ├── LanguageProvider
129
- └── PublicPage (+ all sub-routes)
309
+ └── MemoryRouter Framework provides this
310
+ └── <PublicContainer> ← Container component (no Router!)
311
+ ├── QueryClientProvider (isolated)
312
+ ├── LanguageProvider
313
+ └── Routes/Route (internal navigation)
130
314
  ```
131
315
 
132
316
  ---
@@ -233,7 +417,7 @@ src/containers/
233
417
  1. Create your container component in `src/containers/MyContainer.tsx`
234
418
  2. Export it from `src/containers/index.ts`
235
419
  3. Add it to the `CONTAINER_MANIFEST` in `src/containers/index.ts`
236
- 4. Ensure it uses `MemoryRouter` (not HashRouter)
420
+ 4. Ensure it does NOT include a Router wrapper (framework provides `MemoryRouter`)
237
421
  5. Give it its own `QueryClient` to avoid cache collisions
238
422
 
239
423
  ---
@@ -258,7 +442,7 @@ src/containers/
258
442
  | -------------------------- | ---------------------------------------- | ---------------------------------------------------- |
259
443
  | Container doesn't render | Missing shared globals | Ensure all Shared Dependencies are on `window` |
260
444
  | Styles don't apply | Missing `containers.css` | Load the CSS file alongside the JS bundle |
261
- | Routing doesn't work | Using HashRouter instead of MemoryRouter | Containers must use MemoryRouter |
445
+ | Routing doesn't work | Container includes a Router wrapper | Remove all Router wrappers (framework provides MemoryRouter) |
262
446
  | Query cache conflicts | Sharing parent's QueryClient | Each container needs its own `QueryClient` instance |
263
447
  | `cva.cva` runtime error | Global set to lowercase `cva` | Use uppercase `CVA` for the global name |
264
448
  | Navigation does nothing | Using legacy string with `onNavigate` | Use structured `NavigationRequest` object instead |
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.8.7",
3
+ "version": "1.8.10",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",