-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #486 from steven-tey/edge-runtime
- Loading branch information
Showing
11 changed files
with
266 additions
and
184 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { ratelimit } from "@/lib/upstash"; | ||
import { LOCALHOST_IP, isValidUrl } from "@dub/utils"; | ||
import { ipAddress } from "@vercel/edge"; | ||
import { getToken } from "next-auth/jwt"; | ||
import { NextFetchEvent, NextRequest } from "next/server"; | ||
import { getMetaTags } from "./utils"; | ||
|
||
export const runtime = "edge"; | ||
|
||
// TODO: waitUntil() is not supported in App Router yet: https://vercel.com/docs/functions/edge-functions/edge-functions-api#waituntil | ||
export async function GET(req: NextRequest, ev: NextFetchEvent) { | ||
const url = req.nextUrl.searchParams.get("url"); | ||
if (!url || !isValidUrl(url)) { | ||
return new Response("Invalid URL", { status: 400 }); | ||
} | ||
|
||
// Rate limit if user is not logged in | ||
const session = await getToken({ | ||
req, | ||
secret: process.env.NEXTAUTH_SECRET, | ||
}); | ||
if (!session?.email) { | ||
const ip = ipAddress(req) || LOCALHOST_IP; | ||
const { success } = await ratelimit().limit(ip); | ||
if (!success) { | ||
return new Response("Don't DDoS me pls 🥺", { status: 429 }); | ||
} | ||
} | ||
|
||
const metatags = await getMetaTags(url, ev); | ||
return new Response(JSON.stringify(metatags), { | ||
status: 200, | ||
headers: { | ||
"Content-Type": "application/json", | ||
"Access-Control-Allow-Origin": "*", | ||
}, | ||
}); | ||
} | ||
|
||
export function OPTIONS() { | ||
return new Response(null, { | ||
status: 204, | ||
headers: { | ||
"Access-Control-Allow-Origin": "*", | ||
"Access-Control-Allow-Methods": "GET, OPTIONS", | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { parse } from "node-html-parser"; | ||
import { recordMetatags } from "@/lib/upstash"; | ||
import { isValidUrl } from "@dub/utils"; | ||
import { NextFetchEvent } from "next/server"; | ||
|
||
export const getHtml = async (url: string) => { | ||
try { | ||
const controller = new AbortController(); | ||
const timeoutId = setTimeout(() => controller.abort(), 5000); // timeout if it takes longer than 5 seconds | ||
const response = await fetch(url, { | ||
signal: controller.signal, | ||
headers: { | ||
"User-Agent": "dub-bot/1.0", | ||
}, | ||
}); | ||
clearTimeout(timeoutId); | ||
return await response.text(); | ||
} catch (error) { | ||
if (error.name === "AbortError") { | ||
// Handle fetch request abort (e.g., due to timeout) | ||
console.error("Fetch request aborted due to timeout."); | ||
} else { | ||
// Handle other fetch errors | ||
console.error("Fetch request failed:", error); | ||
} | ||
return null; | ||
} | ||
}; | ||
|
||
export const getHeadChildNodes = (html) => { | ||
const ast = parse(html); // parse the html into AST format with node-html-parser | ||
const metaTags = ast.querySelectorAll("meta").map(({ attributes }) => { | ||
const property = attributes.property || attributes.name || attributes.href; | ||
return { | ||
property, | ||
content: attributes.content, | ||
}; | ||
}); | ||
const title = ast.querySelector("title")?.innerText; | ||
const linkTags = ast.querySelectorAll("link").map(({ attributes }) => { | ||
const { rel, href } = attributes; | ||
return { | ||
rel, | ||
href, | ||
}; | ||
}); | ||
|
||
return { metaTags, title, linkTags }; | ||
}; | ||
|
||
export const getRelativeUrl = (url: string, imageUrl: string) => { | ||
if (!imageUrl) { | ||
return null; | ||
} | ||
if (isValidUrl(imageUrl)) { | ||
return imageUrl; | ||
} | ||
const { protocol, host } = new URL(url); | ||
const baseURL = `${protocol}//${host}`; | ||
return new URL(imageUrl, baseURL).toString(); | ||
}; | ||
|
||
export const getMetaTags = async (url: string, ev?: NextFetchEvent) => { | ||
const html = await getHtml(url); | ||
if (!html) { | ||
return { | ||
title: url, | ||
description: "No description", | ||
image: null, | ||
}; | ||
} | ||
const { metaTags, title: titleTag, linkTags } = getHeadChildNodes(html); | ||
|
||
let object = {}; | ||
|
||
for (let k in metaTags) { | ||
let { property, content } = metaTags[k]; | ||
|
||
property && (object[property] = content); | ||
} | ||
|
||
for (let m in linkTags) { | ||
let { rel, href } = linkTags[m]; | ||
|
||
rel && (object[rel] = href); | ||
} | ||
|
||
const title = object["og:title"] || object["twitter:title"] || titleTag; | ||
|
||
const description = | ||
object["description"] || | ||
object["og:description"] || | ||
object["twitter:description"]; | ||
|
||
const image = | ||
object["og:image"] || | ||
object["twitter:image"] || | ||
object["image_src"] || | ||
object["icon"] || | ||
object["shortcut icon"]; | ||
|
||
ev?.waitUntil( | ||
recordMetatags(url, title && description && image ? false : true), | ||
); | ||
|
||
return { | ||
title: title || url, | ||
description: description || "No description", | ||
image: getRelativeUrl(url, image), | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.