@farcaster/snap-hono 1.1.8 → 1.2.1
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/fallback.d.ts +5 -0
- package/dist/fallback.js +66 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +107 -47
- package/dist/og-image.d.ts +46 -0
- package/dist/og-image.js +628 -0
- package/dist/payloadToResponse.d.ts +2 -2
- package/dist/payloadToResponse.js +3 -3
- package/dist/renderSnapPage.d.ts +19 -2
- package/dist/renderSnapPage.js +98 -46
- package/package.json +4 -2
- package/src/fallback.ts +81 -0
- package/src/index.ts +142 -49
- package/src/og-image.ts +878 -0
- package/src/payloadToResponse.ts +6 -6
- package/src/renderSnapPage.ts +225 -105
package/src/payloadToResponse.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MEDIA_TYPE,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
type SnapHandlerResult,
|
|
4
|
+
validateSnapResponse,
|
|
5
|
+
snapResponseSchema,
|
|
6
6
|
} from "@farcaster/snap";
|
|
7
7
|
|
|
8
8
|
type PayloadToResponseOptions = {
|
|
@@ -13,13 +13,13 @@ type PayloadToResponseOptions = {
|
|
|
13
13
|
const DEFAULT_LINK_MEDIA_TYPES = [MEDIA_TYPE, "text/html"] as const;
|
|
14
14
|
|
|
15
15
|
export function payloadToResponse(
|
|
16
|
-
payload:
|
|
16
|
+
payload: SnapHandlerResult,
|
|
17
17
|
options: Partial<PayloadToResponseOptions> = {},
|
|
18
18
|
): Response {
|
|
19
19
|
const resourcePath = options.resourcePath ?? "/";
|
|
20
20
|
const mediaTypes = options.mediaTypes ?? [...DEFAULT_LINK_MEDIA_TYPES];
|
|
21
21
|
|
|
22
|
-
const validation =
|
|
22
|
+
const validation = validateSnapResponse(payload);
|
|
23
23
|
if (!validation.valid) {
|
|
24
24
|
return new Response(
|
|
25
25
|
JSON.stringify({
|
|
@@ -35,7 +35,7 @@ export function payloadToResponse(
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const finalized =
|
|
38
|
+
const finalized = snapResponseSchema.parse(payload);
|
|
39
39
|
return new Response(JSON.stringify(finalized), {
|
|
40
40
|
status: 200,
|
|
41
41
|
headers: {
|
package/src/renderSnapPage.ts
CHANGED
|
@@ -1,16 +1,115 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type {
|
|
2
|
+
SnapPageElementInput,
|
|
3
|
+
SnapHandlerResult,
|
|
4
|
+
PaletteColor,
|
|
5
|
+
} from "@farcaster/snap";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_THEME_ACCENT,
|
|
8
|
+
PALETTE_LIGHT_HEX,
|
|
9
|
+
PALETTE_COLOR_ACCENT,
|
|
10
|
+
} from "@farcaster/snap";
|
|
11
|
+
|
|
12
|
+
type SnapPage = SnapHandlerResult["page"];
|
|
13
|
+
type SnapPageButton = NonNullable<SnapPage["buttons"]>[number];
|
|
14
|
+
|
|
15
|
+
// ─── OG meta ────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export type RenderSnapPageOptions = {
|
|
18
|
+
/** Absolute URL of the /~/og-image PNG route. */
|
|
19
|
+
ogImageUrl?: string;
|
|
20
|
+
/** Canonical pathname + search of the snap page (e.g. "/snap" or "/"). */
|
|
21
|
+
resourcePath?: string;
|
|
22
|
+
/** Optional og:site_name value (e.g. from SNAP_OG_SITE_NAME env). */
|
|
23
|
+
siteName?: string;
|
|
12
24
|
};
|
|
13
25
|
|
|
26
|
+
type PageMeta = {
|
|
27
|
+
title: string;
|
|
28
|
+
description: string;
|
|
29
|
+
imageUrl?: string;
|
|
30
|
+
imageAlt?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function extractPageMeta(page: SnapPage): PageMeta {
|
|
34
|
+
let title = "Farcaster Snap";
|
|
35
|
+
let description = "";
|
|
36
|
+
let imageUrl: string | undefined;
|
|
37
|
+
let imageAlt: string | undefined;
|
|
38
|
+
|
|
39
|
+
for (const el of page.elements.children) {
|
|
40
|
+
if (el.type === "text") {
|
|
41
|
+
const style = el.style;
|
|
42
|
+
const content = el.content;
|
|
43
|
+
if (style === "title" && title === "Farcaster Snap" && content) {
|
|
44
|
+
title = content;
|
|
45
|
+
} else if (
|
|
46
|
+
(style === "body" || style === "caption") &&
|
|
47
|
+
!description &&
|
|
48
|
+
content
|
|
49
|
+
) {
|
|
50
|
+
description = content;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (el.type === "image" && !imageUrl) {
|
|
54
|
+
imageUrl = el.url;
|
|
55
|
+
imageAlt = el.alt;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
title,
|
|
61
|
+
description: description || title,
|
|
62
|
+
imageUrl,
|
|
63
|
+
imageAlt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildOgMeta(opts: {
|
|
68
|
+
title: string;
|
|
69
|
+
description: string;
|
|
70
|
+
pageUrl: string;
|
|
71
|
+
ogImageUrl?: string;
|
|
72
|
+
imageAlt?: string;
|
|
73
|
+
siteName?: string;
|
|
74
|
+
}): string {
|
|
75
|
+
const { title, description, pageUrl, ogImageUrl, imageAlt, siteName } = opts;
|
|
76
|
+
|
|
77
|
+
const imgUrl = ogImageUrl ?? undefined;
|
|
78
|
+
const twitterCard = imgUrl ? "summary_large_image" : "summary";
|
|
79
|
+
|
|
80
|
+
const lines = [
|
|
81
|
+
`<meta name="description" content="${esc(description)}">`,
|
|
82
|
+
`<meta property="og:title" content="${esc(title)}">`,
|
|
83
|
+
`<meta property="og:description" content="${esc(description)}">`,
|
|
84
|
+
`<meta property="og:url" content="${esc(pageUrl)}">`,
|
|
85
|
+
`<meta property="og:type" content="website">`,
|
|
86
|
+
`<meta property="og:locale" content="en_US">`,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
if (siteName) {
|
|
90
|
+
lines.push(`<meta property="og:site_name" content="${esc(siteName)}">`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (imgUrl) {
|
|
94
|
+
lines.push(`<meta property="og:image" content="${esc(imgUrl)}">`);
|
|
95
|
+
lines.push(
|
|
96
|
+
`<meta property="og:image:alt" content="${esc(imageAlt ?? title)}">`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
lines.push(
|
|
101
|
+
`<meta name="twitter:card" content="${twitterCard}">`,
|
|
102
|
+
`<meta name="twitter:title" content="${esc(title)}">`,
|
|
103
|
+
`<meta name="twitter:description" content="${esc(description)}">`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (imgUrl) {
|
|
107
|
+
lines.push(`<meta name="twitter:image" content="${esc(imgUrl)}">`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
14
113
|
const FC_ICON = `<svg viewBox="0 0 520 457" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M519.801 0V61.6809H458.172V123.31H477.054V123.331H519.801V456.795H416.57L416.507 456.49L363.832 207.03C358.81 183.251 345.667 161.736 326.827 146.434C307.988 131.133 284.255 122.71 260.006 122.71H259.8C235.551 122.71 211.818 131.133 192.979 146.434C174.139 161.736 160.996 183.259 155.974 207.03L103.239 456.795H0V123.323H42.7471V123.31H61.6262V61.6809H0V0H519.801Z" fill="currentColor"/></svg>`;
|
|
15
114
|
|
|
16
115
|
function esc(s: string): string {
|
|
@@ -21,20 +120,24 @@ function esc(s: string): string {
|
|
|
21
120
|
.replace(/"/g, """);
|
|
22
121
|
}
|
|
23
122
|
|
|
24
|
-
function accentHex(accent:
|
|
25
|
-
return
|
|
123
|
+
function accentHex(accent: PaletteColor | undefined): string {
|
|
124
|
+
return accent && PALETTE_LIGHT_HEX[accent]
|
|
125
|
+
? PALETTE_LIGHT_HEX[accent]
|
|
126
|
+
: PALETTE_LIGHT_HEX[DEFAULT_THEME_ACCENT];
|
|
26
127
|
}
|
|
27
128
|
|
|
28
|
-
function colorHex(
|
|
29
|
-
|
|
30
|
-
|
|
129
|
+
function colorHex(
|
|
130
|
+
color: PaletteColor | typeof PALETTE_COLOR_ACCENT | undefined,
|
|
131
|
+
accent: string,
|
|
132
|
+
): string {
|
|
133
|
+
if (!color || color === PALETTE_COLOR_ACCENT) return accent;
|
|
134
|
+
return PALETTE_LIGHT_HEX[color] ?? accent;
|
|
31
135
|
}
|
|
32
136
|
|
|
33
137
|
// ─── Element renderers ──────────────────────────────────
|
|
34
138
|
|
|
35
|
-
function renderElement(el:
|
|
36
|
-
|
|
37
|
-
switch (type) {
|
|
139
|
+
function renderElement(el: SnapPageElementInput, accent: string): string {
|
|
140
|
+
switch (el.type) {
|
|
38
141
|
case "text":
|
|
39
142
|
return renderText(el, accent);
|
|
40
143
|
case "image":
|
|
@@ -59,25 +162,20 @@ function renderElement(el: Record<string, unknown>, accent: string): string {
|
|
|
59
162
|
return renderGroup(el, accent);
|
|
60
163
|
case "divider":
|
|
61
164
|
return `<hr style="border:none;border-top:1px solid #E5E7EB;margin:4px 0">`;
|
|
62
|
-
case "spacer":
|
|
63
|
-
|
|
64
|
-
small: "8px",
|
|
65
|
-
medium: "16px",
|
|
66
|
-
large: "24px",
|
|
67
|
-
};
|
|
68
|
-
return `<div style="height:${
|
|
69
|
-
sizes[(el.size as string) ?? "medium"] ?? "16px"
|
|
70
|
-
}"></div>`;
|
|
71
|
-
}
|
|
165
|
+
case "spacer":
|
|
166
|
+
return renderSpacer(el);
|
|
72
167
|
default:
|
|
73
168
|
return "";
|
|
74
169
|
}
|
|
75
170
|
}
|
|
76
171
|
|
|
77
|
-
function renderText(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
172
|
+
function renderText(
|
|
173
|
+
el: Extract<SnapPageElementInput, { type: "text" }>,
|
|
174
|
+
_accent: string,
|
|
175
|
+
): string {
|
|
176
|
+
const style = el.style;
|
|
177
|
+
const content = esc(el.content);
|
|
178
|
+
const align = el.align ?? "left";
|
|
81
179
|
const styles: Record<string, string> = {
|
|
82
180
|
title: "font-size:20px;font-weight:700;color:#111",
|
|
83
181
|
body: "font-size:15px;line-height:1.5;color:#374151",
|
|
@@ -90,27 +188,24 @@ function renderText(el: Record<string, unknown>, _accent: string): string {
|
|
|
90
188
|
};text-align:${align}">${content}</div>`;
|
|
91
189
|
}
|
|
92
190
|
|
|
93
|
-
function renderImage(
|
|
94
|
-
|
|
95
|
-
|
|
191
|
+
function renderImage(
|
|
192
|
+
el: Extract<SnapPageElementInput, { type: "image" }>,
|
|
193
|
+
): string {
|
|
194
|
+
const url = esc(el.url);
|
|
195
|
+
const aspect = el.aspect ?? "16:9";
|
|
96
196
|
const [w, h] = aspect.split(":").map(Number);
|
|
97
197
|
const ratio = w && h ? `${w}/${h}` : "16/9";
|
|
98
198
|
return `<div style="aspect-ratio:${ratio};border-radius:8px;overflow:hidden;background:#F3F4F6"><img src="${url}" alt="${esc(
|
|
99
|
-
|
|
199
|
+
el.alt ?? "",
|
|
100
200
|
)}" style="width:100%;height:100%;object-fit:cover"></div>`;
|
|
101
201
|
}
|
|
102
202
|
|
|
103
|
-
function renderGrid(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const cells = el
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
color?: string;
|
|
110
|
-
content?: string;
|
|
111
|
-
}>;
|
|
112
|
-
const cellSize = (el.cellSize as string) ?? "auto";
|
|
113
|
-
const gap = (el.gap as string) ?? "small";
|
|
203
|
+
function renderGrid(
|
|
204
|
+
el: Extract<SnapPageElementInput, { type: "grid" }>,
|
|
205
|
+
): string {
|
|
206
|
+
const { cols, rows, cells } = el;
|
|
207
|
+
const cellSize = el.cellSize ?? "auto";
|
|
208
|
+
const gap = el.gap ?? "small";
|
|
114
209
|
const gapPx: Record<string, string> = {
|
|
115
210
|
none: "0",
|
|
116
211
|
small: "2px",
|
|
@@ -135,11 +230,12 @@ function renderGrid(el: Record<string, unknown>): string {
|
|
|
135
230
|
}">${cellsHtml}</div>`;
|
|
136
231
|
}
|
|
137
232
|
|
|
138
|
-
function renderProgress(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
233
|
+
function renderProgress(
|
|
234
|
+
el: Extract<SnapPageElementInput, { type: "progress" }>,
|
|
235
|
+
accent: string,
|
|
236
|
+
): string {
|
|
237
|
+
const { value, max, label } = el;
|
|
238
|
+
const color = colorHex(el.color, accent);
|
|
143
239
|
const pct = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
|
144
240
|
const labelHtml = label
|
|
145
241
|
? `<div style="font-size:13px;color:#6B7280;margin-bottom:4px">${esc(
|
|
@@ -149,19 +245,18 @@ function renderProgress(el: Record<string, unknown>, accent: string): string {
|
|
|
149
245
|
return `<div>${labelHtml}<div style="height:8px;background:#E5E7EB;border-radius:4px;overflow:hidden"><div style="height:100%;width:${pct}%;background:${color};border-radius:4px"></div></div></div>`;
|
|
150
246
|
}
|
|
151
247
|
|
|
152
|
-
function renderBarChart(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}>;
|
|
248
|
+
function renderBarChart(
|
|
249
|
+
el: Extract<SnapPageElementInput, { type: "bar_chart" }>,
|
|
250
|
+
accent: string,
|
|
251
|
+
): string {
|
|
252
|
+
const { bars } = el;
|
|
158
253
|
const max =
|
|
159
|
-
|
|
160
|
-
const defaultColor = colorHex(el.color
|
|
254
|
+
el.max ?? Math.max(...bars.map((b: { value: number }) => b.value), 1);
|
|
255
|
+
const defaultColor = colorHex(el.color, accent);
|
|
161
256
|
|
|
162
257
|
let html = `<div style="display:flex;align-items:flex-end;gap:12px;height:120px">`;
|
|
163
258
|
for (const bar of bars) {
|
|
164
|
-
const color = bar.color
|
|
259
|
+
const color = colorHex(bar.color, defaultColor);
|
|
165
260
|
const pct = max > 0 ? (bar.value / max) * 100 : 0;
|
|
166
261
|
html += `<div style="flex:1;display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end">`;
|
|
167
262
|
html += `<div style="font-size:11px;color:#6B7280;margin-bottom:4px">${bar.value}</div>`;
|
|
@@ -175,12 +270,11 @@ function renderBarChart(el: Record<string, unknown>, accent: string): string {
|
|
|
175
270
|
return html;
|
|
176
271
|
}
|
|
177
272
|
|
|
178
|
-
function renderList(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}>;
|
|
273
|
+
function renderList(
|
|
274
|
+
el: Extract<SnapPageElementInput, { type: "list" }>,
|
|
275
|
+
): string {
|
|
276
|
+
const style = el.style ?? "ordered";
|
|
277
|
+
const { items } = el;
|
|
184
278
|
|
|
185
279
|
let html = "";
|
|
186
280
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -204,11 +298,11 @@ function renderList(el: Record<string, unknown>): string {
|
|
|
204
298
|
}
|
|
205
299
|
|
|
206
300
|
function renderButtonGroup(
|
|
207
|
-
el:
|
|
301
|
+
el: Extract<SnapPageElementInput, { type: "button_group" }>,
|
|
208
302
|
accent: string,
|
|
209
303
|
): string {
|
|
210
|
-
const options = el
|
|
211
|
-
const layout =
|
|
304
|
+
const { options } = el;
|
|
305
|
+
const layout = el.style ?? "row";
|
|
212
306
|
const dir = layout === "stack" ? "column" : "row";
|
|
213
307
|
let html = `<div style="display:flex;flex-direction:${dir};gap:8px">`;
|
|
214
308
|
for (const opt of options) {
|
|
@@ -220,13 +314,12 @@ function renderButtonGroup(
|
|
|
220
314
|
return html;
|
|
221
315
|
}
|
|
222
316
|
|
|
223
|
-
function renderSlider(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const maxLabel = el.maxLabel as string | undefined;
|
|
317
|
+
function renderSlider(
|
|
318
|
+
el: Extract<SnapPageElementInput, { type: "slider" }>,
|
|
319
|
+
accent: string,
|
|
320
|
+
): string {
|
|
321
|
+
const { label, min, max, minLabel, maxLabel } = el;
|
|
322
|
+
const value = el.value ?? (min + max) / 2;
|
|
230
323
|
|
|
231
324
|
const labelHtml = label
|
|
232
325
|
? `<div style="font-size:13px;color:#6B7280;margin-bottom:4px">${esc(
|
|
@@ -243,14 +336,19 @@ function renderSlider(el: Record<string, unknown>, accent: string): string {
|
|
|
243
336
|
return `<div>${labelHtml}<div style="display:flex;align-items:center;gap:8px">${minL}<input type="range" min="${min}" max="${max}" value="${value}" disabled style="flex:1;accent-color:${accent};opacity:0.7">${maxL}</div></div>`;
|
|
244
337
|
}
|
|
245
338
|
|
|
246
|
-
function renderTextInput(
|
|
247
|
-
|
|
339
|
+
function renderTextInput(
|
|
340
|
+
el: Extract<SnapPageElementInput, { type: "text_input" }>,
|
|
341
|
+
): string {
|
|
342
|
+
const placeholder = esc(el.placeholder ?? "");
|
|
248
343
|
return `<input type="text" placeholder="${placeholder}" disabled style="width:100%;padding:10px 12px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:14px;color:#9CA3AF;font-family:inherit;box-sizing:border-box">`;
|
|
249
344
|
}
|
|
250
345
|
|
|
251
|
-
function renderToggle(
|
|
252
|
-
|
|
253
|
-
|
|
346
|
+
function renderToggle(
|
|
347
|
+
el: Extract<SnapPageElementInput, { type: "toggle" }>,
|
|
348
|
+
accent: string,
|
|
349
|
+
): string {
|
|
350
|
+
const label = esc(el.label);
|
|
351
|
+
const { value } = el;
|
|
254
352
|
const bg = value ? accent : "#D1D5DB";
|
|
255
353
|
const tx = value ? "20px" : "2px";
|
|
256
354
|
return `<div style="display:flex;align-items:center;justify-content:space-between">
|
|
@@ -259,10 +357,23 @@ function renderToggle(el: Record<string, unknown>, accent: string): string {
|
|
|
259
357
|
</div>`;
|
|
260
358
|
}
|
|
261
359
|
|
|
262
|
-
function
|
|
263
|
-
|
|
360
|
+
function renderSpacer(
|
|
361
|
+
el: Extract<SnapPageElementInput, { type: "spacer" }>,
|
|
362
|
+
): string {
|
|
363
|
+
const sizes: Record<string, string> = {
|
|
364
|
+
small: "8px",
|
|
365
|
+
medium: "16px",
|
|
366
|
+
large: "24px",
|
|
367
|
+
};
|
|
368
|
+
return `<div style="height:${sizes[el.size ?? "medium"] ?? "16px"}"></div>`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function renderGroup(
|
|
372
|
+
el: Extract<SnapPageElementInput, { type: "group" }>,
|
|
373
|
+
accent: string,
|
|
374
|
+
): string {
|
|
264
375
|
let html = `<div style="display:flex;gap:12px">`;
|
|
265
|
-
for (const child of children) {
|
|
376
|
+
for (const child of el.children) {
|
|
266
377
|
html += `<div style="flex:1">${renderElement(child, accent)}</div>`;
|
|
267
378
|
}
|
|
268
379
|
html += `</div>`;
|
|
@@ -272,8 +383,8 @@ function renderGroup(el: Record<string, unknown>, accent: string): string {
|
|
|
272
383
|
// ─── Buttons ────────────────────────────────────────────
|
|
273
384
|
|
|
274
385
|
function renderButtons(
|
|
275
|
-
buttons:
|
|
276
|
-
layout:
|
|
386
|
+
buttons: SnapPage["buttons"],
|
|
387
|
+
layout: SnapPage["button_layout"],
|
|
277
388
|
accent: string,
|
|
278
389
|
): string {
|
|
279
390
|
if (!buttons || buttons.length === 0) return "";
|
|
@@ -293,13 +404,15 @@ function renderButtons(
|
|
|
293
404
|
|
|
294
405
|
let html = `<div style="${wrap}${dir};gap:8px;margin-top:12px">`;
|
|
295
406
|
for (let i = 0; i < buttons.length; i++) {
|
|
296
|
-
const btn = buttons[i]!;
|
|
297
|
-
const label = esc(btn.label
|
|
298
|
-
const style =
|
|
407
|
+
const btn: SnapPageButton = buttons[i]!;
|
|
408
|
+
const label = esc(btn.label);
|
|
409
|
+
const style = btn.style ?? (i === 0 ? "primary" : "secondary");
|
|
299
410
|
const bg = style === "primary" ? accent : "transparent";
|
|
300
411
|
const color = style === "primary" ? "#fff" : accent;
|
|
301
412
|
const border = style === "primary" ? "none" : `2px solid ${accent}`;
|
|
302
|
-
|
|
413
|
+
const pad = style === "primary" ? "18px 16px" : "10px 16px";
|
|
414
|
+
const minH = style === "primary" ? "min-height:52px;" : "";
|
|
415
|
+
html += `<button onclick="showModal()" style="flex:1;${minH}padding:${pad};border-radius:10px;background:${bg};color:${color};border:${border};font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;box-sizing:border-box">${label}</button>`;
|
|
303
416
|
}
|
|
304
417
|
html += `</div>`;
|
|
305
418
|
return html;
|
|
@@ -307,16 +420,26 @@ function renderButtons(
|
|
|
307
420
|
|
|
308
421
|
// ─── Main renderer ──────────────────────────────────────
|
|
309
422
|
|
|
310
|
-
export function renderSnapPage(
|
|
423
|
+
export function renderSnapPage(
|
|
424
|
+
snap: SnapHandlerResult,
|
|
425
|
+
snapOrigin: string,
|
|
426
|
+
opts?: RenderSnapPageOptions,
|
|
427
|
+
): string {
|
|
311
428
|
const page = snap.page;
|
|
312
429
|
const accent = accentHex(page.theme?.accent);
|
|
313
430
|
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
431
|
+
const meta = extractPageMeta(page);
|
|
432
|
+
const pageTitle = esc(meta.title);
|
|
433
|
+
const resourcePath = opts?.resourcePath ?? "/";
|
|
434
|
+
const pageUrl = snapOrigin.replace(/\/$/, "") + resourcePath;
|
|
435
|
+
const ogMeta = buildOgMeta({
|
|
436
|
+
title: meta.title,
|
|
437
|
+
description: meta.description,
|
|
438
|
+
pageUrl,
|
|
439
|
+
ogImageUrl: opts?.ogImageUrl,
|
|
440
|
+
imageAlt: meta.imageAlt ?? meta.imageUrl ? meta.title : undefined,
|
|
441
|
+
siteName: opts?.siteName,
|
|
442
|
+
});
|
|
320
443
|
|
|
321
444
|
const snapUrl = encodeURIComponent(snapOrigin + "/");
|
|
322
445
|
|
|
@@ -324,17 +447,13 @@ export function renderSnapPage(snap: SnapResponseInput, snapOrigin: string): str
|
|
|
324
447
|
let elementsHtml = "";
|
|
325
448
|
for (const el of page.elements.children) {
|
|
326
449
|
elementsHtml += `<div style="margin-bottom:12px">${renderElement(
|
|
327
|
-
el
|
|
450
|
+
el,
|
|
328
451
|
accent,
|
|
329
452
|
)}</div>`;
|
|
330
453
|
}
|
|
331
454
|
|
|
332
455
|
// Render buttons
|
|
333
|
-
const buttonsHtml = renderButtons(
|
|
334
|
-
page.buttons as Array<Record<string, unknown>> | undefined,
|
|
335
|
-
page.button_layout as string | undefined,
|
|
336
|
-
accent,
|
|
337
|
-
);
|
|
456
|
+
const buttonsHtml = renderButtons(page.buttons, page.button_layout, accent);
|
|
338
457
|
|
|
339
458
|
return `<!DOCTYPE html>
|
|
340
459
|
<html lang="en">
|
|
@@ -342,6 +461,7 @@ export function renderSnapPage(snap: SnapResponseInput, snapOrigin: string): str
|
|
|
342
461
|
<meta charset="utf-8">
|
|
343
462
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
344
463
|
<title>${pageTitle}</title>
|
|
464
|
+
${ogMeta}
|
|
345
465
|
<style>
|
|
346
466
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
347
467
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0A0A0A;min-height:100vh;display:flex;align-items:center;justify-content:center;flex-direction:column;padding:24px}
|