前言
关于博客系统,相信大家早已驾轻就熟,网上有很多以markdown
驱动的博客框架,如vuepress,hexo等,这类框架的本质是生成静态站点,而个人开发的博客系统大多是使用数据库的全栈项目,这两种方式各有各的好处,这里就不做比较了
这篇文章我们将自己独立去开发并部署一个以markdown
驱动的静态站点博客,所用技术栈如下:
React
TypeScript
Next.js
tailwindcss
Vercel
部署
注意: 本文只是演示使用Next.js
从 0 到 1 构建并部署一个个人博客项目,不会对项目构建过程中所用到的技术做详细的讲解,不过不用担心,只要跟着文章一步一步来,小白都能成功部署自己的个人博客!
项目仓库地址:https://github.com/Chen0807AiLJX/next-blog最终效果可见:https://next-blog-eosin-six.vercel.app/
现在让我们开始吧!
开始之前请确保自己电脑上配置的有Node.js 12.13.0
或更高版本。
1、创建 Next.js 项目
要创建 Next.js
应用程序,请打开终端,cd
进入到要在其中创建应用程序的目录,然后运行以下命令:
npx create-next-app@latest --typescript ailjx-blog
复制代码
上述代码表示:通过create-next-app
创建名为ailjx-blog
的TypeScript
版本的Next.js
应用程序
用vscode
打开ailjx-blog
项目,目录结构如下:
在项目根目录终端运行以下命令启动项目:
打开http://localhost:3000/显示如下页面:
2、安装 tailwindcss
在项目根目录终端运行以下命令:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
复制代码
生成tailwindcss
配置文件:
此时项目里会多出两个文件:tailwind.config.js
和postcss.config.js
修改tailwind.config.js
文件里的content
为:
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./styles/**/*.css",
],
复制代码
在pages
文件夹下的_app.tsx
文件的第一行添加:
import "tailwindcss/tailwind.css";
复制代码
之后重新启动项目
3、添加布局页面
准备一张自己的头像(建议比例为 1:1,这里演示用的头像文件名为author.jpg
)
在public
文件夹下新建images
文件夹,将你的头像图片放入其中,并删除public
文件夹下的svg
文件
public
文件为项目的静态文件,可直接通过地址访问,如访问演示所用头像:http://localhost:3000/images/author.jpg
项目根目录下新建components
文件夹,并添加布局文件layout.tsx
:
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
const name = "Ailjx"; // 名称,根据需要修改
export const siteTitle = "Ailjx Blog"; // 网站标题,根据需要修改
interface Props {
children: React.ReactNode;
home?: boolean;
}
export default function Layout({ children, home }: Props) {
return (
<div className='max-w-2xl mx-auto px-4 mt-12 mb-24'>
<Head>
<link rel='icon' href='/favicon.ico' />
<meta name='description' content='AiljxBlog——Ailjx的博客' />
<meta
property='og:image'
content={`https://og-image.vercel.app/${encodeURI(
siteTitle
)}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
/>
<meta name='og:title' content={siteTitle} />
<meta name='twitter:card' content='summary_large_image' />
</Head>
<header className='flex flex-col items-center'>
{home ? (
<>
<Image
priority
src='/images/author.jpg'
className='rounded-full'
height={144}
width={144}
alt={name}
/>
<h1 className='text-5xl font-extrabold tracking-tighter my-4'>
{name}
</h1>
</>
) : (
<>
<Link href='/'>
<a>
<Image
priority
src='/images/author.jpg'
className='rounded-full'
height={108}
width={108}
alt={name}
/>
</a>
</Link>
<h2 className='text-2xl my-4'>
<Link href='/'>
<a>{name}</a>
</Link>
</h2>
</>
)}
</header>
<main>{children}</main>
{!home && (
<div className='mt-12'>
<Link href='/'>
<a>← 返回首页</a>
</Link>
</div>
)}
</div>
);
}
复制代码
这里使用了几个Next
自带的组件:
4、新建 markdown 文章
项目根目录下新建posts
文件夹,添加一个markdown
文件,如:
欢迎来到我的博客.md
---
title: "欢迎来到我的博客"
date: "2022-08-08"
---
## 欢迎你!
复制代码
注意: 需要在每个markdown
文件的顶部通过---
添加元数据,元数据需要有title
字段表示文章标题,date
字段表示日期,如上面欢迎来到我的博客.md
的元数据为:
---
title: "欢迎来到我的博客"
date: "2022-08-08"
---
复制代码
这些数据在我们渲染markdown
内容时需要用到
5、解析 markdown 内容
需要安装以下插件:
在项目根目录终端运行以下命令安装上述插件:
npm i remark-prism date-fns gray-matter next-mdx-remote remark-external-links
复制代码
npm i @types/remark-prism --D
复制代码
在项目根目录新建存放工具函数的utils
文件夹,里面新建处理markdown
文件的posts.ts
:
import fs from "fs";
import path from "path";
// gray-matter:获取元数据
import matter from "gray-matter";
// date-fns:处理日期
import { parseISO } from "date-fns";
import { serialize } from "next-mdx-remote/serialize";
// remark-prism:markdown代码高亮
import prism from "remark-prism";
// externalLinks:使markdown的链接是在新页面打开链接
import externalLinks from "remark-external-links";
interface MatterMark {
data: { date: string; title: string };
content: string;
[key: string]: unknown;
}
// posts目录的路径
const postsDirectory = path.join(process.cwd(), "posts");
// 获取posts目录下的所有文件名(带后缀)
const fileNames = fs.readdirSync(postsDirectory);
// 获取所有文章用于展示首页列表的数据
export function getSortedPostsData() {
// 获取所有md文件用于展示首页列表的数据,包含id,元数据(标题,时间)
const allPostsData = fileNames.map((fileName) => {
// 去除文件名的md后缀,使其作为文章id使用
const id = fileName.replace(/\.md$/, "");
// 获取md文件路径
const fullPath = path.join(postsDirectory, fileName);
// 读取md文件内容
const fileContents = fs.readFileSync(fullPath, "utf8");
// 使用matter提取md文件元数据:{data:{//元数据},content:'内容'}
const matterResult = matter(fileContents);
return {
id,
...(matterResult.data as MatterMark["data"]),
};
});
// 按照日期从进到远排序
return allPostsData.sort(({ date: a }, { date: b }) =>
// parseISO:字符串转日期
parseISO(a) < parseISO(b) ? 1 : -1
);
}
// 获取格式化后的所有文章id(文件名)
export function getAllPostIds() {
// 这是返回的格式:
// [
// {
// params: {
// id: '......'
// }
// },
// {
// params: {
// id: '......'
// }
// }
// ]
return fileNames.map((fileName) => {
return {
params: {
id: fileName.replace(/\.md$/, ""),
},
};
});
}
// 获取指定文章内容
export async function getPostData(id: string) {
// 文章路径
const fullPath = path.join(postsDirectory, `${id}.md`);
// 读取文章内容
const fileContents = fs.readFileSync(fullPath, "utf8");
// 使用matter解析markdown元数据和内容
const matterResult = matter(fileContents);
return {
content: await serialize(matterResult.content, {
mdxOptions: { remarkPlugins: [prism, externalLinks] },
}),
...(matterResult.data as MatterMark["data"]),
};
}
复制代码
posts.ts
里有三个主要的函数:
getSortedPostsData
:在首页用于展示文章列表
getAllPostIds
:获取指定格式的所有文章id
(文件名),这个格式是Next
所要求的
因为我们在写文章详情页面时需要使用动态路由,每个文章的id
就是一个路由,并且我们使用的Next
静态站点生成会在项目打包构建时直接生成所有的html
文件,需要把每一个路由对应的页面都构建出来,Next
会根据getAllPostIds
函数返回的这种格式的数据去构建每一个html
页面
getPostData
:获取文章详情,在文章详情页面会用到
6、添加首页
首页会展示文章列表,会用到一个日期渲染组件,我们先创建一下
在components
文件夹下新建date.tsx
文件:
import { parseISO, format } from "date-fns";
interface Props {
dateString: string;
}
export default function Date({ dateString }: Props) {
const date = parseISO(dateString);
return (
<time dateTime={dateString} className='text-gray-500'>
{format(date, "yyyy年MM月dd日")}
</time>
);
}
复制代码
修改pages
文件夹下的index.tsx
文件如下:
import type { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import Layout, { siteTitle } from "../components/layout";
import Link from "next/link";
import Date from "../components/date";
import { getSortedPostsData } from "../utils/posts";
interface Props {
allPostsData: {
date: string;
title: string;
id: string;
}[];
}
const Home: NextPage<Props> = ({ allPostsData }) => {
return (
<Layout home>
<div>
<Head>
<title>{siteTitle}</title>
</Head>
<section className='text-xl leading-normal text-center'>
<p>你好,我是 Ailjx</p>
<p>一个又菜又爱玩的前端小白,欢迎来到我的博客!</p>
</section>
<section className='text-xl leading-normal pt-4'>
<h2 className=' text-2xl my-4 font-bold'>Blog</h2>
<ul>
{allPostsData.map(({ id, date, title }) => (
<li key={id} className='mb-5'>
<Link href={`/posts/${id}`}>
<a>{title}</a>
</Link>
<br />
<small>
<Date dateString={date} />
</small>
</li>
))}
</ul>
</section>
</div>
</Layout>
);
};
export const getStaticProps: GetStaticProps = async () => {
// 获取文章列表
const allPostsData = getSortedPostsData();
return {
props: {
allPostsData,
},
};
};
export default Home;
复制代码
修改styles
文件夹下的globals.css
如下:
a {
color: #0070f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
max-width: 100%;
display: block;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
position: absolute;
}
::-webkit-scrollbar-thumb {
background-color: #0070f3;
}
::-webkit-scrollbar-track {
background-color: #ddd;
}
复制代码
删除style
文件夹下的Home.module.css
此时运行项目,打开http://localhost:3000/可见:
7、添加文章详情页面
在pages
文件夹下创建posts
文件夹,在其中创建[id].tsx
文件:
import type { GetStaticProps, GetStaticPaths } from "next";
import Layout from "../../components/layout";
import { getAllPostIds, getPostData } from "../../utils/posts";
import Head from "next/head";
import Date from "../../components/date";
import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";
// 引入代码高亮css
import "prismjs/themes/prism-okaidia.min.css";
interface Props {
postData: {
title: string;
date: string;
content: MDXRemoteProps;
};
}
export default function Post({ postData }: Props) {
return (
<Layout>
<Head>
<title>{postData.title}</title>
</Head>
<h1 className='text-3xl font-extrabold my-4 tracking-tighter'>
{postData.title}
</h1>
<Date dateString={postData.date} />
<article className='py-8 prose prose-h1:mt-8'>
<MDXRemote {...postData.content} />
</article>
</Layout>
);
}
// getStaticProps和getStaticPaths只在服务器端运行,永远不会在客户端运行
export const getStaticPaths: GetStaticPaths = async () => {
// 获取所有文章id,即所有路由
const paths = getAllPostIds();
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
// 获取文章内容
const postData = await getPostData(params!.id as string);
return {
props: {
postData,
},
};
};
复制代码
之后在首页点击文章列表跳转到文章详情页面:
到此一个简单的博客项目就写好了
8、Vercel 部署
没有Github
账号的先去注册一个账号
在Github上新建一个名为next-blog
的仓库(名称自己根据需要修改):
仓库权限公共私有都可,并且不需要使用README
或其他文件对其进行初始化
在我们的博客项目根目录下运行以下命令推送代码到Github
仓库里:
git remote add origin https://github.com/<username>/next-blog.git
git branch -M main
git push -u origin main
复制代码
请将上述第一行命令origin
后面的地址替换成你的仓库地址,一般是将<username>
替换为你Gitub
的用户名,next-blog
替换成你仓库的名称
之后刷新仓库查看代码:
项目仓库地址:https://github.com/Chen0807AiLJX/next-blog
细心的大佬应该会发现我们这样提交代码是有问题的,因为我们并没有合并本地代码到本地仓库,所以提交到Github
仓库的代码并不是我们最终的效果,而是创建Next.js
时的初始效果。
不过不用担心,我们在后面会对其进行处理。当然,你也可以现在处理,直接将最新的代码同步到仓库,这样你就免了后面我们对其处理的操作
打开Vercel,没有Vercel
账号的点击右上角的注册按钮进行注册,注册时选择通过Github
注册,登录时也使用Github
登录
登录Vecel
成功后打开 https://vercel.com/import/git或https://vercel.com/new或点击新建项目按钮,之后进入到以下页面:
这个页面中会自动获取你的Github
仓库,选择你刚刚推送博客项目的仓库,点击Import
按钮,之后直接点击Deploy
按钮:
稍等片刻,出现以下页面就部署成功了:
点击上述页面左侧的页面预览框就能跳转到你部署成功的网页了,但这时你会发现部署的页面不是我们最终的页面,而是创建Next.js
时的初始页面,这是因为我们在Git
提交代码到仓库时没有合并本地代码,我们重新提交一下就行了
我们可以在VScode
里快速提交代码到仓库:
点击同步更改后会开始转圈,等待转圈结束就提交成功了,之后什么都不用干,仓库代码更新后Vercel
会自动部署!!!
打开https://vercel.com/dashboard能查看到你已经部署的项目和对应的网页地址:
好啦,到此我们的任务就全部完成了,之后需要添加文章只需要在项目的posts
文件内新建markdown
文件就行了(不要忘记在markdown
顶部添加元数据),更新完文章提交代码到仓库即可
结语
这次使用Next.js
搭建个人博客只是一个小小的尝试,可以说是只搭建了一个骨架,其实走完整个流程你应该会有很多有趣的想法去完善填充你的博客,因为基础功能我们已经实现,剩下的就是锦上添花的操作了,这完全取决于你
项目仓库地址:https://github.com/Chen0807AiLJX/next-blog最终效果可见:https://next-blog-eosin-six.vercel.app/
参考资料:
如果本篇文章对你有所帮助,还请客官一件四连!❤️
评论