リモート卒展を支える技術2020

# Webサイトをリリースしました
https://racoon.vlue.blue

<img width="1431" alt=" 2020-03-21 12.22.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/29370809-117c-1755-1a20-a32709faa6ea.png">

今年はコロナの影響で卒業式が中止になったりしましたね。
それと比べるとあまり知られてないかもしれませんが、
美大・アート系の学校の卒業展示、
いわゆる **卒展** も数多くが中止を余儀なくされました。

部活でいうと引退試合が中止になったようなものです。
悲しいですね。

そこで、卒展で展示する予定だった作品をTwitteにアップするムーブメントが起きました。
[#桑沢2020]() [#かってに卒制展]() などのハッシュタグで様々な作品が見れます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">卒業制作<br><br>くつ下の柄によって靴の表情が変わる靴を制作。<br><br>透明や半透明の素材を使っている靴が増えていているけど『くつ下を見せるため』の靴はないよなぁってとこから発想しました。<br>LINE(くつ下の色彩を見せる)FRAME(くつ下の柄を見せる)の2種類デザインしました。<a href="https://twitter.com/hashtag/%E6%A1%91%E6%B2%A22020?src=hash&amp;ref_src=twsrc%5Etfw">#桑沢2020</a> <a href="https://t.co/nq0apG4uzs">pic.twitter.com/nq0apG4uzs</a></p>&mdash; 蓮(Hasu) (@hasu_ird) <a href="https://twitter.com/hasu_ird/status/1233350675431485440?ref_src=twsrc%5Etfw">February 28, 2020</a></blockquote>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/%E6%A1%91%E6%B2%A22020?src=hash&amp;ref_src=twsrc%5Etfw">#桑沢2020</a> <br><br>私は紙で椅子をつくりました。<br><br>桑沢の卒展は、自分にとってすごくすごく特別な想いがありました。<br><br>小学生から憧れ続けた桑沢卒展、中止。 <a href="https://t.co/2Q1oZ0RvAm">pic.twitter.com/2Q1oZ0RvAm</a></p>&mdash; 藤原光平 (@fuji_kohei) <a href="https://twitter.com/fuji_kohei/status/1232842391867912192?ref_src=twsrc%5Etfw">February 27, 2020</a></blockquote>

これらの作品をアーカイブしたのが今回のWebサイトになります。

# 使用技術
- React
- GatsbyJS
- GraphQL
- Twitter API
- S3
- CloudFront
- Lambda@Edge

特に要となっているのは [GatsbyJS](https://www.gatsbyjs.org/) です。

# GatsbyJS とは
詳細は色々と記事が見つかるのでそちらに任せますが、
（例：[Reactの最強フレームワークGatsby.jsの良さを伝えたい！！](https://qiita.com/hppRC/items/00739eaf9ae7fc95c1ca)）

GatsbyJSとは、静的サイトジェネレータであり、
Reactアプリケーションを動かすフレームワークです。
npmでインストールして、create-react-app的なノリで使用できます。

ビルドしてHTMLを生成する部分はGo製の [Hugo](https://gohugo.io/) みたいのをイメージしてもらえればいいんですが、Gatsby はただの静的ジェネレータにはとどまりません。

**完全な**Reactアプリとして動作するので、
外部のデータソースと接続すれば、**動的な**アプリとして振る舞います。

この辺りの仕組みを理解するには以下の記事が最高にわかりやすいです。

[Reactベース静的サイトジェネレータGatsbyの真の力をお見せします](https://qiita.com/uehaj/items/1b7f0a86596353587466)
<img width="860" alt=" 2020-03-21 12.47.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/7958a63d-1dc3-6c1f-6bc5-59b344433048.png">

GatsbyはGraphQLを介してデータにアクセスするのですが、
ビルド時には、ローカルのGraphQLからデータを抽出して静的ファイルを出力します。
それに加えて、ブラウザでの実行時に外部のデータソースと接続してアプリにマージすることもできるのです。

例えばブログアプリを作るときに、記事はマークダウンとしてソースに含めてビルドし、
スター数やコメントはDynamoDBに保存してAppSync経由でリアルタイムに取得・表示する、
といった設計とかが可能。

# データソースをどうするかで迷う
ブログやコーポレーションサイト、ポートフォリオなどの事例が多く、
それらではファイルシステムがデータソースとなっていました。
任意のディレクトリにマークダウンとしてデータを記述しておく形です。

今回使うデータは、元ツイートと、ツイートに含まれる画像の2つです。
Twitter APIの制限もあるし、コンテンツもちゃんとフィルタリングしたかったので、
静的に固めることは早々に決めたのですが、マークダウンはちょっと抵抗があります。

最初はローカルでMySQLを立てて実装しました。
データベースの接続もプラグインによって幅広くサポートされています。

<img width="1381" alt=" 2020-03-21 12.59.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/25494d3a-ed1d-ce69-6865-73f4af667f4c.png">

ただSQLは情報が少なく、ジョインが絡む複雑なクエリを書くのが難しかったり、
サロゲートキーがGraphQLが生成するNodeのIDとバッティングしたり、
なんやかんやで面倒だったので途中からJSONに変更しました。
静的なのにソースとデータが分離されてるのもなんか気持ち悪いし。。。

ファイルシステムのプラグインを使えばJSONもこんな感じで簡単にクエリできます。

```json:tweets.json
[
    {
        "tweet_id": "1232842391867912192",
        "created_at": "2020-02-27T01:38:25.000Z",
        "text": "#桑沢2020 \n\n私は紙で椅子をつくりました。\n\n桑沢の卒展は、自分にとってすごくすごく特別な想いがありました。\n\n小学生から憧れ続けた桑沢卒展、中止。 https://t.co/2Q1oZ0RvAm",
        "hashtags": "桑沢2020",
        "user_id": "1093916680504336384",
        "user_screen_name": "fuji_kohei",
        "embed_html": "<blockquote class=\"twitter-tweet\"><p lang=\"ja\" dir=\"ltr\"><a href=\"https://twitter.com/hashtag/%E6%A1%91%E6%B2%A22020?src=hash&amp;ref_src=twsrc%5Etfw\">#桑沢2020</a> <br><br>私は紙で椅子をつくりました。<br><br>桑沢の卒展は、自分にとってすごくすごく特別な想いがありました。<br><br>小学生から憧れ続けた桑沢卒展、中止。 <a href=\"https://t.co/2Q1oZ0RvAm\">pic.twitter.com/2Q1oZ0RvAm</a></p>&mdash; kohei_fuji (@fuji_kohei) <a href=\"https://twitter.com/fuji_kohei/status/1232842391867912192?ref_src=twsrc%5Etfw\">February 27, 2020</a></blockquote>\n",
        "images": [
            "https://pbs.twimg.com/media/ERvvnNqUYAAKtTL.jpg",
            "https://pbs.twimg.com/media/ERvvnVmVUAA71I6.jpg",
            "https://pbs.twimg.com/media/ERvvnYPU4AA5u5w.jpg",
            "https://pbs.twimg.com/media/ERvvnsOVUAA5_ht.jpg"
        ]
    },
    {...
```


```js
export const query = graphql`
  query {
    allTweetsJson {
      edges {
        node {
          id
          tweet_id
          text
          user_screen_name
          images
        }
      }
    }
  }
`
```

# ツイートの取得
Twitter APIを使ってツイートを取得してMySQLにぶっ込むNode.jsのスクリプトが手元にあったのでそれを流用しました（最初MySQLを使ってたのはこういう事情もある）。

流れとしてはこんな感じ。

1. それっぽいハッシュタグで検索をかける。
2. 取得したツイートでサイトに展示するものを目視で選定する。
3. 選定したツイートの埋め込みコードをAPIで取得・保存する。


検索の `search/tweets` ではパラメータに `include_entities: true` を指定して、画像のURLが取れるようにする（ものによっては画像があるのになぜか取れない謎）。

キーワードはハッシュタグを指定するのだが、以下のようにしてリツイートを省かないとノイズが大きい。
`{q: "#岡山県立大学卒展2020 exclude:retweets}`
レートリミットがあるので、効率的に取得するためにcountは最大の100件を指定。
最新のツイートが取得されるので、さかのぼって取得するループを回すためには、
都度 `max_id` を指定してカーソルを移動させる必要がある。

埋込み用のコードは `statuses/oembed` で取得できる。
この埋め込みコードには、ツイート展開用の

```html
<script async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"></script>
```

が含まれるが、各ツイートに含める必要はないので、これは削除しておく。


必要なフィールドをいい感じにJSONに保存してデータの準備は完了。

# 無限スクロール
ページングはユーザー側の操作が手間なので避けたくて、
TwitterやPinterestのような無限スクロールを実装した。

![racoon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/2bf1d2d7-5388-b6b3-a717-3c87e7c78c78.gif)

`IntiniteScroll` の `dataLength` が21ずつ増えているのがわかると思う。

Twitterのウィジェットが重いので、カード展開前に素のテキストが一瞬見えてしまうことがあるが、もう少しみ読み込み位置を早くすれば対処は可能。
いまはコンテナの高さの85%まで来ると読み込みが走るようになっている。

ライブラリには [react-infinite-scroll-component](https://www.npmjs.com/package/react-infinite-scroll-component) を使用した。
これをラップしたコンポーネントを作り、
親コンポーネントの `useState`, `useEffect`で更新を制御している。

```jsx
let allTweets = null

export default ({data}) => {  
  const [tweets, setTweets] = useState([])
  const [hasMore, setHasMore] = useState(true)
  const [page, setPage] = useState(0)
  const PER = 21 // dividable by 3

  useEffect(() => {
    allTweets = data.allTweetsJson.edges.sort(() => { return Math.random() - .5 })
    fetchTweets() // initialize
  }, [])

  const fetchTweets = () => {
    const pageResult = allTweets.slice(page * PER, page * PER + PER)
    setTweets(tweets.concat(pageResult))
    setPage(page + 1)

    if (page * PER >= allTweets.length) {
      setHasMore(false)
    }
    
    if (window.twttr) {
      window.twttr.widgets.load(document.getElementById(styles.tweet_container))
    }
  }
```

まず useEffect 内で全てのツイートをJSONから読み込んでランダムにソートしている。
なるべくいろんな作品を見てもらえるように閲覧のたびにファーストビューの作品は入れ替えたかったので、ランダムに取得する必要があった。

静的に作った場合だとこれは不可能。
ビルド時に順番は固定されてしまい、再ビルドされるまでは変わらないことになってしまう。
これは無限スクロールの話とは別で、静的なページにはデータを持たせずに、
非同期に外部からデータを取得してくる設計にする必要がある。

そうなると別にGatsbyじゃなくてよくね...という話になるのだが、
他のページは静的にビルドしているし、動的にしたいのはここだけ、
そこはReactでいろいろ書けばいいよね！というのがまさにこのフレームワークの強みなんだと思う。

無限スクロールの話に戻るが、propsには、新しいページの結果だけでなく、
これまでの結果をマージして渡す必要があるので結合している。

```jsx
const pageResult = allTweets.slice(page * PER, page * PER + PER)
setTweets(tweets.concat(pageResult))
```

そして新たに追加されたツイートをカードとして展開するには、再度ウィジェットの読み込みを走らせる必要がある（[Twitterの公式ドキュメント](https://developer.twitter.com/en/docs/twitter-for-websites/javascript-api/guides/scripting-loading-and-initialization)）。


```jsx
if (window.twttr) 
  window.twttr.widgets.load(document.getElementById(styles.tweet_container))
}
```

そもそもカードの展開には最初 [gatsby-plugin-twitter](https://www.gatsbyjs.org/packages/gatsby-plugin-twitter/) というプラグインを使っているのだが、
こいつの仕組みがよくわからなかったのでやめた。
`widgets.js` が読み込まれた形跡がなく、
`twttr` オブジェクトもスコープに存在しないため、`load()` を呼び出すことができなかったからだ。

代わりに自前で `<script>`タグを追加する方法を取っている。

```js
import Helmet from "react-helmet"

<Helmet>
  <script src="https://platform.twitter.com/widgets.js" type="text/javascript" async></script>
</Helmet>
```

# Pinterest風レイアウト
ツイートには最大4枚の画像が含まれるので、それらを取り出してランダムに並べる画面を別で作っています。
無限スクロールで仕組みは前述のものと同じです。
![localhost_8000_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/8417e1a6-fecd-9bf4-ea9f-3a083f6bb7f3.png)


GatsbyJSで画像を使う際は、`gatsby-image` コンポーネント使うことが推奨されています。
[Working with Images in Gatsby](https://www.gatsbyjs.org/docs/working-with-images/)

これは何かというと、閲覧しているデバイスに応じて複数のサイズ・解像度に画像を最適化してくれるライブラリで、GraphQLでクエリできるようになっています。
他にも以下のような特徴があります。

>gatsby-image is a plugin that automatically creates React components for optimized images that:
>
・Loads the optimal size of image for each device size and screen resolution
・Holds the image position while loading so your page doesn’t jump around as images load
・Uses the “blur-up” effect i.e. it loads a tiny version of the image to show while the full image is loading
・Alternatively provides a “traced placeholder” SVG of the image
・Lazy loads images, which reduces bandwidth and speeds the initial load time
Uses WebP images, if browser supports the format

使い方には `fluid` と `fixed` があって、
コンテナのサイズに応じて伸び縮みする `fluid` と、固定サイズの `fixed` を使い分けます。
レスポンシブ対応で、スマホサイズでは `fixed` にするみたいな使い分けをしています。

GraphQLはこういう風に書く

```graphql
query($id: String!) {
  tweetsJson(tweet_id: { eq: $id }) {
    id
    text
    tweet_id
    embed_html
    featuredImg {
      childImageSharp {
        fluid(maxWidth: 400, quality: 100) {
          ...GatsbyImageSharpFluid
        }
        fixed(width: 200, height: 200) {
          ...GatsbyImageSharpFixed
        }
      }
    }
  }
}
```

と、ここまで説明したところでなんなんですが、
アニメーションやスタイルの関係上、この画面ではあえて使っていません。
flexboxと一緒に使うと表示されなかったり、なかなか使い勝手が難しいライブラリです😢

（※ホバー時に元ツイートの情報を表示するようにしてたりする）
![racoon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/de6d40bc-3b78-6110-69cc-80bca5561a8a.gif)

スタイルの問題は他にもあって、

Pinterest風にマルチカラムにするのにCSSのcolumnsプロパティを使っているんですが、
画像の高さがそれぞれ違うので次ページ読み込み時に並び替えが起こってしまいます。
columnsはコンテンツを左から埋めていくので、
画像が追加された時に前ページの画像が全て左に寄せられてしまい、
見ていたコンテンツが消える事象が発生することなります。



これではユーザーが？となるので、これまで見ていたページの並びは固定するために、
ページごとにcolumnsを指定するコンテナでラップすることにしました。

![localhost_8000___3_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/75c65954-a899-af20-3918-548fe31a1e10.png)

画像の組み合わせによっては不要な余白が生じてしまうのですが。
別の仕組みでグリッドレイアウトを作れば良い話なんですが、
時間があるときでもやろうと思います。

# 外部画像を最適化するトリック
ソースファイルに含まれているデータは `tweets.json` 1ファイルのみです。

```json:tweets.json
[
    {
        "tweet_id": "1232842391867912192",
        "created_at": "2020-02-27T01:38:25.000Z",
        "text": "#桑沢2020 \n\n私は紙で椅子をつくりました。\n\n桑沢の卒展は、自分にとってすごくすごく特別な想いがありました。\n\n小学生から憧れ続けた桑沢卒展、中止。 https://t.co/2Q1oZ0RvAm",
        "hashtags": "桑沢2020",
        "user_id": "1093916680504336384",
        "user_screen_name": "fuji_kohei",
        "embed_html": "<blockquote class=\"twitter-tweet\"><p lang=\"ja\" dir=\"ltr\"><a href=\"https://twitter.com/hashtag/%E6%A1%91%E6%B2%A22020?src=hash&amp;ref_src=twsrc%5Etfw\">#桑沢2020</a> <br><br>私は紙で椅子をつくりました。<br><br>桑沢の卒展は、自分にとってすごくすごく特別な想いがありました。<br><br>小学生から憧れ続けた桑沢卒展、中止。 <a href=\"https://t.co/2Q1oZ0RvAm\">pic.twitter.com/2Q1oZ0RvAm</a></p>&mdash; kohei_fuji (@fuji_kohei) <a href=\"https://twitter.com/fuji_kohei/status/1232842391867912192?ref_src=twsrc%5Etfw\">February 27, 2020</a></blockquote>\n",
        "images": [
            "https://pbs.twimg.com/media/ERvvnNqUYAAKtTL.jpg",
            "https://pbs.twimg.com/media/ERvvnVmVUAA71I6.jpg",
            "https://pbs.twimg.com/media/ERvvnYPU4AA5u5w.jpg",
            "https://pbs.twimg.com/media/ERvvnsOVUAA5_ht.jpg"
        ]
    },
```

画像のURLとしてTwitterのURLが格納されていますが、
アプリケーションから参照しているのはこのURLではありません。


これは作品の詳細ページですが、画像の`src`は
`/static/f6862a78b0b14267e3e9188b58d053a6/25252/ESUa1ooUYAAOn4I.jpg`
となっています。
<img width="1434" alt=" 2020-03-22 19.28.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/45bb0603-a3c0-5330-7632-3ffe2ea141e0.png">

GatsbyJSの仕様では、プロジェクトルートに `static` という名前のフォルダを置いて、
画像などのアセットを含めるのが通例です。
そうすると、ビルド時に成果物の `public` の下に `static` が残ります。

しかし今回は `static` の下に画像を置くのではなく、JSONにリモートのURLを記述しているだけです。
これをビルドの成果物に含める、かつ、先述の `gatsby-image` の最適化を施す必要があります。

gatsby-imageに処理をさせるためには、なんらかの形でこのURLをGraphQLのNodeに変換する必要があるのですが、これを `createRemoteFileNode` という [Gatsby Node APIs](https://www.gatsbyjs.org/docs/node-apis/) を利用して実現します。
そしてGraphQLのresolverとしてこのNodeを登録します。
（うまく説明できなくてすまぬ...）
[GraphQLのリゾルバとは](https://www.apollographql.com/docs/tutorial/resolvers/#what-is-a-resolver)

コードはこんな感じ。

```js:gatsby-node.js
exports.createResolvers = ({
  actions,
  cache,
  createNodeId,
  createResolvers,
  store,
  reporter,
}) => {
  const { createNode } = actions
  createResolvers({
    TweetsJson: {
      featuredImg: {
        type: [`File`],
        resolve(source, args, context, info) {
          return source.images.map((image) => {
            return createRemoteFileNode({
              url: image,
              store,
              cache,
              createNode,
              createNodeId,
              reporter,
            })
          })
        },
      },
    },
  })
}
```

まずこのファイルだが、プロジェクトルートにある `gatsby-node.js` いうファイルで、
ビルド時に実行されるもの。
ここにコードを書いておくと、GraphQLのNode生成などのイベントを拾ってなんらかの処理を行うことができる。

そこで `createResolvers ` を呼んで、TweetsJson（`tweets.json`から生成されたスキーマ）に `featuredImg` というフィールドを生やしている。
中身は `File` の配列で（画像が複数枚の場合があるため）、
`createRemoteFileNode` の戻り値を追加している。

この前処理があってこそ、前述のこのクエリが実行できるという仕組み。

```graphql
query($id: String!) {
  tweetsJson(tweet_id: { eq: $id }) {
    id
    text
    tweet_id
    embed_html
    featuredImg {
      childImageSharp {
        fluid(maxWidth: 400, quality: 100) {
          ...GatsbyImageSharpFluid
        }
        fixed(width: 200, height: 200) {
          ...GatsbyImageSharpFixed
        }
      }
    }
  }
}
```


Nodeにあるフィールドを追加したければ、
`onCreateNode` で `createNodeField`を呼ぶのがシンプルで簡単な方法です。
実際 [チュートリアル](https://www.gatsbyjs.org/tutorial/part-seven/) でもそうやっています。

わざわざ `createResolvers` なんてややこしいのを使っているのはちょっとしたハックで、
これが、全てのスキーマの処理が完了してから最後に呼ばれるAPIだからです。

`TweetsJson` というスキーマの元はJSONファイルであり、
このファイルをまずGraphQLのスキーマに変換する処理が発生します。
必ずこの作業が終わった後に処理をする必要があるため、`createResolvers` にしてあるというわけです。

これで無事に `gatsby-image` が処理できる形にリモートの画像URLが変換されたので、
ビルド時に様々なデバイスサイズ、解像度向けに `static` 配下に画像が生成されるようになります。

# 作品ページの生成
作品ごとのページを作ります。

![racoon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/e74384bb-06d1-37e7-19e3-5e2bd7f0cf48.gif)
（サムネイルをクリックすると画像が変わる仕様。）


チュートリアルでは、マークダウンから記事ページを作成しています。
[Programmatically create pages from data](https://www.gatsbyjs.org/tutorial/part-seven/)
ユースケースとしてはこういう使い方が多いと思いますが、今回のデータソースはJSONです。

GatsbyJSでは、あらかじめ `src/pages` に置いてあるファイルはビルド時に自動的にページとして出力されています。

それ以外に任意にページを作成したい場合は、先述の `gatsby-node.js` から `createPage` APIを呼び出すことになります。

```js:gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const result = await graphql(`
    query {
      allTweetsJson {
        edges {
          node {
            id
            tweet_id
            images
          }    
        }
      }
    }
  `)
  result.data.allTweetsJson.edges.forEach(({ node }, index) => {
    createPage({
      path: `/work/${node.tweet_id}/`,
      component: path.resolve(`./src/templates/work.js`),
      context: {
        id: node.tweet_id,
        next: index !== result.data.allTweetsJson.edges.length - 1 ? result.data.allTweetsJson.edges[index + 1].node : null,
        prev: index !== 0 ? result.data.allTweetsJson.edges[index - 1].node : null,
      },
    })
  })
}
```

`component` にテンプレートとなる Reactコンポーネントのファイルを渡しています。
`context` で渡した値は、テンプレート側で `pageContext` で受けることができます。
前後のページのリンクを作りたかったので、ここでは前後のNodeを受け渡すようにしています。

```js:src/templates/work.js
export default ({data, pageContext}) => {

}
```

`id`は該当記事を取得するページクエリで `$id` で参照します。


```js/src/templates/work.js
export const query = graphql`
  query($id: String!) {
    tweetsJson(tweet_id: { eq: $id }) {
      id
      text
      tweet_id
      embed_html
      hashtags
      images
      featuredImg {
        publicURL
        childImageSharp {
          fluid(maxWidth: 400, quality: 100) {
            ...GatsbyImageSharpFluid
          }
          fixed(width: 200, height: 200) {
            ...GatsbyImageSharpFixed
          }
        }
      }
    }
  }
```

URLは `path` で指定するのですが、Nodeの `id` は避けたほうがよいです。
コンテンツの追加/削除、再ビルドで変化する可能性があるので、
不変なユニークな値を指定しておくのが無難です。
チュートリアルにならってスラッグを指定するといいと思います。



# OGP画像
デフォルトで `SEO` というコンポーネントがついてくる。
内部的には [react-helmet](https://github.com/nfl/react-helmet) を使っている。
作品のページでは、その作品の画像やテキストに切り替えたいので以下のようにして使う。

```jsx
<SEO description={data.tweetsJson.text} image={data.tweetsJson.featuredImg[0].publicURL} />
```

`createRemoteFileNode` で Nodeにした外部画像ですが、
出力された `static/***`のパスを `publicURL` で取得することができます。

作品のOGPはこんな感じ。
<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/iiiXq7mwBx">https://t.co/iiiXq7mwBx</a></p>&mdash; 青いエンジニア🦋 (@itmono_sakuraya) <a href="https://twitter.com/itmono_sakuraya/status/1241287606211510274?ref_src=twsrc%5Etfw">March 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


通常のOGPと変わっていることがわかります。
<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://twitter.com/hashtag/%E6%A1%91%E6%B2%A22020?src=hash&amp;ref_src=twsrc%5Etfw">#桑沢2020</a> <a href="https://twitter.com/hashtag/%E3%81%8B%E3%81%A3%E3%81%A6%E3%81%AB%E5%8D%92%E5%88%B6%E5%B1%95?src=hash&amp;ref_src=twsrc%5Etfw">#かってに卒制展</a> <a href="https://twitter.com/hashtag/%E5%B2%A1%E5%B1%B1%E7%9C%8C%E7%AB%8B%E5%A4%A7%E5%AD%A6%E5%8D%92%E5%B1%952020?src=hash&amp;ref_src=twsrc%5Etfw">#岡山県立大学卒展2020</a> <a href="https://twitter.com/hashtag/%E6%97%A5%E6%9C%AC%E5%B7%A5%E5%AD%A6%E9%99%A2%E5%8D%92%E5%88%B6%E5%B1%95?src=hash&amp;ref_src=twsrc%5Etfw">#日本工学院卒制展</a><a href="https://t.co/Q8Sgg3F5ot">https://t.co/Q8Sgg3F5ot</a></p>&mdash; 青いエンジニア🦋 (@itmono_sakuraya) <a href="https://twitter.com/itmono_sakuraya/status/1241290265282797568?ref_src=twsrc%5Etfw">March 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
（アライグマ（racoon）は Corona のアナグラムです）

# ホスティング
このサイトは CloudFront で配信しています。
S3のWebホスティング機能は使用していません。

独自ドメイン（CNAME）とSSLも設定済み。

<img width="1370" alt="2020-03-23_0_33_52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/3f007a86-bc35-f796-d4a8-29cde6e541f0.png">



Origin Access Identity を指定して、バケットの直接参照は禁止しています。
<img width="1361" alt="2020-03-23_0_33_46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/7f2057cc-be4d-1fe7-e060-b175840a30ca.png">

以下バケットポリシー

```json
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ******"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{bucket_name}/*"
        }
    ]
}
```

また、サブディレクトリのルートオブジェクトを設定するために、Lambda@edgeをかましてあります。
静的Webサイトホスティングでは、 `/hoge/` へのアクセスを `/hoge/index.html` に補完してくれるのですが、
CloudFront単体ではルートでしかこれが効かないので、自前で対処する必要があります。
サブディレクトリ以下のアクセス `/*/` に対して、
Lambda@Edge を関連づけた Behavior を最優先で設定します。

<img width="1396" alt="2020-03-23_0_33_40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/407a6366-4352-6c9a-bf03-17c8110b85a6.png">


```js:lambda@edge
'use strict';

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    request.uri = request.uri.replace(/\/$/i, '/index.html');
    callback(null, request);
};
```

Lambda@Edgeってなに？っていう方は拙著をお読みください。
[Lambda@Edgeを完全に理解する🧘‍♀️](https://qiita.com/sakuraya/items/b7611af2ddee9fa959d7)

さらにもう一手間。
存在しないURLに対して正しく404を出すために、`Custom Error Response` の設定をします。

<img width="1379" alt="2020-03-23_0_33_33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/5c120c90-1ede-e532-c7ae-fe68e7aba95e.png">

404だけでなく403も設定しているのは、
S3は404エラーを403エラーで**隠蔽することがあるから**です。
ListBucket権限がないのが原因だったりします。

詳しくは。これも拙著です。
[CloudFront×S3で403 Access Deniedが出るときに確認すべきこと](https://qiita.com/sakuraya/items/add2cb7ced954215fb03)

# デプロイ
[gatsby-plugin-s3](https://www.gatsbyjs.org/packages/gatsby-plugin-s3/) でやってます。
`npm run deploy` を流すだけです。

ドキュメントがないのですが、`acl: null` を指定しないと権限エラーになります。

```js
{
  resolve: `gatsby-plugin-s3`,
  options: {
    bucketName: "******",
    acl: null
  },
},
```

# まとめ
こんなところです。
GraphQL初めて触るし、React書くの2年ぶりとかなので割と時間がかかってしまいましたが、
GatsbyJSはかなり可能性のあるフレームワークだなーと感じました。

特有の癖はありつつも、覚えてしまえばまあ。
あまりWebに馴染みのない人が、楽にWebサイトを構築できる手段だと思って使うと痛い目を見るかもしれません。

この記事でも紹介した通り、基本静的で動かしたい、でも動的にしたいところもある、
というユースケースでは力を発揮するんじゃないですかね。

何はともあれみなさんアートを楽しんでください。

<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/MGh77VptDw">https://t.co/MGh77VptDw</a></p>&mdash; 青いエンジニア🦋 (@itmono_sakuraya) <a href="https://twitter.com/itmono_sakuraya/status/1241757383891681281?ref_src=twsrc%5Etfw">March 22, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>