diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index b1fb36d..dd6ed15 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -1,8 +1,10 @@ import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; import { allPosts } from 'contentlayer/generated'; -import { getPostBySlug } from '@/lib/posts'; +import { getPostBySlug } from '@/lib.posts'; import { siteConfig } from '@/lib/config'; +import { ReadingProgress } from '@/components/reading-progress'; +import { PostToc } from '@/components/post-toc'; export function generateStaticParams() { return allPosts.map((post) => ({ @@ -32,37 +34,45 @@ export default function BlogPostPage({ params }: Props) { if (!post) return notFound(); return ( -
-

{post.title}

- {post.feature_image && ( - // feature_image is stored as "../assets/xyz", serve from "/assets/xyz" - // eslint-disable-next-line @next/next/no-img-element - {post.title} - )} - {post.published_at && ( -

- {new Date(post.published_at).toLocaleDateString( - siteConfig.defaultLocale + <> + +

+ +
+

{post.title}

+ {post.feature_image && ( + // feature_image is stored as "../assets/xyz", serve from "/assets/xyz" + // eslint-disable-next-line @next/next/no-img-element + {post.title} )} -

- )} - {post.tags && ( -

- {post.tags.map((t) => ( - - #{t} - - ))} -

- )} -
-
+ {post.published_at && ( +

+ {new Date(post.published_at).toLocaleDateString( + siteConfig.defaultLocale + )} +

+ )} + {post.tags && ( +

+ {post.tags.map((t) => ( + + #{t} + + ))} +

+ )} +
+
+ + ); } diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 5557f6c..26eb8d7 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,6 +1,5 @@ -import Link from 'next/link'; import { getAllPostsSorted } from '@/lib/posts'; -import { siteConfig } from '@/lib/config'; +import { PostCard } from '@/components/post-card'; export const metadata = { title: 'Blog' @@ -12,41 +11,11 @@ export default function BlogIndexPage() { return (

Blog

- +
); } diff --git a/app/page.tsx b/app/page.tsx index d900bd5..9ec6b93 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,45 +1,31 @@ import Link from 'next/link'; import { getAllPostsSorted } from '@/lib/posts'; import { siteConfig } from '@/lib/config'; +import { Hero } from '@/components/hero'; +import { PostCard } from '@/components/post-card'; export default function HomePage() { const posts = getAllPostsSorted().slice(0, siteConfig.postsPerPage); return (
-
-

- 你好,我是 {siteConfig.name} -

-

- {siteConfig.tagline} -

-
+
-

最新文章

-
    +
    +

    最新文章

    + + 所有文章 → + +
    +
    {posts.map((post) => ( -
  • - - {post.title} - - {post.published_at && ( - - {new Date(post.published_at).toLocaleDateString( - siteConfig.defaultLocale - )} - - )} -
  • + ))} -
- - 所有文章 → - +
); diff --git a/components/hero.tsx b/components/hero.tsx new file mode 100644 index 0000000..baefbf6 --- /dev/null +++ b/components/hero.tsx @@ -0,0 +1,67 @@ +import { siteConfig } from '@/lib/config'; + +export function Hero() { + const { name, tagline, social } = siteConfig; + const initial = name?.charAt(0)?.toUpperCase() || 'G'; + + return ( +
+
+
+ {initial} +
+
+

+ {name} +

+

+ {tagline} +

+ {(social.github || social.twitter || social.linkedin || social.email) && ( +
+ {social.github && ( + + GitHub + + )} + {social.twitter && ( + + Twitter + + )} + {social.linkedin && ( + + LinkedIn + + )} + {social.email && ( + + Email + + )} +
+ )} +
+
+
+ ); +} + diff --git a/components/post-card.tsx b/components/post-card.tsx new file mode 100644 index 0000000..e6c82a7 --- /dev/null +++ b/components/post-card.tsx @@ -0,0 +1,64 @@ +import Link from 'next/link'; +import type { Post } from 'contentlayer/generated'; +import { siteConfig } from '@/lib/config'; + +interface PostCardProps { + post: Post; +} + +export function PostCard({ post }: PostCardProps) { + const cover = + post.feature_image && post.feature_image.startsWith('../assets') + ? post.feature_image.replace('../assets', '/assets') + : undefined; + + return ( +
+ {cover && ( + // eslint-disable-next-line @next/next/no-img-element + {post.title} + )} +
+

+ + {post.title} + +

+
+ {post.published_at && ( + + {new Date(post.published_at).toLocaleDateString( + siteConfig.defaultLocale + )} + + )} + {post.tags && post.tags.length > 0 && ( + + {post.tags.slice(0, 3).map((t) => ( + + #{t} + + ))} + + )} +
+ {post.description && ( +

+ {post.description} +

+ )} +
+
+ ); +} + diff --git a/components/post-toc.tsx b/components/post-toc.tsx new file mode 100644 index 0000000..d0d9fb1 --- /dev/null +++ b/components/post-toc.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface TocItem { + id: string; + text: string; + depth: number; +} + +export function PostToc() { + const [items, setItems] = useState([]); + + useEffect(() => { + const headings = Array.from( + document.querySelectorAll('article h2, article h3') + ); + const mapped = headings + .filter((el) => el.id) + .map((el) => ({ + id: el.id, + text: el.innerText, + depth: el.tagName === 'H3' ? 3 : 2 + })); + setItems(mapped); + }, []); + + if (items.length === 0) return null; + + return ( + + ); +} + diff --git a/components/reading-progress.tsx b/components/reading-progress.tsx new file mode 100644 index 0000000..7307b38 --- /dev/null +++ b/components/reading-progress.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +export function ReadingProgress() { + const [mounted, setMounted] = useState(false); + const [progress, setProgress] = useState(0); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = document.documentElement; + const total = scrollHeight - clientHeight; + if (total <= 0) { + setProgress(0); + return; + } + const value = Math.min(100, Math.max(0, (scrollTop / total) * 100)); + setProgress(value); + }; + + handleScroll(); + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, [mounted]); + + if (!mounted) return null; + + return ( +
+
+
+ ); +} + diff --git a/tailwind.config.js b/tailwind.config.js index 5371fa5..f9de1f5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,11 +4,61 @@ module.exports = { content: [ "./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", - "../Blog 文章原稿/**/*.{md,mdx}" + "./content/**/*.{md,mdx}" ], theme: { - extend: {}, + extend: { + typography: (theme) => ({ + DEFAULT: { + css: { + color: theme('colors.slate.700'), + a: { + color: theme('colors.blue.600'), + '&:hover': { + color: theme('colors.blue.700') + } + }, + h1: { + fontWeight: '700', + letterSpacing: '-0.03em' + }, + h2: { + fontWeight: '600', + letterSpacing: '-0.02em' + }, + blockquote: { + fontStyle: 'normal', + borderLeftColor: theme('colors.blue.200'), + color: theme('colors.slate.700'), + backgroundColor: theme('colors.slate.50') + }, + code: { + backgroundColor: theme('colors.slate.100'), + padding: '0.15rem 0.35rem', + borderRadius: '0.25rem' + } + } + }, + dark: { + css: { + color: theme('colors.slate.100'), + a: { + color: theme('colors.blue.400'), + '&:hover': { + color: theme('colors.blue.300') + } + }, + blockquote: { + borderLeftColor: theme('colors.blue.500'), + backgroundColor: theme('colors.slate.900') + }, + code: { + backgroundColor: theme('colors.slate.800') + } + } + } + }) + }, }, plugins: [require('@tailwindcss/typography')], }; -