LoginSignup
3
0

[Nuxt3 SSG] nuxt/contentを使ってソシャゲお知らせページを作った

Last updated at Posted at 2024-01-28

約1年程前に終了したとあるソシャゲの、親の顔より見たニュースページが恋しくなり、もう一度見たくなりました。そこで、Nuxt3のSSGモードで、Markdownベースに作り直してみました。

利用したもの

出来上がったもの

サイト

  • ニュース一覧画面
    7b7a1a1d63bc-20240120.png

  • ニュース詳細画面
    d29a28e27b95-20240120.png
    4161652c6494-20240120.png

  • Lighthouseスコア (モバイル)
    ef72c2b8fe07-20240120.png

ソースコード

AGPLv3にしてますが、ビルド設定とか表に見えない範囲はご自由に再利用ください。

試作コンポーネント集

サイトを作り始める前に一部コンポーネントをVuetify Playgroundで作成しました。
設定不要・登録不要でかなり便利なので、Vuetifyご利用の方にはおすすめです。

お知らせ記事の記入例

73198244335a-20240125.png

工夫したところ

i18n対応

右下のボタンから言語を選択するか、URLを直指定で言語毎に違うMarkdownを表示できるようにしました。

レイアウトの再現をしつつレスポンシブ対応化

詳細画面のキャラクター紹介レイアウトをかなり凝って作りました。なおかつ、各種文字列や幅がレスポンシブになっています。

元ネタとしたソシャゲのページでは、デザインの保持を優先したのか、画面幅を元に訪問時一度だけtransform: scale(1.x)をHTML要素全体にふりかけていましたが、こちらはちゃんとレスポンシブしています。

LightHouseのスコア上位を目指した

出来上がった直後のスコアを測ってみると、パフォーマンススコアとアクセシビリティスコアが80ぐらいと微妙なことになっていました。気になったので可能な限り修正しました。

アクセシビリティスコア改善

  • nuxt/i18nの推奨設定に従い html属性に lang="ja" または lang="en"が付くように
  • バッジなどのコントラスト比が 4.5:1を満たしていなかったので修正
  • 画像にalt設定

パフォーマンススコア改善

  • VuetifyのSASSカスタマイズでcolor-packをやめて 使うものだけ手動でスタイル定義。
  • nuxt/imageを使って画像を事前圧縮

開発で詰まったところ

当たり前のことも含みますが初めてのSSGだったので困ったこと等をまとめます。

※ 下記は今回こうやって回避したという情報であり正しいやり方とは思えないのでご注意ください

プロジェクトの設定

ESLint/Prettierのルール設定がよくわからない

毎回初期化時に困っています、個人的に最初の難関です。手動で色々指定しようとしましたが、よく分からなかったので Nuxtの公式(と思われる) @nuxtjs/eslint-config-typescript をextendsしてお茶を濁しました。

VSCodeでPrettierが効かない

いざESLintとPrettierを設定し、VSCodeで保存したところ、何度やってもprettierが効きませんでした。数時間悩みましたが、単に { "editor.defaultFormatter": "esbenp.prettier-vscode" } が足りていないだけでした... 今回使ったVSCodeの設定はworkspaceとしてまとめて上げてありますので、困っている方はそれが参考になるかもしれません。

SSGの設定

SPAとしてビルドしようとするとサーバーAPIも付いてくる

nuxt/contentを使ったプロジェクトをSPAとしてビルドしようとすると、サーバーが一緒に生成されます。よく考えるとわかるのですが、nuxt/contentはAPIサーバーをNuxt3に生やすライブラリのため、SPAにしたいと思っても別途サーバーが必要になる仕様です。つまり、SPA(サイトだけ静的ファイル、Contentは別途サーバーをホストする) / SSG(サイトもContentも静的ファイル) / SSR(サイトとContent両方を1サーバーとしてホストする)の3択になります。サーバーを建てたくない場合、HydrationMismatchからは逃げられません。

動的に読み込むページがSSGで動かない

NuxtのSSGは通常、ページルーティングから検出されたURLを元に、初回描画状態をSSGします。これは例えば index.vueabout.vue[id].vueがあれば、その3ファイルのページ読み込み時の状態だけをSSG結果に含みます。

ここで何に困ったかというと、index.vue内でボタンを押したら動的に次ページを読み込む処理ができませんでした。対応するデータが事前レンダリングされていないため、クリックするとfetchに相当する処理で404が吐かれてしまいます。予め /pages/[id].vue のようなURLを決めてページネーションを置くか、なんちゃって動的ロード (一覧データ自体は最初に全てfetchしてそのうちのn番目までだけを描画する)か、サーバーでのみクライアントから呼び出される予定のAPIを先に読みだす変則的な処理(?)の3択になります。今回は見た目を素早く再現することを優先したかったため、なんちゃって動的ロードとしました。

nuxt/contentで書いたページの生成結果がNotFoundになる

デフォルトだとなぜかnuxt/contentを利用しているページの一部がNotFoundになってしまいます。原因は動的ルーティングの一部がPre-renderの自動クロールに引っかからないことでした。Nuxt3のSSGでNuxt Contentを使用するという記事に記載の処理を少しだけ弄って明示的にページとして認識すると解決しました。ここで指定するアドレスはMarkdownファイルのパスではなく、生成結果、実際にユーザーがアクセスする際のパスなので要注意です。

nuxt/i18nとの組み合わせ

nuxt/i18nのパスとnuxt/contentのパスが上手く噛み合わせられない

nuxt/i18nには、自動的に言語個別のルートを生成するルーティング機能があります。これを言語別にフォルダを区切ったnuxt/contentページと組み合わせる場合、ルーティングストラテジーprefixにする必要がありました。

ストラテジー比較

  • prefix (全ロケールのパスにprefix)
  • prefix_except_default (デフォルトロケール以外のパスだけprefix)
  • prefix_and_default (全ロケールのパスprefix + パスにロケールがなければデフォルトロケール)
    • この2つのストラテジーはSSGでデフォルトロケールがどれに当たるかをnuxt/contentに伝える上手いやり方が見当たらず失敗
  • no-prefix (prefixを一切使わない)
    • このストラテジーの場合nuxt/contentを使ったページだけ jaenが付き不自然なURLになる

nuxt/i18nで書いたページの生成結果がNotFoundになる

nuxt/contentと同様の原因で、/ja/enのケースも想定しないと、SSG結果にNotFoundが発生することがあります、パスを手動で指定しましょう。

なお、indexページだけはprefix戦略では /en等にリダイレクトされてしまうことになり、SSGではリダイレクトを処理してくれずエラーになります。そこでIndexページだけi18nパス設定を無効にしました。

ロケール変更時nuxt/i18nのロケールとnuxt/contentのURLを同期する

nuxt/contentの公式サイトに記載がありそうなユースケースですが、見つけられずかなり難航しました。最終的にかなり荒業ですが、nuxt/i18nのロケール変更時、Cookieへ変更先の言語を代入し、ルーター上の言語に相当する文字列を置き換えて別のnuxt/contentのページに遷移させることで解決させました。

nuxt/contentの利用

自作コンポーネントをMarkdown内で使う方法がわからない

nuxt/contentのドキュメントをしっかり読めばちゃんと記載があるのですが、すぐには見つからず困りました。下記に対応するリンクを記載します。

Markdown内の改行が反映されない

改行を改行として認識してもらう

そもそもnuxt/contentでは 1行の改行を改行として認識してくれません。
そのため初めは <br>を大量に書いて誤魔化していました。
この仕様はremark-breaksを使うと回避できます。

マージンを整えるパーツを作成する

しかし、remark-breaksを使ったとしても、2行連続する改行は1行の改行にまとめられてしまうというのが仕様になっています。
先駆者さんはNitroPluginを書いて回避していました。が、今回はVuetify3を使っていることから<br>を連打するより、高さ付きの透明なv-dividerを使うと、実装シンプルで汎用性が高いと感じたのでv-dividerをラップするようなコンポーネントを作成しました。

ホットリロードすると 数バージョン前の結果が表示される

Composableを使って処理を共通化しているためか、ホットリロードすると古いMarkdownを元にしたページがレンダリングされてしまいました。フルリロードするか、ビルドすると正しい結果が描画されたため、ここは耐えて妥協しました...

nuxt/imageの利用

バージョンがよくわからない

Nuxt3用は v1系 (現時点最新は@v1.3.0)、Nuxt2用は v0系です。何も指定せずに単に pnpm install nuxt/image とすると なぜか v0系(@v0.7.X)がインストールされますが、それはNuxt2用ですので、バージョンをよく確認しましょう。

Vuetifyと組み合わせる方法がよくわからない

公式サイトにピッタリな例があったため、それをほぼ再利用し、ProseImg.vueとして nuxt/contentのデフォルトコンポーネントを上書きしました。

GithubPagesへのデプロイ

GithubPagesのビルド結果がおかしい

ビルド結果にWelcome to nuxt! が出てくる

ちゃんとcheckoutして pnpm installも通っているのになぜ?? と頭を抱えざるを得ませんでした... 原因はGithubActionsが自動検出して推奨してきたactions/configure-pages@v4が Nuxt2想定(nuxt.config.js)のアクションで、Nuxt3 (nuxt.config.ts)を検出してくれず、新規ファイルを作成されるためでした。結局 actions/configure-pages@v4の利用は諦めて、手動で代替となるGithubPages用のサブパス対応処理を nuxt.config.tsに設定しました。

ビルドしたページの画像が出ない

nuxt/contentの画像パスは cdnUrlを尊重せず、記載された通りのパスを忠実に描画しようとします。Issueによると、nuxt-content-assetsというライブラリを使うと、markdownと同一パスにある画像ファイルを解決可能になりますが、自環境ではProseImg.vueで上手くレンダリングが行えませんでした。

苦肉の策として、ランタイム設定のbaseURLを読み出して何もなければlocalhost、それ以外ならGithubのURLを返すようなcomposableを作り、それを画像URLに返すことで回避しました。
https://github.com/Dosugamea/recreate-news/blob/main/src/composables/useImageLink.ts

完走した感想

Wordpressをカスタマイズした方が早そう

内容がかなりシンプルなので、プロジェクトを作らずともWordpressのスタイルをカスタマイズしたほうが再現という点では早そうでした。ですが、どうしてもサーバーを保守したくないのでやはりSSGにするしかなかったかなとも思います。

SSGが難しい

Nuxt3の開発体験はまぁまぁ良いですが、nuxt/content、nuxt/i18nを使ったSSGはイマイチでした。もっといいやり方もありそうな気がしますがあまり資料が見つけられず微妙な実装に... 最後に入れた nuxt/imageについてはそれほど躓くことなく、すんなりSSGすることができました。

デプロイが難しい

サブパスを使うデプロイをしようとしたから、というのがあるのですが、丸2日程引っかかりました。正直もう二度とやりたくないので、次からは手持ちの適当なドメインを持ち込んでデプロイします。

今後できればしたいこと

ぱっと見は悪くないですが、ところどころ微妙です。
下記は時間をかければ直せそうな気もしますが疲れ尽きたので一旦これまでにします...

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0