将使用Contentful、Next.js和Apollo创建的个人作品集发布到Github Pages时的备忘录
为了获得关于Headless CMS和Next.js的服务器端渲染(SSR)/ 静态生成(SSG)的经验,我使用了Contentful的GraphQL内容API来获取数据,并通过Next.js来创建了一个网站。
由于我们整理了一系列的操作记录,所以如果你希望今后创建类似的网站,可以参考这些记录。
您可以在我创建的Github Pages上查看部署好的内容。
此外,请参考以下源代码。
任务进行的步骤
实际操作的流程如下:
-
- 作业流程和内容的调研与确认
-
- 环境搭建
引入Next.js
引入Typescript
引入Docker
引入TailwindCSS
引入ESLint
引入husky
在Contentful上创建内容
创建账号
创建内容模型
创建内容
进行locale设置
通过GraphQL Content API验证内容获取操作
对接GraphQL Content API
引入ApolloClient
获取schema
通过graphql-codegen生成类型
在Next.js中创建网站
支持local-only字段
支持SSG
支持Google Tag Manager
创建和部署Github Actions
在进行工作流程和内容的调查和确认时,我们通过研究各种文章来了解工作的概况。我将每个项目参考的文章链接附在下面。
接下来,我们将逐一记录工作内容。
环境建设
本地环境中,yarn和node的版本如下。
$ yarn -v
1.22.10
$ node -v
v14.16.1
首先,请从这个网站上创建一个名为.gitignore的文件,其中包含macos、emacs、vim、node和yarn。
引入Next.js
我是根据这个参考来学习和使用Next.js的。
$ yarn init -v
# package.jsonにてprivate: true を入れておく
$ yarn add -E react react-dom next
接着,将以下脚本添加到package.json中。
...
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
...
由于Next.js是通过参考pages目录来创建页面的,因此您需要创建合适的pages目录,并使用yarn run dev进行验证。
$ mkdir src/pages
$ yarn run dev
之后,我访问了localhost:3000,并确认了页面输出了404错误。
引入Typescript
$ yarn add -D typescript @types/react -E
$ npx tsc --init
我参考了 Contentful 的 Next.js 兼容 Github 仓库中的 tsconfig.json 文件。
引入 Docker
我要创建一个Dockerfile和docker-compose.yml文件。
FROM node:14.16.1-alpine
ENV TZ=Asia/Tokyo
WORKDIR /app
COPY package.json /app
COPY yarn.lock /app
RUN yarn install
COPY . /app
CMD ["yarn", "dev"]
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
command: 'yarn run dev'
ports:
- 3000:3000
volumes:
- .:/app:cached
- node_modules:/app/node_modules
tty: true
stdin_open: true
environment:
# 適宜環境変数を追加していく
volumes:
node_modules:
在构建映像时,诸如node_modules等会导致处理变慢的文件集会被.dockerignore文件排除。
node_modules
yarn-error.log
.husky
.next
.git
out
在执行了$ docker-compose up –build之后,我确认可以访问localhost:3000并查看到404页面。
TailwindCSS的引入
我使用Tailwind CSS进行了CSS标记。CSS属性与类名进行了映射,给我以个人使用起来很方便的印象。还有一个好处是不需要考虑每次组件的类名。
在开始之前,我参考了TailwindCSS的官方网站。
$ yarn add -D -E tailwindcss postcss autoprefixer
$ npx tailwindcss init -p
module.exports = {
mode: 'jit',
purge: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
另外,为了在部署时对CSS文件进行压缩,我们引入了cssnano,并修改了postcss.config.js文件。
$ yarn add -D -E cssnano
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
},
}
在Next.js中,向各个组件应用TailwindCSS,只需要在pages目录下的_app.tsx文件中导入’ tailwindcss/tailwind.css’。请参考此处获得更详细的信息。
对于ESLint的引入
为了统一代码的外观和可读性,我们将参考官方网站并引入ESLint。
$ yarn add -E -D eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-tsc eslint-plugin-tailwindcss @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier
$ npx eslint --init
然后,我们将在.eslintrc.json中记录各种插件的设置和规则。如果有要从ESLint中排除的文件,则在.eslintignore中记录。
{
"env": {
"browser": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "react-hooks", "tailwindcss", "tsc"],
"settings": {
"react": {
"version": "latest"
}
},
"rules": {
"eol-last": ["error", "always"],
"react/prop-types": [0],
"tailwindcss/classnames-order": 2,
"tailwindcss/no-custom-classname": 2,
"tailwindcss/no-contradicting-classname": 2,
"prettier/prettier": [
"warn",
{
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
]
}
}
node_modules
.next
next.config.js
out
在设置了ESLint后,在package.json的scripts中添加了”lint”: “eslint –ext .js,.jsx,.ts,.tsx,.json .”,然后运行”$ yarn run lint –fix”来应用规则。
引入哈士奇
为了在Git提交时运行lint,我们将设置husky和lint-staged。
$ yarn add -D -E husky lint-staged
$ npx husky install
根据Husky的文档,在npm7版本中使用npm set-script命令,但是由于这是npm7版本的命令,因此我们需要自行安装。我们在package.json的scripts中添加了”prepare”: “husky install”。
在npx husky add .husky/pre-commit之后,我们会在package.json中添加对lint-staged的支持。
...
"lint-staged": {
"*.{ts,tsx,json}": "yarn run lint"
}
...
以上就是搭建开发环境。
在Contentful平台上进行内容创作
由于要第一次接触Contentful,所以我做了一些调研。以下是我参考的文章链接。
-
- https://satoruakiyama.com/blog/building-blog-with-nextjs-and-contentful-ja
- https://www.contentful.com/developers/docs/references/graphql/
创建账户
访问Contentful并创建帐户。我将使用Github帐户进行注册。
根据指示,进行输入和确认规程,并创建一个账户。这次我们将介绍免费计划。
然后,输入组织名称并创建空间。如果是个人创建,则建议设置组织名称以便明确为个人,并将空间名称设为使用的网站名称等,以便于理解。
我选择了机构为Adacchi3,空间为adacchi3 portfolio。
创建内容模型
在创建内容时,我们会创建内容模型,这类似于创建DB模式。请点击下图中对应标题部分的内容模型。
创建新的内容模型时,请点击右上方的“Add content type”按钮,如下图所示。
内容类型的名称对应于类似于“Post”或“User”的类的名称。在下面的图像中,它是“Achievement”。如果要添加内容类型字段(对应于属性),请在下面图像的右上角选择“添加字段”。
在字段中可选择的类型有短文本、日期和时间、整数等多种类型。此外,甚至可以对短文本中的URL等进行验证。
在字段类型中有一种类型称为Reference(引用)。它可以将所谓的内容与内容关联起来,并且可以进行has_on,has_many的相关设置。如果不设置验证,您可以将它与任何内容类型关联起来,但如果想要限制,您可以通过下面的设置项来设置与特定内容类型的关联,就像下面的图像所示。
内容创作
接下来,我们将创建内容(条目)。它即是对应数据的记录实例。请点击下图所示的content头部。
如果您想创建新的条目,请点击右上角的”添加条目”,选择内容类型,并输入相应的数据。
输入表单如下图所示。在”Reference”和”设置”字段中,选择”Add existing content”,即可选择已创建的内容。此外,在”NEW CONTENT”下方的内容类型中,可选择创建新内容。
如果将Reference设为has_many,您可以选择多个内容。在选择内容时,可以重新排列所选内容的顺序,并且在数据检索时该顺序将被继承。
在中国,可以将状态分为草稿、更改、已发布和已归档这四种。可以设置只能在预览时查看的内容,以及要在正式发布时反映的内容等。
地域设置
Contentful能够支持多语言内容。在这里,我们将看一下默认的语言设置。请从下面的图像标题中选择设置,并点击地区设置。
点击默认语言。
请从下拉菜单中选择您喜欢的语言。默认情况下,无法更改语言设置,但您可以重新检查选项以添加其他语言。免费计划似乎支持最多两种语言。
使用GraphQL Content API进行内容获取的操作验证
我们将通过GraphQL内容API来确认是否可以获取到所创建的内容。
首先,我们要开始创建 API 密钥。
可以创建两种令牌,即内容传递/预览令牌和内容管理令牌。本次选择内容传递/预览令牌选项卡以获取数据后,点击添加API密钥。
当您创建时,将会获取到SpaceID、Content Delivery API和Content Preview API这三个选项。在Contentful官方的GraphQL文档中,CDA_TOKEN指的是Content Delivery API的访问令牌。Content Delivery API和Content Preview API的区别在于,Content Delivery API只能获取到已发布状态的内容,而在启用了预览设置时,Content Preview API可以获取到草稿状态的内容。所以在开发环境(或预览)和生产环境中需要根据需求进行选择。
使用已发行的CDA_token,尝试访问Contentful提供的GraphiQL。请将URL中的{SPACE}和{CDA_TOKEN}适当替换为实际的SPACE和CDA_TOKEN。URL为https://graphql.contentful.com/content/v1/spaces/{SPACE}/explore?access_token={CDA_TOKEN}。
请在编辑器上创建并执行查询时,同时查看GraphiQL中显示的文档。
如果想要在诸如Altair之类的Chrome扩展中进行确认,请指定GraphQL的端点为https://graphql.contentful.com/content/v1/spaces/{SPACE}/environments/{ENVIRONMENT}?access_token={CDA_TOKEN}即可进行确认。
以上述内容,Contentful上的内容创作已经完成。
支持GraphQL内容API
我们将继续进行适配,使得可以通过Next.js从Contentful的GraphQL Content API中获取数据。
我参考了以下文章的链接。
-
- https://www.apollographql.com/docs/react/get-started/
-
- https://github.com/vercel/next.js/tree/canary/examples/with-apollo
-
- https://github.com/apollographql/apollo-tooling
-
- https://qiita.com/koedamon/items/0fd3a01f7e398e54b747
-
- https://www.graphql-code-generator.com/docs/getting-started/installation
- https://qiita.com/mizchi/items/fb9f598cea94d2c8072d
ApolloClient的安装
我根据ApolloClient的官方文档进行了引入的工作。此外,在Next.js的官方仓库中,有Apollo的实现示例,因此我参考了那里的代码。
如果使用Next.js的静态生成(SSG)功能,需要参考Next.js官方存储库中与Apollo兼容的实现示例。在这个实现示例中,已经有效地利用了缓存机制。它会在getStaticProps中对获取的查询结果进行缓存,然后在每个组件中发出查询时,会使用缓存的查询结果(参考链接)。
获取模式
由于本次是使用TypeScript进行实现,所以需要对在Contentful上创建的内容类型进行类型定义。然而,如果要自己手动创建,随着内容类型的增加,工作时间也会增加,因此我们将努力实现自动生成。
尽管最理想的情况是使用graphql-codegen来轻松生成代码,但因为每次更新查询都需要访问GraphQL内容API而感到有些不方便,所以我决定使用apollo tool来下载模式。
在安装了apollo之后,创建以下文件。在.env文件中写入GraphQL Content API的URL。
module.exports = {
client: {
name: 'client',
includes: ['src/graphql/schema/**/*.{ts,tsx,graphql}'],
excludes: [
'src/graphql/queries/*.{ts,tsx,graphql}',
'src/graphql/generated/*.{ts,tsx,graphql}',
'src/graphql/graphql.schema.json',
],
addTypename: true,
service: {
name: 'contentful graphql endpoint',
url: process.env.GRAPHQL_ENDPOINT,
},
},
}
将”apollo:download”:”apollo client:download-schema src/graphql/schemas/contentful/schema.graphql”添加到package.json的scripts中。
然后我执行了以下命令来获取模式。
$ docker-compose run --rm app /bin/sh
/app # yarn run apollo:download
/app # exit
使用graphql-codegen生成类型
首先,我们将引入graphql-codegen。在初始化过程中,会有一系列问题需要回答,但只要选择相应的选项就可以了。
$ yarn add -D -E @graphql-codegen/cli
$ yarn graphql-codegen init
我已经修改了生成的codegen.yml文件如下。
overwrite: true
schema: "src/graphql/schemas/**/schema.graphql"
documents: "src/graphql/queries/*.graphql"
generates:
src/graphql/generated/graphql.tsx:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
config:
withHOC: false
withHooks: true
withComponents: false
src/graphql/graphql.schema.json:
plugins:
- "introspection"
接下来,执行 `$ yarn run generate` 命令时,将会执行在初始化时添加的脚本,并生成类型。
在生成的graphql.tsx文件中的类型Scalars中,包含了DateTime和Dimension等类型,并且都被定义为any类型。如果不允许使用any类型,可以根据说明文档参考将其更改为string或number类型。
使用Next.js创建网站
我们将使用Next.js来创建pages和components。在这里,我们将记录注意事项。
本地专用字段支持
在此次网站需求中,我们需要支持多语言(日语和英语)对应。虽然Next.js有locales功能,但它不支持SSG,因此我们需要自行处理这个问题。
ApolloClient有一种称为local-only field的功能,可以通过缓存功能来管理本地状态(Apollo官方文档)。
我在设置ApolloClient的文件中进行了如下实现。我们定义了要管理的状态(在这种情况下是多语言设置),并在InMemoryCache中当从field调用locale时参考我们在makeVar中定义的状态。
import { ApolloClient, HttpLink, InMemoryCache, makeVar } from '@apollo/client'
...
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
locale: {
read() {
return localeVar()
},
},
},
},
},
})
...
export const localeVar = makeVar('ja-JP')
然后创建一个查询/模式来参考本地状态的GraphQL,然后运行$ yarn run generate来生成类型。
query Locale {
locale @client
}
type Query {
locale: String!
}
然后,我创建了一个自定义钩子,以便能够引用locale的状态。
import { useLocaleQuery } from '@graphql/generated/graphql'
export const useLocale = (): string => {
const { data: localeData } = useLocaleQuery()
return String(localeData?.locale)
}
在每个页面上,状态的更改通过localeVar(‘en-US’)等进行。基于本次页面结构,/被设计为日语,/en/则被设计为英语,因此我们采用了这种规范。
SSG兼容
如果继续使用托管服务,并且想要使用Github Pages,则无法支持SSR或ISR,只能选择SSG。(据说如果要使用SSR或ISR,推荐使用Vercel)
如果要使用Next.js的SSG,需要在getStaticProps内编写代码来处理各种fetch的数据。我们可以参考Next.js的实现示例来创建这些代码。
因为搜索这里的详细信息会得到很多结果,所以我会省略有关SSG实施内容的详细说明。
为了将其部署到Github Pages并确认实际生成的SSG,请引入serve进行操作验证(此验证是在本地环境而不是Docker上进行的)。
我使用 `$ yarn add -D -E serve` 命令进行了安装,并在 package.json 的 scripts 中添加了以下脚本。
...
"export": "next export",
"serve": "yarn run build && yarn run export && serve ./out",
...
然后,通过运行$ yarn run serve命令,在localhost:5000上可以访问到实际静态生成的页面。
下一个版本的构建中,我们可以确认到以下类型的日志。我觉得能够确认SSG页面的好处很不错。
Page Size First Load JS
┌ ● / 525 B 214 kB
├ /_app 0 B 115 kB
├ ○ /404 3.29 kB 118 kB
├ ○ /articles 1.09 kB 131 kB
├ ● /articles/[slug] 1.16 kB 131 kB
├ ├ /articles/hoge
├ └ /articles/fuga
└ ● /en 527 B 214 kB
+ First Load JS shared by all 115 kB
├ chunks/408.7f9089.js 49.1 kB
├ chunks/commons.4d71e3.js 13.2 kB
├ chunks/framework.9c2f4b.js 42 kB
├ chunks/main.7c63f6.js 7.16 kB
├ chunks/pages/_app.712491.js 2.02 kB
├ chunks/webpack.e76463.js 1.14 kB
└ css/1d7a68075e1fa382472f.css 2.57 kB
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
GTM适配
为了进行网站测量,我们引入了Google标签管理器。
我根据这篇文章的URL引用了react-gtm-module,并在_app.tsx文件中进行了配置。
import React, { useEffect } from 'react'
import { AppProps } from 'next/app'
import { ApolloProvider } from '@apollo/client'
import { useApollo } from '@client'
import 'tailwindcss/tailwind.css'
import TagManager from 'react-gtm-module'
const MyApp: React.FC<AppProps> = ({ Component, pageProps }: AppProps) => {
const apolloClient = useApollo(pageProps)
useEffect(() => {
TagManager.initialize({ gtmId: String(process.env.GTM_ID) })
}, [])
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
)
}
export default MyApp
为了测量单页面应用程序的页面查看量,需要在Google Tag Manager上创建和应用一个名为“History Change”的触发器。您可以搜索“SPA GTM”以获取相关文章进行确认。
创建和部署Github Actions。
我根据这篇文章的URL参考,创建了一个使用Next.js构建、导出并推送到gh-pages的Github Actions。
name: deploy github page
on:
push:
branches:
- master
jobs:
deploy:
name: next build, export and deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: setup node
uses: actions/setup-node@v2
with:
node-version: '14'
- name: get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache dependencies
uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: install dependencies
run: yarn install --prefer-offline
- name: build
env:
GRAPHQL_ENDPOINT: ${{secrets.GRAPHQL_ENDPOINT}}
AUTHOR_ID: ${{secrets.AUTHOR_ID}}
GTM_ID: ${{secrets.GTM_ID}}
run: yarn run build
- name: export
run: yarn run export
- name: add nojekyll
run: touch ./out/.nojekyll
- name: deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./out
在这种情况下,可以通过转到[设置]→[机密]→[新存储库机密]来设置环境变量。
通过上述设置,当将更改推送到master分支时,SSG将被推送到gh-pages分支的文件集。
只需要一个选项,请用中文将以下内容改写:
在 GitHub Pages 的设置中,将源分支更改为 gh-pages,并将目录设置为 /(根目录),这样就可以引用 gh-pages 下的文件了。
总结
通过使用Contentful、Next.js、ApolloClient和TailwindCSS,我展示了如何创建一个个人作品集网站,并使用Github Actions将其发布到Github Pages上。
我的感觉如下所列。
-
- webpack.config.jsを書く覚悟でいたが、Next.js側でよしなにしてくれてとても助かった
Webpack5で動いてました、すごい!
Next.jsでのSSG/SSR/ISRの対応方法をだいたい把握することができた
ApolloClientのlocal-only fieldの概要を掴むことができた
状態管理がApolloClientで一括管理できるのは良い
graphql-codegenが使いやすかった
Contentfulのコンテンツタイプの定義が難しい
何も考えずに作ると無法地帯になりそう
ちゃんとモデル設計するのがベター
GraphQL Content APIから取得したスキーマからgraphql-codegenで生成した型が使いづらかった
Maybe | undefined等で自身のコードがところどころ型定義の良さを使い越せなかった印象
只供参考。