diff --git a/app/(blog)/article/[...slug]/page.tsx b/app/(blog)/article/[...slug]/page.tsx index fb5f30a..ba86c33 100644 --- a/app/(blog)/article/[...slug]/page.tsx +++ b/app/(blog)/article/[...slug]/page.tsx @@ -1,7 +1,6 @@ // import Head from "next/head"; import Link from "next/link"; import { Fragment } from "react"; -// import Text from "@/app/ui/text"; import { Metadata, ResolvingMetadata } from "next"; import { renderBlock } from "@/app/notion/render"; @@ -10,7 +9,7 @@ import { queryPageBySlug, retrieveBlockChildren, retrievePage, -} from "@/app/api/notion"; +} from "@/app/notion/api"; import Shell from "@/components/shells/shell"; import React from "react"; import { cn } from "@/lib/utils"; diff --git a/app/(lobby)/page.tsx b/app/(lobby)/page.tsx index 524907f..0c328f4 100644 --- a/app/(lobby)/page.tsx +++ b/app/(lobby)/page.tsx @@ -1,9 +1,8 @@ // import Image from 'next/image' import Link from 'next/link'; -import { QueryDatabase } from '@/app/api/notion'; +import { QueryDatabase } from '@/app/notion/api'; import '@/app/styles/globals.css' -import Text from '../notion/text'; import Shell from '@/components/shells/shell'; import { PageHeader, PageHeaderHeading, PageHeaderDescription } from '@/components/page-header'; import { Separator } from '@/components/ui/separator'; diff --git a/app/api/unfurl/route.ts b/app/api/unfurl/route.ts new file mode 100644 index 0000000..3cd587b --- /dev/null +++ b/app/api/unfurl/route.ts @@ -0,0 +1,64 @@ +import { unfurl } from "unfurl.js"; +import { NextRequest, NextResponse } from "next/server"; + +type ErrorResponse = { error: string }; +type SuccessResponse = { + title?: string | null; + description?: string | null; + favicon?: string | null; + imageSrc?: string | null; +}; + +const CACHE_RESULT_SECONDS = 60 * 60 * 24; // 1 day + +export async function GET( + req: NextRequest, + res: NextResponse +) { + const searchParams = req.nextUrl.searchParams; + const url = searchParams.get("url"); + + console.log('query url', url); + + if (!url || typeof url !== "string") { + return NextResponse.json( + { + error: "Please enter title", + }, + { + status: 400, + } + ); + } + + return unfurl(url) + .then((unfurlResponse) => { + + console.log(unfurlResponse) + + const response = { + title: unfurlResponse.title ?? null, + description: unfurlResponse.description ?? null, + favicon: unfurlResponse.favicon ?? null, + imageSrc: unfurlResponse.open_graph?.images?.[0]?.url ?? null, + }; + + const res = NextResponse.json(response); + res.headers.set( + "Cache-Control", + `public, max-age=${CACHE_RESULT_SECONDS}` + ); + return res; + }) + .catch((error) => { + console.error(error); + return NextResponse.json( + { + error: "Internal server error", + }, + { + status: 500, + } + ); + }); +} diff --git a/app/notion/_components/bookmark.tsx b/app/notion/_components/bookmark.tsx index dadf6af..781c3f7 100644 --- a/app/notion/_components/bookmark.tsx +++ b/app/notion/_components/bookmark.tsx @@ -1,6 +1,23 @@ +"use client"; + import { cn } from "@/lib/utils"; import RichText from "../text"; import { Icons } from "@/components/icons"; +import { UrlData, useUnfurlUrl } from "@/lib/unfurl"; +import { Skeleton } from "@/components/ui/skeleton"; + +import Image from "next/image"; +import Link from "next/link"; + +import { PlaceholderImage } from "@/components/placeholder-image"; + +import { AspectRatio } from "@/components/ui/aspect-ratio"; +import { + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; interface BookmarkBlockProps { block: any; @@ -17,8 +34,11 @@ export function BookmarkRender({ block, className }: BookmarkBlockProps) { } return ( -
- +
+ ); } + +function LoadingSkeleton() { + return ( +
+ +
+ + +
+
+ ); +} + +function UnfurledBookmarkPreview({ + block, + data, + className, +}: { + block: any; + data: UrlData | null; + className?: string | undefined; +}) { + const { + id, + bookmark: { caption, url }, + } = block; + + const image = data?.imageSrc; + const title = data?.title; + const desc = data?.description; + const icon = data?.favicon; + + return ( +
+ +
+ {/* {url} */} + +
+ + + {title} + + + {desc} + + {url} + +
+
+ {image ? ( + // eslint-disable-next-line @next/next/no-img-element + {url} + ) : ( + + )} +
+
+ + + {caption && caption.length > 0 ? ( +
+ +
+ ) : null} +
+ ); +} + +export function BookmarkPreviewRender({ + block, + className, +}: BookmarkBlockProps) { + const { + id, + bookmark: { caption, url }, + } = block; + const { status, data } = useUnfurlUrl(url); + + console.log(status, data); + if (status == "error") { + return ; + } + if (status == "success") { + return ( + + ); + } + return ; +} diff --git a/app/api/notion.ts b/app/notion/api.ts similarity index 100% rename from app/api/notion.ts rename to app/notion/api.ts diff --git a/app/notion/render.tsx b/app/notion/render.tsx index df8446d..cb73b6e 100644 --- a/app/notion/render.tsx +++ b/app/notion/render.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { CodeRender } from './_components/code'; import { CalloutRender } from './_components/callout'; import { ImageRender } from './_components/image'; -import { BookmarkRender } from './_components/bookmark'; +import { BookmarkPreviewRender, BookmarkRender } from './_components/bookmark'; import { FileRender } from './_components/file'; import { ColumnListRender, ColumnRender } from './_components/column'; import { QuoteRender } from './_components/quote'; @@ -138,7 +138,7 @@ export function renderBlock(block: any, level: number = 1) { return ; case 'bookmark': - return ; + return ; case 'link_preview': return ; diff --git a/lib/unfurl.ts b/lib/unfurl.ts new file mode 100644 index 0000000..426c2ab --- /dev/null +++ b/lib/unfurl.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; + +type RequestStatus = "iddle" | "loading" | "success" | "error"; + +export type UrlData = { + title: string | null; + description: string | null; + favicon: string | null; + imageSrc: string | null; +}; + +export function useUnfurlUrl(url: string) { + const [status, setStatus] = useState("iddle"); + const [data, setData] = useState(null); + useEffect(() => { + if (url) { + setStatus("loading"); + const encoded = encodeURIComponent(url); + fetch(`/api/unfurl?url=${encoded}`) + .then(async (res) => { + if (res.ok) { + const data = await res.json(); + setData(data); + setStatus("success"); + } else { + setStatus("error"); + } + }) + .catch((error) => { + console.error(error); + + setStatus("error"); + }); + } else { + setStatus("error"); + } + }, [url]); + + return { status, data }; +} diff --git a/next.config.js b/next.config.js index 7511a3b..6c4a3fc 100644 --- a/next.config.js +++ b/next.config.js @@ -30,6 +30,14 @@ const nextConfig = { { protocol: "https", hostname: "*.s3.us-west-2.amazonaws.com", + }, + { + protocol: "https", + hostname: "radix-ui.com", + }, + { + protocol: "https", + hostname: "*.medium.com", } ], // unoptimized: true, diff --git a/package-lock.json b/package-lock.json index 70cfd05..799bed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,8 @@ "react-pdf": "^7.7.0", "react-wrap-balancer": "^1.1.0", "tailwind-merge": "^2.2.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "unfurl.js": "^6.4.0" }, "devDependencies": { "@types/ms": "^0.7.34", @@ -2413,6 +2414,57 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2442,6 +2494,17 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -3514,6 +3577,32 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3527,6 +3616,17 @@ "node": ">= 6" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -4343,8 +4443,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "devOptional": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -5377,6 +5476,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -6115,6 +6219,29 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unfurl.js": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/unfurl.js/-/unfurl.js-6.4.0.tgz", + "integrity": "sha512-DogJFWPkOWMcu2xPdpmbcsL+diOOJInD3/jXOv6saX1upnWmMK8ndAtDWUfJkuInqNI9yzADud4ID9T+9UeWCw==", + "dependencies": { + "debug": "^3.2.7", + "he": "^1.2.0", + "htmlparser2": "^8.0.1", + "iconv-lite": "^0.4.24", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/unfurl.js/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/package.json b/package.json index a56f936..035181f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "react-pdf": "^7.7.0", "react-wrap-balancer": "^1.1.0", "tailwind-merge": "^2.2.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "unfurl.js": "^6.4.0" }, "devDependencies": { "@types/ms": "^0.7.34",