Netlify CMSのGatsbyJSテンプレートで個人ブロク作成(+TypeScript)

![Netlify.png](https://qiita-image-store.s3.amazonaws.com/0/189588/964131e6-275b-8d67-3b9d-28b5264efa1f.png)

Netlify CMSのGatsbyテンプレートをTypeScript化する時にやったことのメモです。

こう言うのをJAMstacと言うんですね。それぞれのツールは知っていても、それをまとめた概念と名前があるのは知りませんでした。

[JAMstack \| JavaScript, APIs, and Markup](https://jamstack.org/)
[JAMstack ベストプラクティス](https://jamstack.org/best-practices/)

# 登場人物

- [Netlify](https://www.netlify.com/)
  静的WebサイトホスティングサービスのSaaS
  
    - Functions
 
     実態はAWS Lambda。サーバーレスで関数を動かせる。
     ホスティングと連動し同一ドメイン上で動くので、CORS対策用のプロキシ用とかに使える。
     今回はとりあえず単独で動く所までを確認。

- [Netlify CMS](https://www.netlifycms.org/)
  オープンソースのコンテンツ管理システム。
  GitHubにプッシュすると自動でビルド・デプロイしてくれる。 
  テンプレートからGitHubリポジトリ作成・ビルド・デブロイまで出来る
  チュートリアルがあるので、今回はこれを使用。

- [GatsbyJS](https://www.gatsbyjs.org/)
  Reactで作成できる静的サイトのジェネレータ
  Markdown→GraphQL→Reactで静的ファイルを作成
  
# 動機

React+GraphQLの練習にちょうどいい。
個人ブログ程度であれば、基本的に無料枠で収まる。
環境作成からデプロイまで至れり尽くせり。
GitHubのプライベートリポジトリでも運用できる。
TypeScript化するとVSCodeで補完が効く。

# 事前に必要なもの
- GitHubアカウント
- Node.js
  自分の環境では`v8.15.1`を使用。
  他のバージョンでも大丈夫だとは思うけれど、functionsが`v8.10`で動くので
  とりあえずv8系を選択。

- VSCode
  快適に開発できるようにするため、コレに合わせて設定を変えていきます。
  拡張機能はPrettier、TSLint,ESLintとかを入れている。
  
# 手順

大まかな流れはこんな感じ。

1. テンプレートからデプロイ
2. ローカル開発環境の作成
3. パッケージ最新化
4. TypeScript化

パッケージ類はバージョン差が開くとアップデートがしんどくなるので、初期構築時に出来る限り最新化しておく。
TypeScriptの型は完璧にやろうとするとしんどいので、出来る範囲で。
ビルド時のtslintや型チェックはひとまず置いておく。
Functionsはとりあえず動く所まで確認。

## テンプレートからデプロイ

[Start with a Template \| Netlify CMS | Open-Source Content Management System](https://www.netlifycms.org/docs/start-with-a-template/)

`Gatsby Site Starter`を選択。

githubに`gatsby-starter-netlify-cms`というリポジトリが自動的に作られる。
作成時にリポジトリ名の変更も可能。
作成後に変更することも可能なので、ひとまずこのまま進める。

「Save & Deploy」を押すと、テンプレートをのビルド・デプロイが始まり、
「overview」(プロジェクトのダッシュボード)ページに自動的に移動する。

`Production deploys`の`Production: master@HEAD`にステータスが`PUBLISHED`になるまで待つ。
ここを開くと、コンソールのライブログを見ることが出来るので、ゴリゴリビルドしている様子が分かります。

ビルドが終わると、`https://*******.netlify.com/`にサンプルページ(なぜかKALDI)がデプロイされている。

## CMS adminページ
デプロイされたページには、ブラウザからコンテンツを更新する出来るページ(admin)がある。
ビルド後、githubに登録しているメールアドレスに`You have been invited`というタイトルの招待メールが来るので、`Accept the invite`にアクセスしてパスワードを登録。

CMSのページは`https://*******.netlify.com/admin`で、ページの更新やブログ記事の追加、画像の管理ができる。
更新後、「Publish」ボタンを押すと、更新した内容がgithubに自動的にプッシュされ、ビルドとデプロイが自動的に動く。

## ローカル環境の作成

githubからリポジトリをダウンロードし、ローカル環境で動かしてみる。

```sh
$ git clone git@github.com:****/gatsby-starter-netlify-cms.git
$ cd gatsby-starter-netlify-cms
$ yarn install
$ yarn start
````

- http://localhost:8000/
- http://localhost:8000/___graphql
- http://localhost:8000/.netlify/functions/hello
  サーバーサイド

とりあえず動いているように見えるが、実行時になにやらエラーが出ている。

```
ERROR in Entry module not found: Error: Can't resolve 'babel-loader' 
```

functionsのビルドが出来ていない様子。
githubにビルド済みのファイル`lambda/hello.js`があるので、起動はできるけれど、`src/lambda/hello.js`を更新しても反映されない。

なにやらyarn周りでなにか問題がありそう？
https://github.com/symfony/webpack-encore/issues/237

とりあえず、エラーが出るパッケージをインストールして対応。

```sh
$ yarn add -D \
  babel-loader \
  @babel/plugin-proposal-class-properties \
  @babel/plugin-transform-object-assign
```

ついでに、パッケージを諸々アップデート。

```sh
$ yarn upgrade
```

`package.json`のバージョンが古いと何か気持ち悪いので、`npm-upgrade`でアップデートされたパッケージのバージョンに書き換える。(これは必須ではないです。)

```
  bulma                              ^0.7.0   →     ^0.7.4 
  gatsby                            ^2.1.34   →    ^2.3.13 
  gatsby-image                      ^2.0.23   →    ^2.0.37 
  gatsby-plugin-netlify              ^2.0.6   →    ^2.0.13 
  gatsby-plugin-netlify-cms         ^3.0.15   →    ^3.0.17 
  gatsby-plugin-purgecss             ^3.1.0   →     ^3.1.1 
  gatsby-plugin-react-helmet         ^3.0.4   →    ^3.0.12 
  gatsby-plugin-sass                 ^2.0.7   →    ^2.0.11 
  gatsby-plugin-sharp               ^2.0.15   →    ^2.0.32 
  gatsby-remark-copy-linked-files    ^2.0.7   →    ^2.0.11 
  gatsby-remark-images               ^3.0.1   →    ^3.0.10 
  gatsby-remark-relative-images      ^0.2.1   →     ^0.2.2 
  gatsby-source-filesystem          ^2.0.26   →    ^2.0.28 
  gatsby-transformer-remark          ^2.3.4   →     ^2.3.8 
  gatsby-transformer-sharp           ^2.1.9   →    ^2.1.17 
  lodash                            ^4.17.5   →   ^4.17.11 
  lodash-webpack-plugin             ^0.11.4   →    ^0.11.5 
  netlify-cms                        ^2.6.1   →     ^2.9.0 
  parcel-bundler                     ^1.9.4   →    ^1.12.3 
  prop-types                        ^15.6.0   →    ^15.7.2 
  react                             ^16.8.4   →    ^16.8.6 
  react-dom                         ^16.8.4   →    ^16.8.6 
  uuid                               ^3.2.1   →     ^3.3.2 
  prettier                          ^1.15.3   →    ^1.16.4 
  rimraf                             ^2.6.2   →     ^2.6.3 
```

`yarn start`で起動出来ることを確認。
起動できたら、functionsがビルドされることを確認するため、内容を書き換えてみる。

```js:src/hello.js
export function handler(event, context, callback) {
    console.log("queryStringParameters", event.queryStringParameters)
    callback(null, {
      // return null to show no errors
      statusCode: 200, // http status code
      body: JSON.stringify({
        msg: "はろーわーるど" + Math.round(Math.random() * 10),
      }),
    })
  }
```
更新後、functionsにアクセス。
`http://localhost:8000/.netlify/functions/hello`

うまくいくと、ホットリロードされ、ソースの内容が反映されたことが確認できる。

確認できたら、githubにプッシュ。自動デプロイされるので、
`https://****.netlify.com/.netlify/functions/hello`にアクセスして、デプロイ出来ることを確認。


## TypeScript化(Gatsby編)

プラグインをインストール。

`yarn add gatsby-plugin-typescript`

Gatsbyの設定ファイルにプラグインの読み込みを追加。

```js:gatsby-config.js
  plugins: [
    `gatsby-plugin-typescript`,
    "gatsby-plugin-react-helmet",
  ・・・
```

試しにサンプルソースの一つをTS化してみる。(src/templates/tags.js)

まず、GatsbyのGraphQLノードを作成するソースのところで指定しているコンポーネントの拡張子をjs→tsx化。

```js:gatsby-node.js
      createPage({
        path: tagPath,
        component: path.resolve(`src/templates/tags.tsx`),
        context: {
          tag,
        },
      })
```

ソースの拡張子を変更。エラーが出た箇所を中心にTS化していく。
プロパティの型は、GraphQLのクエリと`gatsby-node.js`のcreatePageで指定したcontextの内容から作成する。

ついでにコンポーネントがクラスなので、FunctionComponent化する。
クラスを使う理由がなければ、ReactHooksが使えるFunctionComponentの方が便利。

```tsx:src/templates/tags.tsx
import React, { FC } from "react";
import Helmet from "react-helmet";
import { Link, graphql } from "gatsby";
import Layout from "../components/Layout";

type Props = {
  data: {
    allMarkdownRemark: {
      edges: Node[];
      totalCount: number;
    };
    site: {
      siteMetadata: {
        title: string;
      };
    };
  };
  pageContext: any;
};

type Node = {
  node: {
    fields: {
      slug: string;
    };
    frontmatter: {
      title: string;
    };
  };
};

const TagRoute: FC<Props> = props => {
  const posts = props.data.allMarkdownRemark.edges;
  const postLinks = posts.map(post => (
    <li key={post.node.fields.slug}>
      <Link to={post.node.fields.slug}>
        <h2 className="is-size-2">{post.node.frontmatter.title}</h2>
      </Link>
    </li>
  ));
  const tag = props.pageContext.tag;
  const title = props.data.site.siteMetadata.title;
  const totalCount = props.data.allMarkdownRemark.totalCount;
  const tagHeader = `${totalCount} post${
    totalCount === 1 ? "" : "s"
  } tagged with “${tag}”`;

  return (
    <Layout>
      <section className="section">
        <Helmet title={`${tag} | ${title}`} />
        <div className="container content">
          <div className="columns">
            <div
              className="column is-10 is-offset-1"
              style={{ marginBottom: "6rem" }}
            >
              <h3 className="title is-size-4 is-bold-light">{tagHeader}</h3>
              <ul className="taglist">{postLinks}</ul>
              <p>
                <Link to="/tags/">Browse all tags</Link>
              </p>
            </div>
          </div>
        </div>
      </section>
    </Layout>
  );
};

export default TagRoute;

export const tagPageQuery = graphql`
  query TagPage($tag: String) {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(
      limit: 1000
      sort: { fields: [frontmatter___date], order: DESC }
      filter: { frontmatter: { tags: { in: [$tag] } } }
    ) {
      totalCount
      edges {
        node {
          fields {
            slug
          }
          frontmatter {
            title
          }
        }
      }
    }
  }
`;
```

`react-helmet`の型定義がないと出るので型定義を追加。

```
$ yarn add -D @types/react-helmet
```

終わったら、「Ctrl+C」でサーバーを止め、`yarn start`で再起動する。
ファイル名を変えたりするとホットリロードが効かなくなるので、その都度再起動が必要。

## TypeScript化(Functions編)

`https://github.com/netlify/netlify-lambda`にTypeScript化の説明があるので、これを参考に進めていく。

まず、必要なパッケージを追加。

```sh
$ yarn add -D @babel/preset-typescript
```

`.babelrc`を追加する。配置するのは`src/lambda`の下。
プロジェクトのルートだと、Gatsbyのビルドに影響が出る。Gatsbyはプラグインで完結しているので、babelのカスタマイズは不要。

```:src/lambda/.babelrc
{
  "presets": [
    "@babel/preset-typescript",
    "@babel/preset-env"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-object-assign",
    "@babel/plugin-proposal-object-rest-spread"
  ]
}
```

次に型定義を登録する。

`yarn add -D @types/aws-lambda`

次は、ソースの拡張子変更(js→ts)と型の追加。ついでに、アロー関数の形に変更。

```ts:src/lambda/hello.ts
import { Handler, APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda";

export const handler: Handler<APIGatewayEvent, APIGatewayProxyResult> = (
  event,
  _contex,
  callback
) => {
  console.log("queryStringParameters", event.queryStringParameters);
  callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      msg: "はろーわーるど TS " + Math.round(Math.random() * 10)
    })
  });
};
```

終わったら、`yarn start`で再起動。
`http://localhost:8000/.netlify/functions/hello`にアクセスして内容が変わったことを確認。

確認できたら、githubにプッシュしてデプロイ。
TS化しても動いていることを確認。

`https://*****.netlify.com/tags/brewing/`
`https://*****.netlify.com/.netlify/functions/hello`

## TS化の続き
これで`src`下のファイルはTS化出来るようになったので、順番に書き換えていく。

他のファイルのTS化のときに引っかかった問題

### svgファイルのインポートのエラー

`src/components/Footer`をTS化すると、VSCode上で次のエラーが出ます。

```
モジュール '../img/social/twitter.svg' が見つかりません。ts(2307)
```

まず、型定義を追加。
他にも型定義が提供されていないパッケージがある場合はこのフォルダに追加していく。

```ts:src/types/index.d.ts
declare module "*.svg" {
  const value: string;
  export default value;
}
```

次に、以下を参考に、`tsconfig.json`を追加。

[gatsby/tsconfig.json at master · gatsbyjs/gatsby · GitHub](https://github.com/gatsbyjs/gatsby/blob/master/examples/using-typescript/tsconfig.json)

```json:tsconfig.json
{
  "include": ["./src/**/*"],
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "lib": ["dom", "es2017"],
    // "allowJs": true,
    // "checkJs": true,
    "jsx": "react",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "noEmit": true,
    "skipLibCheck": true,
    "noImplicitAny": false,
  }
}
```

これで、VSCode上からはエラーが消えた。
あと、プロパティとかの型定義が済んでいないところは、any型になって
エラー表示されてしまうので、`noImplicitAny`に`false`を設定。

## github リポジトリのリネームとprivate化

1. githubの管理ページからrename
2. netlifyのSettings→Build & deploy→Build settingsからリポジトリのアドレスを変更
3. netlifyのSettings→Identity→Git Gatewayからアクセストークンを新規発行
4. ローカルの作業ディレクトリで以下のコマンドを入力
`git remote set-url origin git@github.com:*****/新リポジトリ名.git`


----
とりあえず、ここまで。

ソースは以下にあります。（プライベート化はしてません）
[GitHub - otanu/gatsby-starter-netlify-cms](https://github.com/otanu/gatsby-starter-netlify-cms)