aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2026-01-23 17:39:23 -0700
committerDhravya Shah <[email protected]>2026-01-23 17:39:23 -0700
commit35004c474ad021f4772bd4ba4da41a4d5d9a9b2e (patch)
tree85e0232060bb979cb44ae022729c6198540bf3e0
parentchore: bump package versions (diff)
downloadsupermemory-35004c474ad021f4772bd4ba4da41a4d5d9a9b2e.tar.xz
supermemory-35004c474ad021f4772bd4ba4da41a4d5d9a9b2e.zip
extract metadata ourselves
-rw-r--r--apps/web/app/api/og/route.ts107
-rw-r--r--apps/web/components/new/chat/index.tsx4
-rw-r--r--apps/web/components/new/document-cards/file-preview.tsx6
-rw-r--r--apps/web/components/new/document-cards/google-docs-preview.tsx6
-rw-r--r--apps/web/components/new/document-icon.tsx8
-rw-r--r--apps/web/components/new/document-modal/content/google-doc.tsx5
-rw-r--r--apps/web/components/new/document-modal/content/index.tsx9
-rw-r--r--apps/web/components/new/document-modal/graph-list-memories.tsx4
-rw-r--r--apps/web/lib/analytics.ts5
-rw-r--r--apps/web/package.json1
10 files changed, 82 insertions, 73 deletions
diff --git a/apps/web/app/api/og/route.ts b/apps/web/app/api/og/route.ts
index 5ca6e44c..97f024a5 100644
--- a/apps/web/app/api/og/route.ts
+++ b/apps/web/app/api/og/route.ts
@@ -1,6 +1,4 @@
-import ogs from "open-graph-scraper"
-
-export const runtime = "nodejs"
+export const runtime = "edge"
interface OGResponse {
title: string
@@ -20,7 +18,6 @@ function isValidUrl(urlString: string): boolean {
function isPrivateHost(hostname: string): boolean {
const lowerHost = hostname.toLowerCase()
- // Block localhost variants
if (
lowerHost === "localhost" ||
lowerHost === "127.0.0.1" ||
@@ -31,7 +28,6 @@ function isPrivateHost(hostname: string): boolean {
return true
}
- // Block RFC 1918 private IP ranges
const privateIpPatterns = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
@@ -41,25 +37,20 @@ function isPrivateHost(hostname: string): boolean {
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)
+function extractMetaTag(html: string, patterns: RegExp[]): string {
+ for (const pattern of patterns) {
+ const match = html.match(pattern)
+ if (match?.[1]) {
+ return match[1]
+ .replace(/&amp;/g, "&")
+ .replace(/&lt;/g, "<")
+ .replace(/&gt;/g, ">")
+ .replace(/&quot;/g, '"')
+ .replace(/&#039;/g, "'")
+ .trim()
}
}
-
- if (typeof image === "object" && image !== null && "url" in image) {
- return String(image.url)
- }
-
- return undefined
+ return ""
}
function resolveImageUrl(
@@ -110,46 +101,68 @@ export async function GET(request: Request) {
)
}
- const { result, error } = await ogs({
- url: trimmedUrl,
- timeout: 8000,
- fetchOptions: {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
- },
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), 8000)
+
+ const response = await fetch(trimmedUrl, {
+ signal: controller.signal,
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
},
})
- if (error || !result) {
- console.error("OG scraping error:", error)
+ clearTimeout(timeoutId)
+
+ if (!response.ok) {
return Response.json(
- { error: "Failed to fetch Open Graph data" },
- { status: 500 },
+ { error: "Failed to fetch URL" },
+ { status: response.status },
)
}
- 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,
+ const html = await response.text()
+
+ const titlePatterns = [
+ /<meta\s+property=["']og:title["']\s+content=["']([^"']+)["']/i,
+ /<meta\s+content=["']([^"']+)["']\s+property=["']og:title["']/i,
+ /<meta\s+name=["']twitter:title["']\s+content=["']([^"']+)["']/i,
+ /<title>([^<]+)<\/title>/i,
+ ]
+
+ const descriptionPatterns = [
+ /<meta\s+property=["']og:description["']\s+content=["']([^"']+)["']/i,
+ /<meta\s+content=["']([^"']+)["']\s+property=["']og:description["']/i,
+ /<meta\s+name=["']twitter:description["']\s+content=["']([^"']+)["']/i,
+ /<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i,
+ ]
+
+ const imagePatterns = [
+ /<meta\s+property=["']og:image["']\s+content=["']([^"']+)["']/i,
+ /<meta\s+content=["']([^"']+)["']\s+property=["']og:image["']/i,
+ /<meta\s+name=["']twitter:image["']\s+content=["']([^"']+)["']/i,
+ ]
+
+ const title = extractMetaTag(html, titlePatterns)
+ const description = extractMetaTag(html, descriptionPatterns)
+ const imageUrl = extractMetaTag(html, imagePatterns)
+ const resolvedImageUrl = resolveImageUrl(imageUrl, trimmedUrl)
+
+ const ogResponse: OGResponse = {
+ title,
+ description,
...(resolvedImageUrl && { image: resolvedImageUrl }),
}
- return Response.json(response, {
+ return Response.json(ogResponse, {
headers: {
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
},
})
} catch (error) {
+ if (error instanceof Error && error.name === "AbortError") {
+ return Response.json({ error: "Request timeout" }, { status: 504 })
+ }
console.error("OG route error:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
}
diff --git a/apps/web/components/new/chat/index.tsx b/apps/web/components/new/chat/index.tsx
index 32fe116e..435667b0 100644
--- a/apps/web/components/new/chat/index.tsx
+++ b/apps/web/components/new/chat/index.tsx
@@ -406,7 +406,9 @@ export function ChatSidebar({
whileTap={{ scale: 0.98 }}
>
<NovaOrb size={isMobile ? 26 : 24} className="blur-[0.6px]! z-10" />
- <span className={cn(isMobile && "font-medium")}>Chat with Nova</span>
+ <span className={cn(isMobile && "font-medium")}>
+ Chat with Nova
+ </span>
</motion.button>
</motion.div>
) : (
diff --git a/apps/web/components/new/document-cards/file-preview.tsx b/apps/web/components/new/document-cards/file-preview.tsx
index f30645dc..44c2476b 100644
--- a/apps/web/components/new/document-cards/file-preview.tsx
+++ b/apps/web/components/new/document-cards/file-preview.tsx
@@ -86,7 +86,11 @@ export function FilePreview({ document }: { document: DocumentWithMemories }) {
) : (
<div className="p-3">
<div className="flex items-center gap-1 mb-2">
- <DocumentIcon type={document.type} url={document.url} className="w-4 h-4" />
+ <DocumentIcon
+ type={document.type}
+ url={document.url}
+ className="w-4 h-4"
+ />
<p
className={cn(dmSansClassName(), "text-[10px] font-semibold")}
style={{ color: color }}
diff --git a/apps/web/components/new/document-cards/google-docs-preview.tsx b/apps/web/components/new/document-cards/google-docs-preview.tsx
index 50a56a16..06136e87 100644
--- a/apps/web/components/new/document-cards/google-docs-preview.tsx
+++ b/apps/web/components/new/document-cards/google-docs-preview.tsx
@@ -22,7 +22,11 @@ export function GoogleDocsPreview({
return (
<div className="bg-[#0B1017] p-3 rounded-[18px] gap-3">
<div className="flex items-center gap-2 mb-2">
- <DocumentIcon type={document.type} url={document.url} className="w-4 h-4" />
+ <DocumentIcon
+ type={document.type}
+ url={document.url}
+ className="w-4 h-4"
+ />
<p className={cn(dmSansClassName(), "text-[12px] font-semibold")}>
{label}
</p>
diff --git a/apps/web/components/new/document-icon.tsx b/apps/web/components/new/document-icon.tsx
index a2a502e1..4861e978 100644
--- a/apps/web/components/new/document-icon.tsx
+++ b/apps/web/components/new/document-icon.tsx
@@ -53,13 +53,7 @@ function getFaviconUrl(url: string): string {
}
}
-function FaviconIcon({
- url,
- className,
-}: {
- url: string
- className?: string
-}) {
+function FaviconIcon({ url, className }: { url: string; className?: string }) {
const [hasError, setHasError] = useState(false)
const faviconUrl = getFaviconUrl(url)
diff --git a/apps/web/components/new/document-modal/content/google-doc.tsx b/apps/web/components/new/document-modal/content/google-doc.tsx
index 562bac12..78dc4359 100644
--- a/apps/web/components/new/document-modal/content/google-doc.tsx
+++ b/apps/web/components/new/document-modal/content/google-doc.tsx
@@ -2,10 +2,7 @@
import { useState } from "react"
import { Loader2 } from "lucide-react"
-import {
- extractGoogleDocId,
- getGoogleEmbedUrl,
-} from "@/lib/url-helpers"
+import { extractGoogleDocId, getGoogleEmbedUrl } from "@/lib/url-helpers"
interface GoogleDocViewerProps {
url: string | null | undefined
diff --git a/apps/web/components/new/document-modal/content/index.tsx b/apps/web/components/new/document-modal/content/index.tsx
index c06bc550..39c5a2f0 100644
--- a/apps/web/components/new/document-modal/content/index.tsx
+++ b/apps/web/components/new/document-modal/content/index.tsx
@@ -56,10 +56,7 @@ function getContentType(document: DocumentWithMemories | null): ContentType {
document.metadata?.mimeType?.toString().startsWith("image/")
if (isImage && document.url) return "image"
- if (
- document.type === "tweet" ||
- (document.url && isTwitterUrl(document.url))
- )
+ if (document.type === "tweet" || (document.url && isTwitterUrl(document.url)))
return "tweet"
if (document.type === "text") return "text"
if (document.type === "pdf") return "pdf"
@@ -83,9 +80,7 @@ export function DocumentContent({
switch (contentType) {
case "image":
- return (
- <ImagePreview url={document.url ?? ""} title={document.title} />
- )
+ return <ImagePreview url={document.url ?? ""} title={document.title} />
case "tweet":
return (
diff --git a/apps/web/components/new/document-modal/graph-list-memories.tsx b/apps/web/components/new/document-modal/graph-list-memories.tsx
index 49f918c2..0c2e418f 100644
--- a/apps/web/components/new/document-modal/graph-list-memories.tsx
+++ b/apps/web/components/new/document-modal/graph-list-memories.tsx
@@ -286,9 +286,7 @@ export function GraphListMemories({
type="button"
className={cn(
"text-xs text-[#525D6E] cursor-pointer transition-all text-left w-full",
- expandedMemories.has(memory.id)
- ? ""
- : "line-clamp-2",
+ expandedMemories.has(memory.id) ? "" : "line-clamp-2",
)}
onClick={() => toggleMemory(memory.id)}
>
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts
index 9bc3b7f5..84eda62b 100644
--- a/apps/web/lib/analytics.ts
+++ b/apps/web/lib/analytics.ts
@@ -1,7 +1,10 @@
import posthog from "posthog-js"
// Helper function to safely capture events
-const safeCapture = (eventName: string, properties?: Record<string, unknown>) => {
+const safeCapture = (
+ eventName: string,
+ properties?: Record<string, unknown>,
+) => {
if (posthog.__loaded) {
posthog.capture(eventName, properties)
}
diff --git a/apps/web/package.json b/apps/web/package.json
index 27ca7aa4..178b2e2b 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -80,7 +80,6 @@
"next": "16.0.9",
"next-themes": "^0.4.6",
"nuqs": "^2.5.2",
- "open-graph-scraper": "^6.11.0",
"pdfjs-dist": "5.4.296",
"posthog-js": "^1.257.0",
"random-word-slugs": "^0.1.7",