diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-actions.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-actions.tsx index fd7c62842a4..1f11be290c7 100644 --- a/apps/www/components/LaunchWeek/Ticket/ticket-actions.tsx +++ b/apps/www/components/LaunchWeek/Ticket/ticket-actions.tsx @@ -18,7 +18,7 @@ export default function TicketActions({ username, golden = false }: Props) { const { userData } = useConfData() const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&via=supabase&text=${text}` const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink}` - const downloadUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent( + const downloadUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/lw7-ticket-og?username=${encodeURIComponent( userData.username ?? '' )}` diff --git a/apps/www/components/LaunchWeek/Ticket/ticket-form.tsx b/apps/www/components/LaunchWeek/Ticket/ticket-form.tsx index 44ad832b3d8..d348626896c 100644 --- a/apps/www/components/LaunchWeek/Ticket/ticket-form.tsx +++ b/apps/www/components/LaunchWeek/Ticket/ticket-form.tsx @@ -57,7 +57,7 @@ export default function Form({ defaultUsername = '', setTicketGenerationState }: fetch(`/launch-week/tickets/${username}`).catch((_) => {}) // Prefetch ticket og image. fetch( - `https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent( + `https://obuldanrptloktxcffvn.functions.supabase.co/lw7-ticket-og?username=${encodeURIComponent( username ?? '' )}` ).catch((_) => {}) diff --git a/apps/www/pages/launch-week/tickets/[username].tsx b/apps/www/pages/launch-week/tickets/[username].tsx index 45b3b027df3..9a58b1a3ab1 100644 --- a/apps/www/pages/launch-week/tickets/[username].tsx +++ b/apps/www/pages/launch-week/tickets/[username].tsx @@ -36,7 +36,7 @@ export default function TicketShare({ username, ticketNumber, name, golden, refe return } - const ogImageUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent( + const ogImageUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/lw7-ticket-og?username=${encodeURIComponent( username ?? '' )}` diff --git a/apps/www/supabase/functions/lw7-ticket-og/README.md b/apps/www/supabase/functions/lw7-ticket-og/README.md new file mode 100644 index 00000000000..3eaafadbae2 --- /dev/null +++ b/apps/www/supabase/functions/lw7-ticket-og/README.md @@ -0,0 +1,21 @@ +# Open Graph (OG) Image Generation with Supabase Storage CDN Caching + +Generate Open Graph images with Deno and Supabase Edge Functions and cache the generated image with Supabase Storage CDN. + +- Docs: https://deno.land/x/og_edge@0.0.2 +- Examples: https://vercel.com/docs/concepts/functions/edge-functions/og-image-examples +- Demo: https://obuldanrptloktxcffvn.functions.supabase.co/lw7-ticket-og?username=thorwebdev + +## Run locally + +```bash +supabase functions serve --no-verify-jwt +``` + +Navigate to http://localhost:54321/functions/v1/lw7-ticket-og?username=thorwebdev + +## Deploy + +```bash +supabase functions deploy lw7-ticket-og --no-verify-jwt +``` diff --git a/apps/www/supabase/functions/lw7-ticket-og/handler.tsx b/apps/www/supabase/functions/lw7-ticket-og/handler.tsx new file mode 100644 index 00000000000..2cce5a4bc15 --- /dev/null +++ b/apps/www/supabase/functions/lw7-ticket-og/handler.tsx @@ -0,0 +1,248 @@ +import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0' +import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.7.1' +import { corsHeaders } from '../_shared/cors.ts' + +const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7' +const BACKGROUND_SHAPE = `${STORAGE_URL}/lw7_ticket_shape.png` +const BACKGROUND_IMAGE_STD = `${STORAGE_URL}/lw7_ticket_regular.png` +const BACKGROUND_IMAGE_GOLDEN = `${STORAGE_URL}/lw7_ticket_gold.png` + +// Load custom font +const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf` +const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer()) + +export async function handler(req: Request) { + const url = new URL(req.url) + const username = url.searchParams.get('username') ?? url.searchParams.get('amp;username') + const userAgent = req.headers.get('user-agent') + + try { + if (!username) throw new Error('missing username param') + // Track social shares + const supabaseAdminClient = createClient( + // Supabase API URL - env var exported by default when deployed. + Deno.env.get('SUPABASE_URL') ?? '', + // Supabase API SERVICE ROLE KEY - env var exported by default when deployed. + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' + ) + if (userAgent?.toLocaleLowerCase().includes('twitter')) { + const { error } = await supabaseAdminClient + .from('lw7_tickets') + .update({ sharedOnTwitter: 'now' }) + .eq('username', username) + .is('sharedOnTwitter', null) + if (error) console.log(error.message) + } else if (userAgent?.toLocaleLowerCase().includes('linkedin')) { + const { error } = await supabaseAdminClient + .from('lw7_tickets') + .update({ sharedOnLinkedIn: 'now' }) + .eq('username', username) + .is('sharedOnLinkedIn', null) + if (error) console.log(error.message) + } + + // TODO: reenable storage + // Try to get image from Supabase Storage CDN. + // const storageResponse = await fetch(`${STORAGE_URL}/tickets/${username}.png`) + // if (storageResponse.ok) return storageResponse + + // Get ticket data + const { data, error } = await supabaseAdminClient + .from('lw7_tickets_golden') + .select('name, ticketNumber, golden') + .eq('username', username) + .maybeSingle() + if (error) console.log(error.message) + + const { name = 'Testr Test', ticketNumber = 1234 } = data ?? {} + const golden = data?.golden ?? false + + // Else, generate image ad upload to storage. + const fontData = await font + const numDigits = `${Number(ticketNumber)}`.length + const prefix = `00000000`.slice(numDigits) + + const generatedImage = new ImageResponse( + ( + <> +
+ {/* Background image color */} + + {/* Background image overlay */} + + {/* GitHub Avatar image */} + + {/* Name & username */} +
+
+

+ {name ?? username} +

+
+ {/* Username and supaverified checkmark */} +
+ {`@${username}`} +
+
+ {/* Date */} +

+ December 12th 2022 +

+ {/* URL */} +

+ supabase.com/launch-week +

+
+ {/* Ticket No */} +
+

+ {`No ${prefix}${ticketNumber}`} +

+
+ + ), + { + width: 1200, + height: 630, + fonts: [ + { + name: 'Circular', + data: fontData, + style: 'normal', + }, + ], + headers: { + 'content-type': 'image/png', + 'cache-control': 'public, max-age=31536000, s-maxage=31536000, no-transform, immutable', + 'cdn-cache-control': 'max-age=31536000', + }, + } + ) + + // TODO: reenable storage + // Upload image to storage. + // const { error: storageError } = await supabaseAdminClient.storage + // .from('images') + // .upload(`lw7/tickets/${username}.png`, generatedImage.body!, { + // contentType: 'image/png', + // cacheControl: '31536000', + // upsert: false, + // }) + // if (storageError) throw storageError + + return generatedImage + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + }) + } +} diff --git a/apps/www/supabase/functions/lw7-ticket-og/index.ts b/apps/www/supabase/functions/lw7-ticket-og/index.ts new file mode 100644 index 00000000000..71d2a1df0a6 --- /dev/null +++ b/apps/www/supabase/functions/lw7-ticket-og/index.ts @@ -0,0 +1,11 @@ +// Follow this setup guide to integrate the Deno language server with your editor: +// https://deno.land/manual/getting_started/setup_your_environment +// This enables autocomplete, go to definition, etc. + +import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' + +import { handler } from './handler.tsx' + +console.log(`Function "launchweek-ticket-og" up and running!`) + +serve(handler)