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