search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: dynamic OG images for link previews

generate per-query 1200x630 PNG images via workers-og with dark terminal
aesthetic, filter chips with type-matched colors, top 3 result titles,
and result count. rewrite meta tag injection to use HTMLRewriter with
support for all 5 URL params (q, tag, platform, since, mode).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz 5651c1c9 677aa9c4

+674 -48
+113 -46
site/functions/[[path]].js
··· 1 1 // dynamic OG tags for link previews 2 - export async function onRequest(context) { 3 - const url = new URL(context.request.url); 4 - const query = url.searchParams.get('q'); 5 - const tag = url.searchParams.get('tag'); 2 + const DATE_PRESET_LABELS = { week: 'last week', month: 'last month', year: 'last year' }; 6 3 7 - // if no query or tag param, just serve the static file 8 - if (!query && !tag) { 9 - return context.next(); 10 - } 4 + function presetFromSince(since) { 5 + if (!since) return null; 6 + const now = new Date(); 7 + const d = new Date(since); 8 + const days = Math.round((now - d) / (1000 * 60 * 60 * 24)); 9 + if (days <= 8) return 'week'; 10 + if (days <= 32) return 'month'; 11 + if (days <= 370) return 'year'; 12 + return null; 13 + } 11 14 12 - // fetch the original HTML 13 - const response = await context.next(); 14 - let html = await response.text(); 15 + function buildTitle(params) { 16 + const parts = []; 15 17 16 - // build OG meta tags 17 - let title, description; 18 - if (query && tag) { 19 - title = `"${query}" in #${tag} - leaflet search`; 20 - description = `search results for "${query}" tagged #${tag}`; 21 - } else if (query) { 22 - title = `"${query}" - leaflet search`; 23 - description = `search results for "${query}" on leaflet`; 24 - } else { 25 - title = `#${tag} - leaflet search`; 26 - description = `leaflet documents tagged #${tag}`; 18 + if (params.q) parts.push(`"${params.q}"`); 19 + if (params.tag) parts.push(`#${params.tag}`); 20 + 21 + let suffix = ''; 22 + const modifiers = []; 23 + if (params.platform) modifiers.push(`on ${params.platform}`); 24 + if (params.since) { 25 + const preset = presetFromSince(params.since); 26 + const label = preset ? DATE_PRESET_LABELS[preset] : params.since; 27 + modifiers.push(label); 27 28 } 28 - const ogUrl = url.toString(); 29 + if (modifiers.length > 0) suffix = ` ${modifiers.join(', ')}`; 29 30 30 - // remove existing OG tags 31 - html = html.replace(/<meta property="og:[^"]*"[^>]*>/g, ''); 32 - html = html.replace(/<meta name="twitter:[^"]*"[^>]*>/g, ''); 31 + if (parts.length === 0 && suffix) { 32 + return `search${suffix} - pub search`; 33 + } 34 + if (parts.length === 0) return null; // no params, skip rewrite 33 35 34 - const ogTags = ` 35 - <meta property="og:title" content="${escapeHtml(title)}" /> 36 - <meta property="og:description" content="${escapeHtml(description)}" /> 37 - <meta property="og:url" content="${escapeHtml(ogUrl)}" /> 38 - <meta property="og:type" content="website" /> 39 - <meta name="twitter:card" content="summary" /> 40 - <meta name="twitter:title" content="${escapeHtml(title)}" /> 41 - <meta name="twitter:description" content="${escapeHtml(description)}" /> 42 - `; 36 + return `${parts.join(' in ')}${suffix} - pub search`; 37 + } 43 38 44 - // inject OG tags into <head> 45 - const modifiedHtml = html.replace('</head>', `${ogTags}</head>`); 39 + function buildDescription(params) { 40 + const parts = []; 41 + 42 + if (params.q) parts.push(`search results for "${params.q}"`); 43 + else if (params.tag) parts.push(`documents tagged #${params.tag}`); 44 + else parts.push('search results'); 45 + 46 + if (params.platform) parts.push(`on ${params.platform}`); 47 + if (params.since) { 48 + const preset = presetFromSince(params.since); 49 + const label = preset ? DATE_PRESET_LABELS[preset] : params.since; 50 + parts.push(label); 51 + } 46 52 47 - return new Response(modifiedHtml, { 48 - headers: { 49 - 'content-type': 'text/html;charset=UTF-8', 50 - }, 51 - }); 53 + return parts.join(', '); 52 54 } 53 55 54 - function escapeHtml(str) { 56 + function escapeAttr(str) { 55 57 return str 56 58 .replace(/&/g, '&amp;') 59 + .replace(/"/g, '&quot;') 57 60 .replace(/</g, '&lt;') 58 - .replace(/>/g, '&gt;') 59 - .replace(/"/g, '&quot;') 60 - .replace(/'/g, '&#39;'); 61 + .replace(/>/g, '&gt;'); 62 + } 63 + 64 + export async function onRequest(context) { 65 + const url = new URL(context.request.url); 66 + const q = url.searchParams.get('q'); 67 + const tag = url.searchParams.get('tag'); 68 + const platform = url.searchParams.get('platform'); 69 + const since = url.searchParams.get('since'); 70 + const mode = url.searchParams.get('mode'); 71 + 72 + // if no search params, pass through (static tags in index.html are fine) 73 + if (!q && !tag && !platform && !since) { 74 + return context.next(); 75 + } 76 + 77 + const title = buildTitle({ q, tag, platform, since }); 78 + const description = buildDescription({ q, tag, platform, since }); 79 + 80 + // build og:image URL with same search params 81 + const ogImageUrl = new URL('/og-image', url.origin); 82 + if (q) ogImageUrl.searchParams.set('q', q); 83 + if (tag) ogImageUrl.searchParams.set('tag', tag); 84 + if (platform) ogImageUrl.searchParams.set('platform', platform); 85 + if (since) ogImageUrl.searchParams.set('since', since); 86 + if (mode) ogImageUrl.searchParams.set('mode', mode); 87 + 88 + const ogUrl = url.toString(); 89 + 90 + const response = await context.next(); 91 + 92 + return new HTMLRewriter() 93 + // remove existing OG/twitter meta tags 94 + .on('meta[property^="og:"]', { element(el) { el.remove(); } }) 95 + .on('meta[name^="twitter:"]', { element(el) { el.remove(); } }) 96 + // update <title> 97 + .on('title', { 98 + element(el) { 99 + if (title) el.setInnerContent(escapeAttr(title), { html: true }); 100 + } 101 + }) 102 + // update description meta 103 + .on('meta[name="description"]', { 104 + element(el) { 105 + el.setAttribute('content', description); 106 + } 107 + }) 108 + // inject new OG tags before </head> 109 + .on('head', { 110 + element(el) { 111 + el.append(` 112 + <meta property="og:title" content="${escapeAttr(title || 'pub search')}" /> 113 + <meta property="og:description" content="${escapeAttr(description)}" /> 114 + <meta property="og:url" content="${escapeAttr(ogUrl)}" /> 115 + <meta property="og:type" content="website" /> 116 + <meta property="og:site_name" content="pub search" /> 117 + <meta property="og:image" content="${escapeAttr(ogImageUrl.toString())}" /> 118 + <meta property="og:image:width" content="1200" /> 119 + <meta property="og:image:height" content="630" /> 120 + <meta name="twitter:card" content="summary_large_image" /> 121 + <meta name="twitter:title" content="${escapeAttr(title || 'pub search')}" /> 122 + <meta name="twitter:description" content="${escapeAttr(description)}" /> 123 + <meta name="twitter:image" content="${escapeAttr(ogImageUrl.toString())}" /> 124 + `, { html: true }); 125 + } 126 + }) 127 + .transform(response); 61 128 }
+350
site/functions/og-image.js
··· 1 + import { ImageResponse } from "workers-og"; 2 + 3 + const API_URL = "https://leaflet-search-backend.fly.dev"; 4 + 5 + const DATE_PRESET_LABELS = { 6 + week: "last week", 7 + month: "last month", 8 + year: "last year", 9 + }; 10 + 11 + // reverse-map a since ISO date to a human label 12 + function labelFromSince(since) { 13 + if (!since) return null; 14 + const now = new Date(); 15 + const d = new Date(since); 16 + const days = Math.round((now - d) / (1000 * 60 * 60 * 24)); 17 + if (days <= 8) return DATE_PRESET_LABELS.week; 18 + if (days <= 32) return DATE_PRESET_LABELS.month; 19 + if (days <= 370) return DATE_PRESET_LABELS.year; 20 + return since; // fallback to raw date 21 + } 22 + 23 + // chip colors matching the frontend 24 + const CHIP_COLORS = { 25 + tag: { bg: "rgba(27, 115, 64, 0.3)", border: "#1B7340", text: "#2a9d5c" }, 26 + platform: { 27 + bg: "rgba(180, 100, 64, 0.3)", 28 + border: "#d4956a", 29 + text: "#d4956a", 30 + }, 31 + date: { bg: "rgba(14, 165, 233, 0.3)", border: "#0ea5e9", text: "#38bdf8" }, 32 + mode: { bg: "rgba(139, 92, 246, 0.3)", border: "#8b5cf6", text: "#a78bfa" }, 33 + }; 34 + 35 + function truncate(str, max) { 36 + if (!str) return ""; 37 + return str.length > max ? str.slice(0, max) + "..." : str; 38 + } 39 + 40 + function buildChip(label, type) { 41 + const c = CHIP_COLORS[type] || CHIP_COLORS.tag; 42 + return { 43 + type: "div", 44 + props: { 45 + style: { 46 + background: c.bg, 47 + border: `1px solid ${c.border}`, 48 + color: c.text, 49 + padding: "6px 16px", 50 + borderRadius: "6px", 51 + fontSize: "22px", 52 + fontFamily: '"JetBrains Mono", monospace', 53 + }, 54 + children: label, 55 + }, 56 + }; 57 + } 58 + 59 + async function fetchSearchResults(params) { 60 + const url = new URL(`${API_URL}/search`); 61 + url.searchParams.set("format", "v2"); 62 + url.searchParams.set("limit", "3"); 63 + if (params.q) url.searchParams.set("q", params.q); 64 + if (params.tag) url.searchParams.set("tag", params.tag); 65 + if (params.platform) url.searchParams.set("platform", params.platform); 66 + if (params.since) url.searchParams.set("since", params.since); 67 + if (params.mode) url.searchParams.set("mode", params.mode); 68 + 69 + const controller = new AbortController(); 70 + const timeout = setTimeout(() => controller.abort(), 2000); 71 + try { 72 + const res = await fetch(url.toString(), { signal: controller.signal }); 73 + clearTimeout(timeout); 74 + const data = await res.json(); 75 + return { 76 + results: data.results || [], 77 + total: data.total || (data.results ? data.results.length : 0), 78 + }; 79 + } catch { 80 + clearTimeout(timeout); 81 + return null; 82 + } 83 + } 84 + 85 + async function fetchStats() { 86 + const controller = new AbortController(); 87 + const timeout = setTimeout(() => controller.abort(), 2000); 88 + try { 89 + const res = await fetch(`${API_URL}/stats`, { 90 + signal: controller.signal, 91 + }); 92 + clearTimeout(timeout); 93 + return await res.json(); 94 + } catch { 95 + clearTimeout(timeout); 96 + return null; 97 + } 98 + } 99 + 100 + export async function onRequest(context) { 101 + const url = new URL(context.request.url); 102 + const q = url.searchParams.get("q"); 103 + const tag = url.searchParams.get("tag"); 104 + const platform = url.searchParams.get("platform"); 105 + const since = url.searchParams.get("since"); 106 + const mode = url.searchParams.get("mode"); 107 + 108 + const hasParams = q || tag || platform || since; 109 + 110 + // build chips 111 + const chips = []; 112 + if (tag) chips.push(buildChip(`#${tag}`, "tag")); 113 + if (platform) chips.push(buildChip(platform, "platform")); 114 + if (since) { 115 + const label = labelFromSince(since); 116 + if (label) chips.push(buildChip(label, "date")); 117 + } 118 + if (mode && mode !== "keyword") chips.push(buildChip(mode, "mode")); 119 + 120 + let titles = []; 121 + let total = 0; 122 + let docCount = null; 123 + 124 + if (hasParams) { 125 + const data = await fetchSearchResults({ q, tag, platform, since, mode }); 126 + if (data) { 127 + titles = data.results.map((r) => truncate(r.title || "untitled", 55)); 128 + total = data.total; 129 + } 130 + } else { 131 + // homepage — show stats 132 + const stats = await fetchStats(); 133 + if (stats) { 134 + docCount = stats.documents; 135 + } 136 + } 137 + 138 + // build the image JSX-like structure for workers-og 139 + const children = []; 140 + 141 + // header 142 + children.push({ 143 + type: "div", 144 + props: { 145 + style: { 146 + color: "#888", 147 + fontSize: "28px", 148 + fontFamily: '"JetBrains Mono", monospace', 149 + marginBottom: "8px", 150 + }, 151 + children: "pub search", 152 + }, 153 + }); 154 + 155 + if (hasParams) { 156 + // query 157 + if (q) { 158 + children.push({ 159 + type: "div", 160 + props: { 161 + style: { 162 + color: "#fff", 163 + fontSize: "42px", 164 + fontFamily: '"JetBrains Mono", monospace', 165 + marginTop: "16px", 166 + }, 167 + children: `"${truncate(q, 45)}"`, 168 + }, 169 + }); 170 + } 171 + 172 + // chips row 173 + if (chips.length > 0) { 174 + children.push({ 175 + type: "div", 176 + props: { 177 + style: { 178 + display: "flex", 179 + gap: "12px", 180 + marginTop: "20px", 181 + }, 182 + children: chips.map((c) => c), 183 + }, 184 + }); 185 + } 186 + 187 + // divider 188 + children.push({ 189 + type: "div", 190 + props: { 191 + style: { 192 + width: "100%", 193 + height: "1px", 194 + background: "#333", 195 + marginTop: "28px", 196 + marginBottom: "12px", 197 + }, 198 + children: "", 199 + }, 200 + }); 201 + 202 + // result titles 203 + for (const title of titles) { 204 + children.push({ 205 + type: "div", 206 + props: { 207 + style: { 208 + color: "#888", 209 + fontSize: "24px", 210 + fontFamily: '"JetBrains Mono", monospace', 211 + marginBottom: "8px", 212 + overflow: "hidden", 213 + }, 214 + children: title, 215 + }, 216 + }); 217 + } 218 + 219 + if (titles.length === 0) { 220 + children.push({ 221 + type: "div", 222 + props: { 223 + style: { 224 + color: "#555", 225 + fontSize: "24px", 226 + fontFamily: '"JetBrains Mono", monospace', 227 + }, 228 + children: "no results", 229 + }, 230 + }); 231 + } 232 + 233 + // footer 234 + const footerParts = []; 235 + if (total > 0) footerParts.push(`${total} result${total === 1 ? "" : "s"}`); 236 + footerParts.push("search atproto publishing platforms"); 237 + 238 + children.push({ 239 + type: "div", 240 + props: { 241 + style: { 242 + color: "#555", 243 + fontSize: "20px", 244 + fontFamily: '"JetBrains Mono", monospace', 245 + marginTop: "auto", 246 + }, 247 + children: footerParts.join(" · "), 248 + }, 249 + }); 250 + } else { 251 + // homepage image 252 + children.push({ 253 + type: "div", 254 + props: { 255 + style: { 256 + color: "#555", 257 + fontSize: "24px", 258 + fontFamily: '"JetBrains Mono", monospace', 259 + marginTop: "24px", 260 + }, 261 + children: "search atproto publishing platforms", 262 + }, 263 + }); 264 + 265 + // platform list 266 + const platforms = [ 267 + "leaflet", 268 + "pckt", 269 + "offprint", 270 + "greengale", 271 + "whitewind", 272 + ]; 273 + children.push({ 274 + type: "div", 275 + props: { 276 + style: { 277 + display: "flex", 278 + gap: "12px", 279 + marginTop: "24px", 280 + }, 281 + children: platforms.map((p) => buildChip(p, "platform")), 282 + }, 283 + }); 284 + 285 + if (docCount) { 286 + children.push({ 287 + type: "div", 288 + props: { 289 + style: { 290 + color: "#555", 291 + fontSize: "20px", 292 + fontFamily: '"JetBrains Mono", monospace', 293 + marginTop: "auto", 294 + }, 295 + children: `${docCount.toLocaleString()} documents indexed`, 296 + }, 297 + }); 298 + } 299 + } 300 + 301 + const html = { 302 + type: "div", 303 + props: { 304 + style: { 305 + display: "flex", 306 + flexDirection: "column", 307 + width: "1200px", 308 + height: "630px", 309 + background: "#0a0a0a", 310 + padding: "48px 56px", 311 + fontFamily: '"JetBrains Mono", monospace', 312 + }, 313 + children, 314 + }, 315 + }; 316 + 317 + return new ImageResponse(html, { 318 + width: 1200, 319 + height: 630, 320 + fonts: [ 321 + { 322 + name: "JetBrains Mono", 323 + data: await loadGoogleFont("JetBrains Mono"), 324 + style: "normal", 325 + }, 326 + ], 327 + headers: { 328 + "Cache-Control": "public, max-age=3600", 329 + }, 330 + }); 331 + } 332 + 333 + async function loadGoogleFont(font) { 334 + const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(font)}`; 335 + const css = await ( 336 + await fetch(url, { 337 + headers: { 338 + "User-Agent": 339 + "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", 340 + }, 341 + }) 342 + ).text(); 343 + 344 + const match = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/); 345 + if (!match) { 346 + throw new Error(`Failed to load font: ${font}`); 347 + } 348 + 349 + return await (await fetch(match[1])).arrayBuffer(); 350 + }
+206 -1
site/package-lock.json
··· 5 5 "packages": { 6 6 "": { 7 7 "name": "leaflet-search-site", 8 + "dependencies": { 9 + "workers-og": "^0.0.20" 10 + }, 8 11 "devDependencies": { 9 12 "wrangler": "^4.0.0" 10 13 } ··· 1026 1029 "dev": true, 1027 1030 "license": "MIT" 1028 1031 }, 1032 + "node_modules/@resvg/resvg-wasm": { 1033 + "version": "2.6.2", 1034 + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.6.2.tgz", 1035 + "integrity": "sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==", 1036 + "license": "MPL-2.0", 1037 + "engines": { 1038 + "node": ">= 10" 1039 + } 1040 + }, 1041 + "node_modules/@shuding/opentype.js": { 1042 + "version": "1.4.0-beta.0", 1043 + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", 1044 + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", 1045 + "license": "MIT", 1046 + "dependencies": { 1047 + "fflate": "^0.7.3", 1048 + "string.prototype.codepointat": "^0.2.1" 1049 + }, 1050 + "bin": { 1051 + "ot": "bin/ot" 1052 + }, 1053 + "engines": { 1054 + "node": ">= 8.0.0" 1055 + } 1056 + }, 1029 1057 "node_modules/@sindresorhus/is": { 1030 1058 "version": "7.2.0", 1031 1059 "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", ··· 1069 1097 "node": ">=0.4.0" 1070 1098 } 1071 1099 }, 1100 + "node_modules/base64-js": { 1101 + "version": "0.0.8", 1102 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", 1103 + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", 1104 + "license": "MIT", 1105 + "engines": { 1106 + "node": ">= 0.4" 1107 + } 1108 + }, 1072 1109 "node_modules/blake3-wasm": { 1073 1110 "version": "2.1.5", 1074 1111 "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", ··· 1076 1113 "dev": true, 1077 1114 "license": "MIT" 1078 1115 }, 1116 + "node_modules/camelize": { 1117 + "version": "1.0.1", 1118 + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", 1119 + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", 1120 + "license": "MIT", 1121 + "funding": { 1122 + "url": "https://github.com/sponsors/ljharb" 1123 + } 1124 + }, 1079 1125 "node_modules/color": { 1080 1126 "version": "4.2.3", 1081 1127 "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", ··· 1107 1153 "version": "1.1.4", 1108 1154 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1109 1155 "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1110 - "dev": true, 1111 1156 "license": "MIT" 1112 1157 }, 1113 1158 "node_modules/color-string": { ··· 1135 1180 "url": "https://opencollective.com/express" 1136 1181 } 1137 1182 }, 1183 + "node_modules/css-background-parser": { 1184 + "version": "0.1.0", 1185 + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", 1186 + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", 1187 + "license": "MIT" 1188 + }, 1189 + "node_modules/css-box-shadow": { 1190 + "version": "1.0.0-3", 1191 + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", 1192 + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", 1193 + "license": "MIT" 1194 + }, 1195 + "node_modules/css-color-keywords": { 1196 + "version": "1.0.0", 1197 + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", 1198 + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", 1199 + "license": "ISC", 1200 + "engines": { 1201 + "node": ">=4" 1202 + } 1203 + }, 1204 + "node_modules/css-to-react-native": { 1205 + "version": "3.2.0", 1206 + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", 1207 + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", 1208 + "license": "MIT", 1209 + "dependencies": { 1210 + "camelize": "^1.0.0", 1211 + "css-color-keywords": "^1.0.0", 1212 + "postcss-value-parser": "^4.0.2" 1213 + } 1214 + }, 1138 1215 "node_modules/detect-libc": { 1139 1216 "version": "2.1.2", 1140 1217 "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", ··· 1145 1222 "node": ">=8" 1146 1223 } 1147 1224 }, 1225 + "node_modules/emoji-regex": { 1226 + "version": "10.6.0", 1227 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", 1228 + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", 1229 + "license": "MIT" 1230 + }, 1148 1231 "node_modules/error-stack-parser-es": { 1149 1232 "version": "1.0.5", 1150 1233 "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", ··· 1197 1280 "@esbuild/win32-x64": "0.27.0" 1198 1281 } 1199 1282 }, 1283 + "node_modules/escape-html": { 1284 + "version": "1.0.3", 1285 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1286 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 1287 + "license": "MIT" 1288 + }, 1200 1289 "node_modules/exit-hook": { 1201 1290 "version": "2.2.1", 1202 1291 "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", ··· 1210 1299 "url": "https://github.com/sponsors/sindresorhus" 1211 1300 } 1212 1301 }, 1302 + "node_modules/fflate": { 1303 + "version": "0.7.4", 1304 + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", 1305 + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", 1306 + "license": "MIT" 1307 + }, 1213 1308 "node_modules/fsevents": { 1214 1309 "version": "2.3.3", 1215 1310 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", ··· 1232 1327 "dev": true, 1233 1328 "license": "BSD-2-Clause" 1234 1329 }, 1330 + "node_modules/hex-rgb": { 1331 + "version": "4.3.0", 1332 + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", 1333 + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", 1334 + "license": "MIT", 1335 + "engines": { 1336 + "node": ">=6" 1337 + }, 1338 + "funding": { 1339 + "url": "https://github.com/sponsors/sindresorhus" 1340 + } 1341 + }, 1235 1342 "node_modules/is-arrayish": { 1236 1343 "version": "0.3.4", 1237 1344 "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", 1238 1345 "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", 1239 1346 "dev": true, 1347 + "license": "MIT" 1348 + }, 1349 + "node_modules/just-camel-case": { 1350 + "version": "6.2.0", 1351 + "resolved": "https://registry.npmjs.org/just-camel-case/-/just-camel-case-6.2.0.tgz", 1352 + "integrity": "sha512-ICenRLXwkQYLk3UyvLQZ+uKuwFVJ3JHFYFn7F2782G2Mv2hW8WPePqgdhpnjGaqkYtSVWnyCESZhGXUmY3/bEg==", 1240 1353 "license": "MIT" 1241 1354 }, 1242 1355 "node_modules/kleur": { ··· 1247 1360 "license": "MIT", 1248 1361 "engines": { 1249 1362 "node": ">=6" 1363 + } 1364 + }, 1365 + "node_modules/linebreak": { 1366 + "version": "1.1.0", 1367 + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", 1368 + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", 1369 + "license": "MIT", 1370 + "dependencies": { 1371 + "base64-js": "0.0.8", 1372 + "unicode-trie": "^2.0.0" 1250 1373 } 1251 1374 }, 1252 1375 "node_modules/mime": { ··· 1289 1412 "node": ">=18.0.0" 1290 1413 } 1291 1414 }, 1415 + "node_modules/pako": { 1416 + "version": "0.2.9", 1417 + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", 1418 + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", 1419 + "license": "MIT" 1420 + }, 1421 + "node_modules/parse-css-color": { 1422 + "version": "0.2.1", 1423 + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", 1424 + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", 1425 + "license": "MIT", 1426 + "dependencies": { 1427 + "color-name": "^1.1.4", 1428 + "hex-rgb": "^4.1.0" 1429 + } 1430 + }, 1292 1431 "node_modules/path-to-regexp": { 1293 1432 "version": "6.3.0", 1294 1433 "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", ··· 1303 1442 "dev": true, 1304 1443 "license": "MIT" 1305 1444 }, 1445 + "node_modules/postcss-value-parser": { 1446 + "version": "4.2.0", 1447 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 1448 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 1449 + "license": "MIT" 1450 + }, 1451 + "node_modules/satori": { 1452 + "version": "0.10.14", 1453 + "resolved": "https://registry.npmjs.org/satori/-/satori-0.10.14.tgz", 1454 + "integrity": "sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==", 1455 + "license": "MPL-2.0", 1456 + "dependencies": { 1457 + "@shuding/opentype.js": "1.4.0-beta.0", 1458 + "css-background-parser": "^0.1.0", 1459 + "css-box-shadow": "1.0.0-3", 1460 + "css-to-react-native": "^3.0.0", 1461 + "emoji-regex": "^10.2.1", 1462 + "escape-html": "^1.0.3", 1463 + "linebreak": "^1.1.0", 1464 + "parse-css-color": "^0.2.1", 1465 + "postcss-value-parser": "^4.2.0", 1466 + "yoga-wasm-web": "^0.3.3" 1467 + }, 1468 + "engines": { 1469 + "node": ">=16" 1470 + } 1471 + }, 1306 1472 "node_modules/semver": { 1307 1473 "version": "7.7.3", 1308 1474 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", ··· 1377 1543 "npm": ">=6" 1378 1544 } 1379 1545 }, 1546 + "node_modules/string.prototype.codepointat": { 1547 + "version": "0.2.1", 1548 + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", 1549 + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", 1550 + "license": "MIT" 1551 + }, 1380 1552 "node_modules/supports-color": { 1381 1553 "version": "10.2.2", 1382 1554 "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", ··· 1389 1561 "funding": { 1390 1562 "url": "https://github.com/chalk/supports-color?sponsor=1" 1391 1563 } 1564 + }, 1565 + "node_modules/tiny-inflate": { 1566 + "version": "1.0.3", 1567 + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", 1568 + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", 1569 + "license": "MIT" 1392 1570 }, 1393 1571 "node_modules/tslib": { 1394 1572 "version": "2.8.1", ··· 1418 1596 "pathe": "^2.0.3" 1419 1597 } 1420 1598 }, 1599 + "node_modules/unicode-trie": { 1600 + "version": "2.0.0", 1601 + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", 1602 + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", 1603 + "license": "MIT", 1604 + "dependencies": { 1605 + "pako": "^0.2.5", 1606 + "tiny-inflate": "^1.0.0" 1607 + } 1608 + }, 1421 1609 "node_modules/workerd": { 1422 1610 "version": "1.20251210.0", 1423 1611 "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251210.0.tgz", ··· 1437 1625 "@cloudflare/workerd-linux-64": "1.20251210.0", 1438 1626 "@cloudflare/workerd-linux-arm64": "1.20251210.0", 1439 1627 "@cloudflare/workerd-windows-64": "1.20251210.0" 1628 + } 1629 + }, 1630 + "node_modules/workers-og": { 1631 + "version": "0.0.20", 1632 + "resolved": "https://registry.npmjs.org/workers-og/-/workers-og-0.0.20.tgz", 1633 + "integrity": "sha512-GgWLpfpGxzgqYnT2HdBybXdAePn7+W5zQUl+NUbnggp2zeCaliZdQh5su7XXM5TSAlveSAiwRrG/kOFSIXiOKQ==", 1634 + "dependencies": { 1635 + "@resvg/resvg-wasm": "^2.4.0", 1636 + "just-camel-case": "^6.2.0", 1637 + "satori": "^0.10.11", 1638 + "yoga-wasm-web": "^0.3.3" 1440 1639 } 1441 1640 }, 1442 1641 "node_modules/wrangler": { ··· 1495 1694 "optional": true 1496 1695 } 1497 1696 } 1697 + }, 1698 + "node_modules/yoga-wasm-web": { 1699 + "version": "0.3.3", 1700 + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", 1701 + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", 1702 + "license": "MIT" 1498 1703 }, 1499 1704 "node_modules/youch": { 1500 1705 "version": "4.1.0-beta.10",
+3
site/package.json
··· 5 5 "dev": "wrangler pages dev --port 8788", 6 6 "deploy": "wrangler pages deploy" 7 7 }, 8 + "dependencies": { 9 + "workers-og": "^0.0.20" 10 + }, 8 11 "devDependencies": { 9 12 "wrangler": "^4.0.0" 10 13 }
+2 -1
site/wrangler.json
··· 1 1 { 2 2 "name": "leaflet-search", 3 - "pages_build_output_dir": "." 3 + "pages_build_output_dir": ".", 4 + "compatibility_date": "2024-09-23" 4 5 }