a tool for shared writing and social publishing
at update/reader 156 lines 4.4 kB view raw
1export const maxDuration = 60; 2export const runtime = "nodejs"; 3 4import { NextRequest } from "next/server"; 5import * as z from "zod"; 6import { createClient } from "@supabase/supabase-js"; 7import { Database } from "supabase/database.types"; 8import { 9 getMicroLinkOgImage, 10 getWebpageImage, 11} from "src/utils/getMicroLinkOgImage"; 12let supabase = createClient<Database>( 13 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 14 process.env.SUPABASE_SERVICE_ROLE_KEY as string, 15); 16 17export type LinkPreviewBody = { url: string; type: "meta" | "image" }; 18export async function POST(req: NextRequest) { 19 let body = (await req.json()) as LinkPreviewBody; 20 let url = encodeURIComponent(body.url); 21 if (body.type === "meta") { 22 let result = await get_link_metadata(url); 23 return Response.json(result); 24 } else { 25 let result = await get_link_image_preview(body.url); 26 return Response.json(result); 27 } 28} 29 30export type LinkPreviewMetadataResult = ReturnType<typeof get_link_metadata>; 31export type LinkPreviewImageResult = ReturnType<typeof get_link_image_preview>; 32 33async function get_link_image_preview(url: string) { 34 let image = await getWebpageImage(url, { width: 1400, height: 1213 }); 35 let key = await hash(url); 36 if (image.status === 200) { 37 await supabase.storage 38 .from("url-previews") 39 .upload(key, await image.arrayBuffer(), { 40 contentType: image.headers.get("content-type") || undefined, 41 upsert: true, 42 }); 43 } else { 44 console.log("an error occured rendering the website", await image.text()); 45 } 46 47 return { 48 url: supabase.storage.from("url-previews").getPublicUrl(key, { 49 transform: { width: 240, height: 208, resize: "contain" }, 50 }).data.publicUrl, 51 height: 208, 52 width: 240, 53 }; 54} 55const hash = async (str: string) => { 56 let hashBuffer = await crypto.subtle.digest( 57 "SHA-256", 58 new TextEncoder().encode(str), 59 ); 60 const hashArray = Array.from(new Uint8Array(hashBuffer)); 61 const hashHex = hashArray 62 .map((byte) => byte.toString(16).padStart(2, "0")) 63 .join(""); 64 return hashHex; 65}; 66 67async function get_link_metadata(url: string) { 68 let response = await fetch( 69 `https://iframe.ly/api/iframely?url=${url}&api_key=${process.env.IFRAMELY_KEY!}&_layout=standard`, 70 { 71 headers: { 72 Accept: "application/json", 73 }, 74 next: { 75 revalidate: 60 * 10, 76 }, 77 }, 78 ); 79 80 let json = await response.json(); 81 console.log(json); 82 let result = iframelyApiResponse.safeParse(json); 83 console.log(result.error); 84 return result; 85} 86 87// Iframely API response type - minimal structure based on docs 88let iframelyApiResponse = z.object({ 89 url: z.string(), 90 meta: z 91 .object({ 92 title: z.string().optional(), 93 description: z.string().optional(), 94 author: z.string().optional(), 95 author_url: z.string().optional(), 96 site: z.string().optional(), 97 canonical: z.string().optional(), 98 duration: z.number().optional(), 99 date: z.string().optional(), 100 medium: z.string().optional(), 101 }) 102 .optional(), 103 links: z 104 .object({ 105 player: z 106 .array( 107 z.object({ 108 href: z.string(), 109 rel: z.array(z.string()), 110 type: z.string(), 111 media: z 112 .object({ 113 "aspect-ratio": z.number().optional(), 114 height: z.number().optional(), 115 width: z.number().optional(), 116 }) 117 .optional(), 118 html: z.string().optional(), 119 }), 120 ) 121 .optional(), 122 thumbnail: z 123 .array( 124 z.object({ 125 href: z.string(), 126 rel: z.array(z.string()), 127 type: z.string(), 128 media: z 129 .object({ 130 height: z.number().optional(), 131 width: z.number().optional(), 132 }) 133 .optional(), 134 }), 135 ) 136 .optional(), 137 image: z 138 .array( 139 z.object({ 140 href: z.string(), 141 rel: z.array(z.string()), 142 type: z.string(), 143 media: z 144 .object({ 145 height: z.number().optional(), 146 width: z.number().optional(), 147 }) 148 .optional(), 149 }), 150 ) 151 .optional(), 152 }) 153 .optional(), 154 html: z.string().optional(), 155 rel: z.array(z.string()).optional(), 156});