HOW - 从0到1搭建自己的博客站点(二)

我们来详细分解「支持 Markdown 博客内容」这一部分的实现,适用于使用 Next.js 搭建静态博客网站的场景。


为什么使用 Markdown?

Markdown 是一种轻量级标记语言,书写简单,格式清晰,非常适合写博客文章。我们可以:

  • 在本地的 posts/ 文件夹中创建 .md 文件
  • 使用 gray-matter 提取文章的元信息(标题、日期、标签等)
  • 使用 remarkremark-html 把 Markdown 转为 HTML 供 React 渲染

项目结构示例

my-blog/
├─ pages/
│  ├─ index.tsx                // 博客首页
│  ├─ posts/
│     └─ [slug].tsx            // 博客详情页(动态路由)
├─ posts/
│  ├─ hello-world.md           // Markdown 文件
│  └─ another-post.md
├─ lib/
│  └─ posts.ts                 // Markdown 处理工具函数
├─ public/
├─ styles/
├─ ...

安装依赖

npm install gray-matter remark remark-html

创建 Markdown 文件

在项目根目录下新建一个 posts/ 文件夹,创建文件 hello-world.md

---
title: "Hello World"
date: "2025-05-26"
tags: ["nextjs", "markdown"]
---

This is my **first** blog post using Markdown!

创建工具函数(lib/posts.ts)

这个文件负责读取 posts 文件夹中的 Markdown 文件,并解析元信息与内容。

// lib/posts.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { remark } from 'remark'
import html from 'remark-html'

const postsDirectory = path.join(process.cwd(), 'posts')

export function getSortedPostsData() {
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map((fileName) => {
    const slug = fileName.replace(/\.md$/, '')
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')

    const matterResult = matter(fileContents)

    return {
      slug,
      ...(matterResult.data as { date: string; title: string; tags?: string[] }),
    }
  })

  return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1))
}

export function getAllPostSlugs() {
  const fileNames = fs.readdirSync(postsDirectory)
  return fileNames.map((fileName) => ({
    params: {
      slug: fileName.replace(/\.md$/, ''),
    },
  }))
}

export async function getPostData(slug: string) {
  const fullPath = path.join(postsDirectory, `${slug}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  const matterResult = matter(fileContents)

  const processedContent = await remark().use(html).process(matterResult.content)
  const contentHtml = processedContent.toString()

  return {
    slug,
    contentHtml,
    ...(matterResult.data as { title: string; date: string }),
  }
}

首页展示文章列表(pages/index.tsx)

import { GetStaticProps } from 'next'
import Link from 'next/link'
import { getSortedPostsData } from '@/lib/posts'

export default function Home({ allPostsData }: { allPostsData: any[] }) {
  return (
    <div className="max-w-2xl mx-auto py-10">
      <h1 className="text-3xl font-bold mb-4">My Blog</h1>
      <ul>
        {allPostsData.map(({ slug, date, title }) => (
          <li key={slug} className="mb-4">
            <Link href={`/posts/${slug}`} className="text-xl text-blue-600 hover:underline">
              {title}
            </Link>
            <div className="text-sm text-gray-500">{date}</div>
          </li>
        ))}
      </ul>
    </div>
  )
}

export const getStaticProps: GetStaticProps = async () => {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData,
    },
  }
}

详情页展示文章内容(pages/posts/[slug].tsx)

import { GetStaticPaths, GetStaticProps } from 'next'
import { getAllPostSlugs, getPostData } from '@/lib/posts'

export default function Post({ postData }: { postData: any }) {
  return (
    <div className="max-w-2xl mx-auto py-10">
      <h1 className="text-3xl font-bold mb-2">{postData.title}</h1>
      <div className="text-sm text-gray-500 mb-6">{postData.date}</div>
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </div>
  )
}

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = getAllPostSlugs()
  return {
    paths,
    fallback: false,
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const postData = await getPostData(params?.slug as string)
  return {
    props: {
      postData,
    },
  }
}

成果展示

  1. 访问 /:可以看到所有 Markdown 博客列表。
  2. 点击文章链接:跳转到对应详情页,内容是从 .md 文件中读取并渲染成 HTML 的。

可选优化

  • 支持 代码高亮:使用 rehype-highlightshiki
  • 使用 MDX 替代 Markdown,可以在文章中嵌入 React 组件
  • SEO 优化:用 <Head> 设置标题、meta 信息
  • 增加标签/分类系统,即接入标签过滤
  • 支持分页
  • 支持自定义文章封面图
  • 支持搜索功能

以 SEO 优化为例

WHAT - SEO(搜索引擎优化) 中我们详细介绍过 SEO 相关内容。

在博客站点搭建中,SEO 是非常重要的一个特性。

下面我会详细讲解如何在 Next.js 博客项目中使用 <Head> 做 SEO 优化,包括:

  1. 为什么要做 SEO
  2. Next.js 中的 <Head> 是什么
  3. 如何在首页和文章详情页中设置标题、描述、关键词等 meta 信息
  4. 进阶:Open Graph(社交媒体分享优化)

为什么要做 SEO 优化?

SEO(Search Engine Optimization)是为了让你的博客页面更容易被搜索引擎(如百度、Google)收录并排名更高,从而获得更多自然流量。

一个典型的博客页面 SEO 要素包括:

  • <title>:页面标题(显示在浏览器标签页 & 搜索结果标题)
  • <meta name="description">:页面简要描述
  • <meta name="keywords">:关键词(虽然现在权重低)
  • Open Graph 标签:用于社交媒体(微信、微博、Twitter、Facebook)分享时展示标题、描述、封面图等

Next.js 中的 Head

Next.js 提供了一个内置的 next/head 组件,允许你在页面中设置 HTML 的 <head> 内容:

import Head from 'next/head'

<Head>
  <title>我的博客标题</title>
  <meta name="description" content="博客描述" />
  <meta name="keywords" content="nextjs, markdown, 博客" />
</Head>

Next.js 会自动将这些内容注入 <head> 标签。


在首页设置 Head

pages/index.tsx:

import Head from 'next/head'

export default function Home() {
  return (
    <>
      <Head>
        <title>我的个人博客 | 主页</title>
        <meta name="description" content="欢迎访问我的个人博客,记录技术与生活。" />
        <meta name="keywords" content="博客, 技术, 前端, Next.js, Markdown" />
      </Head>

      <main className="max-w-2xl mx-auto">
        <h1 className="text-3xl font-bold mb-4">欢迎来到我的博客</h1>
        {/* 博客列表... */}
      </main>
    </>
  )
}

在文章详情页设置动态 title 和 meta

pages/posts/[slug].tsx:

import Head from 'next/head'
import { getPostData } from '@/lib/posts'

export default function Post({ postData }: { postData: any }) {
  return (
    <>
      <Head>
        <title>{postData.title} | 我的博客</title>
        <meta name="description" content={postData.excerpt || postData.contentHtml.slice(0, 160)} />
        <meta name="keywords" content={postData.tags?.join(', ') || '博客, 文章'} />
        <meta property="og:title" content={postData.title} />
        <meta property="og:description" content={postData.excerpt || postData.contentHtml.slice(0, 160)} />
        {/* 可选:添加封面图 */}
        {/* <meta property="og:image" content={postData.coverImage} /> */}
      </Head>

      <article className="max-w-2xl mx-auto">
        <h1 className="text-3xl font-bold">{postData.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </>
  )
}

Tips:

  • postData.excerpt 是你可以在 Markdown frontmatter 中添加的字段,也可以程序生成前 160 个字。
  • og:title / og:description 是 Open Graph 协议,用于社交分享。
  • og:image 可以配合文章封面图使用。

示例 Markdown 文件(添加 meta)

---
title: "使用 Markdown 搭建博客"
date: "2025-05-26"
tags: ["markdown", "博客", "nextjs"]
excerpt: "本篇文章介绍如何用 Next.js 和 Markdown 搭建一个静态博客站点。"
coverImage: "/images/blog-cover.png"
---
正文内容...

建议封装 Head 组件(可复用)

// components/SeoHead.tsx
import Head from 'next/head'

type SeoHeadProps = {
  title: string
  description?: string
  keywords?: string[]
  image?: string
}

export function SeoHead({ title, description, keywords, image }: SeoHeadProps) {
  return (
    <Head>
      <title>{title}</title>
      {description && <meta name="description" content={description} />}
      {keywords && <meta name="keywords" content={keywords.join(', ')} />}
      {title && <meta property="og:title" content={title} />}
      {description && <meta property="og:description" content={description} />}
      {image && <meta property="og:image" content={image} />}
    </Head>
  )
}

在页面中这样使用:

<SeoHead
  title={`${postData.title} | 我的博客`}
  description={postData.excerpt}
  keywords={postData.tags}
  image={postData.coverImage}
/>

SEO 检查工具推荐


另外,还可以进一步:

  • 自动生成 sitemap.xmlrobots.txt
  • next-sitemap 插件管理 SEO 文件
  • 设置全站默认 meta 信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@PHARAOH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值