Skip to content

Commit 4098fc2

Browse files
committed
added some examples to test
1 parent e908f5e commit 4098fc2

File tree

8 files changed

+421
-0
lines changed

8 files changed

+421
-0
lines changed

examples/bar/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { serve } from "https://deno.land/[email protected]/http/server.ts"
2+
3+
serve(async (req) => {
4+
const { name } = await req.json()
5+
const data = {
6+
message: `Hello ${name} from bar!`,
7+
}
8+
9+
return new Response(
10+
JSON.stringify(data),
11+
{ headers: { "Content-Type": "application/json" } },
12+
)
13+
})

examples/foo/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { serve } from "https://deno.land/[email protected]/http/server.ts"
2+
3+
Deno.env.set("foo", "bar");
4+
5+
serve(async (req) => {
6+
const { name } = await req.json()
7+
const data = {
8+
message: `Hello ${name} from foo!`,
9+
}
10+
11+
return new Response(
12+
JSON.stringify(data),
13+
{ headers: { "Content-Type": "application/json", "Connection": "keep-alive" } },
14+
)
15+
})

examples/oak/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
2+
3+
const router = new Router()
4+
router
5+
// Note: path will be prefixed with function name
6+
.get('/oak', (context) => {
7+
context.response.body = 'This is an example Oak server running on Edge Functions!'
8+
})
9+
.post('/oak/greet', async (context) => {
10+
// Note: request body will be streamed to the function as chunks, set limit to 0 to fully read it.
11+
const result = context.request.body({ type: 'json', limit: 0 })
12+
const body = await result.value
13+
const name = body.name || 'you'
14+
15+
context.response.body = { msg: `Hey ${name}!` }
16+
})
17+
.get('/oak/redirect', (context) => {
18+
context.response.redirect('https://www.example.com')
19+
})
20+
21+
const app = new Application()
22+
app.use(router.routes())
23+
app.use(router.allowedMethods())
24+
25+
await app.listen({ port: 8000 })

examples/opengraph/emoji.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const apis = {
2+
twemoji: (code: string) =>
3+
"https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/" + code.toLowerCase() + ".svg",
4+
openmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/[email protected]/svg/",
5+
blobmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/[email protected]/svg/",
6+
noto:
7+
"https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/",
8+
fluent: (code: string) =>
9+
"https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" +
10+
code.toLowerCase() + "_color.svg",
11+
fluentFlat: (code: string) =>
12+
"https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" +
13+
code.toLowerCase() + "_flat.svg",
14+
};
15+
16+
export type EmojiType = keyof typeof apis;
17+
18+
const n = String.fromCharCode(8205), O = /\uFE0F/g;
19+
20+
export function loadEmoji(
21+
code: string,
22+
type?: EmojiType,
23+
): Promise<Response> {
24+
(!type || !apis[type]) && (type = "twemoji");
25+
const A = apis[type];
26+
return fetch(
27+
typeof A == "function" ? A(code) : `${A}${code.toUpperCase()}.svg`,
28+
);
29+
}
30+
31+
export function getIconCode(char: string): string {
32+
return d(char.indexOf(n) < 0 ? char.replace(O, "") : char);
33+
}
34+
35+
function d(j: string) {
36+
const t = [];
37+
let A = 0, k = 0;
38+
for (let E = 0; E < j.length;) {
39+
A = j.charCodeAt(E++),
40+
k
41+
? (t.push((65536 + (k - 55296 << 10) + (A - 56320)).toString(16)),
42+
k = 0)
43+
: 55296 <= A && A <= 56319
44+
? k = A
45+
: t.push(A.toString(16));
46+
}
47+
return t.join("-");
48+
}

examples/opengraph/handler.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from "https://esm.sh/[email protected]";
2+
//import { ImageResponse } from "./image_response.ts";
3+
import { ImageResponse } from 'https://deno.land/x/og_edge/mod.ts';
4+
5+
export async function handler(req: Request) {
6+
return new ImageResponse(
7+
<div
8+
style={{
9+
fontSize: 100,
10+
color: "black",
11+
background: "white",
12+
width: "100%",
13+
height: "100%",
14+
padding: "50px 200px",
15+
textAlign: "center",
16+
justifyContent: "center",
17+
alignItems: "center",
18+
display: "flex",
19+
}}
20+
21+
>
22+
👋, It works!
23+
</div>,
24+
{
25+
width: 1200,
26+
height: 630,
27+
// Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
28+
// Default to 'twemoji'
29+
emoji: "twemoji",
30+
},
31+
);
32+
}
33+
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import type { ReactElement } from "https://esm.sh/[email protected]";
2+
import type { SatoriOptions } from "https://esm.sh/[email protected]";
3+
4+
import satori, { init as initSatori } from "https://esm.sh/[email protected]/wasm";
5+
import { initStreaming } from "https://esm.sh/[email protected]";
6+
7+
import {
8+
initWasm,
9+
Resvg,
10+
} from "https://esm.sh/@resvg/[email protected]";
11+
import { EmojiType, getIconCode, loadEmoji } from "./emoji.ts";
12+
13+
declare module "https://esm.sh/[email protected]" {
14+
interface HTMLAttributes<T> {
15+
/**
16+
* Specify styles using Tailwind CSS classes. This feature is currently experimental.
17+
* If `style` prop is also specified, styles generated with `tw` prop will be overridden.
18+
*
19+
* Example:
20+
* - `tw='w-full h-full bg-blue-200'`
21+
* - `tw='text-9xl'`
22+
* - `tw='text-[80px]'`
23+
*
24+
* @type {string}
25+
*/
26+
tw?: string;
27+
}
28+
}
29+
30+
const resvg_wasm = fetch(
31+
"https://cdn.jsdelivr.net/npm/@vercel/[email protected]/vendor/resvg.simd.wasm",
32+
).then((res) => res.arrayBuffer());
33+
34+
const yoga_wasm = fetch(
35+
"https://cdn.jsdelivr.net/npm/@vercel/[email protected]/vendor/yoga.wasm",
36+
);
37+
38+
const fallbackFont = fetch(
39+
"https://cdn.jsdelivr.net/npm/@vercel/[email protected]/vendor/noto-sans-v27-latin-regular.ttf",
40+
).then((a) => a.arrayBuffer());
41+
42+
const initializedResvg = initWasm(resvg_wasm);
43+
const initializedYoga = initStreaming(yoga_wasm).then((yoga: unknown) =>
44+
initSatori(yoga)
45+
);
46+
47+
const isDev = Boolean(Deno.env.get("NETLIFY_LOCAL"));
48+
49+
type ImageResponseOptions = ConstructorParameters<typeof Response>[1] & {
50+
/**
51+
* The width of the image.
52+
*
53+
* @type {number}
54+
* @default 1200
55+
*/
56+
width?: number;
57+
/**
58+
* The height of the image.
59+
*
60+
* @type {number}
61+
* @default 630
62+
*/
63+
height?: number;
64+
/**
65+
* Display debug information on the image.
66+
*
67+
* @type {boolean}
68+
* @default false
69+
*/
70+
debug?: boolean;
71+
/**
72+
* A list of fonts to use.
73+
*
74+
* @type {{ data: ArrayBuffer; name: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: 'normal' | 'italic' }[]}
75+
* @default Noto Sans Latin Regular.
76+
*/
77+
fonts?: SatoriOptions["fonts"];
78+
/**
79+
* Using a specific Emoji style. Defaults to `twemoji`.
80+
*
81+
* @link https://github.com/vercel/og#emoji
82+
* @type {EmojiType}
83+
* @default 'twemoji'
84+
*/
85+
emoji?: EmojiType;
86+
};
87+
88+
// @TODO: Support font style and weights, and make this option extensible rather
89+
// than built-in.
90+
// @TODO: Cover most languages with Noto Sans.
91+
const languageFontMap = {
92+
"ja-JP": "Noto+Sans+JP",
93+
"ko-KR": "Noto+Sans+KR",
94+
"zh-CN": "Noto+Sans+SC",
95+
"zh-TW": "Noto+Sans+TC",
96+
"zh-HK": "Noto+Sans+HK",
97+
"th-TH": "Noto+Sans+Thai",
98+
"bn-IN": "Noto+Sans+Bengali",
99+
"ar-AR": "Noto+Sans+Arabic",
100+
"ta-IN": "Noto+Sans+Tamil",
101+
"ml-IN": "Noto+Sans+Malayalam",
102+
"he-IL": "Noto+Sans+Hebrew",
103+
"te-IN": "Noto+Sans+Telugu",
104+
devanagari: "Noto+Sans+Devanagari",
105+
kannada: "Noto+Sans+Kannada",
106+
symbol: ["Noto+Sans+Symbols", "Noto+Sans+Symbols+2"],
107+
math: "Noto+Sans+Math",
108+
unknown: "Noto+Sans",
109+
};
110+
111+
async function loadGoogleFont(fonts: string | string[], text: string) {
112+
// @TODO: Support multiple fonts.
113+
const font = Array.isArray(fonts) ? fonts.at(-1) : fonts;
114+
if (!font || !text) return;
115+
116+
const API = `https://fonts.googleapis.com/css2?family=${font}&text=${
117+
encodeURIComponent(
118+
text,
119+
)
120+
}`;
121+
122+
const css = await (
123+
await fetch(API, {
124+
headers: {
125+
// Make sure it returns TTF.
126+
"User-Agent":
127+
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
128+
},
129+
})
130+
).text();
131+
132+
const resource = css.match(
133+
/src: url\((.+)\) format\('(opentype|truetype)'\)/,
134+
);
135+
if (!resource) throw new Error("Failed to load font");
136+
137+
return fetch(resource[1]).then((res) => res.arrayBuffer());
138+
}
139+
140+
type Asset = SatoriOptions["fonts"][0] | string;
141+
142+
const assetCache = new Map<string, Asset | undefined>();
143+
const loadDynamicAsset = ({ emoji }: { emoji?: EmojiType }) => {
144+
const fn = async (
145+
code: keyof typeof languageFontMap | "emoji",
146+
text: string,
147+
): Promise<Asset | undefined> => {
148+
if (code === "emoji") {
149+
// It's an emoji, load the image.
150+
return (
151+
`data:image/svg+xml;base64,` +
152+
btoa(await (await loadEmoji(getIconCode(text), emoji)).text())
153+
);
154+
}
155+
156+
// Try to load from Google Fonts.
157+
if (!languageFontMap[code]) code = "unknown";
158+
159+
try {
160+
const data = await loadGoogleFont(languageFontMap[code], text);
161+
162+
if (data) {
163+
return {
164+
name: `satori_${code}_fallback_${text}`,
165+
data,
166+
weight: 400,
167+
style: "normal",
168+
};
169+
}
170+
} catch (e) {
171+
console.error("Failed to load dynamic font for", text, ". Error:", e);
172+
}
173+
};
174+
175+
return async (...args: Parameters<typeof fn>) => {
176+
const key = JSON.stringify(args);
177+
const cache = assetCache.get(key);
178+
if (cache) return cache;
179+
180+
const asset = await fn(...args);
181+
assetCache.set(key, asset);
182+
return asset;
183+
};
184+
};
185+
186+
export class ImageResponse extends Response {
187+
constructor(element: ReactElement, options: ImageResponseOptions = {}) {
188+
const extendedOptions = Object.assign(
189+
{
190+
width: 1200,
191+
height: 630,
192+
debug: false,
193+
},
194+
options,
195+
);
196+
197+
const result = new ReadableStream({
198+
async start(controller) {
199+
try {
200+
console.log('init yoga wasm');
201+
const fetchRes = await yoga_wasm;
202+
console.log(fetchRes);
203+
const res = await WebAssembly.instantiateStreaming(fetchRes);
204+
console.log(res);
205+
} catch (e) {
206+
console.log('error in init yoga');
207+
console.log(e);
208+
}
209+
210+
await initializedYoga;
211+
await initializedResvg;
212+
const fontData = await fallbackFont;
213+
214+
const svg = await satori(element, {
215+
width: extendedOptions.width,
216+
height: extendedOptions.height,
217+
debug: extendedOptions.debug,
218+
fonts: extendedOptions.fonts || [
219+
{
220+
name: "sans serif",
221+
data: fontData,
222+
weight: 700,
223+
style: "normal",
224+
},
225+
],
226+
loadAdditionalAsset: loadDynamicAsset({
227+
emoji: extendedOptions.emoji,
228+
}),
229+
});
230+
231+
const resvgJS = new Resvg(svg, {
232+
fitTo: {
233+
mode: "width",
234+
value: extendedOptions.width,
235+
},
236+
});
237+
238+
const res = resvgJS.render();
239+
console.log(res);
240+
controller.enqueue(svg);
241+
controller.close();
242+
},
243+
});
244+
console.log('result is ready')
245+
console.log(result);
246+
247+
super(result, {
248+
headers: {
249+
"content-type": "image/png",
250+
"cache-control": isDev
251+
? "no-cache, no-store"
252+
: "public, max-age=31536000, no-transform, immutable",
253+
...extendedOptions.headers,
254+
},
255+
status: extendedOptions.status,
256+
statusText: extendedOptions.statusText,
257+
});
258+
}
259+
}

0 commit comments

Comments
 (0)