「Gatsby x TypeScript」でブログを作りました (1. コンポーネント・画面の作り方のコツ)

Gatsbyでブログを作った人の最初の記事と言えば、だいたい「Gatsbyでブログ作りましたー」でしょう。 なので許してください。

「Gatsby x TypeScript」でブログを作る時に役に立った情報などを自分用のメモというか棚卸しというかまとめていきたいと思います。

このブログ一見シンプルに見えて、ワードプレスで作られたブログと引けを取らないくらい実はいろんな機能が盛り込まれてます。

  • Disqusによるブログコメント投稿機能
  • Algoliaによる全文検索機能
  • Mailgunによる自動返信メール(お問い合わせ時)
  • Google reCAPTCHA v3 (サーバーサイドバリデーションもあり)
  • Google Analytics
  • Iframelyによる記事へのiframeの埋め込み機能
  • JSONLDを使った構造化マークアップ(SEO対策)

全4回に分けて、gatsbyのプラグインで一瞬で実装が終わったものもあればそうでないものもあるので実装する上で参考になったサイトと要点を軽くまとめなながら紹介できたらいいかと思います。

「Gatsbyでのブログ作成手順」は他のブログにあるので手順に関しては紹介しません。

この投稿では 1.コンポーネント・画面の作り方のコツ について書こうと思います。

ブログの雛形を作る

Gatsbyでブログを作る時、なるべくシンプルな雛形から始めたがいいかなとアドバイスしておきます。 Gatsby Starterで紹介されているstarterに見た目がかなり良さそうな奴がいっぱいありますが、仮にそれを使ってブログをつくり始めたとしても

  • なんかコンコンポーネントの実装気に食わない
  • 「CSS in JS」にはしたくないんだよなぁ (styled-componentsで不具合があってもコード読めるレベルじゃないし対応できない)
  • styleプロパティーで基本デザイン整えるのやめてー

とか色々不満が出てきて、結局真っさらにしてやり直したくなるのであまりお勧めできません。
雰囲気を掴む程度に動かす材料としては非常に魅力的です。ぜひ動かして勉強してください。

ちなみに私のブログのスターターはこれです。

takefumi-yoshii/gatsby-starter-typescript

  • ちょうどQiitaの記事でどういうスターターかなども説明されていてわかりやすかった事
  • とりあえず動かしてみてすぐ動いた事
  • ページ生成のコードをgatsby-config.jsに全てベタがきしていく形ではなくgatsby-configディレクトリーを切ってやられていた事
  • コード全体をすぐに見通せた事

などが決め手です。

トップページを作る

ブログの雛形が決まったら、トップページをまずは作ったがいいです。いや、正確にはトップページに配置するコンポーネントを作ったがいいかなって思います。 絵で下書きをする感じで雑に作って配置して雰囲気を感じ取れるようにします。デザインが苦手ならこの段階ではレスポンシブルデザインにしないほうがいいでしょう。 私もデザインが苦手なので、最後の仕上げとしてレスポンシブルデザインにしました。

このようにサイドメニュー付きブログをつくりたいと思っている場合、安易にMaterialUIとかを使ってグリッドの構成をしないほうがいいと思います。 自分も最初は「MaterialUIでシュッと作ってやる」みたいな感じで始めたのですが、

  • カスタマイズしたいと思った時に柔軟なカスタマイズができない
  • 使い始めたものの気づいたらあんまりパーツを使ってない
  • MaterialUI臭が消えない

など最終的には消したくなって消すのに工数がかかってしまうだけです。 MaterialUIなんか使わなくてもflexbox使って flex-grow で調整すればグリッドデザインぽっくなります。

自分は最終的にMaterialUIを消しました。

「全てのパーツをMaterialUIで作るんだー」ぐらいの気合がないとあまりお勧めしません。

コンポーネントを作る

コンポーネントのデザインを作る時に「わかりやすくていいかも」みたいなコツを見つけたので紹介します。

  • 全てのコンポネントをimportしてexportするためのindex.tsxを作る
  • コンポーネント名((例)BlogPost)のディレクトリを作成する

    • その中にindex.tsxを作る
    • その中にコンポーネント名(BlogPost).module.scssを作る

アトミックデザインを使ってデザインしている場合はこんな感じです。

src/components/organisms
├── BlogPost
│   ├── BlogPost.module.scss
│   └── index.tsx
├── index.tsx
src/components/organisms/BlogPost/BlogPost.module.scss
.root {
}
src/components/organisms/index.tsx
import BlogPost from './BlogPost'
export {
  BlogPost
}
src/components/organisms/BlogPost/index.tsx
import React from 'react'
import styles from './BlogPost.module.scss'

type Props = {
  className?: string
}

const BlogPost: React.FC<Props> = ({ className }) => {
  const rootClassName = ClassNames({
    [styles.root]: true,
    [className!]: className
  })

  return (
    <div className={rootClassName}>
      // something
    </div>
  )
}

export default BlogPost

ポイントは何かというと、二個あって、

  • ビルド結果のモジュールされたcss名がわかりやすい。
  • コンポーネントのCSSに利用する側からクラスを指定できるようにしている点

です。

cssがどのようにわかりやすいかというとこのブログのcssを見ればわかりますが、この投稿画面のrootcssは 以下のような感じです。

blog-post-css-name blog-post-css-detail

ちゃんと、内部rootクラスと外部rootクラスが設定されている事がわかるでしょう。

  • 内部rootクラス: BlogPost-module--root--3kV_U
  • 外部rootクラス: post-module--blog_post--1WVB0

cssの構造にも注目してください。外部rootクラスでは利用する側から決めたいcssだけ適用しています。 利用する側から決めたいCSSって何かというと

  • border-shadow
  • margin
  • font-size
  • color

などです。これを利用する側から決めれるようにする事でコンポーネントの再利用性を高めてくれます。

利用側のrootなcssとコンポーネント側のrootなcssを結合するためにJedWatson/classnamesを使ってます。

直感的にわかると思うのですが、valueがtrueなら連結される感じです。

const rootClassName = ClassNames({
  'innerRoot': true,
  [className!]: true 
})

とかくと以下は等価だと思います。

<div className={rootClassName} />
<div classname={`innerRoot ${className}`}/>

このライブラリーがないとこんな感じに書くことになるでしょう。

<div className={ className ? 'innerRoot'  : `innerRoot ${className}` } />

いつもこのパターンでどんなデザインもカバーできる訳ではないですがだいたいカバーできるんじゃないかと考えてます。

※実際はモジュール化されるのでこのような綺麗な名前にはなりません。
※このブログのcssをみたらわかると思います。

コンポーネントを作る (応用)

この考え方を利用してコンポーネントを作ると画面幅でデザイン(いや見た目)を変えたい時とかに綺麗にコードを書くことができます。

例えばこのブログの画面幅で見た目が変わる前のページ・次のページ のコンポーネント(PrevNext)

画面幅が広い時

prev-next-lg.png

画面幅が狭い時

prev-next-sm.png

は以下のような構成でできています。肝心なところだけ書くと

src/components/molecules/PrevNext/index.tsx
import stlyes_lg from './PrevNextLarge.module.scss'
import styles_sm from './PrevNextSmall.module.scss'
import { useHandleResponsible } from 'src/components/hooks'

type Props = {
  className?: string
}

type MakeProps = Props & {
  styles: any
}

const MakePrevNext: React.FC<MakeProps> = ({ styles, className }) => {
  const rootClassName = ClassNames({
    [styles.root]: true,
    [className!]: className
  })

  return (
    <div className={rootClassName}>
      # something
    </div>
  )
}

const PrevNext = React.FC<Props> = ({ className }) => {
  // 画面幅が650px以下だとchangeDesignはtrue
  const { changeDesign } = useHandleResponsible({ breakPoint: 650 })

  if(changeDesign) {
    return <MakePrevNext {...{ styles: styles_sm, className }} />
  } else {
    return <MakePrevNext {...{ styles: styles_lg, className }} />
  }
}

export default PrevNext

このように書くことで

  • 650px以上ではstyles_lgのデザイン
  • 650px以下ではstyles_smのデザイン

が適用されて画面幅でデザインを変えることができます。

メリット

  • みやすい。わかりやすい。画面幅の違いで適用されるcssが独立しているので管理しやすい。

デメリット

  • @media を使って解決するよりレンダリングコストがかかってるかもしれない。(わからない)

所感としてjQuery時代にjQueryまで使って見た目の操作をしようと思う場合にこのパターンが適用できるのかもしれません。 まぁ色んな書き方を知っていた方がいざという時やくに立つかもしれないので紹介してみました。

<></>はReact.fragmentの省略形です。divで囲む必要がなくなるのでお勧めです。

投稿画面を作る

投稿画面はQiitaっぽい見た目がきっとユーザーが見慣れていて親しみやすいだろうなという事でそのようにしました。

投稿画面にはみんなだいたいTypography.jsを使って読みやすさいデザインになるように自動で調整するようだったので自分もそのようにしました。一瞬で読みやすいデザインにしてくれるTypography.jsって凄いですね。

blog-post.png

Qiitaみたいに左サイドに目次を置いてスクロールさせる機能には本当に苦労しましたが、幸いにしてTakumonさんのブログを見つけて記事が丁寧にまとまっていたので実装をお借りしてなんとか実装できた感じです。(ついでに右のSNSシェアボタンの感じもよかったので真似しました)

Takumonさんのブログで紹介されていたコードではReact.Component Classが使われていたのでhooksで書き直して自分のものにしました。非常に勉強になり感謝ですね。

実装の詳細を語ると長くなるのでTakumonさんのブログでみたがいいでしょう。

この投稿画面の構成に関して少し紹介しておきます。

こんな感じになってます。

import styles from './post.module.scss'

<PostLayout
  seo={<SEO {...} />}
  snsShare={<SNSShare {....} />}
  toc={<PostToc {...} />}
>
  <div className={styles.root}>
    <BlogPost className={styles.blog_post} />
    <PersonWhoWrote className={styles.person_who_wrote} />
    <RelatedPostCardList className={styles.related_post} />
    <PrevNext className={styles.prev_next} />
  </div>
</PostLayout>

コンポーネントに設定した利用側から設定できるクラスを設定してページに合うような配置だったりデザインになるようにしてます。

どうしてこうじゃないと思いますか?

src/templates/post/index.tsx
import styles from './post.module.scss'

<PostLayout
  seo={<SEO {...} />}
  snsShare={<SNSShare {....} />}
  toc={<PostToc {...} />}
>
  <div className={styles.root}>
    <div className={styles.blog_post}>
      <BlogPost {...} />
    </div>
    <div className={styles.person_who_worte}>
      <PersonWhoWrote {...} />
    </div>
    <div className={styles.related_post}>
      <RelatedPostCardList {...} />
    </div>
    <div className={styles.prev_next}>
      <PrevNext {...} />
    </div>
  </div>
</PostLayout>

つまり、divで囲んでスタイリングするやり方でないのか?ということ。

もちろん理由があって、styles.related_post とかこんな感じのcssなんです。

post.module.scss
.related_post {
  background-color: white;
  margin-top: 2rem;
  border-radius: 0.4rem;
  // border-shadowの共通パーツです。
  @include common-border-shadow;
}

つまりもし中身の部分が渡したpropsの都合上、コンポーネントがレンダリングされなかったら 空っぽのdivがレンダリングされてしまう事になってしまうのです。

つまりこんな感じです。次のページと関連記事がない状態のレンダリング結果はこうなります。

render-blank-div.png

このような自体を避けるためにコンポーネントをdivで囲んでスタイリングするのではなく、classNameを渡せるように しておくのです。

まとめ

  • Gatsby x TypeScriptなブログ作る時のスターターはtakefumi-yoshii/gatsby-starter-typescriptがおすすめ。
  • コンポーネントにはclassNameをpropsに持たせ、内部rootクラスと外部rootクラスを渡せるようにする。

    • そうする事でコンポーネントの再利用性を高める事ができる。
    • そのために便利なライブラリーはJedWatson/classnamesが便利である。
    • この考え方の発展形として画面幅に応じて適用するcssを変更するテクニックを紹介した。
    const rootClassName = ClassNames({
      [styles.root]: true,
      [className!]: className
    })
    return (
    <div className={rootClassName}>
      // something
    </div>
    )
  • コンポーネントをdivで囲んでスタイリングしない方がいい。

    • コンポーネントがレンダリングされなかった時、空divがレンダリングされてしまう。

以上です。次回は、苦手な人のための2.レスポンシブルデザインのコツの紹介です。

yukihirop

この記事を書いた人

フロントエンド兼バックエンドなWEBエンジニアです。 既存の考え方に囚われずに自由にツールを作るのが好きです。

いつかBioに「Creator of ...」と書けるようなプログラマを目指している人のブログ

関連記事