【实战教程】用 Next.js 和 shadcn-ui 打造现代博客平台

news2025/1/9 14:34:37

image.png

你是否梦想过拥有一个独特、现代化的个人博客平台?今天,我们将一起动手,使用 Next.js 和 shadcn-ui 来创建一个功能丰富、外观精美的博客系统。无论你是刚接触 Web 开发,还是经验丰富的程序员,这个教程都将带你step by step地构建一个完整的博客平台,让你的文字创作之旅从此与众不同!

目录

    • 1. 引言:为什么选择 Next.js 和 shadcn-ui 构建博客平台?
      • 1.1 Next.js 在博客开发中的优势
      • 1.2 shadcn-ui 简介及其特点
    • 2. 项目设置和初始化
      • 2.1 创建 Next.js 项目
      • 2.2 集成 shadcn-ui
      • 2.3 设置项目结构
    • 3. 设计和实现博客的核心功能
      • 3.1 创建博客首页
        • 3.1.1 设计布局组件
        • 3.1.2 实现文章列表展示
      • 3.2 开发文章详情页
        • 3.2.1 使用动态路由
        • 3.2.2 实现 Markdown 渲染
      • 3.3 添加评论功能
        • 3.3.1 设计评论组件
        • 3.3.2 实现评论提交和展示
    • 4. 使用 Next.js API Routes 构建后端
      • 4.1 创建文章 API
        • 4.1.1 获取文章列表
        • 4.1.2 获取单篇文章详情
      • 4.2 实现评论 API
        • 4.2.1 提交新评论
    • 5. 集成数据库
      • 5.1 选择和设置 MongoDB
      • 5.2 创建数据模型
      • 5.3 连接数据库并实现 CRUD 操作
    • 6. 实现用户认证
      • 6.1 设置 NextAuth.js
      • 6.2 创建登录和注册页面
      • 6.3 实现受保护的路由和操作
    • 7. 优化用户界面和用户体验
      • 7.1 响应式设计
      • 7.2 添加加载状态和错误处理
      • 7.3 实现无限滚动加载
    • 8. 性能优化
      • 8.1 实现静态生成(SSG)和增量静态再生(ISR)
      • 8.2 图片优化
      • 8.3 代码分割和懒加载
    • 9. 部署博客平台
      • 9.1 准备生产环境配置
      • 9.2 选择合适的部署平台
      • 9.3 部署过程和注意事项
    • 10. 总结与下一步
      • 10.1 回顾学到的核心概念
      • 10.2 扩展功能的想法
      • 10.3 持续学习的资源推荐

1. 引言:为什么选择 Next.js 和 shadcn-ui 构建博客平台?

在开始动手之前,让我们先了解为什么 Next.js 和 shadcn-ui 是构建现代博客平台的绝佳选择。
image.png

1.1 Next.js 在博客开发中的优势

Next.js 作为一个强大的 React 框架,为博客开发提供了许多优势:

  • 服务器端渲染 (SSR): 提高首屏加载速度,对 SEO 友好。
  • 静态站点生成 (SSG): 预渲染页面,提供极快的加载速度。
  • 增量静态再生 (ISR): 在保持静态生成优势的同时,允许内容动态更新。
  • API 路由: 轻松创建后端 API,无需单独的服务器。
  • 图像优化: 自动优化图片,提升加载性能。
  • 内置 CSS 支持: 简化样式管理,支持 CSS 模块和 Sass。

这些特性使 Next.js 成为构建高性能、SEO 友好的博客平台的理想选择。

1.2 shadcn-ui 简介及其特点

shadcn-ui 是一个现代化的 UI 组件库,它具有以下特点:

  • 可定制性强: 组件设计灵活,易于根据需求进行定制。
  • 无需安装: 直接复制组件代码到项目中使用。
  • TypeScript 支持: 提供类型定义,增强开发体验。
  • 暗黑模式: 内置暗黑模式支持,轻松实现主题切换。
  • 无障碍设计: 组件符合 ARIA 标准,提高可访问性。

使用 shadcn-ui,我们可以快速构建出美观、功能丰富的用户界面,而无需从零开始设计每个组件。

2. 项目设置和初始化

让我们开始动手创建我们的博客平台!
image.png

2.1 创建 Next.js 项目

首先,打开终端,运行以下命令创建一个新的 Next.js 项目:

npx create-next-app@latest my-blog-platform
cd my-blog-platform

在安装过程中,选择以下选项:

✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias? … No

2.2 集成 shadcn-ui

接下来,我们将集成 shadcn-ui 到我们的项目中。运行以下命令:

npx shadcn@latest init

按照提示进行配置,大多数情况下可以选择默认选项。

2.3 设置项目结构

为了保持项目结构清晰,让我们创建一些必要的目录:

mkdir -p src/{components,lib,styles,types}

现在我们的项目结构应该如下所示:

my-blog-platform/
├── src/
│   ├── app/
│   ├── components/
│   ├── lib/
│   ├── styles/
│   └── types/
├── public/
├── .eslintrc.json
├── next.config.js
├── package.json
└── tsconfig.json

3. 设计和实现博客的核心功能

现在我们已经搭建好了基本框架,让我们开始实现博客的核心功能。
image.png

3.1 创建博客首页

3.1.1 设计布局组件

首先,我们需要创建一个基础布局组件。在 src/components 目录下创建 Layout.tsx 文件:

// src/components/Layout.tsx
import React from 'react'
import Link from 'next/link'

interface LayoutProps {
  children: React.ReactNode
}

export default function Layout({ children }: LayoutProps) {
  return (
    <div className="min-h-screen bg-background font-sans antialiased">
      <header className="border-b">
        <nav className="container mx-auto px-4 py-6">
          <Link href="/" className="text-2xl font-bold">
            My Blog
          </Link>
        </nav>
      </header>
      <main className="container mx-auto px-4 py-8">{children}</main>
      <footer className="border-t">
        <div className="container mx-auto px-4 py-6 text-center">
          © 2024 My Blog. All rights reserved.
        </div>
      </footer>
    </div>
  )
}

image.png

3.1.2 实现文章列表展示

接下来,我们将创建一个文章列表组件。新建 src/components/PostList.tsx 文件:

// src/components/PostList.tsx
import React from 'react'
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'

interface Post {
  id: string
  title: string
  excerpt: string
}

interface PostListProps {
  posts: Post[]
}

export default function PostList({ posts }: PostListProps) {
  return (
    <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
      {posts.map((post) => (
        <Card key={post.id}>
          <CardHeader>
            <CardTitle>
              <Link href={`/post/${post.id}`} className="hover:underline">
                {post.title}
              </Link>
            </CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-muted-foreground">{post.excerpt}</p>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}

增加依赖

npx shadcn@latest add card

现在,我们可以更新首页来使用这些组件。编辑 src/app/page.tsx

// src/app/page.tsx
import Layout from '@/components/Layout'
import PostList from '@/components/PostList'

const dummyPosts = [
  { id: '1', title: 'First Post', excerpt: 'This is the first post' },
  { id: '2', title: 'Second Post', excerpt: 'This is the second post' },
  { id: '3', title: 'Third Post', excerpt: 'This is the third post' },
]

export default function Home() {
  return (
    <Layout>
      <h1 className="text-3xl font-bold mb-6">Latest Posts</h1>
      <PostList posts={dummyPosts} />
    </Layout>
  )
}

3.2 开发文章详情页

3.2.1 使用动态路由

Next.js 提供了强大的动态路由功能。让我们创建文章详情页。新建 src/app/post/[id]/page.tsx 文件:

// src/app/post/[id]/page.tsx
import Layout from '@/components/Layout'

interface PostPageProps {
  params: { id: string }
}

export default function PostPage({ params }: PostPageProps) {
  return (
    <Layout>
      <h1 className="text-3xl font-bold mb-6">Post {params.id}</h1>
      <p>This is the content of post {params.id}</p>
    </Layout>
  )
}
3.2.2 实现 Markdown 渲染

大多数博客使用 Markdown 格式。让我们添加 Markdown 渲染功能。首先,安装必要的包:

npm install react-markdown

然后,创建一个 Markdown 渲染组件。新建 src/components/MarkdownRenderer.tsx 文件:

// src/components/MarkdownRenderer.tsx
import React from 'react'
import ReactMarkdown from 'react-markdown'

interface MarkdownRendererProps {
  content: string
}

export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
  return <ReactMarkdown>{content}</ReactMarkdown>
}

更新文章详情页以使用 Markdown 渲染:

// src/app/post/[id]/page.tsx
import Layout from '@/components/Layout'
import MarkdownRenderer from '@/components/MarkdownRenderer'

interface PostPageProps {
  params: { id: string }
}

const dummyPost = {
  id: '1',
  title: 'First Post',
  content: '# Hello\n\nThis is the content of the first post.',
}

export default function PostPage({ params }: PostPageProps) {
  return (
    <Layout>
      <h1 className="text-3xl font-bold mb-6">{dummyPost.title}</h1>
      <MarkdownRenderer content={dummyPost.content} />
    </Layout>
  )
}

3.3 添加评论功能

3.3.1 设计评论组件

让我们创建一个评论组件。新建 src/components/Comments.tsx 文件:

// src/components/Comments.tsx
import React from 'react'
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'

interface Comment {
  id: string
  author: string
  content: string
  createdAt: string
}

interface CommentsProps {
  comments: Comment[]
}

export default function Comments({ comments }: CommentsProps) {
  return (
    <div className="mt-8">
      <h2 className="text-2xl font-bold mb-4">Comments</h2>
      {comments.map((comment) => (
        <Card key={comment.id} className="mb-4">
          <CardHeader>
            <CardTitle>{comment.author}</CardTitle>
            <p className="text-sm text-muted-foreground">
              {new Date(comment.createdAt).toLocaleDateString()}
            </p>
          </CardHeader>
          <CardContent>
            <p>{comment.content}</p>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}
3.3.2 实现评论提交和展示

现在,让我们在文章详情页中添加评论功能。更新 src/app/post/[id]/page.tsx

// src/app/post/[id]/page.tsx
"use client"

import Layout from '@/components/Layout'
import MarkdownRenderer from '@/components/MarkdownRenderer'
import Comments from '@/components/Comments'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { useState } from 'react'

interface PostPageProps {
  params: { id: string }
}

const dummyPost = {
  id: '1',
  title: 'First Post',
  content: '# Hello\n\nThis is the content of the first post.',
}

const dummyComments = [
  { id: '1', author: 'Alice', content: 'Great post!', createdAt: '2023-09-01T12:00:00Z' },
  { id: '2', author: 'Bob', content: 'Thanks for sharing.', createdAt: '2023-09-02T10:30:00Z' },
]

export default function PostPage({ params }: PostPageProps) {
  const [comments, setComments] = useState(dummyComments)
  const [newComment, setNewComment] = useState('')

  const handleSubmitComment = (e: React.FormEvent) => {
    e.preventDefault()
    if (newComment.trim()) {
      const comment = {
        id: String(comments.length + 1),
        author: 'Anonymous', // We'll update this when we add authentication
        content: newComment.trim(),
        createdAt: new Date().toISOString(),
      }
      setComments([...comments, comment])
      setNewComment('')
    }
  }

  return (
    <Layout>
      <h1 className="text-3xl font-bold mb-6">{dummyPost.title}</h1>
      <MarkdownRenderer content={dummyPost.content} />
      <Comments comments={comments} />
      <form onSubmit={handleSubmitComment} className="mt-8">
        <Textarea
          value={newComment}
          onChange={(e) => setNewComment(e.target.value)}
          placeholder="Write a comment..."
          className="mb-4"
        />
        <Button type="submit">Submit Comment</Button>
      </form>
    </Layout>
  )
}
这段代码添加了评论列表和评论提交表单。现在,用户可以查看现有评论并添加新评论。

4. 使用 Next.js API Routes 构建后端

Next.js 的 API Routes 功能允许我们直接在 Next.js 应用中创建 API 端点。让我们为我们的博客平台创建一些基本的 API。

4.1 创建文章 API

4.1.1 获取文章列表

创建 src/app/api/posts/route.ts 文件:

// src/app/api/posts/route.ts
import { NextResponse } from 'next/server'

const posts = [
  { id: '1', title: 'First Post', excerpt: 'This is the first post' },
  { id: '2', title: 'Second Post', excerpt: 'This is the second post' },
  { id: '3', title: 'Third Post', excerpt: 'This is the third post' },
]

export async function GET() {
  return NextResponse.json(posts)
}
4.1.2 获取单篇文章详情

创建 src/app/api/posts/[id]/route.ts 文件:

// src/app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'

const posts = {
  '1': { id: '1', title: 'First Post', content: '# Hello\n\nThis is the content of the first post.' },
  '2': { id: '2', title: 'Second Post', content: '# Greetings\n\nThis is the content of the second post.' },
  '3': { id: '3', title: 'Third Post', content: '# Welcome\n\nThis is the content of the third post.' },
}

export async function GET(request: Request, { params }: { params: { id: string } }) {
  const post = posts[params.id]
  if (post) {
    return NextResponse.json(post)
  } else {
    return NextResponse.json({ error: 'Post not found' }, { status: 404 })
  }
}

4.2 实现评论 API

4.2.1 提交新评论

创建 src/app/api/posts/[id]/comments/route.ts 文件:

// src/app/api/posts/[id]/comments/route.ts
import { NextResponse } from 'next/server'

let comments: { [key: string]: any[] } = {
  '1': [
    { id: '1', author: 'Alice', content: 'Great post!', createdAt: '2023-09-01T12:00:00Z' },
    { id: '2', author: 'Bob', content: 'Thanks for sharing.', createdAt: '2023-09-02T10:30:00Z' },
  ],
}

export async function GET(request: Request, { params }: { params: { id: string } }) {
  const postComments = comments[params.id] || []
  return NextResponse.json(postComments)
}

export async function POST(request: Request, { params }: { params: { id: string } }) {
  const { author, content } = await request.json()
  const newComment = {
    id: String(Date.now()),
    author,
    content,
    createdAt: new Date().toISOString(),
  }
  
  if (!comments[params.id]) {
    comments[params.id] = []
  }
  comments[params.id].push(newComment)
  
  return NextResponse.json(newComment, { status: 201 })
}

现在我们有了基本的 API 端点,可以获取文章列表、单篇文章详情,以及提交和获取评论。

5. 集成数据库

image.png

为了使我们的博客平台更加动态和可扩展,我们需要集成一个数据库。在这个例子中,我们将使用 MongoDB,因为它易于设置和使用。

5.1 选择和设置 MongoDB

首先,我们需要安装必要的依赖:

npm install mongodb

然后,创建一个 MongoDB Atlas 账户并设置一个新的集群。获取连接字符串后,将其添加到项目的环境变量中。创建一个 .env.local 文件:

MONGODB_URI=your_mongodb_connection_string_here

5.2 创建数据模型

让我们创建一些基本的数据模型。在 src/lib 目录下创建 db.ts 文件:

// src/lib/db.ts
import { MongoClient } from 'mongodb'

if (!process.env.MONGODB_URI) {
  throw new Error('Invalid/Missing environment variable: "MONGODB_URI"')
}

const uri = process.env.MONGODB_URI
const options = {}

let client
let clientPromise: Promise<MongoClient>

if (process.env.NODE_ENV === 'development') {
  // 在开发模式下,使用全局变量,以便在 HMR(热模块替换)导致的模块重新加载之间保留该值。
  if (!(global as any)._mongoClientPromise) {
    client = new MongoClient(uri, options)
    ;(global as any)._mongoClientPromise = client.connect()
  }
  clientPromise = (global as any)._mongoClientPromise
} else {
  // In production mode, it's best to not use a global variable.
  client = new MongoClient(uri, options)
  clientPromise = client.connect()
}

export default clientPromise

5.3 连接数据库并实现 CRUD 操作

现在,让我们更新我们的 API 路由以使用 MongoDB。首先,更新文章 API:

// src/app/api/posts/route.ts
import { NextResponse } from 'next/server'
import clientPromise from '@/lib/db'

export async function GET() {
  try {
    const client = await clientPromise
    const db = client.db('blog')
    const posts = await db.collection('posts').find({}).toArray()
    return NextResponse.json(posts)
  } catch (e) {
    console.error(e)
    return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
  }
}

同样,更新单篇文章 API:

// src/app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
import clientPromise from '@/lib/db'
import { ObjectId } from 'mongodb'

export async function GET(request: Request, { params }: { params: { id: string } }) {
  try {
    const client = await clientPromise
    const db = client.db('blog')
    const post = await db.collection('posts').findOne({ _id: new ObjectId(params.id) })
    
    if (post) {
      return NextResponse.json(post)
    } else {
      return NextResponse.json({ error: 'Post not found' }, { status: 404 })
    }
  } catch (e) {
    console.error(e)
    return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 })
  }
}

最后,更新评论 API:

// src/app/api/posts/[id]/comments/route.ts
import { NextResponse } from 'next/server'
import clientPromise from '@/lib/db'
import { ObjectId } from 'mongodb'

export async function GET(request: Request, { params }: { params: { id: string } }) {
  try {
    const client = await clientPromise
    const db = client.db('blog')
    const comments = await db.collection('comments').find({ postId: new ObjectId(params.id) }).toArray()
    return NextResponse.json(comments)
  } catch (e) {
    console.error(e)
    return NextResponse.json({ error: 'Failed to fetch comments' }, { status: 500 })
  }
}

export async function POST(request: Request, { params }: { params: { id: string } }) {
  try {
    const { author, content } = await request.json()
    const client = await clientPromise
    const db = client.db('blog')
    const newComment = {
      postId: new ObjectId(params.id),
      author,
      content,
      createdAt: new Date().toISOString(),
    }
    const result = await db.collection('comments').insertOne(newComment)
    return NextResponse.json({ ...newComment, _id: result.insertedId }, { status: 201 })
  } catch (e) {
    console.error(e)
    return NextResponse.json({ error: 'Failed to add comment' }, { status: 500 })
  }
}

这些更新将使我们的博客平台使用 MongoDB 数据库来存储和检索数据。

6. 实现用户认证

为了让用户能够发表评论和管理自己的文章,我们需要实现用户认证。我们将使用 NextAuth.js,它是一个灵活的认证解决方案,专为 Next.js 设计。

6.1 设置 NextAuth.js

首先,安装 NextAuth.js:

npm install next-auth

然后,创建一个 NextAuth.js 配置文件。在 src/app 目录下创建 api/auth/[...nextauth]/route.ts 文件:

// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"

const handler = NextAuth({
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID as string,
      clientSecret: process.env.GITHUB_SECRET as string,
    }),
  ],
})

export { handler as GET, handler as POST }

确保在 .env.local 文件中添加 GitHub OAuth 应用的凭证:

GITHUB_ID=your_github_client_id
GITHUB_SECRET=your_github_client_secret
NEXTAUTH_SECRET=your_nextauth_secret

6.2 创建登录和注册页面

创建一个简单的登录页面。在 src/app/login/page.tsx 中:

// src/app/login/page.tsx
'use client'

import { signIn } from 'next-auth/react'
import { Button } from '@/components/ui/button'

export default function LoginPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <Button onClick={() => signIn('github')}>Sign in with GitHub</Button>
    </div>
  )
}

6.3 实现受保护的路由和操作

为了保护某些路由或操作,我们可以创建一个高阶组件。在 src/components 目录下创建 ProtectedRoute.tsx

// src/components/ProtectedRoute.tsx
'use client'

import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { data: session, status } = useSession()
  const router = useRouter()

  useEffect(() => {
    if (status === 'loading') return // Do nothing while loading
    if (!session) router.push('/login')
  }, [session, status])

  if (status === 'loading') {
    return <div>Loading...</div>
  }

  return session ? <>{children}</> : null
}

现在,你可以在需要用户登录的页面中使用这个组件。例如,在创建新文章的页面:

// src/app/new-post/page.tsx
import ProtectedRoute from '@/components/ProtectedRoute'
import NewPostForm from '@/components/NewPostForm'

export default function NewPostPage() {
  return (
    <ProtectedRoute>
      <NewPostForm />
    </ProtectedRoute>
  )
}

这样,只有登录的用户才能访问创建新文章的页面。

7. 优化用户界面和用户体验

image.png

7.1 响应式设计

我们的博客平台已经使用了 Tailwind CSS,这使得创建响应式设计变得简单。确保在构建组件时使用 Tailwind 的响应式类,例如 md:, lg: 等。

7.2 添加加载状态和错误处理

为了提升用户体验,我们应该添加加载状态和错误处理。例如,在文章列表页面:

// src/app/page.tsx
'use client'

import { useState, useEffect } from 'react'
import Layout from '@/components/Layout'
import PostList from '@/components/PostList'
import { Spinner } from '@/components/ui/spinner'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'

export default function Home() {
  const [posts, setPosts] = useState([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    async function fetchPosts() {
      try {
        const response = await fetch('/api/posts')
        if (!response.ok) {
          throw new Error('Failed to fetch posts')
        }
        const data = await response.json()
        setPosts(data)
      } catch (err) {
        setError(err.message)
      } finally {
        setIsLoading(false)
      }
    }

    fetchPosts()
  }, [])

  return (
    <Layout>
      <h1 className="text-3xl font-bold mb-6">Latest Posts</h1>
      {isLoading ? (
        <div className="flex justify-center">
          <Spinner size="lg" />
        </div>
      ) : error ? (
        <Alert variant="destructive">
          <AlertTitle>Error</AlertTitle>
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      ) : (
        <PostList posts={posts} />
      )}
    </Layout>
  )
}

7.3 实现无限滚动加载

为了提升大量文章的加载体验,我们可以实现无限滚动。首先,我们需要更新我们的 API 以支持分页:

// src/app/api/posts/route.ts
import { NextResponse } from 'next/server'
import clientPromise from '@/lib/db'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = parseInt(searchParams.get('page') || '1', 10)
  const limit = parseInt(searchParams.get('limit') || '10', 10)

  try {
    const client = await clientPromise
    const db = client.db('blog')
    const posts = await db.collection('posts')
      .find({})
      .sort({ createdAt: -1 })
      .skip((page - 1) * limit)
      .limit(limit)
      .toArray()

    const total = await db.collection('posts').countDocuments()

    return NextResponse.json({
      posts,
      currentPage: page,
      totalPages: Math.ceil(total / limit)
    })
  } catch (e) {
    console.error(e)
    return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
  }
}

然后,我们可以在前端实现无限滚动。我们将使用 react-intersection-observer 库来检测滚动到底部的时机:

npm install react-intersection-observer

更新 src/app/page.tsx

'use client'

import { useState, useEffect } from 'react'
import { useInView } from 'react-intersection-observer'
import Layout from '@/components/Layout'
import PostList from '@/components/PostList'
import { Spinner } from '@/components/ui/spinner'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'

export default function Home() {
  const [posts, setPosts] = useState([])
  const [page, setPage] = useState(1)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState(null)
  const [hasMore, setHasMore] = useState(true)

  const { ref, inView } = useInView({
    threshold: 0,
  })

  useEffect(() => {
    if (inView && hasMore) {
      loadMorePosts()
    }
  }, [inView, hasMore])

  async function loadMorePosts() {
    setIsLoading(true)
    try {
      const response = await fetch(`/api/posts?page=${page}&limit=10`)
      if (!response.ok) {
        throw new Error('Failed to fetch posts')
      }
      const data = await response.json()
      setPosts((prevPosts) => [...prevPosts, ...data.posts])
      setPage((prevPage) => prevPage + 1)
      setHasMore(data.currentPage < data.totalPages)
    } catch (err) {
      setError(err.message)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <Layout>
      <h1 className="text-3xl font-bold mb-6">Latest Posts</h1>
      {posts.length > 0 && <PostList posts={posts} />}
      {error && (
        <Alert variant="destructive">
          <AlertTitle>Error</AlertTitle>
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      )}
      {isLoading && (
        <div className="flex justify-center mt-4">
          <Spinner size="lg" />
        </div>
      )}
      <div ref={ref} style={{ height: '10px' }} />
    </Layout>
  )
}

8. 性能优化

image.png

8.1 实现静态生成(SSG)和增量静态再生(ISR)

Next.js 提供了强大的静态生成和增量静态再生功能。对于博客文章这种不经常更新的内容,我们可以使用这些功能来提高性能。

更新 src/app/post/[id]/page.tsx

import { Suspense } from 'react'
import { notFound } from 'next/navigation'
import Layout from '@/components/Layout'
import MarkdownRenderer from '@/components/MarkdownRenderer'
import Comments from '@/components/Comments'
import clientPromise from '@/lib/db'
import { ObjectId } from 'mongodb'

async function getPost(id: string) {
  const client = await clientPromise
  const db = client.db('blog')
  const post = await db.collection('posts').findOne({ _id: new ObjectId(id) })
  if (!post) {
    notFound()
  }
  return post
}

export async function generateStaticParams() {
  const client = await clientPromise
  const db = client.db('blog')
  const posts = await db.collection('posts').find({}, { projection: { _id: 1 } }).toArray()
  
  return posts.map((post) => ({
    id: post._id.toString(),
  }))
}

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  return (
    <Layout>
      <h1 className="text-3xl font-bold mb-6">{post.title}</h1>
      <MarkdownRenderer content={post.content} />
      <Suspense fallback={<div>Loading comments...</div>}>
        <Comments postId={params.id} />
      </Suspense>
    </Layout>
  )
}

export const revalidate = 3600 // Revalidate every hour

8.2 图片优化

Next.js 提供了内置的图像优化组件。确保在整个应用中使用 next/image 组件:

import Image from 'next/image'

// In your component
<Image src="/path/to/image.jpg" alt="Description" width={500} height={300} />

8.3 代码分割和懒加载

Next.js 默认进行代码分割,但我们可以通过动态导入进一步优化:

import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(() => import('@/components/HeavyComponent'), {
  loading: () => <p>Loading...</p>,
})

9. 部署博客平台

image.png

9.1 准备生产环境配置

确保所有环境变量都已正确设置。创建一个 .env.production 文件来存储生产环境特定的变量。

9.2 选择合适的部署平台

对于 Next.js 应用,Vercel 是一个很好的选择,因为它是由 Next.js 的创建者开发的。

9.3 部署过程和注意事项

  1. 将你的代码推送到 GitHub 仓库。
  2. 在 Vercel 上创建一个新项目,并连接到你的 GitHub 仓库。
  3. 配置你的环境变量。
  4. 部署你的应用。

确保在部署之前运行构建命令并修复任何警告或错误:

npm run build

image.png

10. 总结与下一步

10.1 回顾学到的核心概念

在这个项目中,我们学习了:

  • 使用 Next.js 创建全栈应用
  • 集成 shadcn-ui 构建美观的用户界面
  • 实现服务器端渲染和 API 路由
  • 使用 MongoDB 进行数据持久化
  • 实现用户认证和授权
  • 优化应用性能和用户体验

10.2 扩展功能的想法

  • 实现文章搜索功能
  • 添加标签和分类系统
  • 集成富文本编辑器
  • 实现用户个人资料页面
  • 添加社交分享功能

10.3 持续学习的资源推荐

  • Next.js 官方文档
  • React 官方文档
  • MongoDB 大学
  • Vercel 部署文档

通过这个项目,你已经掌握了使用 Next.js 和 shadcn-ui 构建现代博客平台的核心技能。继续探索和实践,你将能够构建更加复杂和功能丰富的 Web 应用。记住,学习是一个持续的过程,保持好奇心和实践精神,你将在 Web 开发领域取得更大的进步!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2098813.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Next.js 入门指南】5分钟创建你的第一个 Next.js 应用

你是否曾经梦想过构建一个快速、高效且 SEO 友好的 React 应用&#xff1f;今天&#xff0c;我们将一起探索 Next.js —— 一个革命性的 React 框架&#xff0c;它将帮助你轻松实现这个梦想。在接下来的 5 分钟里&#xff0c;你将创建并运行你的第一个 Next.js 应用&#xff0c…

开发团队如何应对突发的技术故障与危机:策略与实践

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《热点时事》 期待您的关注 目录 引言 一、快速响应与问题定位策略 1. 建立紧急响应团队 2. 利用自动化监控工具 3. 快速定位问…

图片转PDF:2024四大转换工具推荐!

在数字化时代&#xff0c;我们经常需要将图片转换成PDF格式&#xff0c;无论是为了打印、存档还是分享。“图片转PDF”已经成为一个常见的需求&#xff0c;而市场上有多种工具可以帮助我们轻松实现这一转换。本文将介绍几款备受好评的图片转PDF工具&#xff01; 福昕PDF转换大…

代码随想录算法训练营第三十四天| 62.不同路径 63. 不同路径 II

62.不同路径 题目&#xff1a; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少…

Java 入门指南:Java 并发编程 —— LockSupport 线程阻塞唤醒类

LockSupport LockSupport 是Java并发包中用于线程阻塞和唤醒的工具类。它提供了一种基于线程的阻塞和唤醒机制&#xff0c;类似于 Object 类中的 wait() 和 notify() 方法&#xff0c;但与 wait() 和 notify() 方法相比&#xff0c;LockSupport&#xff0c;它不依赖于对象的监…

SpringBoot2:配置绑定与自动配置功能源码解读

一、配置绑定 1、作用说明 我们在开发springboot项目时&#xff0c;会有个配置文件&#xff0c;application.properties文件。 我们知道&#xff0c;像什么访问端口、上传功能的相关配置&#xff0c;都会在这里进行配置。 而这些&#xff0c;都是springboot自带的或者第三方j…

储能电站变流器设计与仿真研究(文章复现matlab)

为了有效解决交流子网与直流子网间的功率传输&#xff0c; 降低电流谐波&#xff0c; 基于三相电压源型变流器及变流器的控制方法&#xff0c; 在 MATLAB R2018a 环境下搭建了储能变流器的整体仿真模型。 电路主要由三相电网、 三相 PWM 变流器、Buck/Boost 变换器和蓄电池构成…

MQTT - EMQX安装使用

阅读本文前可先参考&#xff1a; MQTT - 消息队列遥测传输协议 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级、基于发布-订阅模式的消息传输协议&#xff0c;适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境。它在物联网应用中广受…

【html】新建一个html并且在浏览器运行

以下是一个简单的 HTML 小项目&#xff0c;展示一个包含标题、按钮和点击按钮后弹出提示框的基本页面 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthde…

视频提取字幕的软件有哪些?高效转录用这些

探索视频的奥秘&#xff0c;从字幕开始&#xff01;你是否曾被繁复的字幕处理困扰&#xff0c;渴望有一款简单好用的在线免费软件来轻松解锁字幕提取&#xff1f; 告别手动输入的烦恼&#xff0c;我们为你精选了6款视频字幕提取在线免费软件&#xff0c;它们不仅能一键转录&am…

新手常见错误:Unable to find a @SpringBootConfiguration

目录 报错信息 &#xff1a; 1. 使用SpringBootTest注解 2. 使用ContextConfiguration注解 3. 检查包结构和注解&#xff08;一般都是这问题&#xff09; 4. 清理和重建项目 5. 检查依赖 报错信息 &#xff1a; Unable to find a SpringBootConfiguration, you need to use C…

pdf转word怎么转换?2024快速进行文件格式转换的几款软件

pdf转word怎么转换&#xff1f;2024快速进行文件格式转换的几款软件 将PDF文件转换为Word文档是日常工作中常见的需求&#xff0c;尤其是当你需要编辑或重新利用PDF中的内容时。市面上有许多软件可以帮助你轻松完成PDF转Word的任务&#xff0c;以下是五款值得推荐的软件&#…

WordPress 资源展示型下载类主题 CeoMax-Pro_v7.6 开心版

WordPress 资源展示型下载类主题 CeoMax-Pro_v7.6 开心版&#xff1b; CeoMax-Pro是一款极致美观强大的WordPress付费资源下载主题&#xff0c;它能满足您所有付费资源下载的业务需求&#xff01; 你的想法与业务不能被主题所限制&#xff01;CeoMax-Pro强大的功能&#xff0…

Spring Security 认证源码超详细分析

Spring Security 认证源码超详细分析 认证&#xff08;Authentication&#xff09;是系统确认用户信息的重要途径&#xff0c;用户通过认证之后&#xff0c;系统才能明确用户的身份&#xff0c;进而才可以为该用户分配一定的权限&#xff0c;这个过程也叫授权&#xff08;Auth…

智慧理财新纪元:蚂蚁集团“支小宝”智能理财助理深度体验与测评

2023年9月8日&#xff0c;蚂蚁集团宣布推出面向消费者的智能金融助理“支小宝2.0”&#xff0c;这是该公司首个基于金融大模型的应用产品。该产品在语言力、知识力、专业力和安全力方面展现出卓越性能&#xff0c;旨在为用户提供包括行情分析、持仓诊断、资产配置和投资教育陪伴…

MS SQL Server partition by 函数实战三 成绩排名

目录 需求 范例运行环境 视图样本设计 功能实现 基础数据展示 SQL语句 继续排序 小结 需求 假设有若干已更新考试成绩的考生&#xff0c;考试成绩包括总成绩、分项成绩1、分项成绩2&#xff0c;其它信息包括应聘岗位名称、姓名等信息。现希望根据总成绩计算排名&#…

C语言代码练习(第十一天)

今日练习&#xff1a; 25、用指针方法对10个整数按由大到小顺序排序 26、有一个班&#xff0c;3个学生&#xff0c;各学习4门课&#xff0c;计算总平均分数以及第 n 个学生的成绩 27、有一个3 * 4的二维数组&#xff0c;要求用指向元素的指针变量输出二维数组个元素的值。 用指…

mar3d模型文件问题

上一章使用mars3D模型库 遗留一个问题 部分资源不完整 如果模型没有其他依赖文件会正常加载 若有其他依赖就会报错 正常获取到的 缺少文件的 经过观察在gltf文件中发现缺失的是这几个文件 还是通过脚本下载 脚本实例 const fs require(fs); const path require(path); co…

Idea中修改Jsp文件的头部注释模版

文章目录 方法1&#xff0c;启动idea&#xff0c;单击“file”&#xff0c;选择“settings”2&#xff0c;选择Editor——File and Code Templates——other——Jsp files——jsp File.jsp。此时编辑如下图所示的右上区域即可修改模板。 每天学一个小技巧 方法 1&#xff0c;启…

FL Studio 24汉化破解版 v24.4.1.4285(附汉化破解补丁)

FL Studio 24.4.1.4285(汉化破解版是一款无比专业的音乐制作软件&#xff0c;它无论是在功能上还是用户界面都是在业内数一数二的。就拿它的用户界面来说&#xff0c;是目前同类软件中唯一做到100%矢量化&#xff0c;这样能更好地用在4K&#xff0c;5K甚至8K显示器上&#xff0…