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";
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});