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: add link preview for constellation page

Add OG meta tags to constellation.html and extend og-image.js to
generate a constellation-themed card with scattered platform-colored
dots, platform legend, and document count.

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

+206
+11
site/constellation.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 6 6 <title>pub search / constellation</title> 7 + <meta name="description" content="2d semantic map of atproto publishing platforms"> 8 + <meta property="og:title" content="pub search / constellation"> 9 + <meta property="og:description" content="2d semantic map of atproto publishing platforms"> 10 + <meta property="og:type" content="website"> 11 + <meta property="og:image" content="https://pub-search.waow.tech/og-image?page=constellation"> 12 + <meta property="og:image:width" content="1200"> 13 + <meta property="og:image:height" content="630"> 14 + <meta name="twitter:card" content="summary_large_image"> 15 + <meta name="twitter:title" content="pub search / constellation"> 16 + <meta name="twitter:description" content="2d semantic map of atproto publishing platforms"> 17 + <meta name="twitter:image" content="https://pub-search.waow.tech/og-image?page=constellation"> 7 18 <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='2' fill='%231B7340'/><circle cx='8' cy='10' r='1.5' fill='%231B7340' opacity='.6'/><circle cx='24' cy='8' r='1' fill='%231B7340' opacity='.4'/><circle cx='22' cy='22' r='1.5' fill='%231B7340' opacity='.5'/><circle cx='6' cy='24' r='1' fill='%231B7340' opacity='.3'/></svg>"> 8 19 <script> 9 20 (function() {
+195
site/functions/og-image.js
··· 101 101 } 102 102 } 103 103 104 + // platform colors for constellation OG image (core colors from the canvas) 105 + const PLATFORM_DOTS = [ 106 + { color: "#4ade80", label: "leaflet" }, 107 + { color: "#60a5fa", label: "whitewind" }, 108 + { color: "#fbbf24", label: "pckt" }, 109 + { color: "#fb7185", label: "offprint" }, 110 + { color: "#2dd4bf", label: "greengale" }, 111 + { color: "#9ca3af", label: "other" }, 112 + ]; 113 + 114 + // deterministic "random" positions for constellation dots 115 + function constellationDots() { 116 + const dots = []; 117 + const positions = [ 118 + [180, 200], [340, 150], [520, 280], [700, 180], [850, 250], 119 + [240, 350], [450, 180], [620, 340], [780, 300], [950, 200], 120 + [300, 260], [500, 400], [680, 220], [400, 320], [560, 160], 121 + [820, 380], [200, 420], [730, 400], [900, 340], [380, 220], 122 + [150, 300], [480, 250], [650, 380], [770, 160], [920, 420], 123 + ]; 124 + for (let i = 0; i < positions.length; i++) { 125 + const platform = PLATFORM_DOTS[i % PLATFORM_DOTS.length]; 126 + const size = 4 + (i % 3) * 2; 127 + const opacity = 0.4 + (i % 4) * 0.15; 128 + dots.push({ 129 + type: "div", 130 + props: { 131 + style: { 132 + position: "absolute", 133 + left: `${positions[i][0]}px`, 134 + top: `${positions[i][1]}px`, 135 + width: `${size}px`, 136 + height: `${size}px`, 137 + borderRadius: "50%", 138 + background: platform.color, 139 + opacity: opacity, 140 + }, 141 + children: "", 142 + }, 143 + }); 144 + } 145 + return dots; 146 + } 147 + 148 + function buildConstellationImage(docCount) { 149 + const children = []; 150 + 151 + // scattered dots as background decoration 152 + children.push(...constellationDots()); 153 + 154 + // header 155 + children.push({ 156 + type: "div", 157 + props: { 158 + style: { 159 + color: "#888", 160 + fontSize: "28px", 161 + fontFamily: '"JetBrains Mono", monospace', 162 + marginBottom: "8px", 163 + }, 164 + children: "pub search", 165 + }, 166 + }); 167 + 168 + // title 169 + children.push({ 170 + type: "div", 171 + props: { 172 + style: { 173 + color: "#fff", 174 + fontSize: "48px", 175 + fontFamily: '"JetBrains Mono", monospace', 176 + marginTop: "16px", 177 + }, 178 + children: "constellation", 179 + }, 180 + }); 181 + 182 + // subtitle 183 + children.push({ 184 + type: "div", 185 + props: { 186 + style: { 187 + color: "#555", 188 + fontSize: "24px", 189 + fontFamily: '"JetBrains Mono", monospace', 190 + marginTop: "12px", 191 + }, 192 + children: "2d semantic map of the document index", 193 + }, 194 + }); 195 + 196 + // platform legend 197 + children.push({ 198 + type: "div", 199 + props: { 200 + style: { 201 + display: "flex", 202 + gap: "16px", 203 + marginTop: "32px", 204 + }, 205 + children: PLATFORM_DOTS.slice(0, 5).map((p) => ({ 206 + type: "div", 207 + props: { 208 + style: { 209 + display: "flex", 210 + alignItems: "center", 211 + gap: "6px", 212 + }, 213 + children: [ 214 + { 215 + type: "div", 216 + props: { 217 + style: { 218 + width: "8px", 219 + height: "8px", 220 + borderRadius: "50%", 221 + background: p.color, 222 + }, 223 + children: "", 224 + }, 225 + }, 226 + { 227 + type: "div", 228 + props: { 229 + style: { 230 + color: "#666", 231 + fontSize: "18px", 232 + fontFamily: '"JetBrains Mono", monospace', 233 + }, 234 + children: p.label, 235 + }, 236 + }, 237 + ], 238 + }, 239 + })), 240 + }, 241 + }); 242 + 243 + // footer 244 + const footerText = docCount 245 + ? `${docCount.toLocaleString()} documents · explore the index` 246 + : "explore the index"; 247 + children.push({ 248 + type: "div", 249 + props: { 250 + style: { 251 + color: "#555", 252 + fontSize: "20px", 253 + fontFamily: '"JetBrains Mono", monospace', 254 + marginTop: "auto", 255 + }, 256 + children: footerText, 257 + }, 258 + }); 259 + 260 + return { 261 + type: "div", 262 + props: { 263 + style: { 264 + display: "flex", 265 + flexDirection: "column", 266 + position: "relative", 267 + width: "1200px", 268 + height: "630px", 269 + background: "#050505", 270 + padding: "48px 56px", 271 + fontFamily: '"JetBrains Mono", monospace', 272 + }, 273 + children, 274 + }, 275 + }; 276 + } 277 + 104 278 export async function onRequest(context) { 105 279 const url = new URL(context.request.url); 280 + const page = url.searchParams.get("page"); 106 281 const q = url.searchParams.get("q"); 107 282 const tag = url.searchParams.get("tag"); 108 283 const platform = url.searchParams.get("platform"); 109 284 const since = url.searchParams.get("since"); 110 285 const mode = url.searchParams.get("mode"); 286 + 287 + // constellation page 288 + if (page === "constellation") { 289 + const stats = await fetchStats(); 290 + const html = buildConstellationImage(stats ? stats.documents : null); 291 + return new ImageResponse(html, { 292 + width: 1200, 293 + height: 630, 294 + fonts: [ 295 + { 296 + name: "JetBrains Mono", 297 + data: await loadGoogleFont("JetBrains Mono"), 298 + style: "normal", 299 + }, 300 + ], 301 + headers: { 302 + "Cache-Control": "public, max-age=3600", 303 + }, 304 + }); 305 + } 111 306 112 307 const hasParams = q || tag || platform || since; 113 308