@farcaster/snap-hono 1.4.2 → 1.4.4
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/index.js +2 -7
- package/dist/og-image.js +168 -75
- package/dist/renderSnapPage.js +165 -58
- package/package.json +2 -2
- package/src/index.ts +1 -8
- package/src/og-image.ts +205 -82
- package/src/renderSnapPage.ts +167 -59
package/dist/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { cors } from "hono/cors";
|
|
2
|
-
import {
|
|
2
|
+
import { MEDIA_TYPE, ACTION_TYPE_GET, } from "@farcaster/snap";
|
|
3
3
|
import { parseRequest } from "@farcaster/snap/server";
|
|
4
4
|
import { brandedFallbackHtml } from "./fallback.js";
|
|
5
5
|
import { payloadToResponse, snapHeaders } from "./payloadToResponse.js";
|
|
6
6
|
import { renderSnapPage } from "./renderSnapPage.js";
|
|
7
7
|
import { renderSnapPageToPng, renderWithDedup, etagForPage, } from "./og-image.js";
|
|
8
|
-
const defaultData = createDefaultDataStore();
|
|
9
8
|
/**
|
|
10
9
|
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
11
10
|
*
|
|
@@ -32,7 +31,6 @@ export function registerSnapHandler(app, snapFn, options = {}) {
|
|
|
32
31
|
const snap = await snapFn({
|
|
33
32
|
action: { type: ACTION_TYPE_GET },
|
|
34
33
|
request: stripAuthHeaders(c.req.raw),
|
|
35
|
-
data: defaultData,
|
|
36
34
|
});
|
|
37
35
|
const snapJson = JSON.stringify(snap);
|
|
38
36
|
const etag = etagForPage(snapJson);
|
|
@@ -107,7 +105,6 @@ export function registerSnapHandler(app, snapFn, options = {}) {
|
|
|
107
105
|
const response = await snapFn({
|
|
108
106
|
action: { type: ACTION_TYPE_GET },
|
|
109
107
|
request: c.req.raw,
|
|
110
|
-
data: defaultData,
|
|
111
108
|
});
|
|
112
109
|
return payloadToResponse(response, {
|
|
113
110
|
resourcePath,
|
|
@@ -142,7 +139,6 @@ export function registerSnapHandler(app, snapFn, options = {}) {
|
|
|
142
139
|
const response = await snapFn({
|
|
143
140
|
action: parsed.action,
|
|
144
141
|
request: raw,
|
|
145
|
-
data: defaultData,
|
|
146
142
|
});
|
|
147
143
|
return payloadToResponse(response, {
|
|
148
144
|
resourcePath: resourcePathFromRequest(raw.url),
|
|
@@ -186,7 +182,6 @@ async function getFallbackHtml(request, snapFn, ogImageUrl) {
|
|
|
186
182
|
const snap = await snapFn({
|
|
187
183
|
action: { type: ACTION_TYPE_GET },
|
|
188
184
|
request: stripAuthHeaders(request),
|
|
189
|
-
data: defaultData,
|
|
190
185
|
});
|
|
191
186
|
return renderSnapPage(snap, origin, { ogImageUrl, resourcePath, siteName });
|
|
192
187
|
}
|
|
@@ -203,7 +198,7 @@ function snapOriginFromRequest(request) {
|
|
|
203
198
|
request.headers.get("host")?.trim();
|
|
204
199
|
if (host)
|
|
205
200
|
return `${proto}://${host}`.replace(/\/$/, "");
|
|
206
|
-
return "https://
|
|
201
|
+
return "https://docs.farcaster.xyz/snap";
|
|
207
202
|
}
|
|
208
203
|
function clientWantsSnapResponse(accept) {
|
|
209
204
|
if (!accept || accept.trim() === "")
|
package/dist/og-image.js
CHANGED
|
@@ -169,37 +169,66 @@ function colorHex(color, accent) {
|
|
|
169
169
|
return PALETTE_LIGHT_HEX[color] ?? accent;
|
|
170
170
|
}
|
|
171
171
|
function mapText(el) {
|
|
172
|
-
const
|
|
172
|
+
const size = String(el.size ?? "md");
|
|
173
|
+
const weight = String(el.weight ?? "normal");
|
|
173
174
|
const align = el.align ?? "left";
|
|
174
175
|
let content = String(el.content ?? "");
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
caption: { fontSize: 13, color: "#9CA3AF", lineHeight: 1.4 },
|
|
187
|
-
label: {
|
|
188
|
-
fontSize: 13,
|
|
189
|
-
fontWeight: 600,
|
|
190
|
-
color: "#6B7280",
|
|
191
|
-
textTransform: "uppercase",
|
|
192
|
-
letterSpacing: "0.5px",
|
|
193
|
-
},
|
|
176
|
+
content = content
|
|
177
|
+
.replace(/\u2192/g, "->")
|
|
178
|
+
.replace(/\u2190/g, "<-")
|
|
179
|
+
.replace(/\u27a1/gi, "->");
|
|
180
|
+
const sizeStyles = {
|
|
181
|
+
md: { fontSize: 15, lineHeight: 1.5 },
|
|
182
|
+
sm: { fontSize: 13, lineHeight: 1.5 },
|
|
183
|
+
};
|
|
184
|
+
const weightStyles = {
|
|
185
|
+
bold: { fontWeight: 700 },
|
|
186
|
+
normal: { fontWeight: 400 },
|
|
194
187
|
};
|
|
195
|
-
const ts = styleMap[style] ?? styleMap["body"];
|
|
196
188
|
return h("div", {
|
|
197
189
|
display: "flex",
|
|
198
190
|
width: OG_CARD_INNER_WIDTH_PX,
|
|
191
|
+
color: "#374151",
|
|
199
192
|
textAlign: align,
|
|
200
|
-
...
|
|
193
|
+
...(sizeStyles[size] ?? sizeStyles.md),
|
|
194
|
+
...(weightStyles[weight] ?? weightStyles.normal),
|
|
201
195
|
}, content);
|
|
202
196
|
}
|
|
197
|
+
function mapItem(el) {
|
|
198
|
+
const title = String(el.title ?? "");
|
|
199
|
+
const description = el.description ? String(el.description) : undefined;
|
|
200
|
+
return h("div", { display: "flex", flexDirection: "column", gap: 2, padding: "6px 10px" }, h("div", { display: "flex", fontSize: 15, fontWeight: 500, color: "#111" }, title), description
|
|
201
|
+
? h("div", { display: "flex", fontSize: 13, color: "#6B7280", lineHeight: 1.4 }, description)
|
|
202
|
+
: null);
|
|
203
|
+
}
|
|
204
|
+
function mapBadge(el, accent) {
|
|
205
|
+
const label = String(el.label ?? "");
|
|
206
|
+
const color = colorHex(el.color, accent);
|
|
207
|
+
const variant = String(el.variant ?? "default");
|
|
208
|
+
const isFilled = variant === "default";
|
|
209
|
+
const bg = isFilled ? color : "transparent";
|
|
210
|
+
const fg = isFilled ? "#fff" : color;
|
|
211
|
+
const border = isFilled ? undefined : `1px solid ${color}`;
|
|
212
|
+
return h("div", {
|
|
213
|
+
display: "flex",
|
|
214
|
+
alignItems: "center",
|
|
215
|
+
paddingTop: 2, paddingBottom: 2, paddingLeft: 10, paddingRight: 10,
|
|
216
|
+
borderRadius: 9999,
|
|
217
|
+
fontSize: 12,
|
|
218
|
+
fontWeight: 500,
|
|
219
|
+
backgroundColor: bg,
|
|
220
|
+
color: fg,
|
|
221
|
+
...(border ? { border } : {}),
|
|
222
|
+
}, label);
|
|
223
|
+
}
|
|
224
|
+
function mapSeparator() {
|
|
225
|
+
return h("div", {
|
|
226
|
+
display: "flex",
|
|
227
|
+
height: 1,
|
|
228
|
+
backgroundColor: "#E5E7EB",
|
|
229
|
+
width: "100%",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
203
232
|
function mapImage(el, imageMap) {
|
|
204
233
|
const url = el.url;
|
|
205
234
|
const dataUri = imageMap.get(url);
|
|
@@ -332,49 +361,84 @@ function mapBarChart(el, accent) {
|
|
|
332
361
|
const color = bar.color !== undefined && bar.color !== ""
|
|
333
362
|
? colorHex(bar.color, accent)
|
|
334
363
|
: chartDefault;
|
|
335
|
-
const pct = maxVal > 0 ? (bar.value / maxVal) * 100 : 0;
|
|
336
|
-
return h("div", {
|
|
337
|
-
display: "flex",
|
|
338
|
-
flex: 1,
|
|
339
|
-
flexDirection: "column",
|
|
340
|
-
alignItems: "center",
|
|
341
|
-
height: "100%",
|
|
342
|
-
justifyContent: "flex-end",
|
|
343
|
-
}, h("div", { display: "flex", fontSize: 11, color: "#6B7280", marginBottom: 4 }, String(bar.value)), h("div", {
|
|
344
|
-
display: "flex",
|
|
345
|
-
width: "100%",
|
|
346
|
-
height: `${pct}%`,
|
|
347
|
-
backgroundColor: color,
|
|
348
|
-
borderRadius: "4px 4px 0 0",
|
|
349
|
-
minHeight: 4,
|
|
350
|
-
}), h("div", { display: "flex", fontSize: 11, color: "#9CA3AF", marginTop: 4 }, bar.label.slice(0, 12)));
|
|
364
|
+
const pct = maxVal > 0 ? Math.min(100, (bar.value / maxVal) * 100) : 0;
|
|
365
|
+
return h("div", { display: "flex", flexDirection: "row", alignItems: "center", gap: 8, width: OG_CARD_INNER_WIDTH_PX }, h("div", { display: "flex", width: 80, fontSize: 12, color: "#6B7280", justifyContent: "flex-end" }, bar.label.slice(0, 20)), h("div", { display: "flex", flex: 1, height: 10, backgroundColor: "#E5E7EB", borderRadius: 9999, overflow: "hidden" }, h("div", { display: "flex", height: 10, width: `${pct}%`, backgroundColor: color, borderRadius: 9999 })), h("div", { display: "flex", width: 32, fontSize: 12, color: "#6B7280" }, String(bar.value)));
|
|
351
366
|
});
|
|
352
|
-
return h("div", {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
367
|
+
return h("div", { display: "flex", flexDirection: "column", gap: 8, width: OG_CARD_INNER_WIDTH_PX }, ...barNodes);
|
|
368
|
+
}
|
|
369
|
+
function mapCellGrid(el, accent) {
|
|
370
|
+
const cols = Number(el.cols ?? 2);
|
|
371
|
+
const rows = Number(el.rows ?? 2);
|
|
372
|
+
const cells = Array.isArray(el.cells) ? el.cells : [];
|
|
373
|
+
const gap = String(el.gap ?? "sm");
|
|
374
|
+
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
375
|
+
const gapPx = gapMap[gap] ?? 1;
|
|
376
|
+
const cellW = Math.floor((OG_CARD_INNER_WIDTH_PX - (cols - 1) * gapPx) / cols);
|
|
377
|
+
const cellMap = new Map();
|
|
378
|
+
for (const c of cells) {
|
|
379
|
+
cellMap.set(`${Number(c.row ?? 0)},${Number(c.col ?? 0)}`, { color: c.color, content: c.content });
|
|
380
|
+
}
|
|
381
|
+
const rowNodes = [];
|
|
382
|
+
for (let r = 0; r < rows; r++) {
|
|
383
|
+
const cellNodes = [];
|
|
384
|
+
for (let c = 0; c < cols; c++) {
|
|
385
|
+
const cell = cellMap.get(`${r},${c}`);
|
|
386
|
+
const bg = cell?.color ? colorHex(cell.color, accent) : "#F3F4F6";
|
|
387
|
+
cellNodes.push(h("div", {
|
|
388
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
389
|
+
width: cellW, height: cellW > 28 ? 28 : cellW, borderRadius: 4,
|
|
390
|
+
backgroundColor: bg, border: "1px solid #E5E7EB",
|
|
391
|
+
fontSize: 10, fontWeight: 600, color: "#374151",
|
|
392
|
+
}, cell?.content ?? ""));
|
|
393
|
+
}
|
|
394
|
+
rowNodes.push(h("div", { display: "flex", flexDirection: "row", gap: gapPx }, ...cellNodes));
|
|
395
|
+
}
|
|
396
|
+
return h("div", { display: "flex", flexDirection: "column", gap: gapPx, width: OG_CARD_INNER_WIDTH_PX }, ...rowNodes);
|
|
360
397
|
}
|
|
361
398
|
function mapElement(el, accent, imageMap) {
|
|
362
399
|
const type = el.type;
|
|
363
400
|
switch (type) {
|
|
364
401
|
case "text":
|
|
365
402
|
return mapText(el);
|
|
403
|
+
case "item":
|
|
404
|
+
return mapItem(el);
|
|
405
|
+
case "badge":
|
|
406
|
+
return mapBadge(el, accent);
|
|
366
407
|
case "image":
|
|
367
408
|
return mapImage(el, imageMap);
|
|
409
|
+
case "separator":
|
|
368
410
|
case "divider":
|
|
369
|
-
return
|
|
411
|
+
return mapSeparator();
|
|
370
412
|
case "progress":
|
|
371
413
|
return mapProgress(el, accent);
|
|
372
|
-
case "list":
|
|
373
|
-
return mapList(el);
|
|
374
414
|
case "toggle_group":
|
|
375
415
|
return mapButtonGroup(el, accent);
|
|
416
|
+
case "input": {
|
|
417
|
+
const label = el.label ? String(el.label) : "";
|
|
418
|
+
const placeholder = el.placeholder ? String(el.placeholder) : "";
|
|
419
|
+
return h("div", { display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX }, label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null, h("div", {
|
|
420
|
+
display: "flex", padding: "10px 12px", borderRadius: 8,
|
|
421
|
+
border: "1px solid #E5E7EB", backgroundColor: "#fff",
|
|
422
|
+
fontSize: 14, color: "#9CA3AF",
|
|
423
|
+
}, placeholder || " "));
|
|
424
|
+
}
|
|
425
|
+
case "switch": {
|
|
426
|
+
const label = el.label ? String(el.label) : "";
|
|
427
|
+
const checked = Boolean(el.defaultChecked);
|
|
428
|
+
const bg = checked ? accent : "#D1D5DB";
|
|
429
|
+
return h("div", { display: "flex", alignItems: "center", justifyContent: "space-between", width: OG_CARD_INNER_WIDTH_PX }, h("div", { display: "flex", fontSize: 14, color: "#374151" }, label), h("div", { display: "flex", width: 44, height: 24, borderRadius: 12, backgroundColor: bg, position: "relative" }, h("div", { display: "flex", width: 20, height: 20, borderRadius: 10, backgroundColor: "#fff", position: "absolute", top: 2, left: checked ? 20 : 2 })));
|
|
430
|
+
}
|
|
431
|
+
case "slider": {
|
|
432
|
+
const label = el.label ? String(el.label) : "";
|
|
433
|
+
return h("div", { display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX }, label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null, h("div", { display: "flex", height: 10, backgroundColor: "#E5E7EB", borderRadius: 9999, width: "100%" }, h("div", { display: "flex", height: 10, width: "50%", backgroundColor: accent, borderRadius: 9999 })));
|
|
434
|
+
}
|
|
435
|
+
// Legacy types kept for backward compat with older specs
|
|
436
|
+
case "list":
|
|
437
|
+
return mapList(el);
|
|
376
438
|
case "bar_chart":
|
|
377
439
|
return mapBarChart(el, accent);
|
|
440
|
+
case "cell_grid":
|
|
441
|
+
return mapCellGrid(el, accent);
|
|
378
442
|
case "group": {
|
|
379
443
|
const children = el.children ?? [];
|
|
380
444
|
const childNodes = children
|
|
@@ -388,8 +452,8 @@ function mapElement(el, accent, imageMap) {
|
|
|
388
452
|
}
|
|
389
453
|
function mapButton(btn, accent, i) {
|
|
390
454
|
const label = String(btn.label ?? "");
|
|
391
|
-
const
|
|
392
|
-
const isPrimary =
|
|
455
|
+
const variant = btn.variant ?? btn.style ?? "secondary";
|
|
456
|
+
const isPrimary = variant === "primary";
|
|
393
457
|
// Primary CTA: generous vertical padding + minHeight so Satori/Yoga renders a tall tap target
|
|
394
458
|
// (small padding deltas are easy to miss; flexBasis:0 rows can also under-measure height).
|
|
395
459
|
const py = isPrimary ? 18 : 10;
|
|
@@ -425,21 +489,21 @@ function linesForWrappedText(charCount, innerWidthPx, avgCharPx) {
|
|
|
425
489
|
return Math.max(1, Math.ceil((charCount * 1.12) / cpl));
|
|
426
490
|
}
|
|
427
491
|
function estimateTextHeight(el) {
|
|
428
|
-
const
|
|
492
|
+
const size = String(el.size ?? "md");
|
|
429
493
|
const content = String(el.content ?? "");
|
|
430
494
|
const w = OG_CARD_INNER_WIDTH_PX;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
495
|
+
if (size === "sm")
|
|
496
|
+
return linesForWrappedText(content.length, w, 7) * 20;
|
|
497
|
+
return linesForWrappedText(content.length, w, 7.5) * 23;
|
|
498
|
+
}
|
|
499
|
+
function estimateItemHeight(el) {
|
|
500
|
+
const title = String(el.title ?? "");
|
|
501
|
+
const desc = el.description ? String(el.description) : "";
|
|
502
|
+
const w = OG_CARD_INNER_WIDTH_PX;
|
|
503
|
+
let total = linesForWrappedText(title.length, w, 7.5) * 23 + 12;
|
|
504
|
+
if (desc)
|
|
505
|
+
total += linesForWrappedText(desc.length, w, 7) * 20;
|
|
506
|
+
return total;
|
|
443
507
|
}
|
|
444
508
|
function estimateImageHeight(el, imageMap) {
|
|
445
509
|
const url = el.url;
|
|
@@ -499,18 +563,38 @@ function estimateElementHeight(el, imageMap) {
|
|
|
499
563
|
switch (type) {
|
|
500
564
|
case "text":
|
|
501
565
|
return estimateTextHeight(el);
|
|
566
|
+
case "item":
|
|
567
|
+
return estimateItemHeight(el);
|
|
568
|
+
case "badge":
|
|
569
|
+
return 24;
|
|
502
570
|
case "image":
|
|
503
571
|
return estimateImageHeight(el, imageMap);
|
|
572
|
+
case "separator":
|
|
504
573
|
case "divider":
|
|
505
574
|
return 1;
|
|
506
575
|
case "progress":
|
|
507
576
|
return estimateProgressHeight(el);
|
|
577
|
+
case "input":
|
|
578
|
+
return (el.label ? 20 : 0) + 42;
|
|
579
|
+
case "switch":
|
|
580
|
+
return 28;
|
|
581
|
+
case "slider":
|
|
582
|
+
return (el.label ? 20 : 0) + 16;
|
|
508
583
|
case "list":
|
|
509
584
|
return estimateListHeight(el);
|
|
510
585
|
case "toggle_group":
|
|
511
586
|
return estimateButtonGroupHeight(el);
|
|
512
|
-
case "bar_chart":
|
|
513
|
-
|
|
587
|
+
case "bar_chart": {
|
|
588
|
+
const bars = Array.isArray(el.bars) ? el.bars : [];
|
|
589
|
+
return Math.max(1, bars.length) * 26;
|
|
590
|
+
}
|
|
591
|
+
case "cell_grid": {
|
|
592
|
+
const rows = Number(el.rows ?? 2);
|
|
593
|
+
const gap = String(el.gap ?? "sm");
|
|
594
|
+
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
595
|
+
const gapPx = gapMap[gap] ?? 1;
|
|
596
|
+
return rows * 28 + (rows - 1) * gapPx;
|
|
597
|
+
}
|
|
514
598
|
case "group": {
|
|
515
599
|
const children = el.children ?? [];
|
|
516
600
|
if (children.length === 0)
|
|
@@ -557,19 +641,28 @@ function estimateDefaultOgHeight(elements, imageMap, buttons, buttonLayout) {
|
|
|
557
641
|
return Math.min(OG_MAX_HEIGHT_PX, Math.max(OG_MIN_HEIGHT_PX, Math.ceil(outerH)));
|
|
558
642
|
}
|
|
559
643
|
// ─── Spec helpers ─────────────────────────────────────
|
|
560
|
-
/** Walk the flat spec from root and collect
|
|
644
|
+
/** Walk the flat spec from root, recursing into stack containers, and collect leaf elements as El objects. */
|
|
561
645
|
function specToElementList(spec) {
|
|
646
|
+
function collect(keys) {
|
|
647
|
+
const result = [];
|
|
648
|
+
for (const key of keys) {
|
|
649
|
+
const el = spec.elements[key];
|
|
650
|
+
if (!el)
|
|
651
|
+
continue;
|
|
652
|
+
// Recurse into stack and item_group containers
|
|
653
|
+
if ((el.type === "stack" || el.type === "item_group") && el.children?.length) {
|
|
654
|
+
result.push(...collect(el.children));
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
result.push({ type: el.type, ...el.props });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
562
662
|
const rootEl = spec.elements[spec.root];
|
|
563
663
|
if (!rootEl?.children)
|
|
564
664
|
return [];
|
|
565
|
-
return rootEl.children
|
|
566
|
-
.map((key) => {
|
|
567
|
-
const el = spec.elements[key];
|
|
568
|
-
if (!el)
|
|
569
|
-
return null;
|
|
570
|
-
return { type: el.type, ...el.props };
|
|
571
|
-
})
|
|
572
|
-
.filter((el) => el != null);
|
|
665
|
+
return collect(rootEl.children);
|
|
573
666
|
}
|
|
574
667
|
/** Extract button elements (type: "button") from the spec. */
|
|
575
668
|
function specToButtons(spec) {
|