diff options
| author | Dhravya Shah <[email protected]> | 2026-01-23 17:39:23 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2026-01-23 17:39:23 -0700 |
| commit | 35004c474ad021f4772bd4ba4da41a4d5d9a9b2e (patch) | |
| tree | 85e0232060bb979cb44ae022729c6198540bf3e0 | |
| parent | chore: bump package versions (diff) | |
| download | supermemory-35004c474ad021f4772bd4ba4da41a4d5d9a9b2e.tar.xz supermemory-35004c474ad021f4772bd4ba4da41a4d5d9a9b2e.zip | |
extract metadata ourselves
| -rw-r--r-- | apps/web/app/api/og/route.ts | 107 | ||||
| -rw-r--r-- | apps/web/components/new/chat/index.tsx | 4 | ||||
| -rw-r--r-- | apps/web/components/new/document-cards/file-preview.tsx | 6 | ||||
| -rw-r--r-- | apps/web/components/new/document-cards/google-docs-preview.tsx | 6 | ||||
| -rw-r--r-- | apps/web/components/new/document-icon.tsx | 8 | ||||
| -rw-r--r-- | apps/web/components/new/document-modal/content/google-doc.tsx | 5 | ||||
| -rw-r--r-- | apps/web/components/new/document-modal/content/index.tsx | 9 | ||||
| -rw-r--r-- | apps/web/components/new/document-modal/graph-list-memories.tsx | 4 | ||||
| -rw-r--r-- | apps/web/lib/analytics.ts | 5 | ||||
| -rw-r--r-- | apps/web/package.json | 1 |
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(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/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", |