前回のあらすじ

  • Gatsbyの導入と動作確認
  • Strapiのデータ構造作成

この記事ですること

  • Gatsbyを用いたフロントエンドのデザイン
  • Strapiからのデータ取得
  • 合わせてブログにする

Gatsbyでのフロント構築

GatsbyはReactを用いた静的サイトジェネレータである。
つまり、使うには多少なりともReactの知識が要求される。
私は知識0である。
というわけで、まずは参考にしているチュートリアルからコピペしていき、それを読み解きつつ進めていく。
ただ、javascriptをtypescriptに置き換えていくため、若干の差異は生まれる。

ちなみにtypesctiptを使う際、拡張子を.tsxではなく.tsにしてしまったことで2時間ほど嵌ったので気をつけてほしい。

要はコンポーネントをhtmlタグっぽく呼べるかどうかの違いなのだが、そんなことはしらなかったよ。

https://strapi.io/blog/build-a-static-blog-with-gatsby-and-strapi

ここを参考に以下のファイルを作っていく。
ソースを全部貼ると長すぎるのでGithubリンクだけ置いておく。

コピペして終わりというのも後から読んだら意味不明なので、ソースの一部を抜粋して解説をする。
ただ、今Strapi上で書いていたデータが飛んだんで雑な解説になる。

ソース内で使うライブラリも入れておく。
markdownをパースするのと日時をいい感じに表示してくれるのだ。

yarn add react-markdown react-moment moment

ソース解説

src/pages/index.js

const IndexPage = () => (
    <Layout>
        <StaticQuery
            query={graphql`
        query {
          allStrapiArticle {
            edges {
              node {
                strapiId
                title
                category {
                  name
                }
                image {
                  publicURL
                }
              }
            }
          }
        }
      `}
            render={data => (
                <div className="uk-section">
                    <div className="uk-container uk-container-large">
                        <h1>Dreamer</h1>
                        <ArticlesComponent articles={data.allStrapiArticle.edges} />
                    </div>
                </div>
            )}
        />
    </Layout>
)

まずはTOPページ。
Layoutというタグで囲まれているのが気になる。
これはlayout.tsxのLayoutという要素の引数になると考えていいと思う。
そっちでなにをするかは後で説明する。
次にStaticQueryとは何なのか。
中の要素を見ればStrapiで作った構造の一部が見える。
このような書き方でStrapiからデータを読み込むことができる。
今回は全ての記事からstrapiIdという内部的に持っているID、タイトル、カテゴリ、サムネイル用のイメージを取得している。
contentという記事の内容部分に関しては記事を一覧表示する目的なので今回は不要のため取得していない。
決まった書き方で必要なデータだけを取得できるのがGraphQLとなる。

というわけでフロントの構造とバックエンドからのデータ取得の仕組みは多少理解できたと思う。
GraphQLのクエリに関しては、 http://localhost:8000/__graphqlhttp://localhost:1337/graphql を見ると幸せになれる。

後は取得したデータをArticlesComponentというものに渡して表示してもらう。

つまりこのファイルでやっていることは、

  • Strapiから記事一覧を取得する
  • 他に渡して表示を依頼する
  • ブログタイトルを表示する

これだけだ。かんたん。

src/components/layout.tsx

const Layout = ({ children }: IProps) => {
  return (
    <>
      <Seo title="" />
      <Nav />
      <main>{children}</main>
    </>
  )
}

次にindexを囲んでいたLayoutだ。
ここのchildrenに先程のindexの中身が入ってくる。
それを上にSeoとNavをつけてmainで囲んで表示してくれる。
要はLayoutで囲めば共通処理してくれますよという感じだ。

SeoはSEO対策、Navはヘッダーの表示をしてくれている。
内部には解説するほどのコードはないので説明はなし。

src/components/articles.tsx

const Articles = ({ articles }: IProps) => {
    const leftArticlesCount = Math.ceil(articles.length / 5)
    const leftArticles = articles.slice(0, leftArticlesCount)
    const rightArticles = articles.slice(leftArticlesCount, articles.length)


    return (
        <div>
            <div className="uk-child-width-1-2" data-uk-grid>
                <div>
                    {leftArticles.map((article: StrapiArticleEdge, i: Number) => {
                        return (
                            <Card article={article} key={"article__${article.node.id}"} />
                        )
                    })}
                </div>
                <div>
                    <div className="uk-child-width-1-2@m uk-grid-match" data-uk-grid>
                        {rightArticles.map((article: StrapiArticleEdge, i: Number) => {
                            return (
                                <Card article={article} key={"article__${article.node.id}"} />
                            )
                        })}
                    </div>
                </div>
            </div>
        </div>
    )
}

divだらけである。
まず左側に大きく表示する記事と右側の記事に分ける。
その後、それぞれの記事をCardというのに投げている。
一覧ページの大枠を作って細かい表示はCardにおまかせという形だ。

src/components/card.tsx

const Card = ({ article }:IProps) => {
    return (
        <Link to={`/articles/${article.node.strapiId}`} className="uk-link-reset">
            <div className="uk-card uk-card-muted">
                <div className="uk-card-media-top">
                    <img
                        src={article.node.image ? article.node.image.publicURL ? article.node.image.publicURL : "" : ""}
                        alt={article.node.image ? article.node.image.publicURL ? article.node.image.publicURL : "" : ""}
                        height="100"
                    />
                </div>
                <div className="uk-card-body">
                    <p id="category" className="uk-text-uppercase">
                        {article.node.category ? article.node.category.name : ""}
                    </p>
                    <p id="title" className="uk-text-large">
                        {article.node.title}
                    </p>
                </div>
            </div>
        </Link>
    )
}

じゃあCardはというと、ここでやっとindexで取得した要素の中身が活躍する。
画像とカテゴリがあるなら表示して、タイトルを大きく見せる。
そしてArticleへの個別リンクを全体につける。

src/templates/article.tsx

export const query = graphql`
  query ArticleQuery($id: String!) {
    strapiArticle(strapiId: { eq: $id }) {
      strapiId
      title
      content
      published_at
      image {
        publicURL
      }
    }
  }
`

const Article = ({ data }:IProps) => {
  const article = data.strapiArticle
  return (
    <Layout>
      <div>
        <div
          id="banner"
          className="uk-height-medium uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light uk-padding uk-margin"
          data-src={article.image ? article.image.publicURL : ""}
          data-srcset={article.image ? article.image.publicURL : ""}
          data-uk-img
        >
          <h1>{article.title}</h1>
        </div>

        <div className="uk-section">
          <div className="uk-container uk-container-small">
            <ReactMarkdown source={article.content ? article.content : ""} />
            <p>
              <Moment format="MMM Do YYYY">{article.published_at}</Moment>
            </p>
          </div>
        </div>
      </div>
    </Layout>
  )
}

となると個別ページの表示が気になってくる。
ここでIDを指定して記事を一つ取得している。
今度はちゃんとcontentやpublished_atも使用するので含まれている。
そしてサムネイルを上部に表示して、下にcontentのmarkdownをReactMarkdownを使ってHTML化したものが表示されることになる。
これもLayoutで囲っているので最終的にはNavやSeoと一緒に出力されることになる。

さてここで疑問なのが、IDはどこから来たのかだ。
その答えがgatsby-node.jsにある。

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const result = await graphql(
    `
        {
          articles: allStrapiArticle {
            edges {
              node {
                strapiId
              }
            }
          }
          categories: allStrapiCategory {
            edges {
              node {
                strapiId
              }
            }
          }
        }
      `
  )

  if (result.errors) {
    throw result.errors
  }

  // Create blog articles pages.
  const articles = result.data.articles.edges
  const categories = result.data.categories.edges

  articles.forEach((article, index) => {
    createPage({
      path: `/articles/${article.node.strapiId}`,
      component: require.resolve("./src/templates/article.tsx"),
      context: {
        id: article.node.strapiId,
      },
    })
  })

  categories.forEach((category, index) => {
    createPage({
      path: `/categories/${category.node.strapiId}`,
      component: require.resolve("./src/templates/category.tsx"),
      context: {
        id: category.node.strapiId,
      },
    })
  })
}

ここは大切なので抜粋なしで貼る。
最初に書いているのが、ページ作成時にやることだ。
Gatsbyは静的サイトを作る。その書き出しの指示を行っている部分となる。
まず、articleとcategoryのIDのみを全て取得している。
その後、そのIDをキーにcreatePageで個別のページを作成するループを回している。
ここで上で疑問点に挙げていたIDの供給元が分かるはずだ。
ついでにリンク先のパスとどのテンプレを使うかも指定しているのが読み取れる。
このようにして、外部のデータからページを作成して出力している。

以上でソースの解説は終了。
なんとなく動きの流れがつかめていたら嬉しい。

ブログの起動

さて、ローカル環境が全て整ったはずだ。
http://localhost:1337にアクセスできることを確認してから、gatsby developを実行しよう。
http://localhost:8000にアクセスすれば、ブログのトップページといえなくもない画面が表示されるはずだ。

ここまでで参考サイトのチュートリアルが終わっているように一旦区切りとなる。
この後は使っているプラグインとデザインを自分好みに変える過程を紹介する。
人によって大幅に変わってくる部分なので私がしたことをサラッと説明していく。
もし詳しく知りたいならこのブログを隅まで触ってから、Githubのリンクからソースを読んでくれ。
https://github.com/Tim0401/dreamer-gatsby/

その後は遂にブログを全世界に公開することになる。
今のブログのデザインに満足しているならデプロイの項まで読み飛ばそう。

次の記事: Gatsby+Strapiでブログを構築した話(5) Strapiプラグイン紹介+トラブルシューティング