用 gatsby 搭建博客

选择 gatsby 主要有几点理由:

  • 基于 react
  • 内置 markdown 处理器
  • 生态良好,插件较丰富
  • 无后端、部署简单

比较明显的缺点应该就是需要在本地编辑文章和上传,但我也经常在本地写 markdown 文章,所以对我而言问题不大

搭建开发环境

先确保 node 已安装,然后全局安装 gatsby-cli,基于 gatsby-starter-blog 来快速开启博客页面

npm install -g gatsby-cli
gatsby new my-blog https://github.com/gatsbyjs/gatsby-starter-blog
cd my-blog
npm run dev

然后就可以打开 localhost:8000 访问页面了,刚开始还是一些模板代码,可以替换或者去掉

GraphQL

页面数据是通过 GraphQL 查询拿到的,在你本地启动 Gatsby 服务时,也会同步启动 GraphQL 的服务。你的文件、图片等所有资源会被 Gatsby 和一些安装的插件解析到 GraphQL 的节点上,通过特定的语法就可以按需获取需要的数据

简而言之,Gatsby 的工作原理就是通过 GraphQL 的 api 和你指定的语法完成数据的按需获取,再用获取到的数据渲染成静态网页

接入评论功能

可以通过 utterances + github issues 实现,实际上就是先拿到用户的 github 信息,然后将评论作为 issue 推送至指定的仓库。这样也就不用专门去搞个数据库存储评论数据了

  1. 创建一个存放评论信息的 github 仓库
  2. 安装 utterances 并授权
  3. 配置信息,比如按文章名作为 issue 名称。可以参考这个 https://utteranc.es/
  4. 新建一个 Comments 组件
import * as React from "react";
import { useEffect, useRef } from "react";

const Comments = () => {
  const commentsRef = useRef < HTMLDivElement > null;
  useEffect(() => {
    const script = document.createElement("script");
    script.src = "https://utteranc.es/client.js";
    script.setAttribute("repo", "GitHubJackson/blog-comments");
    script.setAttribute("issue-term", "title");
    script.setAttribute("label", "💬");
    script.setAttribute("theme", "github-light");
    script.setAttribute("crossorigin", "anonymous");
    script.async = true;

    if (commentsRef.current) {
      commentsRef.current.appendChild(script);
    }

    return () => {
      if (commentsRef.current) {
        commentsRef.current.innerHTML = "";
      }
    };
  }, []);
  return <div ref={commentsRef} />;
};

export default Comments;

src/templates/blog-post.js 中插入组件

//...
<Layout location={location} title={siteTitle}>
  //...
  <Comments />
</Layout>
//...

效果如图:

新增页面

直接在 pages 文件夹下新增页面即可,可以直接用 typescript 编写组件(tsx),项目已经默认支持

我的博客页面如下:

  • Archive 归档,文章归档页,按照发布时间排序
  • Categories 分类,文章分类页
  • Tags 标签,文章标签页、以标签云的方式呈现
  • About 关于,展示作者的信息、提供留言板
  • Lab 实验室,展示自己的一些小项目

上面几个是比较常见的博客页面了,还可以往后追加自己的 Github 主页等等

在文章前加上对应信息,比如:

---
title: 文章标题
createTime: "2020-10-23"
updateTime: ""
type: "js"
tags: "js,class,es6,原型,面向对象"
description: "balabala..."
---

markdown 文件会被 gatsby-plugin-remark 解析成 markdownRemark 的节点,以上描述信息会被解析到 frontmatter

可以通过修改 frontmatter 代码获取指定数据,比如我想获取文章分类,新建一个分类页面categories.tsx,参考代码如下

// categories.tsx
import { graphql } from "gatsby";
import * as React from "react";
import Layout from "../components/layout";
import "../css/categories.css";

export default ({ data, location }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`;
  // 拿到所有的文章数据
  let posts = data.allMarkdownRemark.nodes;
  let categories: any[] = [];
  // 计算各分类文章的数量
  posts.forEach((post) => {
    const current = categories.find(
      (category) => category.title === post.frontmatter.type
    );
    if (!current) {
      categories.push({
        title: post.frontmatter.type,
        count: 1,
      });
    } else {
      current.count = current.count + 1;
    }
  });

  return (
    <Layout location={location} title={siteTitle}>
      {categories.map((category) => {
        return (
          <div key={category.title} className="category">
            {category.title}{category.count}</div>
        );
      })}
    </Layout>
  );
};

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(
      sort: { fields: [frontmatter___createTime], order: DESC }
    ) {
      nodes {
        excerpt
        fields {
          slug
        }
        // 通过该字段查询文章开头的描述信息
        frontmatter {
          type
        }
      }
    }
  }
`;

最终的分类页面参考 https://blog.zhouweibin.top/categories/

文章相关

toc

基于 Tocbot 为 md 文章增加一个目录

npm install tocbot

在文章代码中增加目录初始化逻辑如下:

// blog-post.tsx
useEffect(() => {
  // ...
  // 指定作为目录标题的标签
  const headerArr = ["H1", "H2", "H3", "H4"];
  const blogContentNode = document.getElementsByClassName("blog-content")[0];
  if (!blogContentNode?.children?.length) {
    return;
  }
  // 遍历文章节点,给所有的目录标题节点增加 id,可用于锚点定位
  // @ts-ignore
  [...blogContentNode.children].forEach((child) => {
    if (headerArr.includes(child.nodeName)) {
      // 去除空格以及多余标点
      let headerId = child.innerText.replace(
        // eslint-disable-next-line no-useless-escape
        /[\s|\~|`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\||\|\[|\]|\{|\}|\;|\:|\"|\'|\,|\<|\.|\>|\/|\?|\:|\,|\。]/g,
        ""
      );
      headerId = headerId.toLowerCase();
      // NOTE 需要确保name唯一,最好加个自增id
      child.setAttribute("id", headerId + "-" + num);
      num++;
    }
  });
  tocbot.init({
    // Where to render the table of contents.
    tocSelector: ".js-toc",
    // Where to grab the headings to build the table of contents.
    contentSelector: ".blog-content",
    // Which headings to grab inside of the contentSelector element.
    headingSelector: "h1, h2, h3, h4",
  });
  // ...
});

通过修改对应的类名可以调节目录样式。如果感觉自己写的样式不好看,可以去掘金或者其他网站借鉴下源码 ~

参考 https://tscanlin.github.io/tocbot/

阅读时长

gatsby-transformer-remark 插件已经帮我们计算好了阅读时长,直接修改 GraphQL,再到组件代码中获取

// helper/utils.ts
export function formatReadingTime(minutes: number) {
  let cups = Math.round(minutes / 5);
  if (cups > 4) {
    return `${new Array(Math.round(cups / 4))
      .fill("🍚")
      .join("")}${minutes} mins`;
  } else {
    return `${new Array(cups || 1).fill("🍵").join("")}${minutes} mins`;
  }
}
// pages/index.tsx
// 找个合适的位置
<span style={{ marginLeft: 8 }}>{`${formatReadingTime(
  post.timeToRead
)}`}</span>;
// ...
export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(
      sort: { fields: [frontmatter___createTime], order: DESC }
    ) {
      nodes {
        excerpt
        fields {
          slug
        }
        timeToRead
        frontmatter {
          createTime(formatString: "YYYY/MM/DD")
          updateTime(formatString: "YYYY/MM/DD")
          title
          type
          tags
          description
        }
      }
    }
  }
`;

效果如下:

夜间模式

借助 gatsby-plugin-use-dark-mode 来实现夜间模式,保护眼睛 ~

yarn add gatsby-plugin-use-dark-mode @fisch0920/use-dark-mode
// use-dark-mode 和 Gatsby v3 的react版本有冲突
// 所以这里使用社区的fork解决版本  @fisch0920/use-dark-mode

增加插件

// post-config.js
plugins: [
  "gatsby-plugin-use-dark-mode",
  // ...
];

组件就先简单用 antd 的 Switch 组件

// components/dark-mode-toggle.tsx
import * as React from "react";
import useDarkMode from "@fisch0920/use-dark-mode";
import { Switch } from "antd";

const DarkModeToggle = () => {
  const darkMode = useDarkMode(false);
  return (
    <Switch
      checked={darkMode.value}
      onChange={darkMode.toggle}
      checkedChildren=""
      unCheckedChildren=""
    />
  );
};

export default DarkModeToggle;

在布局组件 Layout.js 中引入组件

import DarkModeToggle from "./dark-mode-toggle";
import useDarkMode from "@fisch0920/use-dark-mode";

//...
const darkMode = useDarkMode(false);
//...
<DarkModeToggle mode={darkMode} />

增加夜间模式相关的全局样式

/* src/style.css */
/* 主题模式 */
body.light-mode {
  background-color: #fff;
  color: #333;
  transition: background-color 0.3s ease;
}
body.dark-mode {
  background-color: #212121;
  color: #999;
  transition: background-color 0.3s ease;
}
.dark-mode .global-header {
  background-color: #212121;
  transition: background-color 0.3s ease;
}

上面只是实现了基础功能,DarkModeToggle 组件建议自行美化下。点击切换模式应该就能看到效果了。但实际效果可能还会有问题,比如有一些你自定义颜色或背景色的模块,需要在 src/style.css 针对性地去定义夜间模式下的颜色

其他组件

返回顶部

当文章太长时,往往需要增加一个返回顶部的小按钮,便于快速回到顶部,主要代码如下:

// blog-post.tsx
function handleScrollToTop() {
    // 滚动到顶部
    document.documentElement.scrollTo({
      top: 0,
      behavior: "smooth",
    });
}
// ...
useEffect(() => {
    // ...
    function handleScroll(e) {
      const rootElement = document.documentElement;
      const scrollToTopBtn = document.querySelector(
        ".back-to-top"
      ) as HTMLElement;
      const scrollTotal = rootElement.scrollHeight - rootElement.clientHeight;
      if (rootElement.scrollTop / scrollTotal > 0.5) {
        // 显示按钮
        scrollToTopBtn.style.bottom = "36px";
        scrollToTopBtn.style.opacity = "1";
      } else {
        // 隐藏按钮
        scrollToTopBtn.style.bottom = "-36px";
        scrollToTopBtn.style.opacity = "0";
      }
    }
    document.addEventListener("scroll", handleScroll);
    return (() => {
        document.removeEventListener("scroll", handleScroll);
    })
})

按钮样式自行发挥吧,可以简单写个过渡动画 ~

分析网站

可以通过谷歌的 Google Analytics 来分析自己的网站,包括网站流量、访客信息、访问设备、浏览次数等

其实工作原理就类似于埋点,将一段谷歌的代码注入博客网页,它会帮忙收集和分析登录网页的用户信息

教程详情参照 设置 Google Analytics(分析)全局网站代码

获取到代码后,将其注入到 components/seo.js 组件中即可

import { Helmet } from "react-helmet";

<Helmet>
  {/* <!-- Global site tag (gtag.js) - Google Analytics --> */}
  <script
    async
    src="https://www.googletagmanager.com/gtag/js?id=你的跟踪ID"
  ></script>
  <script>
    {`
    window.dataLayer = window.dataLayer || []; 
    function gtag() {dataLayer.push(arguments)}
    gtag('js', new Date()); gtag('config', '你的跟踪ID');
  `}
  </script>
</Helmet>;

这其实是 react-helmet 这个依赖帮忙将这段代码注入到网页的 head 中,感兴趣可以自行去了解 ~

sitemap

可以借助 gatsby-plugin-sitemap 插件自动生成 sitemap

// gatsby-config.js
module.exports = {
  siteMetadata: {
    siteUrl: `https://blog.zhouweibin.top`,
  },
  plugins: [`gatsby-plugin-sitemap`],
};

打包部署后,会自动在根目录下生成 sitemap 文件。我是用的 v5 版本的插件,会生成 sitemap 文件夹,存放 sitemap-index.xml(站点地图索引,会指向最终的站点地图),可以通过 ’https://blog.zhouweibin.top/sitemap/sitemap-index.xml’ 访问验证

之后可以上 google(google 站点地图)或百度(百度收录)上传站点地图。上传会有延迟,不代表 sitemap 失效,大概半小时生效吧

seo 优化

其实这方面已经有做了一些处理,参见 components/seo.js

部署到服务器

需要有个服务器部署博客页面(也可以试试用 CMS),我个人使用 centos 系统,服务器的话,选腾讯云阿里云的轻量服务器就行,新人可以点以下链接领取大额优惠券

还有个人域名、https 证书也都是个人网站需要的,建议在同一家云服务方按需购买

配置 nginx

配置下 nginx ,可以接入个人域名,以及配置二级域名转发等等

yum install nginx
systemctl start nginx  # 启动
systemctl enable nginx # 开发自启动

nginx 配置文件目录为 /etc/nginx/nginx.conf,简单配置如下:

#...
http {
    #...
    server {
	    listen       8080;
        server_name  localhost;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            root /home/blog-next;      # 静态资源存放位置,比如博客项目打包生成的 public 文件
            index index.html;
        }
    }
}

修改完,重启一下 nginx systemctl restart nginx

然后通过 http://[你的ip地址]:8080 其实就可以访问到你的页面了

追加个人域名、https 和二级域名转发的配置如下:

https 证书在腾讯云或者阿里云都有对应的免费证书可领

#...
http {
    #...
    server {
	    listen       8080;
        server_name  zhouweibin.top;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            root /home/blog-next;
            index index.html;
            #try_files $uri $uri/ /index.html;  # 单页面应用需要该配置
        }
    }

    server {
        listen       80;
        server_name  blog.zhouweibin.top;       # 二级域名转发
        location / {
            proxy_pass http://127.0.0.1:8080;
	          #root /home/blog-next;
            #index index.html;
            #try_files $uri $uri/ /index.html;
        }
    }

# Settings for a TLS enabled server.
    server {
        listen       443 ssl http2;
        listen       [::]:443 ssl http2;
        server_name  blog.zhouweibin.top;
        root         /usr/share/nginx/html;

        ssl_certificate "/etc/nginx/cert/6687351_blog.zhouweibin.top.pem";     # 指向证书存放位置
        ssl_certificate_key "/etc/nginx/cert/6687351_blog.zhouweibin.top.key";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

	    location / {
	        proxy_pass http://127.0.0.1:8080;
	    }
    }
}

打包上传

npm run build

将打包生成的 public 文件夹上传到服务器 nginx 指定的静态资源文件夹,比如我是放在 /home/blog-next。接下来就可以通过 https://blog.zhouweibin.top 访问了

服务器相关操作可以参考我之前总结的文章 - 服务器环境入门级搭建

快速上传文件可以用 FileZilla 可视化界面直接操作

自动部署

可以借助 github 和 jenkins 实现一个简单的自动部署能力

  1. 先创建一个 github 项目,与本地项目关联上
  2. 搭建 jenkins 环境。这个可以参考我之前写的文章 - 服务器环境入门级搭建
  3. github 给项目添加一个 webhook,和 jenkins 关联上

jenkins shell 脚本如下:

#!/bin/sh
cd /var/lib/jenkins/workspace/blog-next
rm -rf node_modules public
npm config set registry https://registry.npmmirror.com/ # 也可以在项目中增加 npmrc 文章指定默认源
npm install #安装项目中的依赖
npm run build
cd public
cd /home #进入web项目根目录
if [ ! -d "blog-next" ]; then
  sudo mkdir blog-next
fi
cd /home/blog-next #进入web项目根目录
sudo rm -rf *
sudo mv /var/lib/jenkins/workspace/blog-next/public/* ./  #移动刚刚打包好的项目到web项目根目录

接下来就可以尝试提交代码(git push),测试下自动部署的能力了

最后

接下来,就可以开始经营你的个人博客了,写博客、实现更多的功能(文章目录、分类等)、增加其他页面,等等…这篇文章也会在后续持续更新,带来更多的玩法 ~!

参考