选择 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 推送至指定的仓库。这样也就不用专门去搞个数据库存储评论数据了
- 创建一个存放评论信息的 github 仓库
- 安装 utterances 并授权
- 配置信息,比如按文章名作为 issue 名称。可以参考这个 https://utteranc.es/
- 新建一个 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",
});
// ...
});
通过修改对应的类名可以调节目录样式。如果感觉自己写的样式不好看,可以去掘金或者其他网站借鉴下源码 ~
阅读时长
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 实现一个简单的自动部署能力
- 先创建一个 github 项目,与本地项目关联上
- 搭建 jenkins 环境。这个可以参考我之前写的文章 - 服务器环境入门级搭建
- 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
),测试下自动部署的能力了
最后
接下来,就可以开始经营你的个人博客了,写博客、实现更多的功能(文章目录、分类等)、增加其他页面,等等…这篇文章也会在后续持续更新,带来更多的玩法 ~!