Skip to content

Commit

Permalink
feat: add blog post page
Browse files Browse the repository at this point in the history
  • Loading branch information
CaliCastle committed May 16, 2023
1 parent 195453b commit 2231867
Show file tree
Hide file tree
Showing 17 changed files with 811 additions and 602 deletions.
23 changes: 2 additions & 21 deletions app/(main)/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Container } from '~/components/ui/Container'
import { kvKeys } from '~/config/kv'
import { navigationItems } from '~/config/nav'
import { env } from '~/env.mjs'
import { prettifyNumber } from '~/lib/math'
import { redis } from '~/lib/redis'

function NavLink({
Expand Down Expand Up @@ -37,26 +38,6 @@ function Links() {
)
}

function formatNumber(n: number, inChinese = false): string {
if (inChinese) {
if (Math.abs(n) >= 100000000) {
return (n / 100000000).toFixed(1) + '亿'
} else if (Math.abs(n) >= 10000) {
return (n / 10000).toFixed(1) + '万'
} else {
return Intl.NumberFormat('en-US').format(n)
}
}

if (Math.abs(n) >= 1000000) {
return (n / 1000000).toFixed(1) + 'm'
} else if (Math.abs(n) >= 1000) {
return (n / 1000).toFixed(1) + 'k'
} else {
return Intl.NumberFormat('en-US').format(n)
}
}

async function TotalPageViews() {
let views: number
if (env.VERCEL_ENV === 'production') {
Expand All @@ -70,7 +51,7 @@ async function TotalPageViews() {
<UsersIcon className="h-4 w-4" />
<span title={`${Intl.NumberFormat('en-US').format(views)}次浏览`}>
总浏览量&nbsp;
<span className="font-medium">{formatNumber(views, true)}</span>
<span className="font-medium">{prettifyNumber(views, true)}</span>
</span>
</span>
)
Expand Down
102 changes: 54 additions & 48 deletions app/(main)/blog/BlogPosts.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Image from 'next/image'
import Link from 'next/link'
import Balancer from 'react-wrap-balancer'

import {
Expand All @@ -7,64 +8,69 @@ import {
HourglassIcon,
ScriptIcon,
} from '~/assets'
import { prettifyNumber } from '~/lib/math'
import { redis } from '~/lib/redis'
import { getLatestBlogPosts } from '~/sanity/queries'

export async function BlogPosts() {
const posts = await getLatestBlogPosts()
const postIdKeys = posts.map(({ _id }) => `post:views:${_id}`)
const views = await redis.mget<number[]>(postIdKeys.join(' '))

const fakePosts = [
{
title: '陈皓:以磊落之心,点亮科技之光',
publishedAt: '2023-05-15',
readingTime: 15,
mainImage:
'https://cdn.sanity.io/images/i81ys0da/production/a6b97145fdb5e4887ae18609b608980b5428957d-1200x630.jpg',
category: '随笔',
},
]
export function BlogPosts() {
return (
<>
{fakePosts.map(({ title, mainImage, publishedAt, category }, idx) => (
<div
key={idx}
className="group relative flex aspect-[240/135] w-full flex-col justify-end gap-16 rounded-3xl bg-white p-6 transition-shadow after:absolute after:inset-0 after:rounded-3xl after:bg-[linear-gradient(180deg,transparent,rgba(0,0,0,.7)_55%,#000_82.5%,#000)] after:opacity-100 after:ring-2 after:ring-zinc-200 after:transition-opacity hover:shadow-xl hover:after:opacity-70 dark:after:ring-zinc-800/70"
>
<Image
src={mainImage}
alt=""
className="rounded-[22px] object-cover"
fill
/>
<span className="z-10 flex w-full flex-col gap-2">
<h2 className="text-xl font-bold tracking-tight text-zinc-50">
<Balancer>{title}</Balancer>
</h2>
<span className="flex items-center justify-between">
<span className="inline-flex items-center space-x-3">
<span className="inline-flex items-center space-x-1 text-sm font-medium text-zinc-400">
<CalendarIcon />
<span>
{Intl.DateTimeFormat('zh').format(new Date(publishedAt))}
{posts.map(
(
{ slug, title, mainImage, publishedAt, categories, readingTime },
idx
) => (
<Link
key={idx}
href={`/blog/${slug}`}
className="group relative flex aspect-[240/135] w-full flex-col justify-end gap-16 rounded-3xl bg-white p-6 transition-shadow after:absolute after:inset-0 after:rounded-3xl after:bg-[linear-gradient(180deg,transparent,rgba(0,0,0,.7)_55%,#000_82.5%,#000)] after:opacity-100 after:ring-2 after:ring-zinc-200 after:transition-opacity hover:shadow-xl hover:after:opacity-70 dark:after:ring-zinc-800/70"
>
<Image
src={mainImage.asset.url}
alt=""
className="rounded-[22px] object-cover"
placeholder="blur"
blurDataURL={mainImage.asset.lqip}
fill
/>
<span className="z-10 flex w-full flex-col gap-2">
<h2 className="text-xl font-bold tracking-tight text-zinc-50">
<Balancer>{title}</Balancer>
</h2>
<span className="flex items-center justify-between">
<span className="inline-flex items-center space-x-3">
<span className="inline-flex items-center space-x-1 text-sm font-medium text-zinc-400">
<CalendarIcon />
<span>
{Intl.DateTimeFormat('zh').format(new Date(publishedAt))}
</span>
</span>
</span>

<span className="inline-flex items-center space-x-1 text-sm font-medium text-zinc-400">
<ScriptIcon />
<span>{category}</span>
</span>
</span>
<span className="inline-flex items-center space-x-3 text-xs font-medium text-zinc-300/60">
<span className="inline-flex items-center space-x-1">
<CursorClickIcon />
<span>15k</span>
<span className="inline-flex items-center space-x-1 text-sm font-medium text-zinc-400">
<ScriptIcon />
<span>{categories.join(', ')}</span>
</span>
</span>
<span className="inline-flex items-center space-x-3 text-xs font-medium text-zinc-300/60">
<span className="inline-flex items-center space-x-1">
<CursorClickIcon />
<span>{prettifyNumber(views[idx] ?? 0, true)}</span>
</span>

<span className="inline-flex items-center space-x-1">
<HourglassIcon />
<span>15分钟</span>
<span className="inline-flex items-center space-x-1">
<HourglassIcon />
<span>{readingTime.toFixed(1)}分钟</span>
</span>
</span>
</span>
</span>
</span>
</div>
))}
</Link>
)
)}
</>
)
}
146 changes: 146 additions & 0 deletions app/(main)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'

import {
CalendarIcon,
CursorClickIcon,
HourglassIcon,
ScriptIcon,
} from '~/assets'
import { PostPortableText } from '~/components/PostPortableText'
import { Prose } from '~/components/Prose'
import { Container } from '~/components/ui/Container'
import { env } from '~/env.mjs'
import { prettifyNumber } from '~/lib/math'
import { redis } from '~/lib/redis'
import { getBlogPost } from '~/sanity/queries'

export const generateMetadata = async ({
params,
}: {
params: { slug: string }
}) => {
const post = await getBlogPost(params.slug)
if (!post) {
notFound()
}

const { title, description, mainImage } = post

return {
title,
description,
openGraph: {
title,
description,
images: [
{
url: mainImage.asset.url,
},
],
type: 'article',
},
twitter: {
images: [
{
url: mainImage.asset.url,
},
],
title,
description,
},
} satisfies Metadata
}

export default async function BlogPage({
params,
}: {
params: { slug: string }
}) {
const post = await getBlogPost(params.slug)
if (!post) {
notFound()
}

let views: number
if (env.VERCEL_ENV === 'production') {
views = await redis.incr(`post:views:${post._id}`)
} else {
views = 35900
}

return (
<Container className="mt-16 lg:mt-32">
<div className="xl:relative">
<div className="mx-auto max-w-2xl">
{/*{previousPathname && (*/}
{/* <button*/}
{/* type="button"*/}
{/* onClick={() => router.back()}*/}
{/* aria-label="Go back to articles"*/}
{/* className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"*/}
{/* >*/}
{/* <ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />*/}
{/* </button>*/}
{/*)}*/}
<article>
<header className="flex flex-col">
<div className="relative mb-7 aspect-[240/135] w-full md:mb-12 md:scale-110">
<Image
src={post.mainImage.asset.url}
alt={post.title}
className="rounded-3xl ring-1 ring-zinc-900/5 transition dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20"
placeholder="blur"
blurDataURL={post.mainImage.asset.lqip}
unoptimized
fill
/>
</div>
<div className="flex w-full items-center justify-between space-x-4 text-sm font-medium text-zinc-600/80 dark:text-zinc-400/80">
<span className="inline-flex items-center space-x-1.5">
<ScriptIcon />
<span>{post.categories.join(', ')}</span>
</span>
<time
dateTime={post.publishedAt}
className="flex items-center space-x-1.5"
>
<CalendarIcon />
<span>
{Intl.DateTimeFormat('zh').format(
new Date(post.publishedAt)
)}
</span>
</time>
</div>
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<Balancer>{post.title}</Balancer>
</h1>
<p className="my-5 text-sm font-medium text-zinc-500">
<Balancer>{post.description}</Balancer>
</p>
<div className="flex w-full items-center space-x-4 text-sm font-medium text-zinc-700/50 dark:text-zinc-300/50">
<span className="inline-flex items-center space-x-1.5">
<CursorClickIcon />
<span>{prettifyNumber(views ?? 0, true)}次点击</span>
</span>

<span className="inline-flex items-center space-x-1.5">
<HourglassIcon />
<span>{post.readingTime.toFixed(1)}分钟阅读</span>
</span>
</div>
</header>
<Prose className="mt-8">
<PostPortableText value={post.body} />
</Prose>
</article>
</div>
</div>
</Container>
)
}

export const runtime = 'edge'
3 changes: 2 additions & 1 deletion app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export default function BlogHomePage() {
<div className="flex flex-col gap-6 pt-6">
<h2 className="flex items-center text-sm font-semibold text-zinc-900 dark:text-zinc-100">
<PencilSwooshIcon className="h-5 w-5 flex-none" />
<span className="ml-2">最新文章</span>
<span className="ml-2">近期文章</span>
</h2>
{/* @ts-expect-error Server Component */}
<BlogPosts />
</div>
<aside className="space-y-10 lg:sticky lg:top-8 lg:h-fit lg:pl-16 xl:pl-20">
Expand Down
10 changes: 7 additions & 3 deletions app/api/favicon/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export async function GET(req: NextRequest) {
return NextResponse.error()
}

let iconUrl = 'https://cali.so/favicon_blank.png'

try {
const cachedFavicon = await redis.get<string>(getKey(url))
if (cachedFavicon) {
Expand All @@ -46,13 +48,13 @@ export async function GET(req: NextRequest) {
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
const shortcutFavicon = $('link[rel="shortcut icon"]').attr('href')
const finalFavicon = appleTouchIcon ?? favicon ?? shortcutFavicon
if (finalFavicon) {
iconUrl = new URL(finalFavicon, url).href
}
Expand All @@ -65,5 +67,7 @@ export async function GET(req: NextRequest) {
console.error(e)
}

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

return renderFavicon(iconUrl)
}
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const description =
'我叫 Cali,一名开发者,设计师,细节控,同时也是佐玩创始人,目前带领着佐玩致力于创造一个充满创造力的工作环境,同时鼓励团队创造影响世界的产品。'
export const metadata: Metadata = {
metadataBase: new URL('https://cali.so'),
title,
title: {
template: '%s | Cali Castle',
default: title,
},
description,
keywords: 'Cali,Cali Castle,郭晓楠,佐玩,创始人,CEO,开发者,设计师,细节控,创新',
themeColor: [
Expand Down
Loading

0 comments on commit 2231867

Please sign in to comment.