2022年4月18日月曜日

GatsbyでWebサイトを4つ作ってみたメモ

 

https://tenderfeel.xsrv.jp/javascript/5680/

3月からGatsbyでWebサイトを4つ作った。
ずっとVue(Nuxt)とちちくりあっていたのでReactを触るのは3〜4年ぶりだった。

  1. microCMS+Gatsby
    練習用。JavaScript、Chakra。趣味。
    ちょうどウマ娘を始めたのでウマのデータベースを練習がてら作っていたものの、フルオートできんせいでなかなか進まなかった上にゲームをやめたので未完成でお蔵入り。とはいえいい基礎練になったと思う。
  2. WordPress+Gatsby
    イベントのLP。JavaScript、ThemeUI。新規。
    ずらせないリリース日があり、中身はサイト制作と同時進行で考えるというので、Wordpressでコンテンツ編集できるようにした。
  3. WordPress+Gatsby→Markdown+Gatsby
    コーポレートサイト。TypeScript、Emotion。内部リニューアル。
    5年前にReactで作られたものを、表はそのままリニューアル。元のソースがWordpressだったからWordpressをソースに使おうと思っていたが、大人の事情によりWordpressを使わないことになったのでMarkdown化した。
  4. Markdown+Gatsby
    ブログ。TypeScript、Chakra。新規。
    3と同じWordpressで管理するつもりだったが、Markdown化したためこちらもMarkdownをソースにした。デザイナー不在でレイアウトも自分でやるハメに。

Nuxt.jsのこと

NuxtStatic Site Generator機能が搭載されてるので静的サイト生成はできる。それで社内ツールを作ったりもしたんだけど、Vue3対応版が出たら陳腐化するという懸念があるので、これを最後に業務では使うのをやめている。

Nuxt自体は一度使ったらVueを素で使う気が失せるほど便利だから、V3になっても影響の少ないサイトであるならNuxtで作るのもありだと思う。

Next.jsのこと

手を出したいなあという気持ちはあるが、機会がない。

Reactに再入門してみての気づき

3〜4年ほど前のReact vs Vueの世界でVueの手を取ったのは「JSの中にHTMLとCSSが混在するの気持ち悪い」という感情があって、「1ファイルの中でもそれぞれのコードをブロックごとに分離して書けるVueならその気持ち悪さが少ない」という理由だった。ツイッタランドを見渡すと似たようなことを呟いてる人が結構いたので、自分だけじゃないんだと随分安心したものである。同じ思想の人がいたからこそVueが生まれたんだと思うが、そういう思想の持ち主にとってVueは溺れるものが掴む藁だったと思う。
あとはAngularとかで触ったのも含めて昔のTypeScriptの使用感が悪くてTypeScriptに苦手意識を持っていたというのもあった。

今思えば実に頭の硬い年寄りじみた理由だなあと思うけど、気持ち悪さを堪えてまで独学する気は起きなかったので、Vueを選ぶことに抵抗はなかった。とはいえ、Reactから派生したVueが生き残れるかは選んだ時点ではわからなかったし、逆に消える可能性の方が高いまであったので、廃れたらReactを覚えるつもりだった。

フロントエンドにはめずらしく、Vueは消えずに今も使えているのだけど、久しぶりにReactに戻ったらあの時の気持ち悪さが1ミリも残っていなかった。それどころか逆に「全部JSで書けるのめっちゃ楽」と思うようになってた。
時間はかかったけど苦手克服できたから結果オーライかな。

TypeScriptは…まだ素で書くより時間はかかるけど、ReactとVue3の時はなるべく使うようにしている。

利用したGatsbyプラグイン

ほとんどがスターターに含まれているもので、特に珍しいものは使ってない。

コンテンツ関連

gatsby-source-wordpress

WordPressをコンテンツのソースとして利用できるようにする。Wordpress側にGraphQLプラグインとGatsbyプラグインが必須。configに設定追加すれば連携完了の大変便利なプラグイン。

gatsby-source-filesystem

指定したディレクトリにあるファイルをGraphQLで利用できるようにする。スターターにデフォルトで入っているプラグイン。

gatsby-transformer-remark

RemarkというMarkdownパーサーでmdファイルを解析するプラグイン。Remarkに対応するプラグインを利用することで、画像や動画の埋め込みにも対応できる。

gatsby-transformer-json

JSONを解析してJavaScriptオブジェクトに変換する。
項目が決まっている繰り返しのコンテンツはJSONをソースにしてページ生成するのが楽だった。

gatsby-plugin-twitter

Twitterの埋め込み。

gatsby-plugin-smoothscroll

ゆっくりめにスクロールさせるやつ。

gatsby-plugin-s3

GatsbyサイトをS3バケットにデプロイできるようにするプラグイン。

画像関連

gatsby-plugin-image

提供される専用のコンポーネントによってレスポンシブ画像を利用できるようにするプラグイン。gatsby-plugin-sharp と gatsby-transformer-sharp が必須。

gatsby-plugin-react-svg

SVGファイルをコンポーネントぽく扱えるようにする。

SEO関連

gatsby-plugin-react-helmet

React Helmet のGatsbyプラグイン。提供されるコンポーネントを利用することでheadタグに任意のタグを追加できる。メタタグの実装部分は自分で書く必要があるが、スターターには簡単なものが実装済みになっているのでそれを改変すると楽。

gatsby-plugin-sitemap

サイトマップを生成する。

gatsby-plugin-manifest

マニフェストファイルを生成する

gatsby-plugin-google-gtag

Google Analyticsを利用するプラグインとしてgatsby-plugin-google-analyticsがあるが、こちらはGA3まで(UAから始まるIDのやつ)しか対応していないので、GA4を利用する場合はgtagの方を利用する。

ユーティリティ

gatsby-plugin-typegen

TypeScript / Flow定義ファイルを自動的に生成する。TypeScript化するなら必須といえる。

gatsby-plugin-alias-imports

設定したエイリアスをimportで利用できるようにする。Nuxtぽく書けるようにしてた。

1
2
3
4
5
6
7
8
9
10
11
12
{
    resolve: `gatsby-plugin-alias-imports`,
    options: {
      alias: {
        '@/src': path.resolve(__dirname, '../src'),
        '@/components': path.resolve(__dirname, '../src/components'),
        '@/images': path.resolve(__dirname, '../src/images'),
        '@/templates': path.resolve(__dirname, '../src/templates'),
      },
      extensions: [],
    },
  },

スタイリング

CLIで提案されるstyling systemから条件に合うのを見繕って使ってみた。(Chakraは除く)

小規模なサイトであれば陳腐化したら全部作り直すほうが早かったりするので、スタイルについては一番早く実装できるものという基準だけで選んでいる。

ThemeUI

テイザーサイトなのでデザイン(=テーマ)があって、それを一から作らなければならず、文字サイズだとか空白とかもデザイン通りに設定しなければならない。という条件だったので選択した。
レスポンシブとか地味に面倒なこともやっつけてくれるから便利だと思う。

Emotion

コーポレートサイトは元のサイトのデザインと5年ほど前に作られたReactのソースがあり、その元のソースをなるべく流用したかった。ので、CSSをそのままコピペして使うために @emotion/css を利用した。UIコンポーネントライブラリは特に利用してない。
動きは framer-motion でつけてて動画に被ってるアニメーションは GSAP だが、GSAPだけでも良かった気はする。

Chakra

ブログの時デザイナー不在だった。適当にやってもイマドキな雰囲気になって、時間ないからコンポーネントもやっつけてほしい。という需要を満たすのがChakraであった。すごいぞChakra。便利だぞChakra。framer-motionにも対応で動かすのも自由自在だ。

WordPressをソースとして利用する

GatsbyのソースにWorpdpressを利用するかどうか。
私は以下の2点のどちらかに当てはまるならWordpressはありだと思う。

  1. Worpdressで作られたサイトをGatsbyで静的にしたい。更新はWordprssで継続する
  2. コンテンツを非エンジニアが編集するサイトで、リッチなビジュアルエディタが必要

①の場合はプラグイン追加してフロントを変更するだけなのですぐできる。Wordpressは非公開領域に移動しといた方が安全と思う。
②はWeb業界だと「Wordpressなら使ったことがある」という人が結構多いためである。使い方の説明をする手間も省けたりする。

逆に以下に当てはまるのであれば、Wordpress以外の管理方法を考えた方が良い。

  1. データベースとして利用する
  2. コンテンツをエンジニアが編集する

①の場合は専用のAPIを組むほうが良いが、そういうことができるエンジニアがいないならWordpressを使うのも致し方なしかもしれない。WordpressではないHeadressCMSを使うという手もある。
②の場合はMakrdownやJSONを使うのをお勧めする。

WordPressの面倒なところは、Wordpressの存在そのものがセキュリティホールになってしまうということだと思う。なのでWordpressを使う場合で自前でサーバーを構築するならセキュリティ周り考慮できるエンジニアが必要不可欠になってくる。

WordPressのローカル環境

WordPressのローカル環境といえば…

  1. XAMPP
  2. VCCW
  3. Docker
  4. Local

と4つほど使ったことあるんだけど今のお気に入りは Local。使いやすい。

WordPressとGatsbyの連携

公式がプラグイン出してるからインストールして手順通りにやれば脳死で構築できる。

みんな大好きACFもGraphQL対応しているので問題なく使えるぞ。
GraphQL未対応の場合は自分で拡張書かないといけないのだが、そんなに難しくない。

[WordPress] GraphQLフィールドの追加とGatsbyでの利用

WordPress記事のMarkdown化

WordPressのデータをMarkdownにしたいと思った時誰もが真っ先に考えるのは「プラグインの存在」だろう。Wordpressなら大抵のことは誰かが先にやっててプラグイン作ってる。

探してみたらやっぱりあったので使ってみた所、何度かエラーで詰まったけどエクスポートすることができた。

このプラグインはブロックエディターのDOMには未対応らしいので、出力されたMarkdowonファイル内にギャラリーなどのHTMLソースが中途半端に残っていたりする。その辺は置換で変換しておくなどで対応する必要があるものの、frontmatterも書き込んでくれるので便利だった。

Markdownをソースとして利用する

ブログ的なコンテンツにはうってつけだと思う。Gitで管理できるのも良いところ。
でもブログとしてあるあるな機能をMarkdownでつけようとしたら結構面倒だった。

frontmatterによるタグ機能

frontmatterのYamlで配列でタグを指定すれば、

1
2
3
---
tags: ['Unity', 'ゲーム']
---

タグページでGraphQLのクエリをこのように指定すればタグが取れるので、

1
2
3
4
5
6
7
8
9
10
export const pageQuery = graphql`
  query TagsPage {
    allMarkdownRemark(limit: 2000) {
      group(field: frontmatter___tags) {
        fieldValue
        totalCount
      }
    }
  }
`

この配列を処理すればタグクラウド的な一覧ができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const TagsPage = ({ data }: { data: GatsbyTypes.Query }): JSX.Element => {
  const group = data.allMarkdownRemark.group
  return (
    <div>
        <h2>タグ</h2>
        <ul>
          {group.map((tag) => (
            <li key={tag.fieldValue} >
              <Link
                to={`/tags/${tag.fieldValue}/`}
              >
                {tag.fieldValue}{' '}
                <span>
                  {tag.totalCount}
                </span>
              </Link>
            </li>
          ))}
        </ul>
    </div>
  )
}

記事詳細とかの場合は childMarkdownRemark.frontmatter.tags で取れる。

1
2
3
4
5
6
file(sourceInstanceName: { eq: "blog" }, name: { eq: $id }) {
  childMarkdownRemark {
    frontmatter {
      tags
    }
}

配列なのでループ処理するだけ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Tags = ({ post })=> {
  const tags = post.childMarkdownRemark?.frontmatter?.tags
  return (
  <ul aria-label='タグ'>
    {tags?.map((tag) => {
      return (
        <li key={tag} >
          <Link to={`/tags/${tag}/`}>
            {tag}
          </Link>
        </li>
      )
    })}
  </ul>
  )
}

カテゴリーも同じようなやり方で実装できると思う。

サイト内検索の実装

Gatsby公式サイトでググると出てくるサイト内検索プラグインは、日本語だと微妙な動作になってしまうので実質使い物にならなかった。

それで日本語でもいけてる検索の実装をしてみたら長くなったので詳細は別記事でまとめているが、自前でサイト内検索実装するとなかなか難しいので、SaaSに頼るのが妥当かなと思う。

Locationデータ利用上の注意

Gatsbyはクライアント側のルーティングに@reach/routerを使用している。
ページコンポーネントでルーターが渡す location データを利用できる。

フルパスが必要だったときに、location.origin + location.pathname がお手軽なんだけども、

1
2
3
4
const Page = ({ location, data }) => {
  console.log(location)
  return <div>{location.origin + location.pathname}</div>
}

これが有効なのは window.location にアクセスできるブラウザレンダリング時のみで、サーバーサイドレンダリング時は location.origin は undefinedになってしまうのだった。
helmetでogタグを設定する時にこれをやってて og:image がTwitterで表示されない(Twitterはフルパスじゃないと画像が表示されない)という現象にハマってしまった😓

解決策は公式サイトにあるようにGraphQLでsiteMetadataを取得するなり設定ファイルをimportするなりしてsiteURLを利用することである。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react"
import { graphql } from "gatsby"
const Page = ({ location, data }) => {
  const canonicalUrl = data.site.siteMetadata.siteURL + location.pathname
  return <div>The URL of this page is {canonicalUrl}</div>
}
export default Page
export const query = graphql`
  query PageQuery {
    site {
      siteMetadata {
        siteURL
      }
    }
  }
`

#gatsby-focus-wrapperのtabindex

素の状態だと body>#___gatsby>#gatsby-focus-wrapper というDOM構造になるのだが、#gatsby-focus-wrapper に tabindex="-1" が設定されている影響で、タブフォーカスの挙動が変わってしまう。

これはキーボードとかで操作するユーザーのために、常に最初からフォーカスが始まるように設定されているそうだが、マウスクリックした一番近い場所からフォーカスが始まるというデフォルトの挙動を問答無用で変更してしまうのはいかがなものかと思うが、実際キーボード操作しかしない人はどう思ってるんだろうか?

レイアウトのコンポーネントとかで属性を削除すれば解除できる:

1
2
3
4
5
6
7
8
// #gatsby-focus-wrapper要素のtabindexとstyleを除去
  React.useEffect(() => {
    const gatsbyFocusWrapper = document.getElementById('gatsby-focus-wrapper')
    if (gatsbyFocusWrapper) {
      gatsbyFocusWrapper.removeAttribute('style')
      gatsbyFocusWrapper.removeAttribute('tabIndex')
    }
  }, [])

Cloudflareで発生したMP4の問題

最初はCloudflareでデプロイをしていたが、確認の段階でSafariだと動画が再生されないという問題が発生した。
コミュニティをmp4で検索してみると、3年前から延々と同じ問題の報告が上がっていた。

原因はmp4のステータスが200になってしまうことだった。
Safariはステータス206しか許さないので、Safariだけ動画が再生されないという現象が起きる。

サーバー側でできることは何もないとサーバーサイドエンジニアに言われてしまったので、渋々fetch経由でmp4を取得するコンポーネントを作って対応した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
 * Cloudflareが動画ファイルを200で返す不具合の回避策
 * fetchで読み込んだblobをvideoタグに設定する
 */
import * as React from 'react'
import { css, SerializedStyles } from '@emotion/react'
 
type FetchVideoProps = {
  src?: string | undefined
  width?: string | number | undefined
  height?: string | number | undefined
  loop: boolean
  videoStyle?: SerializedStyles | string
  onLoadedData?: () => void
  onEnded?: () => void
}
 
const FetchVideo = React.forwardRef<HTMLVideoElement, FetchVideoProps>(
  (props: FetchVideoProps, ref: React.Ref<HTMLVideoElement> | undefined) => {
    const { width, height, loop, src, videoStyle, onLoadedData, onEnded } =
      props
 
    if (!src) return <></>
 
    React.useEffect(() => {
      const controller = new AbortController()
      let video = ref?.current
 
      fetch(src, { signal: controller.signal })
        .then((response) => response.blob())
        .then((data) => {
          console.log(`&#x1f4fc; fetched video(${src})`)
          if (video && video.src === '') {
            video.src = URL.createObjectURL(data)
          }
           
          if (onLoadedData) {
            onLoadedData()
          }
        })
        .catch((error) => {
          console.error(`&#x1f4fc;  ${error}`)
        })
 
      return () => {
        controller.abort() // 読み込み中断
        video.pause()
        video = null
      }
    }, [src])
 
    return (
      <video
        ref={ref}
        width={width}
        height={height}
        css={css`
          ${videoStyle}
        `}
        controlsList='nodownload'
        muted
        playsInline
        loop={loop}
        onEnded={onEnded}
        role='presentation'
      />
    )
  }
)
 
FetchVideo.displayName = 'FetchVideo'
export default FetchVideo

Cloudflareはおねだんの安さから選定されたのだが、この件について問い合わせると「Cloudflare Pagesはリクエストのバイト範囲をまだサポートしてないからこの問題が発生している。feature request taskにはなってるけどスケジューリングされてないからいつ実装されるかは不明(意訳)」という回答だった。動画が再生されないって結構致命的と思うんだけどなあ…?

このほかにもサポートの返信がやたら遅いとか、ビルドが遅い(10分以上かかってた)とか、コード送信メールがなかなか届かんとか気になることが色々とあったので、結局 GasbyCloud に乗り換えたのだった。

GasbyCloudはビルドが1分くらいで終わるので大変良いです。

Gatsby Cloudの価格修正

最初に気づいたのは選定したサーバーサイドエンジニアなんだけど、突然価格が変わったのでびっくりしていた。こういうところが外部サービスの利用でリスクになるんだなあと思わされた。

GatsbyCloud自体はビルド早いしプルリクプレビューもできるしで便利だと思うが、最初に選択したサービスやプランが永劫続くという保証はどこにもないということを忘れないようにしたい。

0 コメント:

コメントを投稿