Next.js + RailsでポートフォリオサイトをISR対応＆メンテナンスフリー化した

2年ほど前に[Nuxt.jsを使ってポートフォリオサイトを作成](https://qiita.com/Y_uuu/items/f3e3382ca9f9f22e6677)しました。
今回、このサイトをNext.js + Railsでリニューアルしたので、経緯を記事にまとめます。

## リニューアル後のページ

https://portfolio.y-uuu.net/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/c37c9589-179b-16fe-42dc-4b3ffa4c9c65.png)

デザインは[前回のもの](https://old.portfolio.y-uuu.net)を踏襲していて、ほとんど変わっていません。

## リポジトリ

- フロントエンド: https://github.com/yuuu/portfolio_v2_ui
- バックエンド: https://github.com/yuuu/portfolio_v2_api

## リニューアルの目的

### Next.jsを使って何か作りたい

![nextjs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/9eca2758-1fa9-ee8f-75ea-380805574699.png)

昨年からReactやNext.jsを触ってノウハウを蓄積するようにしています。私自身普段はRailsを使った開発をしているので、Next.jsを採用するとしたらRailsと組み合わせて使う可能性が高いです。

昨今のフロントエンド界隈の盛り上がりを横目に、フロントエンドにNext.js/バックエンドにRailsを用いて、何か作りたいと思っていました。

### デプロイなしで内容を更新したい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/c99c9435-ab5b-e379-9686-8ec8c06a4da5.png)

前回、勢いでポートフォリオサイトを作成したものの、単なる静的ページとして公開していたので記載内容を変更するためにはVueコンポーネントを直接編集する必要がありました。

今回は管理ページを別途作成し、ログインすることで記載内容を容易に追加・変更できるようにしています。

### メンテナンスフリーにしたい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/9b852e77-deca-750c-74db-bf859c6e9f4b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/18e0901e-675e-5d0f-cf3e-ba58318fd008.png)

リニューアル後のサイトではQiitaやZennに公開した記事、SpeakerDeckに公開したスライドをを自動的に収集し、メンテナンスしなくても内容が自動更新されるようになっています。

## システム構成

Next.jsのデプロイ先としてVercelを、Railsのデプロイ先としてherokuを使っています。
また、画像の格納先としてAWSのS3を利用しました。

![Untitled_Portfolio_Site_-_Cacoo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/e42ecc1f-6fef-4afb-612c-bdd72c7544d2.png)

## ライブラリ・フレームワーク

### フロントエンド

- React
- Next.js
- React Hook Form
- react-dropzone
- react-spinners
- react-tippy
- axios
- SWR
- Tailwind CSS

### バックエンド

- Ruby on Rails
- devise
- faraday
- rails_same_site_cookie
- AWS SDK for Ruby V3

## 実装上のポイント

### ISR(Incremental Static Regeneration)

Next.jsを使ってISRを実現しています。herokuがレスポンスを返す時間に関わらず、来訪者がすぐにページを閲覧できるようにする狙いです。

以下はISRの挙動の解説です。

Next.js(ISR有効)は、アクセスがあった際に生成済みの静的ページをレスポンスします。このとき、herokuへのアクセスは発生しません。

![Notification_Center.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/bf6f0fd6-1175-eb90-2883-1c3c5f0f1b7c.png)

前回のページ生成から指定した時間を経過した後にアクセスが発生すると、静的ページを再生成します。このときNext.jsはサーバーサイドでページを再生成するのを待たず、いったん前回の静的ページをレスポンスします。

![Untitled_Portfolio_Site_-_Cacoo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/21b46688-e76f-785b-283b-b72df6b5e3e1.png)

再生成が完了すると、以降その静的ページをレスポンスします。

前提として、herokuのFreeプランだと30分間アクセスがない場合にdynoがSleepするので、次にアクセスがあった場合にdynoが起動するまで数十秒ほど待たされてしまうという問題があります。本来の使い方ではないかもしれませんが、バックエンドの処理に時間がかかる場合でも生成済みのページを即時にレスポンスできるという点で、ISRは有用だと感じました。

### 記事・スライドの自動収集

![yuuu-portfolio-v2-api_·_Scheduler___Heroku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/d87132b0-fa73-39f8-fb68-fb317abe2700.png)


Heroku Schedulerを使うことで、日次でrakeタスクを実行して、記事・スライドを自動収集しています。

Qiitaは[API](https://qiita.com/api/v2/docs#get-apiv2authenticated_useritems)を、Zennは[Feed](https://zenn.dev/zenn/articles/zenn-feed-rss)を使って、自分自身の記事を収集しDBに保存しています。

```ruby
namespace :qiita do
  desc "Fetch articles from qiita"
  task fetch: :environment do
    res = Faraday.get('https://qiita.com/api/v2/users/Y_uuu/items?per_page=100')
    return if res.status != 200

    items = JSON.parse(res.body)
    items.each do |item|
      next if Article.find_by(link: item['url']).present?

      item_res = Faraday.get(item['url'])
      next if res.status != 200

      Article.create(
        title: item['title'],
        body: item['body'].truncate(100) + '...',
        published_at: Time.zone.parse(item['created_at']),
        link: item['url'],
      )
    end
  end
end
```

SpeakerDeckは収集方法を悩んだのですが、よくよく調べると https://speakerdeck.com/yuuu.atom のように、自身のアカウント名の末尾に `.atom` を付与することでFeedを取得できることがわかったので、これを使って収集するようにしました。

### 認証

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/1ee4b94d-6205-147d-fb30-e995aa8524e4.png)

最初は「SPAの認証はJWT」という思い込みがあったのですが、いろいろ調べていくうちに「cookieを使った認証でも問題ない」との結論に至りました。認証のバックエンドもRailsで、deviseというGemを使ったよくある実装です。

ただし、今回はCrossOriginな構成のためつまづきポイントが多くありました。具体的な実装方法は別記事にまとめたので、興味のある方は参照ください。

[Rails 6.1対応版: APIモードのRailsに対してCrossOriginなSPAからSession認証する方法](https://qiita.com/Y_uuu/items/1651b181f06499c5cede)

### ファイルアップロード

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/81ef5447-6f35-7fa0-9601-fc07d69d3873.png)

当初は、バックエンドがRailsということで、Active Storageを使ってファイルアップロードを実現する予定でした。実装をしていく上で「わざわざActive Storageを使う必要があるのか？」という疑問が生じ、最終的にはS3の署名付きURLを使ってアップロード・閲覧する方式に変更したという経緯があります。

こちらも別記事にまとめたので、興味のある方は参照ください。

[RailsをバックエンドとしたSPAでのファイルアップロード機能の作り方に悩んだ話](https://zenn.dev/y_uuu/articles/baaee6d181fda7)

## 感想

### ISRが良い

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/92ac1775-1a5b-7c08-f06b-7cc6c43343d5.png)

SSRとSSGのいいとこ取りができていて良いです。SSGのようにデプロイのビルドが長くなることもなく、かつSSRを使った場合に比べてページの表示が高速なので満足です。

### Vercelが良い

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/d78d3a4b-cd87-7dd8-d46e-2c02c4d15a04.png)

今回初めてVercelを使ってみたのですが、GitHubのリポジトリを指定するだけで簡単にCI/CDを構築できました。前回Netlifyを使った時も同様の感動があったのですが、とかくNext.jsを使う場合はほとんど設定が不要で、噂通りVercelとの組み合わせがベストだと実感しました。

### バックエンドのRails・herokuも良い

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/7b3e1630-7d5f-176b-2c0d-603b11f5dc78.png)


死んだと言われて久しいRailsですが、自分にとってはやはり最速で実装ができるフレームワークです。バックエンドは必要最低限実装しつつ、フロントエンドの実装に注力するスタイルで開発が進められました。

herokuを使うことでデプロイも非常に簡単でした。

## まとめ

個人的には十分満足できるポートフォリオサイトが完成しました。

Next.js + Railsで何か作ろうとしている人の参考になれば幸いです。質問・感想などありましたら、ぜひコメントをお願いします。