用 Gatsby 搭建静态博客 1

926 阅读7分钟

[原文地址]

前几天跟老铁们聊天,有位老铁说想弄一个博客只靠 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 : programing
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 -