使用next.js搭建一个免费的博客网站

使用next.js搭建一个免费的博客网站

你好!这是我用Next.js搭建的第一个项目blogs💡,访问🪜原作者视频地址

Pasted image 20241012150557.png

完成项目的过程中,我掌握了 reactnext.jstypeScripttailwindcssprisma(数据库) kinde(登录授权)vercel(部署)的部分知识🍻 搭建部署过程我梳理并总结成了文档,希望你能喜欢 ~

一. 搭建过程

1. 创建并初始化项目

1.1 创建一个my-blogs的文件夹,在my-blogs中创建next.js应用:
text
复制代码
npx create-next-app@14.2.3 .

或最新版本。

text
复制代码
npx create-next-app@latest .

选择的一些配置。

text
复制代码
√ 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
1.2 初始化部分文件:
tsx
复制代码
// src\app\page.tsx export default function Page() {   return (     <div>       Page     </div>   ) }
css
复制代码
/* src\app\globals.css */ @tailwind base; @tailwind components; @tailwind utilities;

2. 组件化,布局,响应式容器,首页,底部,头部实现

对next.js项目结构不熟悉可以查看文档 Project Structure。 在编写tailwindcss时推荐插件:Tailwind CSS IntelliSense,Tailwind Snippets。

2.1 组件化:

将响应式容器,头部,底部独立成组件,创建目录。

text
复制代码
src\components\container.tsx src\components\header.tsx src\components\footer.tsx
2.2 布局:
tsx
复制代码
// src\app\layout.tsx import type { Metadata } from "next"; import { Inter } from "next/font/google"; import Header from "../components/header"; import Footer from "../components/footer"; import Container from "../components/container"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); //网页字体 export const metadata: Metadata = { //静态元数据   title: "My Posts",   description: "Generated by create next app", }; export default function RootLayout({   children, }: Readonly<{   children: React.ReactNode; }>) {   return (     <html lang="en">       <body className={`${inter.className} bg-zinc-100 text-zinc-900 `}>         <Container>           <Header/>           {children}           <Footer/>         </Container>       </body>     </html>   ); }
2.3 响应式容器:
tsx
复制代码
// src\component\container.tsx export default function Container({ children }: Readonly<{ children: React.ReactNode }>) {     return (         <div className="max-w-[1100px] mx-auto bg-white flex flex-col min-h-screen border-l border-r">             {children}         </div>     ) }
2.4 首页:
tsx
复制代码
// src\app\page.tsx export default function Page() {   return (     <main className="text-center pt-32 px-5">       <h1 className="text-4xl md:text-5xl font-bold mb-5">欢迎来到我的博客!</h1>       <p className="max-w-[750px] mx-auto leading-8">         Lorem ipsum dolor sit amet consectetur         adipisicing elit. Ducimus,a         natus? Dolores veritatis perferendis         non doloribus numquam praesentium         aliquid tempora qui similique,         exercitationem distinctio labore culpa         nam natus consequuntur rem!       </p>     </main>   ) }
2.5 底部:
tsx
复制代码
// src\component\footer.tsx export default function Footer() {     return (         /* 当前Footer是在一个垂直方向的Flexbox容器中,mt-auto (margin-top:auto),可以将footer推到容器的底部。 */         <div className="mt-auto text-center text-zinc-400 py-5 px-7 border-t">             <small>&copy; 2024. All Rights Reserved.</small>         </div>     ) }
2.6 头部:

Image标签中src放的地址可能会出现==next-image-unconfigured-host==提示,原因在next.js官方文档可以找到next/image Un-configured Host,解决方法是在next.config.mjs中增加Image配置。

js
复制代码
// next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = {     images: {         remotePatterns: [             {                 protocol: 'https',                 hostname: 'bytegrad.com'             }         ]     } }; export default nextConfig;
tsx
复制代码
// src\component\header.tsx 'use client' //当前页面是客户端组件 import Image from "next/image" //next.js提供的image组件 import Link from "next/link"; //next.js提供的路由组件 import { usePathname } from "next/navigation"; //通过usePathname获取当前页面路由地址 const navLinks = [     {         href: '/',         label: 'Home'     },     {         href: "/posts",         label: "Posts"     },     {         href: '/create-post',         label: "Create post"     } ] export default function Header() {     const pathname = usePathname();     return (         <header className="flex justify-between items-center py-4  px-7 border-b">             <Link href='/'>                 <Image                     src="https://bytegrad.com/course-assets/youtube/example-logo.png"                     alt='logo'                     className="w-[35px] h-[35px]"                     width='35'                     height='35'                 />             </Link>             <nav>                 <ul className="flex gap-x-5 text-[14px]">                     {                         navLinks.map((link) => (                             <li key={link.href}>                                 <Link                                     className={`font-bold ${pathname === link.href ? 'text-zinc-900' : "text-zinc-400"                                         }`}                                     href={link.href}                                 >                                     {link.label}                                 </Link>                             </li>                         ))                     }                 </ul>             </nav>         </header>     ) }

3. Posts页面,通过动态路由进入post详情,Create Post页面

3.1 创建posts页面(文档 Pages

点击标题进入详情页面时,通过标题对应的id进入到子路由页面 /posts/${post.id},每次点击标题的id都是不固定的,所以接下来需要用到动态路由。

tsx
复制代码
// src\app\posts\page.tsx import Link from "next/link"; export default async function Posts() {     const response = await fetch('https:dummyjson.com/posts?limit=10');     const { posts } = await response.json()     return (         <main className="text-center pt-16 px-5">             <h1 className="text-4xl md:text-5xl font-bold mb-5">all posts</h1>             <ul >                 {posts.map((post: { id: number, title: string }) => (                     <li key={post.id} className="mb-3">                         <Link href={`/posts/${post.id}`} className="text-2xl font-bold">{post.title}</Link>                     </li>                 ))}             </ul>         </main>     ) }
3.2 通过 动态路由进入post详情
tsx
复制代码
// src/app/posts/[id]/page.tsx export default async function Page({ params }: { params: { id: string } }) {     const response = await fetch(`https://dummyjson.com/posts/${params.id}`);     const post = await response.json();     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) }

这样,就可以动态获取到每个posts标题对应的内容了。此时会有一些小问题:1.id不存在时如何处理。2.详情页面渲染等待时卡顿。 优化:加入404页面,loading页面。

tsx
复制代码
// src/app/posts/[id]/page.tsx import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) { await new Promise((resolve) => setTimeout(resolve, 1000));//不会立即完成。会等待1秒钟后调用resolve函数     const response = await fetch(`https://dummyjson.com/posts/${params.id}`);     const post = await response.json();     //id不存在,跳转到404页面     if (!post.id) {         notFound();     }     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) }
tsx
复制代码
// src/app/posts/[id]/not-found.tsx export default function NotFound() {     return (         <div>             No post found for that id.         </div>     ) }
tsx
复制代码
// src/app/posts/[id]/loading.tsx export default function Loading() {     return (         <div className="text-center mt-5">             Loading...         </div>     ) }

最后将Posts中标题列表独立成单独的组件。

tsx
复制代码
// src/components/posts-list.tsx import Link from "next/link"; export default async function PostsList() {     const response = await fetch('https:dummyjson.com/posts?limit=10');     const { posts } = await response.json()     return (         <ul >             {posts.map((post: { id: number, title: string }) => (                 <li key={post.id} className="mb-3">                     <Link href={`/posts/${post.id}`} className="text-2xl font-bold">{post.title}</Link>                 </li>             ))}         </ul>     ) }
tsx
复制代码
// src/app/posts/page.tsx import PostsList from "@/components/posts-list"; import { Suspense } from "react"; export default function Posts() {     return (         <main className="text-center pt-16 px-5">             <h1 className="text-4xl md:text-5xl font-bold mb-5">all posts</h1>             {/* 当ui尚未加载时显示loading */}             <Suspense fallback="Loading...">                 <PostsList />             </Suspense>         </main>     ) }
3.3 Create Post页面
tsx
复制代码
// src\app\create-post\page.tsx import Form from '@/components/form'; export default async function CreatePost() {     return (         <main className="text-center pt-16">             <h1 className='text-4xl md:text-5xl font-bold mb-5'>Create post</h1>             <Form />         </main>     ) }
tsx
复制代码
// src\components\form.tsx import { createPost } from "@/actions/actions"; export default function Form() {     return (         <form         action={createPost}             className="flex flex-col max-w-[400px] mx-auto space-x-2 gap-2 my-10"         >             <input                 name="title"                 type='text'                 placeholder="Title for new post"                 className="border rounded px-3 h-10"                 required             />             <textarea                 name="body"                 placeholder="Body content for new post"                 className="border rounded px-3 py-2"                 rows={6}                 required             />             <button className="h-10 bg-blue-500 px-5 rounded text-white">Submit</button>         </form>     ) }

4. 用prisma数据库存放数据

4.1 安装prisma

安装依赖。

text
复制代码
npm i prisma -save-dev

初始化数据库后会在根目录生成一个 prisma文件夹和 .env文件。

text
复制代码
npx prisma init --datasource-provider sqlite

定义一个Post模型。

prisma
复制代码
// prisma\schema.prisma // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client {   provider = "prisma-client-js" } datasource db {   provider = "sqlite"   url      = env("DATABASE_URL") } //定义一个post模型 model Post {   id       Int      @id @default(autoincrement())   title    String   body     String   createAt DateTime @default(now())   updateAt DateTime @updatedAt }

更新数据库,成功后prisma文件夹内会生成一个dev.db文件。

text
复制代码
npx prisma db push

查看数据库UI,成功后进入 http://localhost:5555 ,点击post进入查看刚才创建的数据。

text
复制代码
npx prisma studio
4.2 使用prisma

在actions中配置请求。

ts
复制代码
// src\actions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; export async function createPost(formData:FormData) {     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       }) }
ts
复制代码
// src\lib\db.ts import { PrismaClient } from '@prisma/client' const prismaClientSingleton = () => { // 创建一个新的 Prisma Client 实例,以便后续与数据库交互。   return new PrismaClient() } declare const globalThis: {   prismaGlobal: ReturnType<typeof prismaClientSingleton>; } & typeof global; const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() // 确保在整个应用程序中只有一个 Prisma Client 实例,避免了多个实例同时存在导致的连接问题 export default prisma if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma // 如果当前环境不是生产环境,则将 prisma 实例赋值给 globalThis.prismaGlobal

此时,在create post页面中输入内容并提交后,在 http://localhost:5555 中查看数据是否已经写入进prisma。 接下来对posts-list列表页,post详情页获取数据的方法进行改造。

tsx
复制代码
// src\components\posts-list.tsx import prisma from "@/lib/db"; import Link from "next/link"; export default async function PostsList() {     await new Promise((resolve) => setTimeout(resolve, 1000));     const posts = await prisma.post.findMany();     return (         <ul>             {posts.map((post) => (                 <li key={post.id} className="mb-3">                     <Link href={`/posts/${post.id}`} >{post.title}</Link>                 </li>             ))}         </ul>     ) }
tsx
复制代码
// src\app\posts\[id]\page.tsx import prisma from "@/lib/db"; import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) {     await new Promise((resolve) => setTimeout(resolve, 1000));//不会立即完成。会等待1秒钟后调用resolve函数     const post = await prisma.post.findUnique({         where: {             id: parseInt(params.id)         }     })     if (!post) {         notFound();     }     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) }
4.3 优化页面未及时更新

此时有一个问题需要解决:当在create post中创建数据后,切换到posts页面时,标题列表中的数据没有更新到最新创建的数据。解决方法:每次访问 /posts 路由时都刷新缓存。

tsx
复制代码
// src\sctions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; import { revalidatePath } from "next/cache"; export async function createPost(formData:FormData) {     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       })     revalidatePath("/posts")//访问该路由的时候刷新缓存 }

5. kinde授权登录

5.1 安装kinde和配置

如果我们希望登录的用户才能访问create post页面,可以用kinde进行登录授权。视频教程 1:14:28

text
复制代码
1. 点击 start for free 2. 点击 add application 3. 输入项目名 选择 Back-end web 后 save 4. Quick start 选择 Next.js 后 save 5. 点击 Existing codebase 后看到 Set callback URL, Set logout URL 点击 Set 6. 下载依赖 npm i @kinde-oss/kinde-auth-nextjs 7. 继续滚动页面到 Update environment vars 复制粘贴到根部录下.env.local中,注意:未避免泄露信息,请不要将环境变量中的信息提交;检查.gitignore文件 8. 继续滚动页面到 API endpoints 创建目录 `src\app\api\auth\[kindeAuth]\route.js` 9. 选择 Authentication 选择 Social connenctions 中 登录的方式 后 save
text
复制代码
npm i @kinde-oss/kinde-auth-nextjs
js
复制代码
// src\app\api\auth\[kindeAuth]\route.js import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server"; export const GET = handleAuth();
5.2 使用kinde

需要用到 中间件,在每次访问 Create post 页面时验证是否有登录信息。

ts
复制代码
// src\middleware.ts import {withAuth} from "@kinde-oss/kinde-auth-nextjs/middleware"; import {NextRequest} from 'next/server'; export default function middleware(req:NextRequest) { // 定义一个中间件函数,接收请求对象 req   return withAuth(req, {     isReturnToCurrentPage: true // 设置选项,表示在认证后返回当前页面   }); } export const config = { // 导出配置对象   matcher: ["/create-post"]// 设置匹配路径,只有当请求路径为 "/create-post" 时才会应用该中间件 };

每次/create-post页面发起请求时进行验证是否登录授权。

tsx
复制代码
// src\actions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; export async function createPost(formData:FormData) {     /* 每次发起请求时,都进行验证是否授权 */     const {isAuthenticated} = getKindeServerSession();     if(!(await isAuthenticated())){         redirect("/api/auth/login");     }     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       })     revalidatePath("/posts")//访问该路由的时候刷新缓存 }

加入logout按钮。

tsx
复制代码
// src\app\create-post\page.tsx import { LogoutLink } from '@kinde-oss/kinde-auth-nextjs/components'; import Form from '@/components/form'; export default async function CreatePost() {     return (         <main className="text-center pt-16">             <h1 className='text-4xl md:text-5xl font-bold mb-5'>Create post</h1>             <Form />             <LogoutLink>logout</LogoutLink>         </main>     ) }

(可忽略)将环境变量统一放入一个文件夹内,将 .env.local 中内容复制粘贴到 .env 中,只保留 .env , 注意请在.gitignore中配置不要将.env文件提交到远程仓库。

二. 部署过程

将代码部署到 vercel:先把代码提交到github仓库,然后在vercel中导入项目进行部署。(在首次部署的过程中会出现以下问题。 视频教程1:56:10

prisma数据库问题

在package.json中 "scripts" 中加入。

text
复制代码
"postinstall":"prisma generate"

进入vercel中刚才导入的项目,选择Storge,创建一个用于生产的数据库。

text
复制代码
- 选择 Postgres 后 Create - Accept --> Create - 进入新建的数据库 - 选择prisma 复制粘贴到本地 schema.prisma 中替换掉 datasource db 对应代码 - 选择.env.local 复制粘贴到本地 .env 中替换掉 DATABASE_URL - 终端将数据库模型同步到数据库 npx prisma db push - 再次将代码提交到github,此时vercel会自动部署刚才提交的代码。若没有自动部署也可以点进项目中选择Deployments 选择 Redeploy 重新部署,绿灯表示部署成功
环境变量问题

在预览项目是,切换到post页面发现此时postlist数据为空,create post页面打不开,发现地址还是本地的,所以需要将URL地址替换成生产URL地址。

text
复制代码
- 选择 Project 复制 Domains 中 生产URL地址 - 选择 Settings 选择 Environment Variables - 选择 KINDE_SITE_URL 修改 value 替换成 https://你的生产地址 后 SAVE - 选择 KINDE_POST_LOGOUT_REDIRECT_URL 修改 value 替换成 https://你的生产地址 后 SAVE - 选择 KINDE_POST_LOGIN_REDIRECT_URL 修改 value 替换成 https://你的生产地址/create-post 后 SAVE

还需要在kinde中配置退出登录后重定向地址。

text
复制代码
- 进入kinde中之前建好的项目内 - 选择 Details - 滚动至 Allowed callback URLs 加入地址 https://你的生产地址/auth/kinde_callback - 滚动至 Allowed logout redirect URLs 加入地址 https://你的生产地址 后 SAVE

最后再部署一次,等到Status变绿后,此时你已经自己搭建并部署了你的博客🎉🎉🎉 如果我的文档对你有帮助,别忘了Star⭐️~ 我会更加积极的更新🍻

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP