Skip to content

Commit 7e5e5fb

Browse files
authored
feat: add iframe embed API for go-web (#16)
Implements a pure-JS embed API (no React dependency) following rscexplorer's pattern: - src/embed.ts: Public mount() function, creates iframe + postMessage - example/embed.html & embed-entry.tsx: iframe React app - example-iframe-embed/: test page with dynamic example switching - rsbuild config: CORS proxy, multi-entry build, port 5969 - README: document iframe embed usage and options
1 parent 9bf912c commit 7e5e5fb

File tree

7 files changed

+665
-3
lines changed

7 files changed

+665
-3
lines changed

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,48 @@ const html = generateSSGHTML({
7676

7777
The `./ssg` export uses Node.js `fs`/`path` and must not be bundled into browser code.
7878

79+
### Iframe Embed (no React required)
80+
81+
For non-React sites (Hugo, Jekyll, plain HTML, etc.), use the iframe embed API. The host page only loads a tiny JS file — React runs inside the iframe.
82+
83+
```html
84+
<div id="demo" style="height: 500px;"></div>
85+
<script type="module">
86+
import { mount } from 'https://go.lynxjs.org/embed.js';
87+
88+
const embed = mount('#demo', {
89+
example: 'hello-world',
90+
defaultFile: 'src/App.tsx',
91+
});
92+
93+
// Switch example dynamically:
94+
// embed.update({ example: 'css' });
95+
96+
// Clean up:
97+
// embed.destroy();
98+
</script>
99+
```
100+
101+
Options:
102+
103+
| Option | Type | Description |
104+
|--------|------|-------------|
105+
| `example` | `string` | **Required.** Example folder name |
106+
| `defaultFile` | `string` | Initial file to display (default: `'src/App.tsx'`) |
107+
| `defaultTab` | `'preview' \| 'web' \| 'qrcode'` | Default preview tab |
108+
| `exampleBasePath` | `string` | Base path or full URL for example data |
109+
| `img` | `string` | Static preview image URL |
110+
| `defaultEntryFile` | `string` | Default entry file for web preview |
111+
| `highlight` | `string` | Line highlight spec, e.g. `'{1,3-5}'` |
112+
| `entry` | `string \| string[]` | Filter entry files in tree |
113+
79114
## Development
80115

81116
```bash
82117
pnpm dev
83118
```
84119

85-
This starts the standalone example app at `localhost:3000`.
120+
This starts the standalone example app at `localhost:5969`.
86121

87122
### Lynx examples
88123

example-iframe-embed/index.html

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Go Web — Iframe Embed Test</title>
7+
<style>
8+
* { box-sizing: border-box; margin: 0; padding: 0; }
9+
10+
body {
11+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
12+
background: #f0f0f0;
13+
color: #333;
14+
padding: 40px;
15+
}
16+
17+
h1 {
18+
font-size: 24px;
19+
margin-bottom: 8px;
20+
}
21+
22+
p {
23+
color: #666;
24+
margin-bottom: 24px;
25+
line-height: 1.5;
26+
}
27+
28+
.demo-container {
29+
height: 500px;
30+
border-radius: 12px;
31+
overflow: hidden;
32+
box-shadow: 0 4px 24px rgba(0,0,0,0.12);
33+
margin-bottom: 32px;
34+
}
35+
36+
.controls {
37+
margin-bottom: 24px;
38+
display: flex;
39+
gap: 12px;
40+
flex-wrap: wrap;
41+
}
42+
43+
button {
44+
padding: 8px 16px;
45+
border-radius: 6px;
46+
border: 1px solid #ccc;
47+
background: #fff;
48+
cursor: pointer;
49+
font-size: 14px;
50+
}
51+
52+
button:hover { background: #f5f5f5; }
53+
button.active { background: #0071e3; color: #fff; border-color: #0071e3; }
54+
55+
code {
56+
background: #e8e8e8;
57+
padding: 2px 6px;
58+
border-radius: 4px;
59+
font-size: 13px;
60+
}
61+
62+
.code-block {
63+
background: #1e1e1e;
64+
color: #d4d4d4;
65+
padding: 16px 20px;
66+
border-radius: 8px;
67+
font-family: 'SF Mono', 'Fira Code', Menlo, monospace;
68+
font-size: 13px;
69+
line-height: 1.6;
70+
overflow-x: auto;
71+
margin-bottom: 32px;
72+
white-space: pre;
73+
}
74+
75+
h2 {
76+
font-size: 18px;
77+
margin-bottom: 12px;
78+
}
79+
80+
.info {
81+
background: #e3f2fd;
82+
border: 1px solid #90caf9;
83+
border-radius: 8px;
84+
padding: 12px 16px;
85+
margin-bottom: 24px;
86+
font-size: 14px;
87+
line-height: 1.5;
88+
}
89+
90+
</style>
91+
</head>
92+
<body>
93+
<h1>Iframe Embed API Test</h1>
94+
<p>
95+
This page demonstrates the <code>@lynx-js/go-web</code> iframe embed API.
96+
It loads the Go component inside an iframe via a pure JS <code>mount()</code> call —
97+
no React dependency needed on the host page.
98+
</p>
99+
100+
<div class="info">
101+
<strong>How to test:</strong><br/>
102+
1. Start dev server: <code>cd example && pnpm dev</code> (serves on <strong>localhost:5969</strong>)<br/>
103+
2. Open this file in a browser (or <code>npx serve example-iframe-embed</code>)<br/>
104+
Example data is proxied from <strong>go.lynxjs.org</strong> via <code>/proxy-lynx-examples</code>.
105+
</div>
106+
107+
<h2>Usage</h2>
108+
<div class="code-block">&lt;div id="demo" style="height: 500px"&gt;&lt;/div&gt;
109+
&lt;script type="module"&gt;
110+
import { mount } from 'https://go.lynxjs.org/embed.js';
111+
mount('#demo', {
112+
example: 'hello-world',
113+
defaultFile: 'src/App.tsx',
114+
exampleBasePath: 'https://go.lynxjs.org/lynx-examples',
115+
});
116+
&lt;/script&gt;</div>
117+
118+
<h2>Live Demo</h2>
119+
120+
<div class="controls" id="controls"></div>
121+
122+
<div class="demo-container" id="demo"></div>
123+
124+
<script type="module">
125+
// ---------------------------------------------------------------------------
126+
// Config
127+
// ---------------------------------------------------------------------------
128+
129+
// The embed React app runs on localhost (cd example && pnpm dev).
130+
// Example data is proxied through the dev server to avoid CORS issues.
131+
// The proxy rewrites /proxy-lynx-examples/* → go.lynxjs.org/lynx-examples/*
132+
const EMBED_BASE = 'http://localhost:5969';
133+
const EXAMPLE_BASE_PATH = '/proxy-lynx-examples';
134+
135+
// ---------------------------------------------------------------------------
136+
// Inline mount() — mimics src/embed.ts for local testing
137+
// In production you'd: import { mount } from 'https://go.lynxjs.org/embed.js'
138+
// ---------------------------------------------------------------------------
139+
140+
function mount(container, options) {
141+
const el = typeof container === 'string'
142+
? document.querySelector(container)
143+
: container;
144+
145+
if (!el) throw new Error('Container not found: ' + container);
146+
147+
const embedUrl = new URL(EMBED_BASE + '/embed');
148+
149+
const iframe = document.createElement('iframe');
150+
iframe.src = embedUrl.href;
151+
iframe.style.cssText = 'width: 100%; height: 100%; border: none;';
152+
153+
let currentOptions = { ...options };
154+
155+
function handleMessage(event) {
156+
if (event.source !== iframe.contentWindow) return;
157+
if (event.data?.type === 'go-embed:ready') {
158+
iframe.contentWindow.postMessage({
159+
type: 'go-embed:init',
160+
options: currentOptions,
161+
}, '*');
162+
}
163+
}
164+
165+
window.addEventListener('message', handleMessage);
166+
el.innerHTML = '';
167+
el.appendChild(iframe);
168+
169+
return {
170+
iframe,
171+
update(newOptions) {
172+
currentOptions = { ...currentOptions, ...newOptions };
173+
iframe.contentWindow.postMessage({
174+
type: 'go-embed:update',
175+
options: newOptions,
176+
}, '*');
177+
},
178+
destroy() {
179+
window.removeEventListener('message', handleMessage);
180+
el.innerHTML = '';
181+
},
182+
};
183+
}
184+
185+
// ---------------------------------------------------------------------------
186+
// Fetch available examples from production
187+
// ---------------------------------------------------------------------------
188+
189+
async function fetchExampleList() {
190+
// Try a few known examples; the production site doesn't expose a directory listing.
191+
// Requests go through the dev server proxy to avoid CORS.
192+
const knownExamples = [
193+
'hello-world', 'text', 'view', 'image', 'css',
194+
'scroll-view', 'list', 'navigator',
195+
];
196+
197+
const available = [];
198+
await Promise.all(
199+
knownExamples.map(async (name) => {
200+
try {
201+
const res = await fetch(`${EMBED_BASE}${EXAMPLE_BASE_PATH}/${name}/example-metadata.json`, { method: 'HEAD' });
202+
if (res.ok) available.push(name);
203+
} catch { /* skip */ }
204+
}),
205+
);
206+
207+
// Preserve original order
208+
return knownExamples.filter((n) => available.includes(n));
209+
}
210+
211+
// ---------------------------------------------------------------------------
212+
// Mount demos
213+
// ---------------------------------------------------------------------------
214+
215+
const examples = await fetchExampleList();
216+
if (examples.length === 0) {
217+
examples.push('hello-world'); // fallback
218+
}
219+
220+
const EXAMPLES = examples.map((name) => ({
221+
name,
222+
file: 'src/App.tsx',
223+
}));
224+
225+
// Mount main demo
226+
let embed = mount('#demo', {
227+
example: EXAMPLES[0].name,
228+
defaultFile: EXAMPLES[0].file,
229+
defaultTab: 'web',
230+
exampleBasePath: EXAMPLE_BASE_PATH,
231+
});
232+
233+
// Create example switcher buttons
234+
const controls = document.getElementById('controls');
235+
EXAMPLES.forEach((ex, i) => {
236+
const btn = document.createElement('button');
237+
btn.textContent = ex.name;
238+
if (i === 0) btn.classList.add('active');
239+
btn.addEventListener('click', () => {
240+
controls.querySelectorAll('button').forEach(b => b.classList.remove('active'));
241+
btn.classList.add('active');
242+
embed.destroy();
243+
embed = mount('#demo', {
244+
example: ex.name,
245+
defaultFile: ex.file,
246+
defaultTab: 'web',
247+
exampleBasePath: EXAMPLE_BASE_PATH,
248+
});
249+
});
250+
controls.appendChild(btn);
251+
});
252+
253+
</script>
254+
</body>
255+
</html>

example/embed.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Go Web Embed</title>
7+
<style>
8+
* { box-sizing: border-box; }
9+
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }
10+
body {
11+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
12+
}
13+
#embed-root {
14+
display: flex;
15+
flex-direction: column;
16+
height: 100%;
17+
overflow: hidden;
18+
}
19+
</style>
20+
</head>
21+
<body>
22+
<div id="embed-root"></div>
23+
<script type="module" src="./src/embed-entry.tsx"></script>
24+
</body>
25+
</html>

example/rsbuild.config.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,28 @@ for (const name of exampleNames) {
4444
export default defineConfig({
4545
plugins: [pluginReact(), pluginSass()],
4646

47+
server: {
48+
port: 5969,
49+
proxy: {
50+
// Proxy requests to production examples when local examples are not available.
51+
// This avoids CORS issues when testing the embed with go.lynxjs.org data.
52+
'/proxy-lynx-examples': {
53+
target: 'https://go.lynxjs.org',
54+
pathRewrite: { '^/proxy-lynx-examples': '/lynx-examples' },
55+
changeOrigin: true,
56+
},
57+
},
58+
},
59+
4760
html: {
48-
template: './index.html',
61+
template: ({ entryName }) =>
62+
entryName === 'embed' ? './embed.html' : './index.html',
4963
},
5064

5165
source: {
5266
entry: {
5367
index: './src/main.tsx',
68+
embed: './src/embed-entry.tsx',
5469
},
5570
define: {
5671
// Inject the example list as a build-time constant

0 commit comments

Comments
 (0)