不用 starter 做 Gatsby.js Blog

13 min read

# tech

醞釀要開一個部落格很久,卻遲遲沒有勇氣,但眼看一年都已經到了六月這樣不行 ...... 就慢慢的把之前零零碎碎的筆記寫成一篇篇文章。

而當然也包含半年前,心血來潮的看到 Gatsby 這個新的架構,心裡想著:「如果可以使用 Component 的話是不是就可以做出很多很酷的資料視覺化文章了!」的初衷,開始使用 Gatsby 將這個部落格的心得記錄。

什麼是 Gatsby.js

正如官網介紹的:

Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps

Gatsby 是一個基於 React 的 JAMStack Framework,適合以靜態為主、動態為輔,而且頁面數量有限的狀況,例如:部落格、作品集這類型的網站,但反之如果內容量非常的大的時候,像是:動輒好幾十萬商品電商網站,就不適合使用 Gatsby,因為產生頁面的時候會產生到天荒地老,但如果是一個小規模的內容網站 Gatsby 可以節約大量照護後端的成本。

不過 Gatsby 怎麼幫我們做到這些事情?

在開發過程中,可以想像你是公關企劃,你將你的資訊內容依照他規範好的規則來寫一整份策展企劃書,所以之後 Gatsby 這個執行單位,就會照著你的這份企劃幫你打造出一個完整展覽所需的所有場佈道具,當你今天要在各個地方辦展覽的時候,就根據當時規劃已經做好的道具擺到你要辦展覽的地方就可以了。

對應到 Gatsby 的架構,資訊內容就是我們的文章、數據等資料,而他的規則就是 GraphQL 來放進企劃書,也就是 React 前端開發,而就會 Gatsby 將內容預先產生好作為靜態文件放在伺服器上,之後只要在根據你當初預設好的規則存取特定頁面就行了。

然而 Gatsby 一定要完全靜態的嗎?並不,就像在企劃裡面你當然可以保留一些想要動態改變的部分,在 Gatsby 的架構裡面也有 API 讀取資料再渲染的空間,不過這是比例上的問題,如果你的內容的確以靜態為主,Gatsby 的處理架構會比較有效率。

Gatsby 體驗

根據 Gatsby 的選擇樹,加上 Gatsby 成功的社群經營,最常使用的就是 Gatsby 官網上提供的 Starter:

只要照著他的指令輸入,例如說,我們直接在 starter 頁面 搜尋我們需要的 starter 接著照著理面寫的步驟就可以快速產生一個網站了,以官方的 gatsby-starter-blog 為例:

只要安裝完 gatsby:

npm install -g gatsby-cli

輸入這一行:

gatsby new gatsby-starter-blog https://github.com/gatsbyjs/gatsby-starter-blog

...... 就成功了,驚為天人。

不使用 Starter

然而,使用 starter 的壞處就是超難修改,尤其 Gatsby 本身就已經打包很多東西的前提下,所以為了理解這個工具,整理了在 Front End Master 上 Introduction of Gatsby 的課程筆記與重新理解,今天要從最陽春的 Gatsby 開始疊床架屋打造出現在的這個部落格。

這一塊的閱讀可能需要一些先備知識:

  • React
  • React Router
  • React Hook
  • GraphQL

如果沒有可以先從其他文章來了解一下這些工具,那就開始吧!

File Structure

先從 file structure 來看,一個 Gatsby 專案大概會有下面這些內容:

  • /caches:由 Gatsby 自動產生,用來存放開發時的暫存檔案的地方,可以透過 gatsby clean 來重新產生。
  • /public:由 Gatsby 自動產生,用來存放最後打包檔的地方,類似於 /dist 的地方。
  • /static:不需要由 webpack 處理的素材,像是:Favicon。
  • /src:主要開發的資料夾
  • /assets:整個網站會共同使用到的素材,像是:Logo, Avatar 這類的素材。
  • /components:整個網站會用到的 react components,像是:<Header /><List /><Menu /> 等等。
  • /hooks:統整用來取得數據的 GtaphQL Hooks 們。
  • /pages:部落格中的一些靜態頁面,像是:About, Category 等等。
  • /posts:部落格中的一篇篇文章 MDX 檔案與相關的圖片跟 components。
  • /templates:產生文章的 template react components。
  • Gatsby APIs gatsby-browser.js:管理在頁面出現之前要引入、會是共同設定的 API。 gatsby-config.js:管理網站基本資訊跟 plugin 設定的 API。 gatsby-node.js:管理 Gatsby 要怎麼產生頁面的 API。

接著來實作部分,接下來 npm init 一個新專案,接著安裝 gatsby, react 跟 react dom,這些是打造 Gatsby 網站最基本基本需要的三個 dependencies。

npm install gatsby react react-dom

接著,這時候在 /pages 裡面放入第一個 index.js react component 檔案,例如:

import react from 'react';
export default () => <div>Hello World!</div>;

並且執行 gatsby develop,就可以看到最最最小的 Gatsby 網站開始在 develope server 上運作了,之後如果要開新頁面就放在 pages 裡面透過 react-router 的來切換頁面,並使用 gatby 打包過後的 <Link /> 元件來切換頁面。

React

有了第一個頁面之後,永遠記得:

Gatsby 的本體其實還是 React。

在進入 Gatsby 龐大生態系的時候,一度會覺得迷失?有些東西好像 Gatsby 沒有附等等 ... 但其實它還是 React 只是 Gatsby 基於這之上再提供一些好用的功能,所以開發方式大抵跟平常開發 React 專案一樣的邏輯。

在第一個頁面之後,我們就像平常 React 開發一個個新頁面一樣,並將元件分離增加復用性來開發,例如:部落格中到處會出現的 Tag:

import React from 'react';
import { Link } from 'gatsby';
import { TagStyle } from '../style';

const Tag = ({ tag, total = null }) => {
	return (
		<TagStyle>
			<Link to="/category"># {tag}</Link>
		</TagStyle>
	);
};
export default Tag;

Styling

樣式的部分,在這個專案裡面使用了 Styled Component,但 Gatsby 的運作方式基本上同 React,你也可以用純 CSS 檔、或是其他 CSS Framework。由於部落格的專案規模不會太大,加上自己的使用習慣,所以最後選擇使用 Styled Component。

將通用 styled components 放在 ./src/style.js 之中,方便後續 components 共用。

import styled from "styled-components"
import { Link } from 'gatsby';
import { createGlobalStyle } from "styled-components"

const font = {
	small: '0.8rem',
	p: '1rem',
	h5: '1.2rem',
	...
}

export const NavLink = styled(Link)`
	margin: 0 20px 0 0;
	color: gray;
	font-size: ${font.h6};
	font-weight: 700;
	...
`
...

React Hooks

今天無論是要做 SEO、顯示文章內容等等,這些會跟著資料變動的資訊,習慣上會統一放在 Hooks 之中來管理,這邊放了 Custom Hooks 可以幫助我們將一些重複使用的 functions 被不同的 Components 複用,例如:取得文章資料等等的 API 就會放在這邊。

所以這邊我們使用 Gatsby 幫我們打包好的 useStaticQuery,透過這個 Hook 來使用 GraphQL 讀取資料:

import { graphql, useStaticQuery } from &#39;gatsby&#39;;
const usePosts = () =&gt; {
	const data = useStaticQuery(graphql`
		query {
			allMdx(sort: {fields: frontmatter___date, order: DESC}) {
				nodes {
					frontmatter {
						slug
						author
						date
						title
						tags
					}
					tableOfContents
					excerpt
				}
			}
		}
`)

return data.allMdx.nodes.map((post, idx) =&gt; ({
	title: post.frontmatter.title,
	author: post.frontmatter.author,
	...
	}))
}

export default usePosts;

那不熟悉 GraphQL 的人,Gatsby 也很貼心的提供的 GraphQL playground 讓我們查看現在的 Gatsby 有哪些資料可以存取:

可以在這邊選取想要的資料後,複製貼上產生的 GraphQL query。

Templates & Generator

現在獨立的頁面基本上都可以依照上面的方式讀取資料開發出來了,不過如果今天是大量的文章想要產生一系列新頁面的時候怎麼辦?我們可以將這類型的元件獨立放在 ./src/templates 之中來管理。

在這邊就不得不提 Gatsby 對 MDX 的支援,MDX 是可以引入元件的 markdown 檔案格式,例如:在這個部落格中有些文章是互動式文章,這些文章並非獨立成一個 page 來處理,而是一樣使用 MDX 然後再引入 components 來互動,這也是為什麼最後部落格選擇 Gatsby 的原因,作為一個喜歡資訊設計的開發者,怎麼能不用這個打造一個 Exploration Explanation 的部落格呢(尖叫)。

所以我們從 template 裡面的 post 的 template 來看這一塊 Gatsby 是怎麼串連起來的,下面是這個部落格專案的部分程式碼:

import React from 'react';
import { graphql } from 'gatsby';
import Layout from '../components/layout';
import { MDXRenderer } from 'gatsby-plugin-mdx';

...

const PostTemplate = ({ data: { mdx: post }, pageContext}) => {
	return (
		<Layout>
			...
			<MDXRenderer>{post.body}</MDXRenderer>
			...
		</Layout>
	)
}

export default PostTemplate;

第一步是引入 Gatsby plugin,首先 npm 安裝 gatsby-plugin-mdx 這個 Gatsby plugin,順帶一提,在 Gatsby plugin 中,gatsby 開頭的 plugin 就是官方 plugin。並且在 gatsby-conofig.js plugins 引入跟調整參數。

第二步是在 template 裡面建立 pageContext,搭配 GraphQL 就可以讓頁面產生器知道這個模板是基於什麼資訊背景:

export const query = graphql`
	query($slug: String!) {
		mdx(frontmatter: { slug: { eq: $slug } }) {
			frontmatter {
				title
				author
				tags
				date
				slug
			}
			excerpt
			tableOfContents
			body
		}
	}
`
...

最後要跟 Gatsby 說:「這邊是需要先 build 出來的內容!」,所以在 ./src/gatsby-node.js 之中,使用 gatsby 提供的 API 創建頁面:

exports.createPages = async ({ actions, graphql, reporter }) => {
	const result = await graphql(`
		query {
			allMdx {
				nodes {
					frontmatter {
						...	
					}
				}
			}
		}
	`);
	
	if (result.error) {
		reporter.panic('failed to get post', result.error);
	}
	
	const posts = result.data.allMdx.nodes;
	
	posts.forEach((post, idx) => {
		actions.createPage({
			path: post.frontmatter.slug,
			component: require.resolve('./src/templates/post.js'),
			context: {
				slug: post.frontmatter.slug,
				...
			},
		});
	});
};

這邊可能會疑惑的是:哪些資料應該要屬於 pageContext ? 哪些放在 gatsby-node.js

區別的原則就是:專屬於這一頁的內容,例如:文章內容、文章 slug 等等就可以放在 pageContext;如果是共用的,例如:文章數、pagination 就比較適合保留在 gatsby-node.js 或獨立成一個 Hooks 來處理。

那建立好之後,未來我們在 .src/posts 資料夾下寫的文章們 gatsby 就會自動幫我們從裡面讀取文章內容資料 build 出一系列的頁面啦!

Plugins

當有了部落格文章頁面跟獨立頁面之後,接下來就是永無止盡的優化這個部落格,無論是 PWA, SEO, Lazy Load 等等,Gatsby 都已經有很多現成的 plugin 只要在 plugins 頁面中搜尋跟著步驟安裝就可以輕易地使用,以這個部落格來說,就引入了這些 plugins:

"gatsby-mdx": "^1.0.0",
"gatsby-plugin-google-analytics": "^2.3.3",
"gatsby-plugin-manifest": "^2.4.10",
"gatsby-plugin-mdx": "^1.0.71",
"gatsby-plugin-offline": "^3.2.8",
"gatsby-plugin-react-helmet": "^3.1.22",
"gatsby-plugin-robots-txt": "^1.5.1",
"gatsby-plugin-sharp": "^2.4.5",
"gatsby-plugin-styled-components": "^3.1.19",
"gatsby-plugin-web-font-loader": "^1.0.4",
"gatsby-remark-code-titles": "^1.1.0",
"gatsby-remark-images": "^3.1.44",
"gatsby-remark-prismjs": "^3.3.31",
"gatsby-remark-relative-images": "^0.2.3",
"gatsby-remark-responsive-iframe": "^2.2.32",
"gatsby-source-filesystem": "^2.1.48",

參考資料