用 Gatsby 搭建静态博客 1

Posted by Sir0xb on 2019-01-31 18:56:00 +0800

前几天跟老铁们聊天,有位老铁说想弄一个博客只靠 SEO 被搜索,想再次回归曾经那份朴素。于是我又想起了我那放置许久的博客。

闲聊之余网上找了找有没有不错的静态博客框架可用,毕竟好久没有更新博客的框架了。 机缘巧合,看到的文章推荐使用的第一项就是 Gatsby,上官网看了看貌似还不错。后来才知道 React 官网也是用这个框架搭建的 😝。

正好年前有点闲暇时间,着手弄了弄。从 jekyll、hexo、octopress,现在更新到了 Gatsby。以下是一点点经验,分享给大家。


1. 项目初建

先安装项目工具:

npm i -g gatsby-cli

生成项目:

npm new [your project name]

可以查看目录下的 package.json 了解项目命令。

运行如下命令,就能看到项目工具生成的基础博客站。

gatsby develop

简单说明一下项目结构:

目录/文件 说明
src/components 放置组件的目录
src/pages 基础方式渲染的页面目录(根据文件名形成路由)
gatsby-config.js 中间件配置及网站基本信息配置文件
gatsby-node.js 高级方式渲染页面(根据路径配置方式生成路由)
<以下是后续添加的部分>
src/queries Graphql 查询语句放置目录
src/templates 高级方式渲染页面所用的模版放置目录
static 发布时自动合并到发布目录的静态文件放置目录
deploy.sh 正式发布站点的脚本

2. 修改导航栏

先把文件 src/components/header 转成目录 src/components/Header/

将原文件里的代码拷入 index.jsx 再适当的修改修改。

...
class Header extends React.Component {
constructor(props) {
super(props)
 
this.state = {
currentMenu: 'home'
}
}
 
componentDidMount() {
const { pathname } = window.location
 
const extract = pathname.split('/')[1]
 
this.setState({ currentMenu: extract === '' ? 'home' : extract })
 
if (extract === '' || /^[0-9]+$/.test(extract)) {
this.setState({ currentMenu: 'home' })
} else if (extract === 'about') {
this.setState({ currentMenu: 'about' })
} else if (extract === 'blog' || extract.length === 24) {
this.setState({ currentMenu: 'blog' })
} else {
this.setState({ currentMenu: 'null' })
}
}
 
render() {
const { siteTitle } = this.props
const { currentMenu } = this.state
 
return <div className="header">
<div className="container">
<ul>
<li className="site-title">
<h1><Link to="/">{siteTitle}</Link></h1>
</li>
<li className={currentMenu === 'home' ? 'menu-item currentMenu' : 'menu-item'}>
<h3><Link to="/">Home</Link></h3>
</li>
<li className={currentMenu === 'blog' ? 'menu-item currentMenu' : 'menu-item'}>
<h3><Link to="/blog">Blog</Link></h3>
</li>
<li className={currentMenu === 'about' ? 'menu-item currentMenu' : 'menu-item'}>
<h3><Link to="/about">About</Link></h3>
</li>
</ul>
</div>
</div>
}
}
...

增加了当前菜单的 state 进行菜单状态管理。

ul > li 横向序列化,第一个用作网站标题,其他的作为菜单项使用。

注:
1. 首页判断部分,后续要增加首页翻页功能,所以增加了数字判断。
2. 博文判断部分,准备用24位随机码作为每个文章的地址。所以除了 blog 还增加了随机码长度的判断。

样式部分就根据个人喜好开发就好了。


3. 博文列表

作为博客,最主要的还是博文部分,先把博文部分弄出来再说。

之前的博文都是用 markdown 写的,先安装 markdown 解析工具并进行配置。

npm i gatsby-source-filesystem
npm i gatsby-transformer-remark
npm i gatsby-plugin-catch-links

修改 gatsby 插件配置 gatsby-config.js:

...
plugins: [
...
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'pages',
path: `${__dirname}/src/pages`
}
},
'gatsby-transformer-remark',
'gatsby-plugin-catch-links',
...
]
...

配置好之后,在 src/pages 目录里直接创建 md 文件或是创建目录并在里边创建 md 文件都可以。markdown 解析部分完成了。

如果留意观察过启动时控制台的提示,就知道访问 http://localhost:8000/___graphql 就能够进行 GraphiQL 查询了。

先看一下博文头部信息:

---
path       : '/yp63Vswica5FHmJGE479XP5k'
title      : '用 Gatsby 搭建静态博客 1'
date       : 2019-01-31 18:56:00  +0800
comments   : true
categories : programming
author     : Sir0xb
tags       : [Gatsby, React]
---

这些信息很重要,都是一会儿要被查询的字段。

打开 http://localhost:8000/___graphql,在左侧搜索条件输入:

{
allMarkdownRemark(sort: {fields: [frontmatter___date], order: DESC}) {
edges {
node {
id
html
frontmatter {
path
title
date
comments
author
tags
}
excerpt
}
}
}
}

看到查询结果大概就明白各个字段代表什么意思,不做过多解释。

有了能够正常运行的查询表达式,可以开始我们的渲染工作了。

还记得改造菜单时候添加了一个路径 /blog 吗? 在 src/pages 里创建一个 blog.js

import React from 'react'
import { Link, graphql } from 'gatsby'
 
const BlogPage = ({ data }) => (
<div>
<h1>This is the blog page</h1>
{data.allMarkdownRemark.edges.map(post => (
<div key={ post.node.id }>
<h3>{post.node.frontmatter.title}</h3>
<small>Posted by {post.node.frontmatter.author} on {post.node.frontmatter.date}</small>
<br/>
<br/>
<Link to={post.node.frontmatter.path}>Read More</Link>
<br/>
<br/>
<hr/>
</div>
))}
</div>
)
 
export const pageQuery = graphql`
{
allMarkdownRemark(sort: {fields: [frontmatter___date], order: DESC}) {
edges {
node {
id
html
frontmatter {
path
title
date
comments
author
tags
}
excerpt
}
}
}
}
`
 
export default BlogPage

Gatsby 用 GraphiQL 查询文件的逻辑就是,通过导出 pageQuery 进行数据查询,并把结果注入到当前 Component 的 props 的 data 里。

重新启动之后,点击菜单 blog 看到所有文章列表了。


4. 博文预览

点击博文发现页面 404 了。原因是在 src/pages 里没有找到我们24位随机码路径对应的文件。

这时候我们就要用到高级方式渲染页面的功能了。

我们先做一个博文预览的模版文件 src/templates/post.js

import React from 'react'
import { graphql } from 'gatsby'
 
import Layout from '../components/layout'
import SEO from '../components/seo'
 
import './style.css'
 
const Template = ({ data }) => {
const post = data.markdownRemark
 
return <Layout>
<SEO title={post.frontmatter.title} />
<button
className="go-back"
onClick={() => { window.history.back() }}
>Go back</button>
<div className="blog-post">
<h1>{post.frontmatter.title}</h1>
<h4>Posted by {post.frontmatter.author} on {post.frontmatter.date}</h4>
<div dangerouslySetInnerHTML={{__html: post.html}}></div>
</div>
</Layout>
}
 
export const postQuery = graphql`
query BlogxxxPostByPath($path: String!) {
markdownRemark(frontmatter: { path: { eq: $path } }) {
html
frontmatter {
path
title
author
date
}
}
}
`
 
export default Template

我们把 blog.js 里面的查询抽离到 src/queries/queryAll.js 里。 blog.js 文件里的查询先不动。(blog.js 文件后续就放弃不用了)

打开 gatsby-node.js 文件。

const path = require('path')
 
const queryAll = require('./src/queries/queryAll')
 
exports.createPages = ({ boundActionCreators, graphql }) => {
const { createPage } = boundActionCreators
 
return new Promise((resolve, reject) => {
resolve(
graphql(queryAll).then(result => {
if (result.errors) reject(result.errors)
 
// 根据文章ID生成页面
const postTemplate = path.resolve('./src/templates/post.js')
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path      : node.frontmatter.path,
component : postTemplate
})
})
})
)
})
}

重启之后再点开文章,是不是可以正常渲染了。


5. 增加翻页器

写了多年的博客那么多的博文,如果一次性全部显示出来就不友好了。

解决方案就是加个翻页器,先把 gatsby 的翻页器工具安上。

npm i gatsby-paginate

翻页器功能可以使用官网 Demo 里提供的代码,当然也可以自己开发。

我比较喜欢前后都有《最前》、《最后》以及《上一页》、《下一页》按钮,页码部分低位至少留有两个页码,高位也至少留有两个页码,并且当前页码的前后各留有两个页码的翻页方式。

那我们先把翻页器组件实现一下。src/components/Paginator/index.jsx

import React from 'react'
import { Link } from 'gatsby'
 
import './style.css'
 
const getRandomStr = (len = 15) => {
let text = ''
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}
 
const Paginator = ({ index, pageCount, relativeUrl }) => {
let result = []
 
result.push(<Link key={getRandomStr()} to={relativeUrl}>{'«'}</Link>)
if (index <= 2) {
result.push(<Link key={getRandomStr()} to={relativeUrl}>{'<'}</Link>)
} else {
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${index - 1}`}>{'<'}</Link>)
}
 
if (pageCount < 11) {
Object.keys(Array.from({ length: pageCount })).forEach((item, listIndex) => {
result.push(
<Link
key={getRandomStr()}
className={listIndex + 1 === index ? 'currentPage' : ''}
to={`${relativeUrl}/${listIndex === 0 ? '' : listIndex + 1}`}
>{listIndex + 1}</Link>
)
})
} else {
if (index <= 5) {
// 低数 index + 2   高位两个
Object.keys(Array.from({ length: index + 2 })).forEach((item, listIndex) => {
result.push(
<Link
key={getRandomStr()}
className={listIndex + 1 === index ? 'currentPage' : ''}
to={`${relativeUrl}/${listIndex === 0 ? '' : listIndex + 1}`}
>{listIndex + 1}</Link>
)
})
result.push(<span key={getRandomStr()}>...</span>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${pageCount - 1}`}>{pageCount - 1}</Link>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${pageCount}`}>{pageCount}</Link>)
} else if (index >= pageCount - 4) {
// 低位两个   高位 index - 2 到顶
result.push(<Link key={getRandomStr()} to={`${relativeUrl}`}>1</Link>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/2`}>2</Link>)
result.push(<span key={getRandomStr()}>...</span>)
// pageCount - (index - 2) + 1 = pageCount - index + 3
Object.keys(Array.from({ length: pageCount - index + 3  })).forEach((item, listIndex) => {
let newIndex = listIndex + index - 3
result.push(
<Link
key={getRandomStr()}
className={newIndex + 1 === index ? 'currentPage' : ''}
to={`${relativeUrl}/${newIndex === 0 ? '' : newIndex + 1}`}
>{newIndex + 1}</Link>
)
})
} else {
// 低位两个  中间 index - 2 ~ index + 2  高位两个
result.push(<Link key={getRandomStr()} to={`${relativeUrl}`}>1</Link>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/2`}>2</Link>)
result.push(<span key={getRandomStr()}>...</span>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${index - 2}`}>{index - 2}</Link>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${index - 1}`}>{index - 1}</Link>)
result.push(<Link key={getRandomStr()} className="currentPage" to={`${relativeUrl}/${index}`}>{index}</Link>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${index + 1}`}>{index + 1}</Link>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${index + 2}`}>{index + 2}</Link>)
result.push(<span key={getRandomStr()}>...</span>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${pageCount - 1}`}>{pageCount - 1}</Link>)
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${pageCount}`}>{pageCount}</Link>)
}
}
 
if (index === pageCount) {
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${pageCount}`}>{'>'}</Link>)
} else {
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${index + 1}`}>{'>'}</Link>)
}
result.push(<Link key={getRandomStr()} to={`${relativeUrl}/${pageCount}`}>{'»'}</Link>)
 
return <div className="paginator">
{result}
</div>
}
 
export default Paginator

注:因为翻页器可能会用在首页,也可能用在博文页,所以传入了相对路径 relativeUrl

src/pages/blog.js 更名或删除。我们要通过高级方式生成博文页面,不再使用原来的页面了。

再做一个带翻页器的博文页面渲染模版。src/templates/posts.js

import React from 'react'
import Link from 'gatsby-link'
 
import Layout from '../components/layout'
import SEO from '../components/seo'
import Paginator from '../components/Paginator'
 
const Template = ({ pageContext }) => {
const {
group,
index,
pageCount 
} = pageContext
 
return <Layout>
<SEO title="Blog" />
<Paginator index={index} pageCount={pageCount} relativeUrl="/blog" />
{group.map(({ node }) => (
<div className="normal-homepage-item" key={node.id}>
<h3>{node.frontmatter.title}</h3>
<small>Posted by {node.frontmatter.author} on {node.frontmatter.date}</small>
<br/>
<br/>
<Link to={node.frontmatter.path}>Read More</Link>
<br/>
</div>
))}
<Paginator index={index} pageCount={pageCount} relativeUrl="/blog" />
</Layout>
}
 
export default Template

有了翻页器组件,有了模版,就差数据了。

修改下 gatsby-node.js

const path = require('path')
const createPaginatedPages = require('gatsby-paginate')
 
const queryAll = require('./src/queries/queryAll')
 
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions
 
return new Promise((resolve, reject) => {
resolve(
graphql(queryAll).then(result => {
if (result.errors) reject(result.errors)
 
// 生成博文翻页
const PostsTemplate = path.resolve('./src/templates/posts.js')
createPaginatedPages({
edges        : result.data.allMarkdownRemark.edges,
createPage   : createPage,
pageTemplate : PostsTemplate,
pageLength   : 10,
pathPrefix   : 'blog'
})
 
// 根据文章ID生成页面
const postTemplate = path.resolve('./src/templates/post.js')
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path      : node.frontmatter.path,
component : postTemplate
})
})
})
)
})
}

注:由于 gatsby-paginate 要求的 Gatsby 版本要比默认的高,所以我把 Gatsby 版本升级到了最高。于是这里有了点变化。原先的 boundActionCreators 变成了 actions


- THE END -