Amplify + Next.jsでフロントから登録した画像で動的にOGPする

## この記事は何？
Amplify + Next.jsのプロジェクトで、「S3にフロントからアップロードした画像でページのOGPを設定したい」という要件があり、試行錯誤の末成功したのでそのまとめです。

## ざっくりアウトライン
この記事でやることの実現方法のざっくりアウトラインをまずは示します

1. Amplify + Next.jsからS3へのアップロードをする
1. アップロード時にACLを `public-read` に設定する
1. SSRでS3の該当画像へのリンクを作成する
1. OGPのメタタグを設定する

この中に普通にやると壁がいくつかあります。それをひとつずつどう解決したかを紹介していきます。

## 壁① S3へのACL設定アップロード

Amplify + Next.jsでのS3への画像アップロードはすでにさまざまな記事が出ています。実際、専用のコンポーネントライブラリなども公開されていますが、今回は独自のUIを実装するため、自分で公式フレームワークの関数を呼び出してアップロードする方法を考えます。

通常のアップロードは以下のコードで済みます。

```typescript
const result = await Storage.put(fileName, data, {
 level: 'public',
 contentType: 'image/png'
})
```

この段階でも公開レベルを設定していますが、これはあくまでS3にアクセスするための認証情報に、誰の情報を利用できるかといったことを設定することしかできません。そのため、この状態で画像を取得する場合、それは有効期限付き、認証情報をパラメータに持ったURLを取得してそれで表示をすることになります。

この仕組みは通常のサービス内で画像を表示するという用途に関しては問題ありませんが、OGPの場合URLが有効期限付きというのはいただけません。
そこでS3にはACL設定というものが存在し、これを設定することで永続的な公開URLを作成できます。

この設定自体は公式のマニュアルにも存在が[明記](https://docs.amplify.aws/lib/storage/upload/q/platform/js#monitor-progress-of-upload)されており、上のコードを以下のように書き換えることで対応ができます。

```typescript
const result = await Storage.put(fileName, data, {
 level: 'public',
 contentType: 'image/png',
 acl: 'public-read'
})
```

ですが、これには一つ落とし穴があります。実はAmplifyのデフォルトの設定だと、S3のACL設定の書き換えが許されておらず、これをそのまま書くと403エラーが返ってきてしまいます。

そこでAmplifyのS3設定を手でいじる必要があります。変更する必要があるのは二箇所、`s3-cloudformation-template.json`と`sotrage-params.json`です。

まずCloudFormationのテンプレートの方では`s3:GetObject`などを探してみてください。これは自動生成の場合、おそらくAmplifyのAuthに設定したユーザーグループごとに設定テンプレートが生成されていると思います。
ここに`s3:GetBucketAcl`, `s3:PutBucketAcl`, `s3:GetObjectAcl`, `s3:PutObjectAcl`から必要なものを追加してあげましょう。Objectとつく方がないと自分の場合写真ごとの設定はうまくいきませんでした。最終的にはこのような形になると思います。（調べるといくつかこの解決法が出てきますが、Authの設定やバージョンによって若干jsonの見た目が異なると思います。ですが基本構造は同じなので、しっかりとどこを編集すればいいのか読み解いてあげましょう）

```json
...
"hogeGroupPolicy": {
  ...
  "Properties": {
    ...
    "PolicyDocument": {
      ...
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            ...
            "s3:GetBucketAcl",
            "s3:PutBucketAcl",
            "s3:GetObjectAcl",
            "s3:PutObjectAcl"
          ],
          ...
         }
      ]
   }
}
...
```

これに加えて`storage-params.json`の方にもAuthのグループごとに許可を与えてあげます。
例えばこのような形です。

```json
...
{
  "admin": [
    "create/update",
    "read",
    "delete",
    "updateacl"
  ]
}
...
```

この二箇所を編集してあげることで、ACLを設定して画像をアップロードすることができます。

## 壁② Amplify + Next.jsでのSSR

こちらは先日公式に対応してくれたのでそちらを用います。細かいところはさまざまな記事で既に述べられているのであまり詳細まで述べませんが、複数環境を運用している場合は`amplify.yml`をプロジェクトフォルダ内で作ることで環境ごとに設定できます。

```yaml
version: 1
backend:
  phases:
    build:
      commands:
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands:
        - nvm install 14.16.0
        - nvm use
        - yarn install
    build:
      commands:
        - yarn run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
 ```

※以下に書いた問題は、[こちら](https://dev.classmethod.jp/articles/amplify-next-js-isr/#toc-9)の問題が原因だったことが判明したので記述を削除させていただきました。
<details>
<summary>2021/8/2削除</summary>
~~なお「複数環境の場合」と言いましたが、この点に関しても注意点があります。
まず公式に言及されていますが、[Amplifyの公式](https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/server-side-rendering-amplify.html#ssr-and-ssg-branches)ではSSRとSSGのブランチが共存ができないことになっています。
これはおそらく自分の関わっているプロジェクトから問い合わせのエスカレーションで追加記載されたものだと思うのですが、実はこれは単にSSRとSSGの共存だけではなく、AmplifyのSSR時のURL設定の手法の方に本当の原因があります。~~
~~というのも、この時問題になるのがAmplify側の生成するホスティングが効かなくなるということなのですが、これは何も異なるビルド方法の共存が原因ではありませんでした。AmplifyのSSRは裏側で専用のLambda@Edgeを立ち上げて、そのCloudFrontのURLにリダイレクトする形でAmplify Hostingを実現します。この時、リダイレクトに使うのがAmplifyにもともと備わっているリダイレクト設定機能なのですが、これが全環境共通の設定になってしまうため、全ての環境のリダイレクト先が一つの環境に向いてしまうのです。~~
~~そのため、自分たちはAmplify Hostingをやめ、自分たちでルーティングを直接CloudFrontに向けることで解決しました。~~
</details>

話は戻りますが、AmplifyでSSRをするポイントは`Amplify.configure`を実行する場所です。[こちら](https://docs.amplify.aws/lib/ssr/q/platform/js#enabling-ssr)に注意書きがある通り、[このissue](https://github.com/vercel/next.js/issues/16977)が解決するまでは`getServerSideProps`や`getStaticPaths`などを実行するページ全てに記載する必要があります。

つまりファイルとしてはこのような形になると思います。

```typescript
// import文

Amplify.configure({...awsExports, ssr: true })

export const getServerSideProps: GetServerSideProps = async (context) => {
  const SSR = withSSRContext()
  const { params } = context
  // 省略
  return {
    props: {
      ogp_path: hogeImageUrl
    }
  }
}

// コンポーネントのコード
```

こうすることで`_app.tsx`で`pageProps`として値を取り出してあげることができるので、metaタグに設定すれば完了です。
ちなみに今回はSSRで行いましたが、2021年7月28日にひっそりとAmplifyのNext.js11対応とISR対応が[リリースされた](https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/server-side-rendering-amplify.html#update-app-nextjs-version)ため、ISRを利用するというのも一つの手だと思います（検証はしていないため、動作は保証しません。）
（2021/08/02更新：ISRの動作検証の結果、既存のプロジェクトに導入するのはまだできない様子でした。[この記事](https://dev.classmethod.jp/articles/amplify-next-js-isr)にあるように新規プロジェクトで行うとうまくいくようです。）

## 壁③ 公開URLの取得方法

最後の壁は実際にOGPのための画像のURLを取得する方法です。
AmplifyのStorageライブラリを使ってしまうと、有効期限つきのものしか取れないため、別の方法でURLをとる必要があります。
ここまでさりげなく端折ってきましたが、実際にAmplify + Next.jsからS3に画像をアップロードするときは
アップロードし→ファイルのkeyを取得し→それをデータベースに保存しておく
という流れになると思います。このkeyを`Storage.get`に渡すことで本来はURLを取得するというわけです。

実はこのkeyはS3内でのフォルダのパスを表しています。例えばogpフォルダにアップロードした場合は、`ogp/12345.png`といった具合に。
S3でのフォルダの一番トップレベルはアクセスレベルになっているので、`public`に指定してあげた場合は`/public/ogp/12345.png`というのが画像のS3内パスです。
このパスまでわかると、S3の公開アクセスURLは自分で作ることができます。具体的には以下のような形になります。

```typescript
`https://${amplifyConfig.aws_user_files_s3_bucket}.s3.${amplifyConfig.aws_user_files_s3_bucket_region}.amazonaws.com/public/${key}`
```

ホスト名はS3のバケット名とバケットのあるリージョンで決定されます。そのためこれはaws-exports.jsから情報をとってくることができます。
またその下はパスを指定してあげればアクセスできます。そのため`public`であげるということさえ決めておけば、このkeyの部分に先程のファイルのkeyが入ることで正しいURLになるのです。

## まとめ

以上、Amplify + Next.jsで動的にOGPを設定する方法を紹介しました。
かなり裏技を駆使した感じになりましたが、昨年のリリースからなんだかんだSSR対応→ISR対応とかなりのスピード感で、進化してきているのもAmplifyの一面だと思うので、今後もっと簡単にできるようになるかもしれません。
そうだとしても、それまで同じことに悩んでいる人の一助になれば幸いです。