将使用Contentful、Next.js和Apollo创建的个人作品集发布到Github Pages时的备忘录

为了获得关于Headless CMS和Next.js的服务器端渲染(SSR)/ 静态生成(SSG)的经验,我使用了Contentful的GraphQL内容API来获取数据,并通过Next.js来创建了一个网站。

由于我们整理了一系列的操作记录,所以如果你希望今后创建类似的网站,可以参考这些记录。

您可以在我创建的Github Pages上查看部署好的内容。

スクリーンショット 2021-05-04 23.18.20.png

此外,请参考以下源代码。

任务进行的步骤

实际操作的流程如下:

    1. 作业流程和内容的调研与确认

 

    1. 环境搭建

引入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帐户进行注册。

スクリーンショット 2021-04-29 20.32.53.png

根据指示,进行输入和确认规程,并创建一个账户。这次我们将介绍免费计划。

然后,输入组织名称并创建空间。如果是个人创建,则建议设置组织名称以便明确为个人,并将空间名称设为使用的网站名称等,以便于理解。

我选择了机构为Adacchi3,空间为adacchi3 portfolio。

创建内容模型

在创建内容时,我们会创建内容模型,这类似于创建DB模式。请点击下图中对应标题部分的内容模型。

スクリーンショット 2021-04-30 8.19.39.png

创建新的内容模型时,请点击右上方的“Add content type”按钮,如下图所示。

スクリーンショット 2021-04-30 8.20.45.png

内容类型的名称对应于类似于“Post”或“User”的类的名称。在下面的图像中,它是“Achievement”。如果要添加内容类型字段(对应于属性),请在下面图像的右上角选择“添加字段”。

在字段中可选择的类型有短文本、日期和时间、整数等多种类型。此外,甚至可以对短文本中的URL等进行验证。

スクリーンショット 2021-04-30 8.22.47.png

在字段类型中有一种类型称为Reference(引用)。它可以将所谓的内容与内容关联起来,并且可以进行has_on,has_many的相关设置。如果不设置验证,您可以将它与任何内容类型关联起来,但如果想要限制,您可以通过下面的设置项来设置与特定内容类型的关联,就像下面的图像所示。

スクリーンショット 2021-04-30 8.23.30.png

内容创作

接下来,我们将创建内容(条目)。它即是对应数据的记录实例。请点击下图所示的content头部。

スクリーンショット 2021-04-30 8.24.15.png

如果您想创建新的条目,请点击右上角的”添加条目”,选择内容类型,并输入相应的数据。

スクリーンショット 2021-04-30 8.25.17.png

输入表单如下图所示。在”Reference”和”设置”字段中,选择”Add existing content”,即可选择已创建的内容。此外,在”NEW CONTENT”下方的内容类型中,可选择创建新内容。

如果将Reference设为has_many,您可以选择多个内容。在选择内容时,可以重新排列所选内容的顺序,并且在数据检索时该顺序将被继承。

スクリーンショット 2021-04-30 8.28.18.png

在中国,可以将状态分为草稿、更改、已发布和已归档这四种。可以设置只能在预览时查看的内容,以及要在正式发布时反映的内容等。

地域设置

Contentful能够支持多语言内容。在这里,我们将看一下默认的语言设置。请从下面的图像标题中选择设置,并点击地区设置。

スクリーンショット 2021-04-30 9.39.57.png

点击默认语言。

スクリーンショット 2021-04-30 9.40.51.png

请从下拉菜单中选择您喜欢的语言。默认情况下,无法更改语言设置,但您可以重新检查选项以添加其他语言。免费计划似乎支持最多两种语言。

スクリーンショット 2021-04-30 9.41.42.png

使用GraphQL Content API进行内容获取的操作验证

我们将通过GraphQL内容API来确认是否可以获取到所创建的内容。

首先,我们要开始创建 API 密钥。

スクリーンショット 2021-04-30 8.29.03.png

可以创建两种令牌,即内容传递/预览令牌和内容管理令牌。本次选择内容传递/预览令牌选项卡以获取数据后,点击添加API密钥。

スクリーンショット 2021-04-30 8.29.45.png

当您创建时,将会获取到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可以获取到草稿状态的内容。所以在开发环境(或预览)和生产环境中需要根据需求进行选择。

スクリーンショット 2021-04-30 8.30.21.png

使用已发行的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中显示的文档。

スクリーンショット 2021-04-30 8.48.53.png

如果想要在诸如Altair之类的Chrome扩展中进行确认,请指定GraphQL的端点为https://graphql.contentful.com/content/v1/spaces/{SPACE}/environments/{ENVIRONMENT}?access_token={CDA_TOKEN}即可进行确认。

スクリーンショット 2021-04-30 8.46.27.png

以上述内容,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

在这种情况下,可以通过转到[设置]→[机密]→[新存储库机密]来设置环境变量。

スクリーンショット 2021-05-05 19.07.55.png

通过上述设置,当将更改推送到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等で自身のコードがところどころ型定義の良さを使い越せなかった印象

只供参考。

广告
将在 10 秒后关闭
bannerAds