Skip to content

Commit

Permalink
feat: add favicon to RichLink
Browse files Browse the repository at this point in the history
  • Loading branch information
CaliCastle committed May 12, 2023
1 parent 5f9e384 commit 8cd1951
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 27 deletions.
6 changes: 1 addition & 5 deletions app/(blog)/Headline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { motion } from 'framer-motion'
import { TbPlanet } from 'react-icons/tb'
import Balancer from 'react-wrap-balancer'

import { ExternalLinkIcon } from '~/components/icons/ExternalLinkIcon'
import { SparkleIcon } from '~/components/icons/SparkleIcon'
import { PeekabooLink } from '~/components/links/PeekabooLink'
import { SocialLink } from '~/components/links/SocialLink'
Expand Down Expand Up @@ -85,10 +84,7 @@ export function Headline() {
>
<Balancer>
我是 Cali,
<PeekabooLink href="https://zolplay.cn">
佐玩
<ExternalLinkIcon className="mx-0.5 -mt-1 inline-block" />
</PeekabooLink>
<PeekabooLink href="https://zolplay.cn">佐玩</PeekabooLink>
创始人,目前带领着佐玩致力于创造一个充满创造力的工作环境,同时鼓励团队创造影响世界的产品。
我热爱开发,设计,创新,享受生活,以及在未知领域中探索。
</Balancer>
Expand Down
59 changes: 59 additions & 0 deletions app/api/favicon/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as cheerio from 'cheerio'
import { type NextRequest, NextResponse } from 'next/server'

import { redis } from '~/lib/redis'

export const runtime = 'edge'
export const revalidate = 60 * 60 * 24 // 1 day

function getKey(url: string) {
return `favicon:${url}`
}

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const url = searchParams.get('url')

if (!url) {
return NextResponse.error()
}

try {
const cachedFavicon = await redis.get<string>(getKey(url))
if (cachedFavicon) {
return NextResponse.json({ iconUrl: cachedFavicon })
}

const res = await fetch(url, {
headers: {
'Content-Type': 'text/html',
},
cache: 'force-cache',
})

let iconUrl = ''
if (res.ok) {
const html = await res.text()
const $ = cheerio.load(html)
const appleTouchIcon = $('link[rel="apple-touch-icon"]').attr('href')
const favicon = $('link[rel="icon"]').attr('href')
const finalFavicon = appleTouchIcon ?? favicon
if (finalFavicon) {
// absolute url
if (finalFavicon.startsWith('http') || finalFavicon.startsWith('//')) {
iconUrl = finalFavicon
}

iconUrl = new URL(finalFavicon, url).href
}
}

await redis.set(getKey(url), iconUrl, { ex: revalidate })

return NextResponse.json({ iconUrl })
} catch (e) {
console.error(e)
}

return NextResponse.error()
}
2 changes: 1 addition & 1 deletion app/api/link-preview/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ImageResponse, type NextRequest, NextResponse } from 'next/server'
import { env } from '~/env.mjs'

const width = 1200
const height = 630
const height = 750

export const runtime = 'edge'
export const revalidate = 60 * 60 // 1 hour
Expand Down
11 changes: 6 additions & 5 deletions components/links/PeekabooLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { motion } from 'framer-motion'
import Link, { type LinkProps } from 'next/link'
import React from 'react'

import { RichLink } from '~/components/links/RichLink'
import { HoverCard } from '~/components/ui/HoverCard'

type PeekabooLinkProps = LinkProps &
Expand Down Expand Up @@ -36,8 +37,8 @@ export function PeekabooLink({
return (
<HoverCard.Root openDelay={0} closeDelay={50} onOpenChange={onOpenChange}>
<HoverCard.Trigger asChild>
<Link
href={`${href}?utm_source=cali.so`}
<RichLink
href={href}
className={clsxm(
'font-semibold text-zinc-800 hover:underline dark:text-zinc-100',
className
Expand All @@ -46,14 +47,14 @@ export function PeekabooLink({
{...props}
>
{children}
</Link>
</RichLink>
</HoverCard.Trigger>
<AnimatePresence mode="wait">
{isOpen && (
<HoverCard.Portal forceMount>
<HoverCard.Content asChild>
<motion.div
className="pointer-events-none relative w-[300px] overflow-hidden !p-0"
className="pointer-events-none relative w-[400px] overflow-hidden !p-0"
initial={{
opacity: 0,
scale: 0.965,
Expand All @@ -66,7 +67,7 @@ export function PeekabooLink({
scale: 1,
y: 0,
transformOrigin: 'center 115px',
height: 150,
height: 250,
}}
exit={{
opacity: 0,
Expand Down
66 changes: 66 additions & 0 deletions components/links/RichLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client'

import { clsxm } from '@zolplay/utils'
import Image from 'next/image'
import Link, { type LinkProps } from 'next/link'
import React from 'react'

import { ExternalLinkIcon } from '~/components/icons/ExternalLinkIcon'

type RichLinkProps = LinkProps &
React.ComponentPropsWithoutRef<'a'> & {
children: React.ReactNode
}
export const RichLink = React.forwardRef<HTMLAnchorElement, RichLinkProps>(
({ children, href, className, ...props }, ref) => {
const [iconUrl, setIconUrl] = React.useState<string | null>(null)
React.useEffect(() => {
fetch(`/api/favicon?url=${href}`)
.then((res) => res.json())
.then((data: { iconUrl?: string }) => {
if (data.iconUrl) {
setIconUrl(data.iconUrl)
}
})
.catch((err) => {
console.error(err)
})
}, [href])

// if it's a relative link, use a fallback Link
if (!href.startsWith('http')) {
return (
<Link href={href} className={className} ref={ref} {...props}>
{children}
</Link>
)
}

return (
<Link
ref={ref}
href={href}
className={clsxm('inline-flex items-center gap-0.5 pr-0.5', className)}
rel="noopener noreferrer"
target="_blank"
{...props}
>
{iconUrl ? (
<Image
src={iconUrl}
alt={`${href} 的图标`}
className="mr-0.5 inline-block h-3.5 w-3.5 rounded-xl"
unoptimized
width={14}
height={14}
/>
) : (
<span className="inline-block h-3.5 w-3.5 rounded-xl bg-zinc-100 dark:bg-zinc-700" />
)}
{children}
<ExternalLinkIcon className="inline-block" />
</Link>
)
}
)
RichLink.displayName = 'RichLink'
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@vercel/edge-config": "^0.1.10",
"@zolplay/react": "^0.5.1",
"@zolplay/utils": "^1.3.4",
"cheerio": "1.0.0-rc.12",
"chrome-aws-lambda": "^10.1.0",
"dayjs": "^1.11.7",
"framer-motion": "^10.12.10",
Expand Down
Loading

0 comments on commit 8cd1951

Please sign in to comment.