aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/api/og
diff options
context:
space:
mode:
authorMahesh Sanikommu <[email protected]>2026-01-13 17:53:28 -0800
committerGitHub <[email protected]>2026-01-13 17:53:28 -0800
commit641db19e35009e22b101f9e673a3af4528de2a30 (patch)
tree698f9c3c22efed5f4061ef7e89b4963da150440a /apps/web/app/api/og
parentreproduce (diff)
downloadsupermemory-641db19e35009e22b101f9e673a3af4528de2a30.tar.xz
supermemory-641db19e35009e22b101f9e673a3af4528de2a30.zip
chore: quick bugs squash across the elements and added few more changes (#671)
Diffstat (limited to 'apps/web/app/api/og')
-rw-r--r--apps/web/app/api/og/route.ts156
1 files changed, 156 insertions, 0 deletions
diff --git a/apps/web/app/api/og/route.ts b/apps/web/app/api/og/route.ts
new file mode 100644
index 00000000..5ca6e44c
--- /dev/null
+++ b/apps/web/app/api/og/route.ts
@@ -0,0 +1,156 @@
+import ogs from "open-graph-scraper"
+
+export const runtime = "nodejs"
+
+interface OGResponse {
+ title: string
+ description: string
+ image?: string
+}
+
+function isValidUrl(urlString: string): boolean {
+ try {
+ const url = new URL(urlString)
+ return url.protocol === "http:" || url.protocol === "https:"
+ } catch {
+ return false
+ }
+}
+
+function isPrivateHost(hostname: string): boolean {
+ const lowerHost = hostname.toLowerCase()
+
+ // Block localhost variants
+ if (
+ lowerHost === "localhost" ||
+ lowerHost === "127.0.0.1" ||
+ lowerHost === "::1" ||
+ lowerHost.startsWith("127.") ||
+ lowerHost.startsWith("0.0.0.0")
+ ) {
+ return true
+ }
+
+ // Block RFC 1918 private IP ranges
+ const privateIpPatterns = [
+ /^10\./,
+ /^172\.(1[6-9]|2[0-9]|3[01])\./,
+ /^192\.168\./,
+ ]
+
+ return privateIpPatterns.some((pattern) => pattern.test(hostname))
+}
+
+function extractImageUrl(image: unknown): string | undefined {
+ if (!image) return undefined
+
+ if (typeof image === "string") {
+ return image
+ }
+
+ if (Array.isArray(image) && image.length > 0) {
+ const first = image[0]
+ if (first && typeof first === "object" && "url" in first) {
+ return String(first.url)
+ }
+ }
+
+ if (typeof image === "object" && image !== null && "url" in image) {
+ return String(image.url)
+ }
+
+ return undefined
+}
+
+function resolveImageUrl(
+ imageUrl: string | undefined,
+ baseUrl: string,
+): string | undefined {
+ if (!imageUrl) return undefined
+
+ try {
+ const url = new URL(imageUrl)
+ return url.href
+ } catch {
+ try {
+ const base = new URL(baseUrl)
+ return new URL(imageUrl, base.href).href
+ } catch {
+ return undefined
+ }
+ }
+}
+
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const url = searchParams.get("url")
+
+ if (!url || !url.trim()) {
+ return Response.json(
+ { error: "Missing or invalid url parameter" },
+ { status: 400 },
+ )
+ }
+
+ const trimmedUrl = url.trim()
+
+ if (!isValidUrl(trimmedUrl)) {
+ return Response.json(
+ { error: "Invalid URL. Must be http:// or https://" },
+ { status: 400 },
+ )
+ }
+
+ const urlObj = new URL(trimmedUrl)
+ if (isPrivateHost(urlObj.hostname)) {
+ return Response.json(
+ { error: "Private/localhost URLs are not allowed" },
+ { status: 400 },
+ )
+ }
+
+ const { result, error } = await ogs({
+ url: trimmedUrl,
+ timeout: 8000,
+ fetchOptions: {
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
+ },
+ },
+ })
+
+ if (error || !result) {
+ console.error("OG scraping error:", error)
+ return Response.json(
+ { error: "Failed to fetch Open Graph data" },
+ { status: 500 },
+ )
+ }
+
+ const ogTitle = result.ogTitle || result.twitterTitle || ""
+ const ogDescription =
+ result.ogDescription || result.twitterDescription || ""
+
+ const ogImageUrl =
+ extractImageUrl(result.ogImage) || extractImageUrl(result.twitterImage)
+
+ const resolvedImageUrl = resolveImageUrl(ogImageUrl, trimmedUrl)
+
+ const response: OGResponse = {
+ title: ogTitle,
+ description: ogDescription,
+ ...(resolvedImageUrl && { image: resolvedImageUrl }),
+ }
+
+ return Response.json(response, {
+ headers: {
+ "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
+ },
+ })
+ } catch (error) {
+ console.error("OG route error:", error)
+ return Response.json({ error: "Internal server error" }, { status: 500 })
+ }
+}