Aokashi Room

作った作品の紹介やレビュー、トラブルシューティングとか色々

WWA FanSquare のサイトを Hugo から Next.js に移行した話

自分は5年くらい前に WWA FanSquare というサークルを設立しました。最初は WWA の薄い本の販売から始まりましたが、次第に WWA COLLECTION といったゲームパブリッシングのお手伝いをしたり、 WWA コミュニティを管理したりしていますが、 WWA コンテストの応募や投票の (ほぼ) 全自動化を実現するために、サイトのシステムを丸々変えなきゃいけなくなりました。

そこで、 WWA FanSquare のサイトを HUGO から Next.js に移行しましたので、移行にあたって大変だった点などを紹介したいと思います。

WWAを使用している人にとっては分かりにくい内容ばっかりなので果たしてこの記事に需要があるかどうか分かりませんが、いかに移行が大変だったか分かってくれたら嬉しいと思ってます。

www.wwafansq.com

スペック

移行前サイト

  • 静的サイトジェネレーターの HUGO で HTML を生成
    • お知らせ記事は Markdown で記述し、各製品の紹介ページは HTML に直接記述
  • デザインフレームワークMaterial Design Lite (MDL) を使用
  • ボタンなどよく使用する部品についてはショートコード機能で共通化
  • JavaScript の実行が必要な場合は素の JavaScript を記述し、 HTML 末尾に script 要素で呼び出し

gohugo.io

getmdl.io

移行後サイト

  • Next.js で構成し、静的 HTML 生成機能で HTML を生成
    • ソースは TypeScript で記述
    • お知らせ記事は引き続き Markdown で記述、変換は remark を使用
  • デザインフレームワークは MUI を使用
  • ボタンなどよく使用する部品についてはコンポーネントで共通化

nextjs.org

mui.com

移行作業以前の2019年くらいに試しに移行したテストサイトもあって、この場合は Nuxt.js と Vuetify で構築していました。

要点

  • 楽だったところ
    • アイコンの名前と中身が MDL と MUI で共通だった
    • CSS ファイルをそのまま使用することができた
    • 使用しているデータが JSON だったため、そのままソースに移行できた
  • 大変だったところ
    • Next.js の Image コンポーネントと今回の製品ページとの相性が悪かった
    • MDX の柔軟性の低さ
    • Grid の単位が MDL と MUI で違いがあった

それぞれの移行について

お知らせ記事

前述の通り、前は HUGO から Markdown ファイルを使用して HTML を生成していましたが、 Next.js には標準で Markdown を JSX に変換する機能は備わっていません。

どうやって Markdown から JSX の変換するかについてですが、最初は MDX、そしてうまくいかなくて remark、そしてその後 markdown-to-jsx という感じで落ち着きました。

ということで最初は公式サイトのドキュメントの通り MDX を使用していました。

nextjs.org

MDX は React コンポーネントをそのまま Markdown で使用できるので「お知らせ記事ちょっといじるだけで使用できるじゃん!余裕!w」と思っていましたが、デフォルトで使用するレイアウトが定義できない問題が見つかり、「なるべくシンプルに Markdown が記述できる」が実現できなくなることから、使用をやめました。

次に使用したのが remark です。 Markdown の文字列から HTML に変換するため、一度ファイルを読み込む必要があります。

remark は一度 AST という加工しやすいデータに変換しなくては行けないため、実装は少し面倒です。その代わりプラグイン機能などがあり柔軟性に優れています。

const Markdown = ({ mdContent }: Props) => {
  const [html, setHtml] = useState('');
  useEffect(() => {
    remark().use(remarkHtml).process(mdContent).then((file) => {
      setHtml(file.toString());
    });
  }, [mdContent]);
  return (
    <div dangerouslySetInnerHTML={{ __html: html }} />
  );
};

また、タイトルなどのメタ情報を取り出す必要もあるため、 gray-matter も使用しました。

---
title: "2022年夏、 WWA FanSquare のサイトが新しくなります!"
date: "2022-07-17T23:00:00+09:00"

image: "/assets/information/2022-new_wwafansquare_site.png"
--- <- gray-matter はこれを境にメタ情報と内容を取り出す

WWA FanSquare のサイトが新しくなります。

## 特徴
- ページ切り替え時のレスポンスが速くなります
- WWA Contest の応募システムと投票システムが組み込まれます

とは言っても、初めて使用するときはエラーの連続でした。

  • ファイルを読み込む際に使用する fs モジュールが見つからない → Next.js の仕様でどこでも Node.js のモジュールを使用することができないので、各ページコンポーネントの getStaticProps 関数を定義してそこに使用する必要がある
  • getStaticPaths is required for dynamic SSG pages and is missing for '/information/[id]'. と出てくる → サイトマップみたいなものを Next.js に知らせないと、生成時どのファイルを使用すれば良いのかわからず実行できないようなので、 getStaticPaths 関数を定義して生成したいページの Path を出力する必要がある
  • Frontmatter の日付データからエラーが出ている → getStaticProps 関数で出力できる内容はシリアライズ化できる変数のみで、 Date オブジェクトのようなインスタンスは持っていくことができない、手間はかかるものの Frontmatter で文字列に変換することにした

これで remark で落ち着くかなと思ったのですが、お知らせ記事で使用するボタンを React のコンポーネントで代用しようとしたところ、 remark では面倒なことがわかりました。

よくよく考えると remark は HTML に変換するので任意の React のコンポーネントを埋め込むのは難しいのでしょう。そしたらそのまま JSX に変換するのが手っ取り早いので、ここで markdown-to-jsx を使用しました。

markdown-to-jsx は Markdown 文字列を与えるだけで文章が展開される超簡単コンポーネントです。先程貼り付けた Markdown コンポーネントをそのまま代用することができます。

github.com

どうして最初から使用しなかったのだろうか・・・。

余談ですが、お知らせ記事は移行に併せて内容を CMS で管理するつもりでした。

が、最初使用を予定していた microCMS では API 数の制限が厳しいのと、内容を変換してまでする余裕があまりなかったことから、引き続き Markdown ファイルによる管理になりました。

WWA の冒険書 (WWA の薄い本) 紹介ページ

Next.js の JSX は HTML に近いため、

  1. HTML をそのまま移行
  2. 閉じタグエラーを修正 (<br><br /> に変換)
  3. 画像ファイルを修正
  4. CSS を移行
  5. MDL の構成要素を MUI のコンポーネントに変更
  6. 余白を調整

の通りに修正を施すのが主な作業になります。

対象が違いますが、 WWA COLLECTION 2 の紹介ページを HTML だけそのまま移行しただけでは下記の通りになります。

なので、 Next.js + MUI の環境に合わせる作業を行う必要があるのです。

CSS についてですが、このページは MDL のデザインだけでは物足りないため、不足分を CSS で実装しています。この辺は CSS ファイルをそのまま移行して、 Next.js の _app.tsx から移行した CSS ファイルを記述することで、 CSS ファイルを移行することができます。

// _app.tsx
import { AppProps } from "next/app";

import "../src/assets/legacy_common.css";
import "../src/assets/works/wwabook_20th/wwabook_20th.css";
import "../src/assets/works/wwa_collection/wwa_collection.css";
import "../src/assets/works/wwa_collection_2/wwa_collection_2.css";

export default function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

ただし、 MDL の CSS は含まれていないので、 MDL の構成要素を一つ一つ MUI に移行する必要があります。

<div class="mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--4-col-phone mdl-card mdl-shadow--2dp">
    <div class="mdl-card__title">
        <h3 class="mdl-card__title-text">DLSite.com</h3>
    </div>
    <div class="mdl-card__supporting-text">
        <p>すぐに読みたい方はDLSite.comにて、PDF形式で販売しています。</p>
        <p>お手持ちのスマートフォンや電子書籍に保存しておくといつでも見ることができます。</p>
    </div>
    <div class="mdl-card__actions mdl-card--border">
        <a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" href="http://www.dlsite.com/home/work/=/product_id/RJ199732.html">
            購入する
        </a>
    </div>
</div>

上の MDL を使用した HTML を下記の通りに MUI を使用した React に置き換えました。

<Grid item sm={12} md={6} lg={4}>
  <Card>
    <CardContent>
      <CardTitle component="h3">DLSite.com</CardTitle>
      <p>
        すぐに読みたい方はDLSite.comにて、PDF形式で販売しています。
      </p>
      <p>
        お手持ちのスマートフォンや電子書籍に保存しておくといつでも見ることができます。
      </p>
    </CardContent>
    <CardActions>
      <Button href="http://www.dlsite.com/home/work/=/product_id/RJ199732.html">
        購入する
      </Button>
    </CardActions>
  </Card>
</Grid>

ちょっと厄介だったのが Grid 周りの仕様です。 Grid は項目を横に分割したい場合に使用していますが、分割単位が MDL と MUI で異なります。

  • MDL: スマホサイズだと幅は4分割、タブレットサイズだと幅は8分割、PC サイズだと幅は12分割
  • MUI: デバイスやウインドウの幅関わらず常に12分割

上記の置き換え前後のコードを見ると、 MDL はすべて 4 で記述していたのに対し、 MUI では幅毎に記載しているのがわかると思います。

そして移行していて大変だった箇所が画像周りです。 Next.js の画像と言うのは、遅延読み込みなどのパフォーマンス向上のために様々な機能を付与しているのですが、これが使用している紹介ページとの相性が悪く、表示されなかったり、クソデカく表示されたりします。

  • まず、画像の読み込み Path を変更。 public ディレクトリ下だと文字列で読み込めたようですが、気付くのが遅く、ゲーム一覧データから取り出すなど以外は直接 import して読み込んでました。
  • 画像の要素を img から Image コンポーネントに変更。
  • width と height を明示的に指定。 周りの要素に応じてサイズが変化したりするなど、使用場面が特殊な画像は指定する必要があります。
  • layout 属性や objectFit 属性などを指定して調整。
  • それでも見えなかったりする場合は CSS で調整。

という感じで移行していました。

<img class="wwabook-20th-logo" src="wwabook_20th-logo.png" alt="World Wide Adventure 20th Anniversary Book - WWAの冒険書 - 20年の軌跡をQuickLoad!!" width="683" height="192">

上記の HTML が下記の JSX に変わりました。

<Image
  className="wwabook-20th-logo"
  src={WWABook20thLogo}
  alt="World Wide Adventure 20th Anniversary Book - WWAの冒険書 - 20年の軌跡をQuickLoad!!"
  width="683"
  height="192"
/>

幸い、 CSS で使用する背景画像については大きな影響はありませんでしたが、 img 要素を背景として使用しているタイトル部分については細かい調整が必要になったと思います。

WWA COLLECTION 紹介ページ

作業手順については薄い本紹介ページと変わりませんが、移行すべき箇所が多く、手数は多くなりました。薄い本の紹介ページに加えて、下記の要素も含まれます。

幸いだったのが、使用しているお知らせや収録ゲーム一覧のデータが JSON で記載していたことで、そのまま TypeScript のソースに移行することができました。

ルーセルも、 SwiperJS を使用していたのですが、 Swiper にも React 版が存在しているのでそのまま React 版に移行することで大きな編集や調整なく移行できました。

「交差オブザーバーによるコンテンツ移動」とは、ユーザーがスクロールすると特定の要素が外から飛び出したりする演出で、

  • WWA COLLECTION の紹介ページではスライムやプレイヤー、シルクハットマンの移動に
  • WWA COLLECTION 2 の紹介ページでは WWA COLLECTION の解説に出てくる3つのカードの移動に

使用されています。

「交差オブザーバー」はある要素がスクロールで見えたかどうかを検出する仕組みのことみたいですね。

以前はスクロール位置の比較で使用していましたが、 IE を気にしなくていい現代には Intersection Observer という便利なブラウザー API が追加されています。

というか、 Next.js は React を使用しているので react-intersection-observer で簡単に実装することができます。

import { useInView } from "react-intersection-observer";

const { ref: slimeRef, inView: slimeInView } = useInView();

return (
  <Grid
    ref={slimeRef} // これがスクロール中に見えたら slimeInView は true になる
    item
    xs={6}
    sm={4}
    className="wwa-collection-introduction-first-image"
  >
    <Slide direction="right" in={slimeInView}> // slimeInView が true になったらスライムの画像が飛び出る
      <Box height="100%" position="relative">
        <Image
          src={SlimeImage}
          alt=""
          layout="fill"
          objectFit="contain"
        />
      </Box>
    </Slide>
  </Grid>
);

飛び出る演出については MUI の Slide コンポーネントを使用しましたが、この飛び出し方は移行前とくらべて挙動が異なります。

移行前の演出

移行後の演出

移行前から変えたくなかったんですが、なかなか機能しなかったので、 MUI にあった Slide コンポーネントで代用することになりました。

ちなみに、 Intersection Observer は隠れるとオフになるため、一度飛び出る箇所から離れてもう一度近付くと同じように飛び出る演出が出ます。

と、以上が、 WWA COLLECTION と WWA COLLECTION 2 共通の紹介ページで行ったことですが、 WWA COLLECTION 2 になるとやや大変な箇所が現れました。

WWA COLLECTION 2 の紹介ページ

タイトルの背景にくじけました。 無限スクロールが思ったほど機能していないのです。

タイトルの背景は各収録作品のスクリーンショットがランダムで敷き詰められ、無限にスクロールされます。これを実現するために、各収録作品のスクリーンショットを敷き詰めた要素を2つ用意して左右に並べた状態で動かしています。

しかし、その敷き詰めた要素の片側が真っ白になって機能していないのです。最初は Next.js の遅延読み込みの誤動作なのか? と思いましたが、原因は CSS の調整で敷き詰めた要素が左右に並ばなくなり、もう片方が下に隠れてしまったようでした。

とは言え、 CSS の調整が必要だったのも、 Next.js の画像の仕様によるものなので、 Next.js が原因となって発生したことに変わりはありません。

その他

MDL も MUI も同じ Material Design の仕様に沿ってはいるんですが、微妙に余白サイズが異なっていたり、構成要素が異なっていたりしているため、元を完全に再現しているわけではありません。なるべく最小限にしつつなるべく最大限の機能を発揮できるように移行してみましたが、 Next.js も MUI も初めて触れるので、多分完璧じゃないかもしれません。

あと、個人的にはこういった構成技術が数年後には廃れていく可能性もあるので、これらのページの移行作業を数年毎に行うのかと思うと、自分が行った作業以上に大変に感じた気がします。 JavaScript のソースが埋め込まれているので、脆弱性の観点から放置するわけにも行きませんし。

今回の目的は WWA Contest にあると思うので、ぜひ、 WWA Contest 2022 をよろしくお願いしま~~~~~す。