forked from CaliCastle/cali.so
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5f9e384
commit 8cd1951
Showing
7 changed files
with
245 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.