用 gatsby 搭建博客网页

这个其实是我搭建博客网页的一些实践,当时选择 gatsby 主要有几点理由:

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

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

搭建开发环境

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

bash 复制代码
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 名称。可以参考这个 utteranc.es/
  4. 新建一个 Comments 组件
jsx 复制代码
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 中插入组件

jsx 复制代码
//...
<Layout location={location} title={siteTitle}>
  //...
  <Comments />
</Layout>
//...

效果如图:

新增页面

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

我的博客页面如下:

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

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

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

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

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

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

jsx 复制代码
// 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
        }
      }
    }
  }
`;

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

文章相关

toc

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

npm install tocbot

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

js 复制代码
// 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",
  });
  // ...
});

通过修改对应的类名可以调节目录样式。如果感觉自己写的样式不好看,可以去掘金或者其他网站借鉴下源码 ~ 更多内容参考 tscanlin.github.io/tocbot/

阅读时长

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

js 复制代码
// 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`;
  }
}
jsx 复制代码
// 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 来实现夜间模式,保护眼睛 ~

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

增加插件

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

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

jsx 复制代码
// 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 中引入组件

javascript 复制代码
import DarkModeToggle from "./dark-mode-toggle";
import useDarkMode from "@fisch0920/use-dark-mode";

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

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

css 复制代码
/* 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 针对性地去定义夜间模式下的颜色

其他组件

返回顶部

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

js 复制代码
// 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 组件中即可

jsx 复制代码
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

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

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

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

seo 优化

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

部署

可以参考我之前写的 Github actions 实现个人网站自动部署

最后

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

参考

相关推荐
也无晴也无风雨25 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational2 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤5 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui