浅谈搭建现代化静态博客

Blog Maker
Back

回顾过去搭建

第一次搭建是在 2020 年的假期,利用电脑搭建了基于Gridea构建的博客。用着一款很简约的主题Chic,成功的搭建了第一个自己的站点。但它有个很大的缺点:只能用本地的客户端,不支持手机,也不容易云端操作。所以说它并不满足我的需求。

之后我发现了一个很好用的程序,叫做Pandoc

Pandoc is a free and open-source document converter, widely used as a writing tool (especially by scholars)[2] and as a basis for publishing workflows.[3] It was created by John MacFarlane, a philosophy professor at the University of California, Berkeley. — From WikiPedia.

它可以转换包括 PDF, MD, HTML, 甚至是Word等十几种格式,还拥有强大的自定义功能。它微不足道的小功能就满足我的博客需求了。我参考了三日月綾香的博客,写出了完全由自己设计的网页,发布了两款主题:CleanConsole

除了这几个偏门的方案,还有一些很流行的静态博客框架,比如 Hexo, Hugo, Jekyll 等。然而已经有了这么多好的选择,我却没有用这些!

今年 2 二月份,我看到了日本的一位前端工程师 Takuya Matsuyama 用 NextJS 制作的的个人网站,了解了其搭建过程,这使我走向了使用 React 元框架的道路。

相比普通的博客网站,用 React 元框架和 NextJS 制作的网站更加先进,现代:

  1. 切换页面时网站只会加载新的未加载的内容,重复利用现有资源,减少了不必要的加载时间,提升了网站性能。
  2. 网站重载不需要刷新,通过比如 Framer-motion 实现动画效果,有着 Native 级的浏览体验
  3. React元框架有着丰富的生态链,可以快速实现比如动画,暗黑模式效果。NextJS 也使得支持更多获取数据的方式,比如 CMS(See NextJS Example),可以实现获取云端内容渲染成文章,即使是没有后端的静态博客!

现在来说说如何用 Nextjs 来制作博客吧!

搭建过程

搭建过程比较复杂,请看完我的所有步骤!

准备工作(终端环境中进行)

首先你需要安装 NodeJS 环境,Windows 用户可以用 chocolatey 通过命令安装,choco install nodejs yarn

选择一个目录并初始化项目

yarn init -y
mkdir pages
mkdir components
mkdir lib
mkdir styles
mkdir _posts
touch next.config.js
touch .eslintrc.json
touch .prettierignore
touch prettier.config.js
yarn add @mdx-js/loader gray-matter next-mdx-remote next-compose-plugins rehype remark remark-html react react-dom next eslint eslint-config-next prettier

ESlint是用来语法检查的工具。 prettier 用来整理代码。

编写文件

编写 package.json

{
  "name": "Willie-homepage",
  "version": "1.1.0",
  "description": "Willie's website",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "prettier": "prettier --write .",
    "lint": "next lint"
  },
  "keywords": [],
  "author": "Willie",
  "license": "MIT",
  "dependencies": {
    "@mdx-js/loader": "latest",
    "date-fns": "latest",
    "gray-matter": "latest",
    "next": "latest",
    "next-compose-plugins": "latest",
    "next-mdx-remote": "latest",
    "react": "latest",
    "react-dom": "latest",
    "rehype": "latest",
    "remark": "latest",
    "remark-html": "latest"
  },
  "devDependencies": {
    "eslint": "latest",
    "eslint-config-next": "latest",
    "prettier": "latest"
  }
}

请将 nameauthor 等个人信息自行修改

编写 next.config.js

module.exports = {
  reactStrictMode: true,
  swcMinify: true,
};

编写 .eslintrc.json

{
  "root": true,
  "extends": "next",
  "rules": {
    "no-unused-vars": [
      "error",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_"
      }
    ],
    "react/display-name": 0
  }
}

编写 .prettierignore

node_modules
.next

编写 prettier.config.js

const options = {
  arrowParens: "avoid",
  singleQuote: true,
  bracketSpacing: true,
  endOfLine: "if",
  semi: false,
  tabwidth: 2,
  trailingComma: "none",
};

创建自定义样式

styles/global.css 里将你所需要样式填写进去

创建获取文章内容的 JS

lib/mdx.js 里写入以下内容

import fs from "fs";
import matter from "gray-matter";
import { serialize } from "next-mdx-remote/serialize";
import path from "path";

const root = process.cwd();

export async function getFiles(type) {
  return fs.readdirSync(path.join(root, "_posts", type));
}

export async function getFileBySlug(type, slug) {
  const source = slug
    ? fs.readFileSync(path.join(root, "_posts", type, `${slug}.mdx`), "utf-8")
    : fs.readFileSync(path.join(root, "_posts", `${type}.mdx`), "utf-8");

  const { data: metaData, content } = matter(source);
  const contentSerialized = await serialize(content);

  return {
    content,
    contentSerialized,
    slug: slug ? slug : null,
    metaData,
  };
}

export async function getAllFilesFrontMatter(type) {
  const files = fs.readdirSync(path.join(root, "_posts", type));

  return files.reduce((allPosts, postSlug) => {
    const source = fs.readFileSync(
      path.join(root, "_posts", type, postSlug),
      "utf-8"
    );
    const { data } = matter(source);

    return [
      {
        ...data,
        slug: postSlug.replace(".mdx", ""),
      },
      ...allPosts,
    ];
  }, []);
}

这段代码获取了文件名作为路径,用 front-matter 获取了 Markdown 文件的内容和媒体,感兴趣的可以在 NextJS 文档中找到相关介绍!

编写 pages/_app.js

import "../styles/global.css";

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

编写 components/BlogPost/index.js

import NextLink from "next/link";

const BlogPost = ({ title, date, excerpt, slug }) => {
  return (
    <NextLink href={`/${slug}`} passHref>
      <li>
        <div className="postdate">
          <span>{date}</span>
        </div>
        <h2>{title}</h2>
        <p className="excerpt">{excerpt}</p>
      </li>
    </NextLink>
  );
};
export default BlogPost;

该代码生成文章列表,根据自己需求编写。

导航栏 Nav 的例子,编写 components/Nav/index.js

const Nav = () => {
  return (
    <nav>
      <h1 className="title">WLL&lsquo;s blog</h1>
      <div>
        <p id="icon"></p>
      </div>
    </nav>
  );
};

export default Nav;

编写 pages/index.js

import BlogPost from "../components/BlogPost";
import { getAllFilesFrontMatter } from "../lib/mdx";
import Head from "next/head";
import Footer from "../components/Footer";
import Nav from "../components/Nav";

const Blog = ({ posts }) => {
  const filteredBlogPosts = posts.sort(
    (a, b) => Number(new Date(b.date)) - Number(new Date(a.date))
  );

  return (
    <>
      <Head>
        <title>WLL&lsquo;s blog</title>
        <meta name="description" content="WLL&lsquo;s blog" />
      </Head>
      <Nav />
      <div className="post-list">
        <h2>※ My Articles</h2>
        <ul>
          {filteredBlogPosts.map((post) => (
            <BlogPost key={post.title} {...post} />
          ))}
        </ul>
      </div>
      <Footer />
    </>
  );
};
export default Blog;

export const getStaticProps = async () => {
  const posts = await getAllFilesFrontMatter("");

  return {
    props: {
      posts,
    },
  };
};

代码中的 Footer 可以参考 Nav 的编写。

编写 pages/[slug].js

import { MDXRemote } from "next-mdx-remote";
import Head from "next/head";
import Footer from "../components/Footer";
import Nav from "../components/Nav";
import { getFileBySlug, getFiles } from "../lib/mdx";

const BlogPost = ({ post }) => {
  const { metaData, contentSerialized } = post;
  return (
    <div>
      <Head>
        <title>{metaData.title}</title>
        <meta name="description" content="{metaData.title}" />
      </Head>
      <Nav />
      <header>
        <div className="card">
          <h1>{metaData.title}</h1>
          <h3 className="date"> ∇ {metaData.date} </h3>
          <p className="author"> ¶ Willie</p>
          <div>
            <p className="postexcerpt">{metaData.excerpt}</p>
          </div>
        </div>
      </header>
      <div className="post">
        <MDXRemote compiledSource={contentSerialized.compiledSource} />
      </div>
      <Footer />
    </div>
  );
};
export default BlogPost;

export const getStaticPaths = async () => {
  const posts = await getFiles("");

  return {
    paths: posts.map((p) => ({
      params: {
        slug: p.replace(/\.mdx/, ""),
      },
    })),
    fallback: false,
  };
};

export const getStaticProps = async ({ params }) => {
  const slug = params?.slug;
  if (slug) {
    try {
      const post = await getFileBySlug("", slug);
      return {
        props: {
          post,
        },
      };
    } catch (error) {
      console.error(error);
    }
  }

  return {
    notFound: true,
  };
};

这里是文章的界面!感谢看到这一步,至此已经大功告成了!

尝试编写文章

整理一下之前写的代码

yarn run prettier

_posts 中编写以 .mdx 后缀的 Markdown 文件

例子

---
title: "Something About Me"
date: "2021-07-18"
excerpt: "About..."
---

# 关于我

还有很多要改善的地方!

# 站点

> 除非另有说明,本博客所有文章均采用`CC BY-NC-SA 4.0 许可协议`。 转载请在文中明显位置注明出处

现在用 yarn run dev 预览吧!


写这篇文章花费了很长时间,也希望网站搭建爱好者们可以参考本文,制作一个完全自己 DIY 的博客。可以参考这个博客!

—-未完待续

© Willie 2022.RSS