🎨

Next.js 14・TailwindCSSでのフロントエンドセットアップ手順

2023/12/23に公開

前提

フォントや色味など、どういう手順でセットアップしてから開発に進むとスムーズか、検証を兼ねた備忘録を作成しました。
(Next.js・TailwindCSS・Storybookの導入までは終わっている前提です。)
Next.js 14でのFont最適化の話が多めとなります。
実作業内で見直しがあれば随時追記していきます。

環境

Next.js 14(App router)・Typescript 5・TailwindCSS 3・Storybook 7などを使用しています。


StorybookでTailwindCSSが動くようにする

(23/12/25追記)若干導入の話寄りですが、やり忘れていたので書きます。
基本的な設定はとても楽になっており、以下のコマンドを実行するだけです。

npm install --save-dev @storybook/addon-styling
node node_modules/@storybook/addon-styling/bin/postinstall.js

yarnでの方法はこちらにまとめられています🙏
https://zenn.dev/nbstsh/scraps/9c992a12425c2c

@のパスエイリアスが通るようにする

Next.jsによりimportの際には@から始まるモジュールパスエイリアスが書かれると思いますが、Storybook上では、以下のようなエラーが起きてしまいます。

ERROR in ./src/app/ui/footer/footer.tsx 4:0-50
Module not found: Error: Can't resolve '@/utils/styleUtils' in '/(省略)/src/app/ui/footer'

なのでパスエイリアスが通るように.storybook/main.tsを修正します。
https://storybook.js.org/docs/builders/webpack#troubleshooting
公式👆の

However, if you're working with a framework that provides a default aliasing configuration (e.g., Next.js, Nuxt)

以下を参照して、次の設定を.storybook/main.tsに書き足します。

.storybook/main.ts
import type { StorybookConfig } from '@storybook/nextjs'
+ const path = require('path')

const config: StorybookConfig = {
    // ...
+  webpackFinal: async (config: any) => {
+    // Add path aliases
+    config.resolve.alias['@'] = path.resolve(__dirname, '../src')
+    return config
+  },
  // ...
}
// ...

tailwind.config.tsを書く

既にFigmaやXdなどでカラースタイルが作成されている前提です。
色味の設定が一番頭を使わずにできるので、まずはtailwind.config.tsextendに書いていきます。
SNSのブランドカラーなどもまとめて書いておくと使い回しが効いていいかもしれません。

tailwind.config.ts
colors: {
    gray: {
    200: '#E8E8E8',
  },
  twitter: '#000000',
  line: '#06C755',
},

fontFamilyも設定することになりますが、一旦後回しにします。
よく使う幅や高さがあれば、これもspacingに書いておくとよさそうです。

tailwind.config.ts
spacing: {
        '1px': '1px',
        '2px': '2px',
        '3px': '3px',
        '10px': '10px',
        '20px': '20px',
        '30px': '30px',
        '60px': '60px',
},

Next.jsでのフォント最適化

next/fontを使う場合

公式を参考に、Next.jsでの設定を行います。
https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
ページ序盤には公式チュートリアルでも出てきた、./src/app/ui/fonts.tsを作成してapp/layout.tsxに適用する方法が書かれていますが、今回はtailwindCSS向けの設定方法が書かれていたので、そちらの方法で行います。
https://nextjs.org/docs/app/building-your-application/optimizing/fonts#with-tailwind-css

この場合fonts.tsは不要となり、./src/app/layout.tsxnext/fontimportします。
序盤で説明されている方法と異なり、定義にvariableを追加してhtmlタグにて呼びます。

./src/app/layout.tsx
+ import { Noto_Sans_JP, Audiowide } from 'next/font/google'

+ const audiowide = Audiowide({
+   display: 'swap',
+   subsets: ['latin'],
+   weight: '400',
+   variable: '--font-audiowide',
+ })
+ const noto = Noto_Sans_JP({
+   display: 'swap',
+   preload: false,
+   variable: '--font-noto',
+ })

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
+    <html lang='ja' className={`${noto.variable} ${audiowide.variable}`}>
      <body>
        {children}
      </body>
    </html>
  )
}

ちなみに、Noto Sans JPは日本語フォントのためsubsetsは不要となり、代わりにpreload: falseとすることで調整しました。またNoto Sans JPはVariableフォントのため、weightの設定は行いませんでした。
(こちらの記事を参考にしました!🙏)
https://zenn.dev/siino/articles/b42d658af571f0#googlefontsのnext%2Ffont対応

tailwind.config.tsにfontFamily部分を追記する

tailwind.config.ts
fontFamily: {
    noto: ['var(--font-noto)', 'sans-serif'],
   audiowide: ['var(--font-audiowide)', 'var(--font-noto)', 'sans-serif'],
},

これで、font-noto、font-audiowideというclassNameが使用できるようになりました。

StorybookにFontを適用する

(23/12/26 追記)先ほど作成したVariableがStorybook上で動作するように.storybook/preview.tsxを編集する必要があります。
(divタグなどを使用するため、preview.tspreview.tsxに変更が必要です)
https://storybook.js.org/docs/writing-stories/decorators#global-decorators

.storybook/preview.tsx
+ import React from 'react'
import type { Preview } from '@storybook/react'
import { withThemeByClassName } from '@storybook/addon-styling'
+ import { Noto_Sans_JP, Audiowide } from 'next/font/google'
import '../src/app/globals.css'

+ const audiowide = Audiowide({
+   display: 'swap',
+   subsets: ['latin'],
+   weight: '400',
+   variable: '--font-audiowide',
+ })

+ const noto = Noto_Sans_JP({
+   display: 'swap',
+   preload: false,
+   variable: '--font-noto',
+ })

const preview: Preview = {
  decorators: [
    (Story) => (
+       <div className={`${`font-sans ${audiowide.variable} ${noto.variable}`}`}>
        <Story />
      </div>
    ),
  ],
}

export default preview

これで、開発環境とStorybook上のどちらでも同じフォントが使用できる状態となります。


next/fontを使わない(Linkタグを使用する)場合

ここまではnext/fontを使用した設定方法でした。しかし、 next/font/googleにはMaterial symbolsが含まれていない(Googleが提供してるのに!) ため、別の手法で読み込む必要があります。
この件については、こちらでディスカッションが行われています:https://github.com/vercel/next.js/discussions/42881

結論から言えば、./src/app/layout.tsxに直接linkタグを書くことができますが、いくつか注意事項があるのでまとめました。

最新のNext.jsではこれまでhead内に書いていた様々なメタデータをNext.jsが用意するmetadata APIを使って、効率よく書くことが可能です。
これには、metaタグおよびlinkタグが含まれていますが、すべてのlinkタグが含まれているわけではありません。そのため、linkタグのrelがどの種類かによって対応が変わります。

./src/app/layout.tsxに以下のように追記するだけで大丈夫です。
その時に、ESlintが警告を出すことがありますが、最新のNext.jsに対応した内容ではない(app routerではなくpages routerにおける対処法となっている)ため、無視してよいでしょう。

./src/app/layout.tsx
<html lang='ja' className={`${noto.variable} ${audiowide.variable}`}>
+     <head> // headを追加する
+         {/* eslint-disable-next-line @next/next/no-page-custom-font */}
+         <link
+           rel='stylesheet'
+           href='https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&display=swap'
+         /> 
+     </head>
    <body>
        {children}
    </body>
</html>

https://nextjs.org/docs/app/api-reference/functions/generate-metadata#unsupported-metadata
こちらの通り、これらはmetadataではサポート外となりますが、ReactDOMのMethodを使うことが可能です。
具体的には、
https://nextjs.org/docs/app/api-reference/functions/generate-metadata#resource-hints
にあるように、./src/app/preload-resources.tsxを作成し、以下のように書きます。

./src/app/preload-resources.tsx
'use client'
 
import ReactDOM from 'react-dom'
 
export function PreloadResources() {
  ReactDOM.preload('...', { as: '...' }) // 使用しなかったので詳細省きます
  ReactDOM.preconnect('https://fonts.googleapis.com')
  ReactDOM.preconnect('https://fonts.gstatic.com', { crossOrigin: 'anonymous' })
  ReactDOM.prefetchDNS('...') // 使用しなかったので詳細省きます
 
  return null
}

preconnectに書いた内容は、

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

を置き換えたものとなります。
https://nhiroki.jp/2021/01/07/crossorigin-attribute
を参考に、crossoriginは属性値を指定しない場合はanonymousとして扱われているとのことだったので、置き換えました。

作成したpreload-resources.tsx./src/app/layout.tsxで読み込みます。

./src/app/layout.tsx
+ import { PreloadResources } from './preload-resources.tsx'
// ...
 <html lang='ja' className={`${noto.variable} ${audiowide.variable}`}>
      <head>
+         <PreloadResources />
// ...

これで完了です。

StorybookにFontを適用する

next/fontを使わない場合、.storybook/preview-head.htmlを作成して、そちらに先ほど使用したのと同じ<Link />を貼る必要があります。

こちらの記事を参考にしました🙏
https://qiita.com/judah/items/ee735a899bf3782d7222

おまけ:material-symbols-outlinedクラスを作成する

このままだと使いにくいので、ついでに./src/app/globals.cssにクラスを作成します。

./src/app/globals.css
/* fallback */
@font-face {
  font-family: 'Material Symbols Outlined';
  font-style: normal;
  font-weight: 400;
  src: url(/material.woff2) format('woff2');
}

.material-symbols-outlined {
  font-family: 'Material Symbols Outlined';
  font-weight: normal;
  font-style: normal;
  font-size: 24px;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  -webkit-font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
}

input::-webkit-input-placeholder {
  font-family: 'Material Symbols Outlined';
  font-size: 24px;
  position: absolute;
  bottom: 7px;
}
input::-moz-placeholder {
  font-family: 'Material Symbols Outlined';
  font-size: 24px;
  position: absolute;
  bottom: 7px;
}
input:-ms-input-placeholder {
  font-family: 'Material Symbols Outlined';
  font-size: 24px;
  position: absolute;
  bottom: 7px;
}
input:-moz-placeholder {
  font-family: 'Material Symbols Outlined';
  font-size: 24px;
  position: absolute;
  bottom: 7px;
}

これらを書いておくと、classNameとして呼び出す・input要素のplaceholderにMaterial Symbolsの適用ができるようになります。


styleUtils.tsxを書く

今の時点で頻出しそうなclassNameの組み合わせがあればまとめておきます。
と言いつつ、書いていく中でまとめたいものが増えていくのがほとんどだと思うので、思いつかなければ思いつかなければファイルだけ作っておくのでも十分だと思います。
今回は./src/app/uiに配置しました。

./src/app/ui/styleUtils.tsx
//ここはお好みでどうぞ
export const heading1Style = 'font-noto text-2xl'

普段は文字のサイズや色、HoverやFocusで使うスタイルをまとめています。

終わりに

一通り、自分がプロジェクトでやる方法をまとめてみました。
Next.jsのApp routerに関する情報自体もまだこれから増えていくところだと思うので、何かお役に立てば幸いです。

実際やっていく中で、作業の順番が前後することが度々あり、本記事の構成も一部公開後に修正を加えました。
セッティングは疎かにできない一方なるべく早く終わらせたい作業だと思うので、今後も見直しがあれば更新予定です。より効率良い手法について、コメントお待ちしております!

Discussion