diff --git a/examples/edge-functions/supabase/functions/tweet-to-image/Tweet.tsx b/examples/edge-functions/supabase/functions/tweet-to-image/Tweet.tsx
new file mode 100644
index 00000000000..969a2353e21
--- /dev/null
+++ b/examples/edge-functions/supabase/functions/tweet-to-image/Tweet.tsx
@@ -0,0 +1,39 @@
+import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0'
+import type { FormattedTweet } from './types.ts'
+
+/**
+ * Supports plain text, images, quote tweets.
+ *
+ * Needs support for images, GIFs, and replies maybe?
+ * Styles use !important to override Tailwind .prose inside MDX.
+ */
+export default function Tweet({
+ id,
+ text,
+ author,
+ media,
+ created_at,
+ public_metrics,
+ referenced_tweets,
+}: FormattedTweet) {
+ const authorUrl = `https://twitter.com/${author.username}`
+ const likeUrl = `https://twitter.com/intent/like?tweet_id=${id}`
+ const retweetUrl = `https://twitter.com/intent/retweet?tweet_id=${id}`
+ const replyUrl = `https://twitter.com/intent/tweet?in_reply_to=${id}`
+ const tweetUrl = `https://twitter.com/${author.username}/status/${id}`
+ const createdAt = new Date(created_at)
+
+ const formattedText = text.replace(/https:\/\/[\n\S]+/g, '').replace('&', '&')
+ const quoteTweet = referenced_tweets && referenced_tweets.find((t) => t.type === 'quoted')
+
+ return (
+
+
+
+ {formattedText}
+ {`- @${author.username}`}
+
+
+
+ )
+}
diff --git a/examples/edge-functions/supabase/functions/tweet-to-image/getTweet.ts b/examples/edge-functions/supabase/functions/tweet-to-image/getTweet.ts
new file mode 100644
index 00000000000..00930cf31d5
--- /dev/null
+++ b/examples/edge-functions/supabase/functions/tweet-to-image/getTweet.ts
@@ -0,0 +1,61 @@
+import type { TwitterApiResponse, Tweet, FormattedTweet } from './types.ts'
+
+export const getTweets: (ids: string[]) => Promise = async (ids: string[]) => {
+ if (ids.length === 0) {
+ return []
+ }
+
+ const queryParams = new URLSearchParams({
+ ids: ids.join(','),
+ expansions:
+ 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id',
+ 'tweet.fields':
+ 'attachments,author_id,public_metrics,created_at,id,in_reply_to_user_id,referenced_tweets,text',
+ 'user.fields': 'id,name,profile_image_url,protected,url,username,verified',
+ 'media.fields': 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics',
+ })
+
+ const response = await fetch(`https://api.twitter.com/2/tweets?${queryParams}`, {
+ headers: {
+ Authorization: `Bearer ${Deno.env.get('TWITTER_API_TOKEN')}`,
+ },
+ })
+
+ const tweets: TwitterApiResponse = await response.json()
+
+ const getAuthorInfo = (author_id: string) => {
+ return tweets.includes.users.find((user) => user.id === author_id)
+ }
+
+ const getReferencedTweets = (mainTweet: Tweet) => {
+ return (
+ mainTweet?.referenced_tweets?.map((referencedTweet) => {
+ const fullReferencedTweet = tweets.includes.tweets.find(
+ (tweet) => tweet.id === referencedTweet.id
+ )
+
+ return {
+ type: referencedTweet.type,
+ author: getAuthorInfo(fullReferencedTweet!.author_id),
+ ...fullReferencedTweet,
+ }
+ }) || []
+ )
+ }
+
+ return (
+ tweets.data.reduce((allTweets, tweet) => {
+ const tweetWithAuthor = {
+ ...tweet,
+ media:
+ tweet?.attachments?.media_keys.map((key: string) =>
+ tweets.includes.media.find((media) => media.media_key === key)
+ ) || [],
+ referenced_tweets: getReferencedTweets(tweet),
+ author: getAuthorInfo(tweet.author_id),
+ }
+
+ return [tweetWithAuthor, ...allTweets]
+ }, []) || [] // If the Twitter API key isn't set, don't break the build
+ )
+}
diff --git a/examples/edge-functions/supabase/functions/tweet-to-image/handler.tsx b/examples/edge-functions/supabase/functions/tweet-to-image/handler.tsx
new file mode 100644
index 00000000000..c55060c77ec
--- /dev/null
+++ b/examples/edge-functions/supabase/functions/tweet-to-image/handler.tsx
@@ -0,0 +1,93 @@
+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.5.0'
+import { corsHeaders } from '../_shared/cors.ts'
+import { getTweets } from './getTweet.ts'
+import Tweet from './Tweet.tsx'
+
+const STORAGE_URL =
+ 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/tweet-to-image'
+
+// 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 tweetId = url.searchParams.get('tweetId')
+
+ if (!tweetId)
+ return new Response(JSON.stringify({ error: 'missing tweetId param' }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 400,
+ })
+
+ try {
+ // Try to get image from Supabase Storage CDN.
+ const storageResponse = await fetch(`${STORAGE_URL}/${tweetId}.png`)
+ if (storageResponse.ok) return storageResponse
+
+ // Else, generate image and upload to storage.
+ const fontData = await font
+
+ const tweets = await getTweets([tweetId])
+ const tweet = tweets[0]
+ console.log('formattedTweets', JSON.stringify(tweets, null, 2))
+ const generatedImage = new ImageResponse(
+ (
+
+
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ // Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
+ // Default to 'twemoji'
+ emoji: 'twemoji',
+ fonts: [
+ {
+ name: 'Circular',
+ data: fontData,
+ style: 'normal',
+ },
+ ],
+ }
+ )
+
+ 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') ?? ''
+ )
+
+ // Upload image to storage.
+ const { error } = await supabaseAdminClient.storage
+ .from('images')
+ .upload(`tweet-to-image/${tweetId}.png`, generatedImage.body!, {
+ contentType: 'image/png',
+ cacheControl: '31536000',
+ upsert: false,
+ })
+ if (error) throw error
+
+ return generatedImage
+ } catch (error) {
+ return new Response(JSON.stringify({ error: error.message }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 400,
+ })
+ }
+}
diff --git a/examples/edge-functions/supabase/functions/tweet-to-image/index.ts b/examples/edge-functions/supabase/functions/tweet-to-image/index.ts
new file mode 100644
index 00000000000..fe187018a89
--- /dev/null
+++ b/examples/edge-functions/supabase/functions/tweet-to-image/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.140.0/http/server.ts'
+
+import { handler } from './handler.tsx'
+
+console.log(`Function "tweet-to-image" up and running!`)
+
+serve(handler)
diff --git a/examples/edge-functions/supabase/functions/tweet-to-image/types.ts b/examples/edge-functions/supabase/functions/tweet-to-image/types.ts
new file mode 100644
index 00000000000..c1ab36294ed
--- /dev/null
+++ b/examples/edge-functions/supabase/functions/tweet-to-image/types.ts
@@ -0,0 +1,74 @@
+export type Tweet = {
+ edit_history_tweet_ids: string[]
+ text: string
+ referenced_tweets:
+ | Array<{
+ type: string
+ id: string
+ }>
+ | undefined
+ attachments: {
+ media_keys: string[]
+ }
+ id: string
+ created_at: string
+ public_metrics: {
+ retweet_count: number
+ reply_count: number
+ like_count: number
+ quote_count: number
+ impression_count: number
+ }
+ author_id: string
+ author?: User
+}
+
+export type Media = {
+ width: number
+ media_key: string
+ type: string
+ url: string
+ height: number
+}
+
+export type User = {
+ name: string
+ url: string
+ verified: boolean
+ profile_image_url: string
+ id: string
+ username: string
+ protected: boolean
+}
+
+// raw
+export type TwitterApiResponse = {
+ data: Tweet[]
+ includes: {
+ media: Media[]
+ users: User[]
+ tweets: Tweet[]
+ }
+}
+
+export type FormattedTweet = {
+ type: string
+ edit_history_tweet_ids: string[]
+ text: string
+ referenced_tweets: FormattedTweet[]
+ attachments: {
+ media_keys: string[]
+ }
+ id: string
+ created_at: string
+ public_metrics: {
+ retweet_count: number
+ reply_count: number
+ like_count: number
+ quote_count: number
+ impression_count: number
+ }
+ author_id: string
+ media: Media[]
+ author: User
+}