Kto-Blog
Published on

nextjs博客项目-进阶研读

Authors
  • avatar
    Name
    Kto

前言

本篇博客基于 Next.js Starter Blog 项目进行二次开发的实践经验分享。这个博客模板采用了现代化的技术栈,包括 Next.js App Router、Tailwind CSS、Contentlayer(MDX 处理)、Framer Motion 等。本文将深入介绍如何基于这个模板进行个性化定制,涵盖组件开发、样式系统、性能优化等多个方面。

一、如何引入图片

方式一:使用本地图片

将图片放置在 public/ 目录下,然后通过 Image 组件引用:

import Image from '@/components/Image'

// 直接使用
;<Image src="/images/your-image.png" alt="描述文字" width={800} height={400} />

方式二:使用远程图片

首先需要在 next.config.js 中配置允许的图片域名:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com', // 替换为你的图片域名
        pathname: '/images/**',
      },
    ],
    // 图片优化配置
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    formats: ['image/webp', 'image/avif'],
  },
}

然后在 MDX 文件中使用:

![图片描述](https://example.com/image.png)

或使用组件:

<Image
  src="https://example.com/image.png"
  alt="描述"
  width={800}
  height={400}
  priority // 首屏图片优先加载
/>

响应式图片处理

使用 sizes 属性实现响应式图片:

<Image
  src="/images/hero.jpg"
  alt="Hero Image"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  style={{ objectFit: 'cover' }}
/>

图片占位符和模糊效果

实现图片加载时的模糊占位效果:

<Image
  src="/images/large.jpg"
  alt="大图"
  width={1200}
  height={600}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>

图片懒加载策略

import Image from 'next/image'

export default function Gallery() {
  return (
    <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
      {[...Array(9)].map((_, i) => (
        <Image
          key={i}
          src={`/images/image-${i}.jpg`}
          alt={`Gallery image ${i}`}
          width={400}
          height={300}
          loading="lazy" // 懒加载
          // 非首屏图片设置 loading="lazy"
        />
      ))}
    </div>
  )
}

二、如何修改样式

全局样式修改

项目使用 Tailwind CSS 作为样式框架。全局样式文件位于 css/tailwind.css

/* css/tailwind.css */

/* 自定义滚动条样式 */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  @apply bg-gray-100 dark:bg-gray-800;
}

::-webkit-scrollbar-thumb {
  @apply rounded-full bg-gray-400 dark:bg-gray-600;
}

::-webkit-scrollbar-thumb:hover {
  @apply bg-gray-500 dark:bg-gray-500;
}

/* 自定义选中文字样式 */
::selection {
  @apply bg-primary/20 text-primary;
}

/* 自定义动画 */
@keyframes custom-animation {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

.animate-custom {
  animation: custom-animation 2s ease-in-out infinite;
}

/* 呼吸灯效果 */
@keyframes breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

.animate-breathe {
  animation: breathe 3s ease-in-out infinite;
}

Tailwind 配置扩展

tailwind.config.js 中可以扩展主题配置:

module.exports = {
  darkMode: 'class', // 基于类的暗黑模式
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './layouts/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: {
        // 添加自定义颜色系列
        brand: {
          50: '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
          800: '#1e40af',
          900: '#1e3a8a',
        },
      },
      fontFamily: {
        // 添加自定义字体
        custom: ['CustomFont', 'sans-serif'],
        display: ['DisplayFont', 'sans-serif'],
      },
      animation: {
        // 添加自定义动画
        'fade-in': 'fadeIn 0.5s ease-in-out',
        'slide-up': 'slideUp 0.5s ease-out',
        'bounce-slow': 'bounce 3s infinite',
      },
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(20px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
      },
      spacing: {
        // 添加自定义间距
        128: '32rem',
        144: '36rem',
      },
      borderRadius: {
        // 添加自定义圆角
        '4xl': '2rem',
      },
      boxShadow: {
        // 添加自定义阴影
        glow: '0 0 20px rgba(59, 130, 246, 0.5)',
      },
    },
  },
  plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
}

CSS 变量系统

使用 CSS 变量实现主题动态切换:

/* css/tailwind.css */

:root {
  /* 颜色变量 */
  --color-primary: 59 130 246; /* RGB values */
  --color-secondary: 139 92 246;

  /* 间距变量 */
  --spacing-unit: 0.25rem;

  /* 字体大小 */
  --font-size-base: 1rem;

  /* 过渡时间 */
  --transition-fast: 150ms;
  --transition-normal: 300ms;
}

.dark {
  --color-primary: 96 165 250;
  --color-secondary: 167 139 250;
}

在 Tailwind 中使用:

<div className="bg-[rgb(var(--color-primary))] p-[calc(var(--spacing-unit)*4)]">动态样式内容</div>

组件级样式修改

对于特定组件,可以直接修改组件文件。例如修改导航栏样式:

// components/Header.tsx
import { useState, useEffect } from 'react'

const nav = `
  sticky top-0 z-50
  bg-white/80 dark:bg-gray-900/80
  backdrop-blur-md
  border-b border-gray-200 dark:border-gray-700
  transition-all duration-300
`

const navLinkClass = `
  relative
  px-4 py-2
  text-gray-700 dark:text-gray-300
  hover:text-primary dark:hover:text-primary
  transition-colors duration-200
  after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-0 after:h-0.5
  after:bg-primary after:transition-all after:duration-300
  hover:after:w-full
`

export default function Header() {
  const [scrolled, setScrolled] = useState(false)

  useEffect(() => {
    const handleScroll = () => {
      setScrolled(window.scrollY > 20)
    }
    window.addEventListener('scroll', handleScroll)
    return () => window.removeEventListener('scroll', handleScroll)
  }, [])

  return (
    <header className={`${nav} ${scrolled ? 'shadow-lg' : ''}`}>
      <nav className="container mx-auto px-4">{/* 导航内容 */}</nav>
    </header>
  )
}

动态样式和条件样式

使用 clsx 或 classnames 实现动态样式:

import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

// 合并 Tailwind 类的工具函数
function cn(...inputs) {
  return twMerge(clsx(inputs))
}

export function Button({ variant = 'primary', size = 'md', children, className }) {
  const baseStyles = 'rounded-lg font-medium transition-all duration-200'

  const variants = {
    primary: 'bg-primary text-white hover:bg-primary-dark',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    outline: 'border-2 border-primary text-primary hover:bg-primary hover:text-white',
  }

  const sizes = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg',
  }

  return (
    <button className={cn(baseStyles, variants[variant], sizes[size], className)}>
      {children}
    </button>
  )
}

三、如何修改背景色

修改全局背景

app/layout.tsx 中修改 body 的背景:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="zh-cn" className="scroll-smooth">
      <body className="bg-gray-50 dark:bg-gray-900">{/* 页面内容 */}</body>
    </html>
  )
}

渐变背景

项目当前使用了渐变背景效果:

// app/layout.tsx
<body className="
  animate-gradient
  bg-gradient-to-r
  from-indigo-50 via-purple-50 to-pink-50
  dark:from-stone-900 dark:via-zinc-900 dark:to-slate-900
  pl-[calc(100vw-100%)]
  text-black antialiased dark:text-white
">

关键类说明:

  • animate-gradient: 自定义动画类,让渐变缓慢移动
  • bg-gradient-to-r: 水平渐变方向
  • from-{color}, via-{color}, to-{color}: 渐变颜色点
  • dark: 前缀: 暗黑模式下的样式
  • pl-[calc(100vw-100%)]: 防止滚动条出现时的布局偏移
  • antialiased: 字体抗锯齿

修改渐变动画

css/tailwind.css 中定义动画:

@keyframes gradient {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

.animate-gradient {
  background-size: 200% 200%;
  animation: gradient 10s ease infinite;
}

/* 更快的渐变动画 */
.animate-gradient-fast {
  background-size: 200% 200%;
  animation: gradient 5s ease infinite;
}

/* 垂直渐变动画 */
@keyframes gradient-vertical {
  0% {
    background-position: 50% 0%;
  }
  50% {
    background-position: 50% 100%;
  }
  100% {
    background-position: 50% 0%;
  }
}

.animate-gradient-vertical {
  background-size: 200% 200%;
  animation: gradient-vertical 15s ease infinite;
}

网格背景效果

创建科技感的网格背景:

// components/GridBackground.tsx
export default function GridBackground() {
  return (
    <div className="fixed inset-0 -z-10">
      <div
        className="
        bg-[linear-gradient(to_right,#80808012_1px,transparent_1px), [linear-gradient(to_bottom,#80808012_1px,transparent_1px)]
        absolute
               inset-0
        bg-[size:24px_24px]
      "
      />
      <div
        className="
        absolute left-0 right-0 top-0 -z-10
        h-[500px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(120,119,198,0.3),rgba(255,255,255,0))]
      "
      />
    </div>
  )
}

动态粒子背景

使用 Canvas 创建动态粒子效果:

// components/ParticleBackground.tsx
'use client'

import { useEffect, useRef } from 'react'

export default function ParticleBackground() {
  const canvasRef = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const ctx = canvas.getContext('2d')
    if (!ctx) return

    canvas.width = window.innerWidth
    canvas.height = window.innerHeight

    const particles: Array<{ x: number; y: number; vx: number; vy: number }> = []

    for (let i = 0; i < 50; i++) {
      particles.push({
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        vx: (Math.random() - 0.5) * 0.5,
        vy: (Math.random() - 0.5) * 0.5,
      })
    }

    function animate() {
      ctx.clearRect(0, 0, canvas.width, canvas.height)

      particles.forEach((p, i) => {
        p.x += p.vx
        p.y += p.vy

        if (p.x < 0 || p.x > canvas.width) p.vx *= -1
        if (p.y < 0 || p.y > canvas.height) p.vy *= -1

        ctx.beginPath()
        ctx.arc(p.x, p.y, 2, 0, Math.PI * 2)
        ctx.fillStyle = 'rgba(59, 130, 246, 0.5)'
        ctx.fill()

        particles.slice(i + 1).forEach((p2) => {
          const dx = p.x - p2.x
          const dy = p.y - p2.y
          const dist = Math.sqrt(dx * dx + dy * dy)

          if (dist < 100) {
            ctx.beginPath()
            ctx.moveTo(p.x, p.y)
            ctx.lineTo(p2.x, p2.y)
            ctx.strokeStyle = `rgba(59, 130, 246, ${0.2 * (1 - dist / 100)})`
            ctx.stroke()
          }
        })
      })

      requestAnimationFrame(animate)
    }

    animate()

    const handleResize = () => {
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return <canvas ref={canvasRef} className="fixed inset-0 -z-10 opacity-30 dark:opacity-20" />
}

四、如何添加组件

创建新组件

components/ 目录下创建新的组件文件:

// components/MyComponent.tsx
import { ReactNode } from 'react'

interface MyComponentProps {
  title: string
  children?: ReactNode
  variant?: 'default' | 'primary' | 'secondary'
}

export default function MyComponent({ title, children, variant = 'default' }: MyComponentProps) {
  const variants = {
    default: 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800',
    primary: 'border-primary bg-primary/5 dark:bg-primary/10',
    secondary: 'border-purple-300 bg-purple-50 dark:border-purple-700 dark:bg-purple-900/20',
  }

  return (
    <div
      className={`
      rounded-lg border p-4 shadow-sm transition-all duration-200
      hover:shadow-md
      ${variants[variant]}
    `}
    >
      <h3 className="mb-2 text-lg font-semibold">{title}</h3>
      {children}
    </div>
  )
}

在 MDX 中使用组件

首先在 components/MDXComponents.tsx 中注册组件:

// components/MDXComponents.tsx
import { MDXComponents } from 'mdx/types'
import Image from './Image'
import TOCInline from './TOCInline'
import MyComponent from './MyComponent'
import CustomLink from './Link'

export const components: MDXComponents = {
  Image,
  TOCInline,
  MyComponent,
  a: CustomLink,
}

然后在 MDX 文件中直接使用:

---
title: 使用自定义组件
date: 2026-01-02
tags: ['nextjs', 'mdx']
---

## 引言

这是一个演示如何使用自定义组件的文章。

<MyComponent title="提示信息" variant="primary">
  这是组件的内容部分。你可以在这里放置任何内容。
</MyComponent>

<MyComponent title="警告信息" variant="secondary">
  这是警告样式的组件。
</MyComponent>

全局组件注册

对于需要在整个应用中使用的组件,在布局文件中引入:

// app/layout.tsx
import ProgressBar from '@/components/ProgressBar'
import GridBackground from '@/components/GridBackground'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <GridBackground />
        <ProgressBar />
        {children}
      </body>
    </html>
  )
}

组件通信和状态管理

使用 Context API 实现跨组件状态共享:

// components/Accordion.tsx
'use client'

import { createContext, useContext, useState, ReactNode } from 'react'

interface AccordionContextType {
  openItems: Set<string>
  toggleItem: (id: string) => void
}

const AccordionContext = createContext<AccordionContextType | null>(null)

function useAccordion() {
  const context = useContext(AccordionContext)
  if (!context) {
    throw new Error('Accordion components must be used within <Accordion>')
  }
  return context
}

export function Accordion({
  children,
  defaultOpen = [],
}: {
  children: ReactNode
  defaultOpen?: string[]
}) {
  const [openItems, setOpenItems] = useState(new Set(defaultOpen))

  const toggleItem = (id: string) => {
    const newOpen = new Set(openItems)
    if (newOpen.has(id)) {
      newOpen.delete(id)
    } else {
      newOpen.add(id)
    }
    setOpenItems(newOpen)
  }

  return (
    <AccordionContext.Provider value={{ openItems, toggleItem }}>
      <div className="space-y-2">{children}</div>
    </AccordionContext.Provider>
  )
}

export function AccordionItem({
  id,
  title,
  children,
}: {
  id: string
  title: string
  children: ReactNode
}) {
  const { openItems, toggleItem } = useAccordion()
  const isOpen = openItems.has(id)

  return (
    <div className="rounded-lg border border-gray-200 dark:border-gray-700">
      <button
        onClick={() => toggleItem(id)}
        className="w-full px-4 py-3 text-left font-medium transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
      >
        <span className="flex items-center justify-between">
          {title}
          <span className={`transform transition-transform ${isOpen ? 'rotate-180' : ''}`}></span>
        </span>
      </button>
      {isOpen && (
        <div className="border-t border-gray-200 px-4 py-3 dark:border-gray-700">{children}</div>
      )}
    </div>
  )
}

使用示例:

<Accordion>
  <AccordionItem id="1" title="第一章节">
    这是第一章节的内容...
  </AccordionItem>
  <AccordionItem id="2" title="第二章节">
    这是第二章节的内容...
  </AccordionItem>
</Accordion>

组合组件模式

创建可组合的卡片组件:

// components/Card.tsx
import { ReactNode } from 'react'

interface CardProps {
  children: ReactNode
  className?: string
}

function Card({ children, className = '' }: CardProps) {
  return (
    <div
      className={`
      rounded-lg border border-gray-200 bg-white
      shadow-sm transition-all duration-200
      hover:shadow-md
      dark:border-gray-700 dark:bg-gray-800
      ${className}
    `}
    >
      {children}
    </div>
  )
}

function CardHeader({ children }: { children: ReactNode }) {
  return <div className="border-b border-gray-200 p-4 dark:border-gray-700">{children}</div>
}

function CardTitle({ children }: { children: ReactNode }) {
  return <h3 className="text-lg font-semibold">{children}</h3>
}

function CardContent({ children }: { children: ReactNode }) {
  return <div className="p-4">{children}</div>
}

function CardFooter({ children }: { children: ReactNode }) {
  return <div className="border-t border-gray-200 p-4 dark:border-gray-700">{children}</div>
}

export { Card, CardHeader, CardTitle, CardContent, CardFooter }

使用示例:

import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/Card'

;<Card>
  <CardHeader>
    <CardTitle>卡片标题</CardTitle>
  </CardHeader>
  <CardContent>
    <p>这是卡片的主要内容...</p>
  </CardContent>
  <CardFooter>
    <button>操作按钮</button>
  </CardFooter>
</Card>

五、如何添加进度条

项目包含一个精美的阅读进度条组件,位于 components/ProgressBar.tsx

进度条实现原理

// components/ProgressBar.tsx
'use client'

import { useEffect, useState } from 'react'
import { motion, useSpring, useTransform } from 'framer-motion'

export function useReadingProgress() {
  const [progress, setProgress] = useState(0)

  useEffect(() => {
    function updateScroll() {
      const currentScrollY = window.scrollY
      const scrollHeight = document.body.scrollHeight - window.innerHeight
      if (scrollHeight > 0) {
        const progress = Number((currentScrollY / scrollHeight).toFixed(2)) * 100
        setProgress(progress)
      }
    }

    // 使用 requestAnimationFrame 优化性能
    let ticking = false
    function onScroll() {
      if (!ticking) {
        window.requestAnimationFrame(() => {
          updateScroll()
          ticking = false
        })
        ticking = true
      }
    }

    window.addEventListener('scroll', onScroll, { passive: true })
    return () => window.removeEventListener('scroll', onScroll)
  }, [])

  return progress
}

export default function ProgressBar() {
  const progress = useReadingProgress()

  // 使用弹性动画
  const scaleX = useSpring(progress / 100, {
    stiffness: 100,
    damping: 30,
    restDelta: 0.001,
  })

  return (
    <motion.div className="fixed left-0 right-0 top-0 z-50 h-1 origin-left" style={{ scaleX }}>
      <div className="h-full bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500" />
    </motion.div>
  )
}

使用进度条

app/layout.tsx 中引入:

import ProgressBar from '@/components/ProgressBar'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ProgressBar />
        {children}
      </body>
    </html>
  )
}

自定义进度条样式

可以修改进度条的样式属性:

// 渐变进度条
<motion.div
  className="fixed top-0 left-0 right-0 h-2 z-50 origin-left"
  style={{ scaleX: progress / 100 }}
>
  <div className="h-full bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500" />
</motion.div>

// 发光效果进度条
<motion.div
  className="fixed top-0 left-0 right-0 h-1 z-50 origin-left"
  style={{ scaleX: progress / 100 }}
>
  <div className="h-full bg-primary shadow-[0_0_10px_rgba(59,130,246,0.8)]" />
</motion.div>

// 多色进度条
<motion.div
  className="fixed top-0 left-0 right-0 h-1 z-50 origin-left bg-gray-200 dark:bg-gray-700"
  style={{ scaleX: progress / 100 }}
>
  <div className="h-full bg-gradient-to-r from-green-400 via-blue-500 to-purple-600" />
</motion.div>

底部进度条

创建底部固定位置的进度条:

// components/BottomProgressBar.tsx
export default function BottomProgressBar() {
  const progress = useReadingProgress()

  return (
    <div className="fixed bottom-0 left-0 right-0 z-50">
      <motion.div className="bg-primary h-1 origin-left" style={{ scaleX: progress / 100 }} />
      <div className="bg-gray-900/80 px-4 py-2 text-center text-sm text-white backdrop-blur-sm">
        阅读进度: {Math.round(progress)}%
      </div>
    </div>
  )
}

圆形进度指示器

创建圆形进度指示器:

// components/CircularProgress.tsx
export default function CircularProgress() {
  const progress = useReadingProgress()

  return (
    <div className="fixed bottom-8 right-8 z-50">
      <div className="relative h-16 w-16">
        <svg className="h-full w-full -rotate-90 transform">
          <circle
            cx="32"
            cy="32"
            r="28"
            stroke="currentColor"
            strokeWidth="4"
            fill="none"
            className="text-gray-200 dark:text-gray-700"
          />
          <motion.circle
            cx="32"
            cy="32"
            r="28"
            stroke="currentColor"
            strokeWidth="4"
            fill="none"
            strokeDasharray={175.93}
            strokeDashoffset={175.93 * (1 - progress / 100)}
            className="text-primary"
          />
        </svg>
        <div className="absolute inset-0 flex items-center justify-center text-xs font-medium">
          {Math.round(progress)}%
        </div>
      </div>
    </div>
  )
}

六、如何添加文章 TOC 组件

文章目录(Table of Contents)组件位于 components/TOCInline.tsx,是一个功能完善的目录导航组件。

TOC 组件功能特性

  • 自动提取文章标题生成目录
  • 滚动时高亮当前章节
  • 平滑滚动到指定章节
  • 响应式设计(桌面端固定右侧)
  • 毛玻璃背景效果
  • 支持嵌套标题层级
  • 性能优化(IntersectionObserver + 防抖)

在文章中使用 TOC

在 MDX 文件中直接使用:

---
title: 你的文章标题
date: 2026-01-02
tags: ['nextjs']
---

## 引言

文章引言内容,介绍文章的主要内容...

<TOCInline />

## 第一节:基础概念

这是第一节的详细内容...

### 1.1 子章节

这是子章节的内容...

## 第二节:进阶应用

这是第二节的详细内容...

TOC 配置选项

组件内部包含可配置的常量:

const CONFIG = {
  SCROLL: {
    OFFSET: 96, // 滚动偏移量,考虑固定头部高度
    DEBOUNCE_DELAY: 100, // 防抖延迟(毫秒)
    CLICK_THRESHOLD: 500, // 点击后的冷却时间(毫秒)
  },
  STYLE: {
    INDENT_MULTIPLIER: 1.25, // 每级标题的缩进倍数
  },
  OBSERVER: {
    rootMargin: '0px 0px -50% 0px', // IntersectionObserver 的根边距
    threshold: [0.25, 0.75], // 触发回调的可见比例阈值
  },
}

自定义 TOC 样式

可以通过修改组件的 className 来自定义样式:

<div
  className="
  no-scrollbar fixed right-4
  top-24 hidden
  max-h-[calc(100vh-8rem)]
  w-64 overflow-y-auto
  rounded-lg border border-gray-200
  bg-white/80 p-4
  shadow-lg
  backdrop-blur-sm
  dark:border-gray-700
  dark:bg-gray-900/80
  xl:block
"
>
  {/* TOC 内容 */}
</div>

样式类说明:

  • fixed right-4 top-24: 固定定位在右侧
  • hidden xl:block: 仅在超大屏幕上显示
  • max-h-[calc(100vh-8rem)]: 最大高度为视口高度减去边距
  • backdrop-blur-sm: 毛玻璃效果
  • no-scrollbar: 隐藏滚动条(自定义类)

TOC 实现原理深度解析

1. 标题提取(Contentlayer)

标题在 Contentlayer 构建时自动提取:

// contentlayer.config.ts
import { extractTocHeadings } from '@pliny/mdx'

export const Blog = defineDocumentType(() => ({
  name: 'Blog',
  filePathPattern: 'blog/**/*.mdx',
  contentType: 'mdx',
  computedFields: {
    toc: {
      type: 'json',
      resolve: (doc) => extractTocHeadings(doc.body.raw),
    },
  },
}))

提取的 TOC 结构:

interface TocItem {
  value: string // 标题文字
  url: string // 锚点链接
  depth: number // 标题层级(1-6)
}

2. 可见性追踪(IntersectionObserver)

使用 IntersectionObserver API 高效追踪标题可见性:

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        setActiveId(entry.target.id)
      }
    })
  },
  {
    rootMargin: '0px 0px -50% 0px', // 当元素到达视口中线时触发
    threshold: [0.25, 0.75], // 25% 和 75% 可见时触发
  }
)

3. 滚动同步优化

使用防抖和 requestAnimationFrame 优化滚动性能:

useEffect(() => {
  const updateActiveId = () => {
    // 计算当前可见的标题
    const headings = document.querySelectorAll('h2, h3, h4')
    // ... 计算逻辑
  }

  let ticking = false
  const onScroll = () => {
    if (!ticking) {
      window.requestAnimationFrame(() => {
        updateActiveId()
        ticking = false
      })
      ticking = true
    }
  }

  window.addEventListener('scroll', onScroll, { passive: true })
  return () => window.removeEventListener('scroll', onScroll)
}, [])

4. 平滑滚动

点击目录项时平滑滚动到对应标题:

const handleClick = (url: string) => {
  const element = document.querySelector(url)
  if (element) {
    const yOffset = -CONFIG.SCROLL.OFFSET // 考虑固定头部
    const y = element.getBoundingClientRect().top + window.scrollY + yOffset

    window.scrollTo({ top: y, behavior: 'smooth' })
  }
}

TOC 进阶定制

添加进度指示器

在 TOC 旁边添加当前阅读进度:

// components/TOCWithProgress.tsx
export default function TOCWithProgress({ toc }: { toc: TocItem[] }) {
  const [activeId, setActiveId] = useState<string>('')

  // 计算当前章节的阅读进度
  const activeIndex = toc.findIndex((item) => item.url === `#${activeId}`)
  const progress = toc.length > 0 ? ((activeIndex + 1) / toc.length) * 100 : 0

  return (
    <div className="relative">
      {/* 环形进度条 */}
      <svg className="h-8 w-8 -rotate-90 transform">
        <circle
          cx="16"
          cy="16"
          r="14"
          stroke="currentColor"
          strokeWidth="2"
          fill="none"
          className="text-gray-200 dark:text-gray-700"
        />
        <circle
          cx="16"
          cy="16"
          r="14"
          stroke="currentColor"
          strokeWidth="2"
          fill="none"
          strokeDasharray={87.96}
          strokeDashoffset={87.96 * (1 - progress / 100)}
          className="text-primary"
        />
      </svg>
      {/* TOC 内容 */}
      <TOCInline toc={toc} />
    </div>
  )
}

折叠式 TOC

添加折叠展开功能:

// components/CollapsibleTOC.tsx
'use client'

import { useState } from 'react'

export default function CollapsibleTOC({ toc }: { toc: TocItem[] }) {
  const [isCollapsed, setIsCollapsed] = useState(false)

  return (
    <div
      className="
      {isCollapsed ? 'w-12'
      : 'w-64'}
      fixed right-4 top-24
      hidden rounded-lg
      border border-gray-200
      bg-white/80 backdrop-blur-sm
      transition-all duration-300 dark:border-gray-700 dark:bg-gray-900/80 xl:block
    "
    >
      <button
        onClick={() => setIsCollapsed(!isCollapsed)}
        className="w-full p-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800"
      >
        {isCollapsed ? '→' : '←'}
      </button>
      {!isCollapsed && (
        <div className="p-4">
          <TOCInline toc={toc} />
        </div>
      )}
    </div>
  )
}

七、其他实用组件

加载进度条 (LoadingBar)

位于 components/LoadingBar.tsx,在路由切换时显示加载进度。

// 使用方法
import LoadingBar from '@/components/LoadingBar'

// 在 layout.tsx 中添加
;<LoadingBar />

特性:

  • 监听页面加载和路由切换
  • 自动完成机制(最多5秒)
  • 渐变色动画效果
  • 防止重复触发

主题切换 (ThemeSwitch)

位于 components/ThemeSwitch.tsx,支持亮色/暗色主题切换。

// 自定义主题切换按钮
import ThemeSwitch from '@/components/ThemeSwitch'

;<ThemeSwitch />

特性:

  • 使用 next-themes 管理主题
  • 优雅的切换动画
  • 太阳/月亮图标
  • 自动适配系统主题

返回顶部 (ScrollTopAndComment)

位于 components/ScrollTopAndComment.tsx,提供返回顶部和跳转评论区的按钮。

// 使用方法
import ScrollTopAndComment from '@/components/ScrollTopAndComment'

;<ScrollTopAndComment />

特性:

  • 滚动超过50px后显示
  • 返回顶部功能
  • 跳转到评论区
  • 平滑滚动动画

代码块复制按钮

// components/CodeBlock.tsx
'use client'

import { useState } from 'react'
import { CheckIcon, ClipboardIcon } from '@heroicons/react/24/outline'

export default function CodeBlock({
  children,
  className,
}: {
  children: React.ReactNode
  className?: string
}) {
  const [copied, setCopied] = useState(false)

  const copyToClipboard = async () => {
    const code = children?.toString()
    await navigator.clipboard.writeText(code || '')
    setCopied(true)
    setTimeout(() => setCopied(false), 2000)
  }

  return (
    <div className="group relative">
      <pre className={className}>
        <code>{children}</code>
      </pre>
      <button
        onClick={copyToClipboard}
        className="absolute right-2 top-2 rounded-md bg-gray-700 p-2 opacity-0 transition-opacity group-hover:opacity-100"
      >
        {copied ? (
          <CheckIcon className="h-4 w-4 text-green-400" />
        ) : (
          <ClipboardIcon className="h-4 w-4 text-gray-300" />
        )}
      </button>
    </div>
  )
}

通知提示组件

// components/Toast.tsx
'use client'

import { useEffect, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'

interface ToastProps {
  message: string
  type?: 'success' | 'error' | 'info'
  duration?: number
}

export function useToast() {
  const [toast, setToast] = useState<ToastProps | null>(null)

  const showToast = (props: ToastProps) => {
    setToast(props)
  }

  return { toast, showToast }
}

export default function Toast({ message, type = 'info', duration = 3000 }: ToastProps) {
  const [visible, setVisible] = useState(true)

  useEffect(() => {
    const timer = setTimeout(() => setVisible(false), duration)
    return () => clearTimeout(timer)
  }, [duration])

  const types = {
    success: 'bg-green-500',
    error: 'bg-red-500',
    info: 'bg-blue-500',
  }

  return (
    <AnimatePresence>
      {visible && (
        <motion.div
          initial={{ opacity: 0, y: -20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
          className={`fixed right-4 top-4 z-50 rounded-lg px-6 py-3 text-white shadow-lg ${types[type]}`}
        >
          {message}
        </motion.div>
      )}
    </AnimatePresence>
  )
}

八、性能优化技巧

React 性能优化

使用 React.memo 防止不必要的重渲染

import { memo } from 'react'

const ExpensiveComponent = memo(function ExpensiveComponent({ data }: { data: any }) {
  // 组件逻辑
  return <div>{/* 渲染内容 */}</div>
})

使用 useMemo 缓存计算结果

import { useMemo } from 'react'

function ProcessedList({ items }: { items: any[] }) {
  const processed = useMemo(() => {
    return items.map((item) => {
      // 昂贵的计算
      return transform(item)
    })
  }, [items])

  return <div>{processed}</div>
}

使用 useCallback 稳定函数引用

import { useCallback } from 'react'

function ParentComponent() {
  const handleClick = useCallback(() => {
    // 处理点击
  }, [])

  return <ChildComponent onClick={handleClick} />
}

代码分割和懒加载

动态导入组件

import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
  loading: () => <div>Loading...</div>,
  ssr: false, // 客户端渲染
})

路由级代码分割

// Next.js App Router 自动进行路由级代码分割
// app/blog/[slug]/page.tsx 会自动分割

图片优化

import Image from 'next/image'

// 正确的图片使用方式
;<Image
  src="/images/photo.jpg"
  alt="描述"
  width={800}
  height={600}
  priority // 首屏图片使用
  loading="lazy" // 非首屏图片使用
  placeholder="blur" // 添加模糊占位符
  blurDataURL="data:image/..." // 模糊数据
/>

字体优化

// app/layout.tsx
import { Space_Grotesk } from 'next/font/google'

const spaceGrotesk = Space_Grotesk({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-space-grotesk',
})

export default function RootLayout({ children }) {
  return (
    <html className={spaceGrotesk.variable}>
      <body>{children}</body>
    </html>
  )
}

九、SEO 优化

元数据配置

// app/layout.tsx
export const metadata = {
  title: {
    default: '网站标题',
    template: '%s | 网站标题',
  },
  description: '网站描述',
  keywords: ['关键词1', '关键词2'],
  authors: [{ name: '作者名' }],
  openGraph: {
    title: '网站标题',
    description: '网站描述',
    url: 'https://example.com',
    siteName: '网站名称',
    images: [
      {
        url: 'https://example.com/og.jpg',
        width: 1200,
        height: 630,
      },
    ],
    locale: 'zh_CN',
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: '网站标题',
    description: '网站描述',
    images: ['https://example.com/og.jpg'],
  },
}

文章元数据

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)

  return {
    title: post.title,
    description: post.summary,
    openGraph: {
      title: post.title,
      description: post.summary,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author],
      images: post.images,
    },
  }
}

结构化数据

// components/StructuredData.tsx
export function BlogStructuredData({ post }: { post: Blog }) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.summary,
    datePublished: post.date,
    author: {
      '@type': 'Person',
      name: post.author,
    },
  }

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  )
}

Sitemap 生成

// app/sitemap.ts
import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://example.com'

  return [
    {
      url: baseUrl,
      lastModified: new Date(),
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
    },
    // 动态生成文章链接
    ...getAllPosts().map((post) => ({
      url: `${baseUrl}/blog/${post.slug}`,
      lastModified: new Date(post.lastmod || post.date),
    })),
  ]
}

十、开发建议

1. 组件设计原则

  • 单一职责:每个组件只负责一个功能
  • 可复用性:通过 props 实现灵活配置
  • 组合优于继承:使用组件组合构建复杂 UI
  • 类型安全:充分利用 TypeScript 类型系统

2. 样式一致性

  • 使用 Tailwind 的设计令牌保持风格统一
  • 定义统一的颜色、间距、字体规范
  • 创建可复用的样式变体

3. 性能优化

  • 使用 React DevTools Profiler 分析性能
  • 实施代码分割减少初始加载体积
  • 优化图片和字体加载
  • 使用缓存策略减少重复请求

4. 响应式设计

  • 移动优先的设计策略
  • 测试不同设备和屏幕尺寸
  • 使用 Tailwind 的响应式修饰符
  • 考虑触摸交互和手势

5. 可访问性

  • 使用语义化 HTML 标签
  • 提供键盘导航支持
  • 添加适当的 ARIA 属性
  • 确保颜色对比度符合标准
  • 为图片添加 alt 描述

6. 代码组织

  • 按功能而非类型组织文件
  • 保持文件结构清晰和一致
  • 使用 barrel exports 简化导入
  • 编写清晰的注释和文档

十一、进阶主题

MDX 自定义插件

// contentlayer.config.ts
import rehypeKatex from 'rehype-katex'
import remarkMath from 'remark-math'

export default defineConfig({
  mdx: {
    remarkPlugins: [
      remarkMath, // 数学公式支持
    ],
    rehypePlugins: [
      rehypeKatex, // KaTeX 渲染
    ],
  },
})

API 路由集成

// app/api/views/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const { slug } = await request.json()

  // 更新浏览计数
  await updateViews(slug)

  return NextResponse.json({ views: await getViews(slug) })
}

评论系统集成

// components/Comments.tsx
'use client'

import { useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'

export default function Comments() {
  const ref = useRef<HTMLDivElement>(null)
  const pathname = usePathname()

  useEffect(() => {
    // 懒加载评论组件
    const script = document.createElement('script')
    script.src = 'https://giscus.app/client.js'
    script.setAttribute('data-repo', 'your-repo')
    script.setAttribute('data-repo-id', 'your-repo-id')
    script.setAttribute('data-theme', 'preferred_color_scheme')
    script.async = true

    ref.current?.appendChild(script)
  }, [pathname])

  return <div ref={ref} className="mt-12" />
}

搜索功能定制

// components/Search.tsx
'use client'

import { KBarProvider, KBarPortal, KBarPositioner } from 'kbar'
import { useActions } from '@/lib/actions'

export default function Search({ children }: { children: React.ReactNode }) {
  const actions = useActions()

  return (
    <KBarProvider actions={actions}>
      <KBarPortal>
        <KBarPositioner>{/* 搜索界面 */}</KBarPositioner>
      </KBarPortal>
      {children}
    </KBarProvider>
  )
}

十二、部署和构建优化

环境变量配置

# .env.local
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
DATABASE_URL=postgresql://...

构建优化

// next.config.js
module.exports = {
  // 启用 SWC 压缩
  swcMinify: true,

  // 生产源映射
  productionBrowserSourceMaps: false,

  // 实验性功能
  experimental: {
    // Turbopack 支持
    turbo: {
      rules: {
        '*.svg': {
          loaders: ['@svgr/webpack'],
          as: '*.js',
        },
      },
    },
  },
}

静态导出

// next.config.js
module.exports = {
  output: 'export', // 静态 HTML 导出
  images: {
    unoptimized: true, // 静态导出时禁用图片优化
  },
}

总结

这个 Next.js 博客模板提供了完善的组件体系和样式系统,通过合理使用和扩展这些功能,可以快速构建出功能完善、外观精美的个人博客。本文涵盖了从基础组件使用到高级性能优化的各个方面,建议在修改前先了解项目结构,按照最佳实践进行定制开发。

学习路径建议

  1. 基础阶段:熟悉项目结构,理解组件系统
  2. 进阶阶段:定制样式,添加自定义组件
  3. 优化阶段:性能优化,SEO 提升
  4. 高级阶段:扩展功能,集成第三方服务

参考资源

评论区

欢迎参与讨论,分享您的想法

加载后显示排序
登录后即可发表评论和回复