Tools for the Atmosphere
tools.slices.network
quickslice
atproto
html
1import { LRUCache } from "lru-cache";
2import { fetchDocument, extractPreview, fetchLexicon, getLexiconType, getLexiconPreviewDef } from "./lib/graphql";
3import { generateOgImage, generateLexiconOgImage } from "./lib/og-image";
4
5const PORT = process.env.PORT || 3000;
6const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
7const CDN_URL = process.env.CDN_URL || "https://tools.slices.network";
8
9// Cache for generated images (max 500, 1 hour TTL)
10const imageCache = new LRUCache<string, Uint8Array>({
11 max: 500,
12 ttl: 1000 * 60 * 60,
13});
14
15// Cache for HTML (5 min TTL)
16const htmlCache = new LRUCache<string, string>({
17 max: 10,
18 ttl: 1000 * 60 * 5,
19});
20
21async function fetchHtml(filename: string): Promise<string> {
22 const cacheKey = filename;
23 const cached = htmlCache.get(cacheKey);
24 if (cached) return cached;
25
26 const url = `${CDN_URL}/${filename}`;
27 const res = await fetch(url);
28 if (!res.ok) {
29 throw new Error(`Failed to fetch ${url}: ${res.status}`);
30 }
31
32 const html = await res.text();
33 htmlCache.set(cacheKey, html);
34 return html;
35}
36
37function escapeHtml(str: string): string {
38 return str
39 .replace(/&/g, "&")
40 .replace(/</g, "<")
41 .replace(/>/g, ">")
42 .replace(/"/g, """);
43}
44
45const server = Bun.serve({
46 port: PORT,
47 async fetch(req) {
48 const url = new URL(req.url);
49 const path = url.pathname;
50
51 // Health check
52 if (path === "/health") {
53 return new Response("ok");
54 }
55
56 // OG Image endpoint: /og/docs/:handle/:slug.png
57 const ogMatch = path.match(/^\/og\/docs\/([^/]+)\/([^/]+)\.png$/);
58 if (ogMatch) {
59 const [, handle, slug] = ogMatch;
60
61 try {
62 const doc = await fetchDocument(handle, slug);
63 if (!doc) {
64 return new Response("Document not found", { status: 404 });
65 }
66
67 // Cache by cid - auto-invalidates when doc changes
68 const cacheKey = doc.cid;
69 const cached = imageCache.get(cacheKey);
70 if (cached) {
71 return new Response(cached, {
72 headers: { "Content-Type": "image/png" },
73 });
74 }
75
76 const imageResponse = await generateOgImage({
77 title: doc.title,
78 preview: extractPreview(doc.blocks),
79 author: doc.actorHandle,
80 avatarUrl: doc.appBskyActorProfileByDid?.avatar?.url,
81 });
82
83 const buffer = new Uint8Array(await imageResponse.arrayBuffer());
84 imageCache.set(cacheKey, buffer);
85
86 return new Response(buffer, {
87 headers: { "Content-Type": "image/png" },
88 });
89 } catch (err) {
90 console.error("OG image error:", err);
91 return new Response("Error generating image", { status: 500 });
92 }
93 }
94
95 // Docs endpoint: /docs/:handle/:slug (with OG tags)
96 const docsMatch = path.match(/^\/docs\/([^/]+)\/([^/]+)$/);
97 if (docsMatch) {
98 const [, handle, slug] = docsMatch;
99
100 try {
101 const docsHtml = await fetchHtml("docs");
102 const doc = await fetchDocument(handle, slug);
103
104 if (!doc) {
105 return new Response(docsHtml, {
106 headers: { "Content-Type": "text/html" },
107 });
108 }
109
110 const preview = extractPreview(doc.blocks);
111 const ogImageUrl = `${BASE_URL}/og/docs/${encodeURIComponent(handle)}/${encodeURIComponent(slug)}.png`;
112
113 const ogTags = `
114 <meta property="og:title" content="${escapeHtml(doc.title)}" />
115 <meta property="og:description" content="${escapeHtml(preview)}" />
116 <meta property="og:image" content="${ogImageUrl}" />
117 <meta property="og:image:width" content="1200" />
118 <meta property="og:image:height" content="630" />
119 <meta property="og:type" content="article" />
120 <meta name="twitter:card" content="summary_large_image" />`;
121
122 const html = docsHtml.replace("<head>", `<head>${ogTags}`);
123 return new Response(html, {
124 headers: { "Content-Type": "text/html" },
125 });
126 } catch (err) {
127 console.error("OG tag injection error:", err);
128 return new Response("Error loading page", { status: 500 });
129 }
130 }
131
132 // Fallback for /docs/* (callback, settings, etc) - serve HTML without OG injection
133 if (path.startsWith("/docs/")) {
134 try {
135 const docsHtml = await fetchHtml("docs");
136 return new Response(docsHtml, {
137 headers: { "Content-Type": "text/html" },
138 });
139 } catch (err) {
140 console.error("Docs fallback error:", err);
141 return new Response("Error loading page", { status: 500 });
142 }
143 }
144
145 // OG Image endpoint for lexicon-explorer: /og/lexicon-explorer/:nsid.png
146 const lexiconOgMatch = path.match(/^\/og\/lexicon-explorer\/(.+)\.png$/);
147 if (lexiconOgMatch) {
148 const nsid = decodeURIComponent(lexiconOgMatch[1]);
149
150 try {
151 const lexicon = await fetchLexicon(nsid);
152 if (!lexicon) {
153 return new Response("Lexicon not found", { status: 404 });
154 }
155
156 // Cache by nsid
157 const cacheKey = `lexicon:${nsid}`;
158 const cached = imageCache.get(cacheKey);
159 if (cached) {
160 return new Response(cached, {
161 headers: { "Content-Type": "image/png" },
162 });
163 }
164
165 const previewDef = getLexiconPreviewDef(lexicon);
166
167 const imageResponse = await generateLexiconOgImage({
168 nsid: lexicon.id,
169 type: getLexiconType(lexicon),
170 author: lexicon.actorHandle || "unknown",
171 jsonDef: previewDef,
172 });
173
174 const buffer = new Uint8Array(await imageResponse.arrayBuffer());
175 imageCache.set(cacheKey, buffer);
176
177 return new Response(buffer, {
178 headers: { "Content-Type": "image/png" },
179 });
180 } catch (err) {
181 console.error("Lexicon OG image error:", err);
182 return new Response("Error generating image", { status: 500 });
183 }
184 }
185
186 // Lexicon explorer endpoint: /lexicon-explorer?nsid=xxx (with OG tags)
187 if (path === "/lexicon-explorer") {
188 const nsid = url.searchParams.get("nsid");
189
190 try {
191 const explorerHtml = await fetchHtml("lexicon-explorer");
192
193 if (!nsid) {
194 // No nsid param, serve without OG injection
195 return new Response(explorerHtml, {
196 headers: { "Content-Type": "text/html" },
197 });
198 }
199
200 const lexicon = await fetchLexicon(nsid);
201 if (!lexicon) {
202 // Lexicon not found, serve without OG injection
203 return new Response(explorerHtml, {
204 headers: { "Content-Type": "text/html" },
205 });
206 }
207
208 const type = getLexiconType(lexicon);
209 const description = lexicon.description || `${type} lexicon by @${lexicon.actorHandle}`;
210 const ogImageUrl = `${BASE_URL}/og/lexicon-explorer/${encodeURIComponent(nsid)}.png`;
211
212 const ogTags = `
213 <meta property="og:title" content="${escapeHtml(nsid)}" />
214 <meta property="og:description" content="${escapeHtml(description)}" />
215 <meta property="og:image" content="${ogImageUrl}" />
216 <meta property="og:image:width" content="1200" />
217 <meta property="og:image:height" content="630" />
218 <meta property="og:type" content="website" />
219 <meta name="twitter:card" content="summary_large_image" />`;
220
221 const html = explorerHtml.replace("<head>", `<head>${ogTags}`);
222 return new Response(html, {
223 headers: { "Content-Type": "text/html" },
224 });
225 } catch (err) {
226 console.error("Lexicon explorer error:", err);
227 return new Response("Error loading page", { status: 500 });
228 }
229 }
230
231 return new Response("Not found", { status: 404 });
232 },
233});
234
235console.log(`OG server running on port ${server.port}`);
236console.log(`Fetching HTML from: ${CDN_URL}`);