a tool for shared writing and social publishing
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});