Headless CMS(WordPress)とGatsby.jsでサイト制作する際の備忘録

静的サイトジェネレーター「Gatsby.js」について知りたかったのでUdemyのコースで学び、その備忘録となります。
以下の内容となっています。

- 「[Gatsby JS: Build static sites with React Wordpress & GraphQL](https://www.udemy.com/course/gatsby-js-react-wordpress-graphql/)」を参考
- PluginはGatsby.jsの「[公式サイト](https://www.gatsbyjs.org)」を参考に設定
- Headless CMS(WordPress)で投稿したdataをGraghQLで取得してGatsby(React)でサイト表示
- 投稿、固定ページ、カスタム投稿ページ、カスタムフィールドに対応
- Contact Formについは未実装
- GraphQLのqueryについては別の記事でまとめています。「[Gatsby.jsでWordPressのAPIをGraphQLで取得しるときによく使うquery](https://qiita.com/macotok/items/8d67b620136cab61e717)」
- 静的Webホスティングサービスの「Netlify」でサイト公開する手順は別の記事でまとめています。[HeadlessCMS(WordPress)とGatsby.jsで制作したサイトをNetlifyで公開する手順](https://qiita.com/macotok/items/a885df944c7a01b0fde3)

## Set Up

### 環境

- MAMPでlocal環境構築
- localのphpmyadminでDB接続

### API

WordPressが提供しているREST APIからdataを取得するのではなくGraphQLでdata取得

REST API例
http://localhost:8888/myawesomeportfolio.io/wp-json/wp/v2/pages

### gatsbyをinstall

````terminal
$ npm install -g gatsby-cli
````

### WordPressのREST APIをGatsbyにデータをpullさせるプラグインを設定

- npm「gatsby-source-wordpress」をinstall
- `gatsby-config.js`にplugin設定。[公式サイト](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-source-wordpress
)を参考
- `baseUrl`でMAMPで設定したWordpressのURLを指定

### WordPressのdataをNode.jsで読み込みRouting処理

- `CreatePage`を設定
    - component(適用したいcomponentのpathを指定)
    - context(componentのpropsで受け取るvalue)
    - path(Routing処理)

```javascript:gatsby-node.js
const _ = require(`lodash`)
const Promise = require(`bluebird`)
const path = require(`path`)
const slash = require(`slash`)

exports.createPages = ({ graphql, actions }) => {
  const { createPage, createRedirect } = actions;
  return new Promise((resolve, reject) => {
    // ==== 固定ページ ====
    graphql(
      `
        {
          allWordpressPage {
            edges {
              node {
                id
                slug
                status
                template
                title
                content
                template
              }
            }
          }
        }
      `
    )
      .then(result => {
        if (result.errors) {
          console.log(result.errors)
          reject(result.errors)
        }

        // Create Page pages.
        const pageTemplate = path.resolve("./src/templates/page.js")
        _.each(result.data.allWordpressPage.edges, edge => {
          createPage({
            path: `/${edge.node.slug}/`,
            component: slash(pageTemplate),
            context: edge.node,
          })
        })
      })
      // ==== END 固定ページ ====

      // ==== 投稿ページ ====
      .then(() => {
        graphql(
          `
            {
              allWordpressPost {
                edges{
                  node{
                    id
                    title
                    slug
                    excerpt
                    content
                  }
                }
              }
            }
          `
        ).then(result => {
          if (result.errors) {
            console.log(result.errors)
            reject(result.errors)
          }
          const postTemplate = path.resolve("./src/templates/post.js")
          _.each(result.data.allWordpressPost.edges, edge => {
            createPage({
              path: `/post/${edge.node.slug}/`,
              component: slash(postTemplate),
              context: edge.node,
            })
          })
          resolve()
        })
      })
    // ==== END 投稿ページ ====
  })
}
```

- `context: edge.node`と指定するとpropsにdataが入る

```javascript:src/templates/page.js
import React from 'react';

export default ({ pageContext }) => (
  <div>
    <h1>
      {pageContext.title}
    </h1>
  </div>
)
```

- pageでredirect処理
- root pathで`/home`にredirect

```javascript
const { createRedirect } = actions;
createRedirect({ fromPath: '/', toPath: '/home', redirectInBrowser: true, isPermanent: true })
```

## 制作

### ツール

- ダミーテキスト生成用の[faker](https://hipsum.co/)を使用
- メニューのAPIを生成のWordPressプラグイン「WP REST API Menus」をでinstall 

### GraphQLで取得したdataにLinkを設定

- GatsbyのLink componentを使用

```javascript
import { graphql, StaticQuery, Link } from 'gatsby';

<StaticQuery query={graphql`
  {
    allWordpressWpApiMenusMenusItems{
      edges{
        node{
          items{
            title
            object_slug
          }
        }
      }
    }
  }
  `} render={props => (
  <div>
    {props.allWordpressWpApiMenusMenusItems.edges[0].node.items.map(item => (
      <Link to={`/${item.object_slug}`} key={item.title}>
        {item.title}
      </Link>
    ))}
  </div>
)} />
```

### styled-componentsを使用

```terminal
$ yarn add gatsby-plugin-styled-components styled-components babel-plugin-styled-components
```

- `gatsby-config.js`のpluginに設定

```javascript:gatsby-config.js
plugins: [
    `gatsby-plugin-styled-components`,
],
```

- globalにstyleを設定する場合は`createGlobalStyle`を使用

```javascript
import styled, { createGlobalStyle } from 'styled-components';

const GlobalStyles = createGlobalStyle`
  @import url('https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i&display=swap');
  body, html {
    font-family: 'Open Sans', san-serif;
    margin: 0 !important;
    padding: 0 !important;
  }
`;

const Layout = ({ children }) => (
 <div>
   <GlobalStyles />
　 {children}
 </div>
);
```

- Linkにstyled-componentsを使用する場合は`styled(Link)`を指定

```javascript
import { graphql, StaticQuery, Link } from 'gatsby';
import styled from 'styled-components';

const ManuItem = styled(Link)`
  color: white;
  display: block;
  padding: 8px 16px;
`;

const MainMenu = () => (
  <StaticQuery query={graphql`
    {
      allWordpressWpApiMenusMenusItems(filter: {
        name: {
          eq: "Main menu"
        }
      }){
        edges{
          node{
            items{
              title
              object_slug
            }
          }
        }
      }
    }
  `} render={props => (
    <MainMenuWrapper>
      {props.allWordpressWpApiMenusMenusItems.edges[0].node.items.map(item => (
        <ManuItem to={`/${item.object_slug}`} key={item.title}>
          {item.title}
        </ManuItem>
      ))}
    </MainMenuWrapper>
  )} />
);
```

### カスタム投稿タイプ「portfolio」を追加

- `gatsby-config.js`の`gatsby-source-wordpress`のoptionsに追加
- `gatby-node.js`に「portfolio」のtemplateを追加

```javascript:gatsby-config.js
includedRoutes: [
  "**/categories",
  "**/posts",
  "**/pages",
  "**/media",
  "**/tags",
  "**/taxonomies",
  "**/users",
  "**/menus",
  "**/portfolio",
]
```

```javascript:gatby-node.js
// ==== カスタム投稿タイプ(portfolio) ====
.then(() => {
  graphql(
    `
      {
        allWordpressWpPortfolio{
          edges{
            node{
              id
              title
              slug
              excerpt
              content
              featured_media{
                source_url
              }
            }
          }
        }
      }
    `
  ).then(result => {
    if (result.errors) {
      console.log(result.errors)
      reject(result.errors)
    }
    const portfolioTemplate = path.resolve("./src/templates/portfolio.js")
    _.each(result.data.allWordpressWpPortfolio.edges, edge => {
      createPage({
        path: `/portfolio/${edge.node.slug}/`,
        component: slash(portfolioTemplate),
        context: edge.node,
      })
    })
    resolve()
  })
})
// ==== カスタム投稿タイプ(portfolio) ====
```

### templateの選択によってレイアウトを変える場合

- WordPressの固定ページにTemplate選択機能を追加、投稿ページに適用
- `gatsby-node.js`にtemplateの種類を追加、その分岐処理

```php:wp-content/themes/wp-gatsby-js-theme-starter-master/portfolio_under_content.php
<?php /* Template name: Portfolio items below content */ ?>
```

```javascript:gatsby-node.js
const pageTemplate = path.resolve("./src/templates/page.js")
const portfolioUnderContentTemplate = path.resolve("./src/templates/portfolioUnderContent.js")
_.each(result.data.allWordpressPage.edges, edge => {
  createPage({
    path: `/${edge.node.slug}/`,
    component: slash(edge.node.template === 'portfolio_under_content.php' ? portfolioUnderContentTemplate : pageTemplate),
    context: edge.node,
  })
})
```

### 「Advanced Custom Fields」を扱う

- WordPressの「Advanced Custom Fields」のAPIを取得プラグインで「ACF to REST API」をinstall
- `gatsby-node.js`でGraphQLで取得するdataに`acf`を追加

```javascript:gatsby-node.js
graphql(
  `
    {
      allWordpressWpPortfolio{
        edges{
          node{
            id
            title
            slug
            excerpt
            content
            featured_media{
              source_url
            }
            acf{
              portfolio_url
            }
          }
        }
      }
    }
  `
)
```

### 投稿一覧画面にページネーション実装

- `gatsby-node.js`でGraphQLで展開したdataを元に`CreatePage`を設定

```javascript:gatsby-node.js
const posts = result.data.allWordpressPost.edges;
const postsPerPage = 2;
const numberOfPages = Math.ceil(posts.length / postsPerPage);
const blogPostListTemplate = path.resolve('./src/templates/blogPostList.js');

Array.from({ length: numberOfPages }).forEach((page, index) => {
  createPage({
    component: slash(blogPostListTemplate),
    path: index === 0 ? '/blog' : `/blog/${index + 1}`,
    context: {
      posts: posts.slice(index * postsPerPage, (index * postsPerPage) + postsPerPage),
      numberOfPages,
      currentPage: index + 1
    },
  });
});
```

- `CreatePage`のdataをtemplatesで画面制作
- `pageContext.posts`はcontextで設定した`posts`で、GraphQLから取得したdata
- `Array.from({ length: pageContext.numberOfPages})`でページャの番号を生成

```javascript:src/templates/blogPostList.js
const blogPostList = ({ pageContext }) => (
  <Layout>
    {
      pageContext.posts.map(post => (
        <div key={post.node.wordpress_id}>
          <h3 dangerouslySetInnerHTML={{ __html: post.node.title }} />
          <p dangerouslySetInnerHTML={{ __html: post.node.content }} />
        </div>
      ))
    }
    {
      Array.from({ length: pageContext.numberOfPages}).map((page, index) => (
        <div key={index}>
          <Link to={index === 0 ? '/blog' : `/blog/${index + 1}`}>
            {index + 1}
          </Link>
        </div>
      ))
    }
  </Layout>
);

export default blogPostList;
```

### 一覧画面のページネーションでCurrentPageかを判定

- style-componentsでpropsを受け取って判定
- `pageContext.currentPage`は`gatsby-node.js`のcontextで設定した値


```javascript
import styled from 'styled-components';

const PageNumberWrapper = styled.div`
  border: 1px solid #eee;
  background-color: ${props => props.isCurrentPage ? '#eee' : 'white'};
`;

<PageNumberWrapper key={index} isCurrentPage={index + 1 === pageContext.currentPage}>
```

### GraphQLで日付フォーマットを指定

- `formatString`で指定

```
{
  allWordpressPost{
    edges{
      node{
        excerpt
        date(formatString: "YYYY/MM/DD hh:mm")
      }
    }
  }
}
```

## その他

### WordPressにカスタム投稿タイプ「portfolio」を追加

- WordPressのthemeで管理しているfunctions.phpに記述

```php:functions.php
<?php
add_theme_support( 'custom-logo' );
add_theme_support( 'menus' );
add_theme_support('post-thumbnails');

function create_custom_portfolio_post_type() {
	register_post_type('portfolio', 
    array(
      'labels' => array(
        'name' => __('portfolio'),
    	  'singular_name' => __('portfolio')
      ),
      'public' => true,
      'show_in_admin_bar' => true,
      'show_in_rest' => true,
    ));
	add_post_type_support('portfolio', array('thumbnail', 'excerpt'));
}

add_action('init', 'create_custom_portfolio_post_type');
```