👏

Next.js×microCMS製のメディアサイトをリニューアルした話

2021/07/02に公開

マナリンクCTOの名人です。

2021年7月1日に、弊社が運営しているマナリンクTeachersというメディアサイトをリニューアルしました!

https://for-teachers.manalink.jp/

このメディアサイトはNext.js×microCMSで構築されています。初回リリースは2020年秋ごろだったので、半年以上運用してのリニューアルとなりガッツリ実装を書き換えました。

そのため本記事では前回の復習も兼ねて、リニューアルの流れやTipsをまとめてみようと思います。

メディアの概要

マナリンクTeachersは、オンライン家庭教師向けの情報をまとめているメディアサイトです。オンライン家庭教師という職業自体がまだメジャーではないため、先にオンライン家庭教師を始めた方々のインタビュー記事であったり、確定申告のやり方や、いろいろな教育系の職業の人に役に立つ情報を発信するメディアとしてリリースしました。

現役エンジニアのかたわらオンライン授業をやっている佐々木先生のインタビューとかは、本記事の読者のみなさんでも面白く読んで頂けると思いますのでぜひご一読頂けると嬉しいです。

https://for-teachers.manalink.jp/interview/beshxitce_sc

リニューアルのBefore/After

Before

FireShot Capture 001 - マナリンクTeachers - オンライン家庭教師の働き方・採用を応援! - for-teachers manalink jp

After

FireShot Capture 008 - マナリンクTeachers - オンライン家庭教師の働き方・採用を応援! - for-teachers manalink jp


初期リリース時のBeforeはシンプルなトップページでしたが、Afterではコンテンツの種類が増え、サイドバーも情報量が増えています。

大きな変更として、記事に中カテゴリ、小カテゴリが追加されました。

Afterのサイドバーにある「カテゴリ」を見ていただくと、記事にカテゴリが存在するのがわかると思います。Beforeではカテゴリは全く無かったので、新しい仕様になります。

簡単にリニューアルの背景を説明します。初期リリースではとりあえず出した、という感じだったのですが、いろいろな記事を出していく中でSEO的に評価が高かったり、実際に家庭教師の方から評判がいい記事など傾向が見えてきました。そこでこれから情報発信に注力していく分野を厳選したいという意思決定がメディアチームの方であったので、それに合わせてリニューアルをした感じです。

リニューアルの流れ

大まかに以下のような流れでリニューアルをしました。

  • メディアチームから企画説明
    • Google SpreadsheetとGoogle Slideでサイトマップとワイヤーフレームをあらかじめ作って持ってきてもらったので話がしやすかったです
    • GSuiteは強い
    • 日頃からmicroCMSから入稿してもらっているため、microCMSでこのようなワークフローでできると嬉しいのですがどうですか、といった提案ベースで要件を持ってきてくれるので議論もしやすかったです
  • 実装(今回は私がデザインも含めてやりました)
  • VercelのPreview Modeでデプロイして、動作確認
  • リリース

実装のTips

microCMSで中カテゴリ、小カテゴリを実現する

今回のリニューアルではmicroCMSで入稿した記事にカテゴリがついています。URLでいうと以下のように記事のIDの前にいくつか階層がある感じです。

https://for-teachers.manalink.jp/independence/taxreturn_manual/hxevcsq180

https://for-teachers.manalink.jp/useful/trouble/katekyo/o_49pqxheq

今回これを実現するには、microCMSのAPI定義で、カテゴリを追加すればいいです。
ただカテゴリを追加するだけではなく、コンテンツ参照という機能を使うことが重要です。

https://blog.microcms.io/lets-relation/

つまり、中カテゴリ、小カテゴリ一覧用のAPIをそれぞれ作成し、各記事から参照するようにAPI定義を定めればいいです。

スクリーンショット 2021-07-02 11 10 25

また、細かいところですが、カテゴリ一覧用のAPIに対してディスクリプション用のカラムを追加しておくことで、カテゴリ一覧の一覧ページでのdescriptionタグを動的に設定することができます。

コンポーネントの粒度

続いてよく言われるコンポーネントの粒度どうするか問題です。個人的な考えとしてAtomic Designにまるっと従うのはドメイン知識の存在を無視していて好きではないので、以下のように分類しました。

  • Pageコンポーネント
    • 各ページまたは画面に1つ対応している。Next.jsなどのフレームワークではpagesといったディレクトリがあるので、明示的にディレクトリを自分で切るわけではない
  • Layoutコンポーネント
    • commonレイアウトコンポーネント
      • ヘッダーやフッター、グローバルナビゲーションなどのアプリケーション内で統一された部品
    • wrapparレイアウトコンポーネント
      • 既存のコンポーネントに対して、あとから周囲のコンポーネントとのレイアウトに必要なものを付与する
  • Domainコンポーネント
    • ドメインオブジェクトを表示する責務
    • DomainコンポーネントはCSSを当てない
  • UIGroupコンポーネント
    • 複数のDOMのかたまりを表示する責務
  • UIPartsコンポーネント
    • ほとんど単一のDOMにCSSを当てただけのコンポーネント

この分類で今回のリニューアルでは漏れが見当たらなかったのですが、細かいところでcommonレイアウトコンポーネントとUIGroupコンポーネントの差が主観になってしまうところなど改善点が残っている気はしています。

ユースケース

メディアサイトでは、いろいろなコンポーネントで似たようなAPIを叩くし、後述するようにmicroCMSから取得した単純なデータをView用にコンバートしたい場面などがありますので、ユースケース層を明示的に切りました。

いろいろと省略しましたが、以下のように型定義を格納するディレクトリと、クエリ別にファイルを分けているディレクトリと、Presentation用のフォーマットなどを格納するディレクトリに分けてみました。

articles
├── domain
│   └── entities
│       ├── interview.ts
├── presentation
│   ├── formatDate.ts
└── queries
    ├── fullTextSearch
    ├── interview
    │   ├── findInterview.ts
    │   └── getInterviews.ts

microCMSで取得した複数種類の記事を同じ一覧に突っ込む型定義

今回頭を悩ませた要件が、「複数種類の記事から、更新日順にソートして最新のN件をトップページに表示したい」というものです。

microCMSは僕が把握している範囲では横断検索できる手法が無いため、愚直にPromise.allでまとめて取得してsort関数でソートしました。

加えて、表示する記事カードコンポーネントではそれぞれの記事に対応したページへのhrefを表現する必要があります。しかしmicroCMS上ではそれぞれの記事の中身は全く同じ構造をしています。なのでそのままデータをView層まで渡しちゃうとhrefを生成できないのです。

前節で話したクエリ関数にて、それぞれのAPIに応じてtypeプロパティを付与することで、コンポーネント側で型安全にhrefを生成しています。

こんな雑な説明で説明になっていない気がするのでとりあえずコードの断片を貼っておきますので察してください・・・w

export type Interview = Readonly<
  MicroCmsArticle & { type: 'interview' }
>;
export const getArticleHref = (article: MixedArticle) => {
  switch (article.type) {
    case 'interview':
      return pagesPath.interview._id(article.id).$url(); // pathpida
      // 以下略
export type RawResponse<T extends { type: string }> = Omit<T, 'type'>;

type TypeAnnotate = {
  (val: RawResponse<Interview>, type: Interview['type']): Interview;
  // 以下略
};

const typeAnnotate: TypeAnnotate = (val, type) => {
  return {
    ...val,
    type,
  };
};

export const convertInterview = (article: RawResponse<Interview>) => typeAnnotate(article, 'interview');

フリーワード検索

microCMS側で全文検索できる機能が提供されているので、それでフリーワード検索が実装できました。またカラム別の検索はできないらしいので、タイトルだけ、本文だけから検索するような要件はできなさそうです。

ちなみにほとんどのページではISRなのですが、全文検索だけ当然ながらSSRです。Next.jsではページごとにISRとSSRを切り替えられることを初めて有効活用できた気がします。

スライダーの実装

トップページ上部に表示されているスライダーの実装はけっこう頑張っていて、左右に進めていくと無限に記事が続いているように見えます。これは理屈としてはある程度端っこに近づくと反対側にtranslateXを飛ばすことで無限に続いているように見せているので、DOMをとんでもない量生成しているわけではないです。

ただ3時間くらい掛かってけっこう大変でしたし、ちょっと動きが安定していないし、 https://react-slick.neostack.com/ のような便利そうなライブラリもあるのでこれらを使ったほうが良いと思います。

Twitter埋め込み

普通にiframeを貼るとページ遷移で再マウントが走ったタイミングで正常に表示されなくなっちゃいます。

https://www.npmjs.com/package/react-twitter-embed

このパッケージを使いました。

内部の実装を軽く読むと https://www.npmjs.com/package/scriptjs を使っているみたいです。twitter widget用のJSを正常に読み込むために使っているっぽいです。

リダイレクト

リニューアルでURL構造がガラッと変わったのでSEO的にリダイレクトはしなければいけません。

next.config.jsを使ってリダイレクト設定が簡単に書けました。めっちゃ便利です。

  async redirects() {
    return [
      {
        source: '/operation/XXXX',
        destination: '/independence/YYYY/XXXX',
        permanent: true,
        // 以下略

今回、メディアチームのインターン生にリダイレクトすべきURLを上記の形式であらかじめ書き出してもらうようにお願いして、Google Documentに書き出してもらったので、コピペで設定が済んでとても楽でした。最初CSVでリストをもらう予定だったのですが、Next.jsの設定が上記のように一定の規則で文字列を書いていくだけでOKだったため、社内でそういう風に情報伝達できてよかったです。

内部リンク

Next.jsで内部リンクを表現するときはpathpidaが便利です。

https://github.com/aspida/pathpida

まとめ

全体を通してNext.jsやっぱり便利だなーという感じです。ISR速いし。pathpida使えるし。割と薄いから設計自由にできるし。

初期の要件だともっとミニマムな技術選定でも良かったはずですが、いずれもっと複雑化した要件になると張ってNext.jsを選んでいたので、功を奏している感じもします。

エンジニア以外のチームとも積極的にコミュニケーション取りながらいいサービスを作ろうとしている体制なので、そんな環境でモダンな技術で効率よく開発していきたい方はぜひカジュアル面談しましょう!

マナリンク Tech Blog

Discussion