One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links ๐Ÿ“… calendar.xyehr.cn
5
fork

Configure Feed

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

chore: remove share page og

authored by

Evan Huang and committed by
GitHub
abcc60e1 a19ae233

-157
-157
app/(app)/share/[id]/opengraph-image.tsx
··· 1 - import { ImageResponse } from "next/og" 2 - import { headers } from "next/headers" 3 - 4 - export const size = { 5 - width: 1200, 6 - height: 630, 7 - } 8 - 9 - export const contentType = "image/png" 10 - 11 - export const revalidate = 0 12 - 13 - const instrumentSansPromises = new Map<number, Promise<ArrayBuffer>>() 14 - 15 - async function loadInstrumentSans(weight: 400 | 500) { 16 - const cached = instrumentSansPromises.get(weight) 17 - if (cached) { 18 - return cached 19 - } 20 - 21 - const promise = (async () => { 22 - const cssResponse = await fetch( 23 - `https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@${weight}&display=swap`, 24 - ) 25 - const css = await cssResponse.text() 26 - const fontUrlMatch = css.match(/src: url\(([^)]+)\) format\('(opentype|truetype|woff2)'\)/) 27 - 28 - if (!fontUrlMatch?.[1]) { 29 - throw new Error(`Unable to resolve Instrument Sans ${weight} font URL`) 30 - } 31 - 32 - const fontResponse = await fetch(fontUrlMatch[1]) 33 - return fontResponse.arrayBuffer() 34 - })() 35 - 36 - instrumentSansPromises.set(weight, promise) 37 - return promise 38 - } 39 - 40 - async function getShareTitle(id: string) { 41 - const fallbackTitle = "One Calendar" 42 - 43 - try { 44 - const headerStore = await headers() 45 - const forwardedHost = headerStore.get("x-forwarded-host") 46 - const host = forwardedHost || headerStore.get("host") 47 - const protocol = headerStore.get("x-forwarded-proto") || "https" 48 - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || (host ? `${protocol}://${host}` : "http://localhost:3000") 49 - const res = await fetch(`${baseUrl}/api/share?id=${id}`, { 50 - cache: "no-store", 51 - }) 52 - 53 - if (res.status === 401) { 54 - const result = await res.json().catch(() => null) 55 - if (result?.requiresPassword) { 56 - return "Protected" 57 - } 58 - 59 - return fallbackTitle 60 - } 61 - 62 - if (!res.ok) { 63 - return fallbackTitle 64 - } 65 - 66 - const result = await res.json() 67 - 68 - if (!result.success || !result.data) { 69 - return fallbackTitle 70 - } 71 - 72 - const event = typeof result.data === "object" ? result.data : JSON.parse(result.data) 73 - return typeof event.title === "string" ? event.title : "Untitled" 74 - } catch { 75 - return fallbackTitle 76 - } 77 - } 78 - 79 - export default async function OpenGraphImage({ params }: { params: { id: string } }) { 80 - const eventTitle = await getShareTitle(params.id) 81 - const [instrumentSans400, instrumentSans500] = await Promise.all([ 82 - loadInstrumentSans(400), 83 - loadInstrumentSans(500), 84 - ]) 85 - 86 - return new ImageResponse( 87 - ( 88 - <div 89 - style={{ 90 - width: "100%", 91 - height: "100%", 92 - background: "#ffffff", 93 - display: "flex", 94 - flexDirection: "column", 95 - alignItems: "flex-start", 96 - justifyContent: "space-between", 97 - padding: "72px", 98 - color: "#000000", 99 - }} 100 - > 101 - <svg 102 - width="57" 103 - height="100" 104 - viewBox="324.5 178.12 367.99 643.88" 105 - xmlns="http://www.w3.org/2000/svg" 106 - > 107 - <g transform="translate(0,1000) scale(0.1,-0.1)" fill="#000000" stroke="none"> 108 - <path d="M4960 8206 c-87 -24 -164 -70 -231 -136 -101 -101 -149 -217 -149 -360 0 -144 48 -259 150 -360 102 -102 218 -150 360 -150 140 0 264 53 365 156 194 198 194 508 0 709 -67 69 -165 125 -253 144 -68 14 -184 13 -242 -3z" /> 109 - <path d="M3616 6859 c-109 -26 -239 -117 -307 -215 -97 -141 -111 -350 -34 -510 61 -126 166 -217 305 -264 55 -19 82 -21 175 -18 102 3 115 6 185 39 147 70 239 172 281 311 17 57 21 88 18 182 -4 109 -5 115 -46 198 -68 136 -202 245 -343 277 -54 13 -180 12 -234 0z" /> 110 - <path d="M4963 6855 c-228 -64 -383 -263 -383 -493 0 -149 45 -259 149 -363 105 -105 212 -149 362 -149 188 0 345 90 443 254 70 117 85 297 35 434 -48 130 -170 250 -306 302 -75 29 -225 37 -300 15z" /> 111 - <path d="M4940 5491 c-91 -29 -142 -61 -211 -130 -103 -103 -149 -214 -149 -361 0 -328 308 -570 629 -495 279 66 450 358 373 636 -46 164 -177 299 -340 349 -83 26 -224 26 -302 1z" /> 112 - <path d="M4980 4149 c-81 -16 -188 -76 -255 -145 -97 -100 -145 -215 -145 -354 0 -147 46 -258 149 -361 105 -105 212 -149 362 -149 455 0 680 547 358 869 -121 122 -296 174 -469 140z" /> 113 - <path d="M3601 2784 c-116 -31 -242 -125 -306 -229 -65 -105 -87 -283 -50 -410 61 -215 263 -365 490 -365 134 0 244 43 343 135 118 109 162 211 162 376 0 160 -46 267 -159 371 -103 96 -213 139 -351 137 -41 0 -99 -7 -129 -15z" /> 114 - <path d="M4959 2785 c-85 -23 -162 -69 -229 -135 -102 -101 -150 -216 -150 -360 0 -147 57 -278 162 -374 205 -187 515 -181 709 13 157 157 193 397 91 600 -56 112 -196 223 -326 257 -63 17 -195 17 -257 -1z" /> 115 - <path d="M6311 2784 c-76 -20 -146 -60 -212 -122 -113 -104 -159 -211 -159 -371 0 -189 74 -329 228 -431 103 -68 259 -97 385 -71 130 28 271 129 336 245 86 151 86 361 1 512 -38 65 -141 164 -208 198 -109 55 -256 71 -371 40z" /> 116 - </g> 117 - </svg> 118 - <div 119 - style={{ 120 - display: "flex", 121 - flexDirection: "column", 122 - alignItems: "flex-start", 123 - marginBottom: "20px", 124 - }} 125 - > 126 - <div 127 - style={{ 128 - fontSize: 72, 129 - fontWeight: 500, 130 - fontFamily: "Instrument Sans", 131 - lineHeight: 1.1, 132 - }} 133 - > 134 - {eventTitle} 135 - </div> 136 - </div> 137 - </div> 138 - ), 139 - { 140 - ...size, 141 - fonts: [ 142 - { 143 - name: "Instrument Sans", 144 - data: instrumentSans400, 145 - style: "normal", 146 - weight: 400, 147 - }, 148 - { 149 - name: "Instrument Sans", 150 - data: instrumentSans500, 151 - style: "normal", 152 - weight: 500, 153 - }, 154 - ], 155 - }, 156 - ) 157 - }