【小規模サイト向け】Next.js ディレクトリ構成とコーディングルール
Next.jsで小規模サイト(ブログやコーポレートサイトなど)を複数人で開発する際のディレクトリ構成とコーディングルールをまとめました。
以下の点を意識した構成になります。
- 小規模サイトの場合、コンポーネントの量は限られるのでディレクトリ構成は複雑になりすぎないようにする
- → ファイルへのアクセスのしやすさを重視
- 複数人での開発なので、できるだけ書き方を統一できるようにする
- → ESLint, StyleLintを導入
- CSS設計やベースコーディングに疎くても一定以上の品質を担保できるようにする
- → jsxコンポーネントとCSSを同じ階層に配置して直感的に編集可能に
- → Sassを採用するが、SASS記法ではなく扱いやすいSCSS記法を採用
はじめに
ディレクトリ構成はこちらの記事を参考にさせて頂きました。とても分かりやすく解説してくれているので是非読んでみてください。
最近はFeature-Driven Folder Structure
という構成が流行っているらしいですが、小規模サイトの場合こちらの構成は少しやりすぎ感があったので今回は見送ることにしました。
ディレクトリ構成
前提として、pages/
やstyles/
などのフォルダはsrc/
にまとめています。このようにコンポーネントファイルをひとつのフォルダにまとめる方法はこちらの記事で解説しています。
src/
配下の構成は以下のようになっています。
src/
├── components
│ ├── base
│ │ └── Header
│ │ ├── Header.jsx
│ │ └── Header.module.scss
│ ├── page
│ │ ├── Index
│ │ │ ├── Index.jsx
│ │ │ └── Index.module.scss
│ │ └── News
│ │ ├── Archive.jsx
│ │ └── Archive.module.scss
│ └── ui
│ └── Button
│ ├── Button.jsx
│ └── Button.module.scss
├── const
│ └── site.js
├── features
│ └── news
│ ├── api
│ │ ├── getNewsCategory.js
│ │ └── getNewsPost.js
│ ├── components
│ │ ├── NewsCategoryList.jsx
│ │ ├── NewsCategoryList.module.scss
│ │ ├── NewsPostList.jsx
│ │ └── NewsPostList.module.scss
│ └── hooks
│ └── useXXX.js
├── lib
│ └── newt.js
├── pages/
└── styles/
これらをざっくり解説するとこのようになります。
src/
├── components/ ・・・ 様々なコンポーネントをまとめたフォルダ
│ ├── base/ ・・・ サイト全体を構成するコンポーネント(ヘッダー、レイアウト系、metaタグなど)
│ ├── page/ ・・・ pages/の中身の実態があるフォルダ
│ └── ui/ ・・・ ボタンなど最小単位のコンポーネント
├── const/ ・・・ 定数を定義するファイルを置くフォルダ
├── features/ ・・・ 特定の機能などをまとめたフォルダ。この中にも複数のコンポーネントが存在する
├── lib/ ・・・ ライブラリなど
├── pages/ ・・・ ページ(デフォルトのもの)
└── styles/ ・・・ サイト内全体で使う共通CSS
全体像が見えたところで、次は各ディレクトリの役割について解説していきます。
components/base/
components/base/
には、サイトを構成するコンポーネントを置いています。
base/
├── EmbedTag/
│ └── GoogleTagManager.jsx
├── Footer/
│ ├── Footer.jsx
│ └── Footer.module.scss
├── Header/
│ ├── Header.jsx
│ └── Header.module.scss
├── Layout/
│ └── Layout.jsx
├── Meta/
│ └── Meta.jsx
└── Wrapper/
├── Inner.jsx
└── Inner.module.scss
EmbedTag/
には計測タグ(Googleアナリティクスやタグマネ)などの外部タグ系が入ります。
ヘッダー・フッターは、同階層にCSSも配置しています。これは最初に書いたこちらを意識しているからです。
CSS設計やベースコーディングに疎くても一定以上の品質を担保できるようにする
→ jsxコンポーネントとCSSを同じ階層に配置して直感的に編集可能に
他のCSSが絡むコンポーネントも全部この構成になります。
Layout.jsx
は以下のような設計になっており、_app.jsx
で呼び出しています。
export default function Layout({ children }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
);
}
<Layout>
<Component {...pageProps} />
</Layout>
meta/
では<head>
のtitleやogpなどのメタタグを定義して、全てのページコンポーネントで呼び出しています。
Wrapper/
ではページ全体を囲うコンポーネントを配置しています。例えば、コンテンツ幅を定義するInner.jsx
がそれに該当します。
components/page/
components/page/
はpages/
の中身の実態があるフォルダです。
つまり、pages/
ではページを生成するために各ページコンポーネントファイルを置き、その中ではcomponents/page/
のコンポーネント1つだけを呼び出し、components/page/
のファイルで様々なコンポーネントを呼び出すような運用になります。
ニュース一覧ページのサンプルを用意しました。
src/
├── components/
│ └── page/
│ └── News/
│ └── Archive.jsx
│ └── Archive.module.scss
└── pages/
└── news/
└── index.jsx // ニュース一覧のトップページ
└── category/
│ └── [slug]/
│ └── index.jsx // カテゴリーごとのニュース一覧
└── page/
└── [slug]/
└── index.jsx // ニュース一覧のxページ目用
export default function News({ newsPostList, newsCategoryList }) {
return (
<>
<div className={styles.category}>
// カテゴリーリンク一覧のコンポーネントを呼び出す
<NewsCategoryList newsCategoryList={newsCategoryList} />
</div>
<div className={styles.post}>
// 投稿一覧のコンポーネントを呼び出す
<NewsPostList newsPostList={newsPostList} />
</div>
</>
);
}
import Archive from '@/components/page/News/Archive';
export default function News({ newsPostList, newsCategoryList }) {
return (
<>
// components/pageの中のコンポーネント1つを呼び出すだけ
<Archive
newsPostList={newsPostList}
newsCategoryList={newsCategoryList}
/>
</>
);
}
上記サンプルのコードとディレクトリ構成では、components/page/News/Archive.jsx
で記事一覧とカテゴリーリンク一覧のコンポーネントを呼び出しています。
そして、pages/news/
には3つのindex.js
があります。これらのページでは表示される記事が変わるだけで、コンポーネント構成は全く同じになります。なので、これらのファイルでcomponents/page/News/Archive.jsx
を呼び出します。
ちなみに、記事一覧やカテゴリーリンク一覧のコンポーネントは、後ほど解説するfeatures/
で定義しています。
ページが二重管理のようになってしまい少し手間ですが、その分共通化できたりと管理しやすい設計になるので採用しました。
この構成のメリットをまとめると以下のようになります。
-
pages/
のコンポーネントの肥大化を防げる-
components/page/
に様々なコンポーネントをまとめられるので、pages/
の中で再利用が可能になる
-
- 理にかなったCSS設計を手軽に導入できる
- 使い回すコンポーネントには
margin
を持たせないことで、再利用をしやすくする - それらのコンポーネントの
margin
はcomponents/page/
の中で定義する- 上記の例だと
components/page/News/Archive.jsx
で配置しているコンポーネントに対してmargin
を定義する
- 上記の例だと
- この部分に関しては、後の「コーディングルール」でもう少し詳しく解説します
- 使い回すコンポーネントには
components/ui/
components/ui/
はサイト内で使い回すコンポーネントを配置します。ボタンや見出しなどが該当します。
ディレクトリ構成は以下のようになります。
.
└── Button
│ ├── Button.jsx
│ └── Button.module.scss
└── Component_1
│ ├── Component_1.jsx
│ └── Component_1.module.scss
└── Component_2
├── Component_2.jsx
└── Component_2.module.scss
これらのコンポーネントは使い回す前提になるので、margin
やwidth
などのCSSプロパティは原則使わないようにします。
components/page/
でページに対して直接呼び出すこともあれば、後ほど解説するfeatures/
で呼び出す場合もあります。
const/
定数(共通の情報)を定義するディレクトリです。
以下はサイト全体で使用する情報を定義したファイルの例です。
export const siteData = {
title: 'サイトタイトル',
desc: '説明文',
url: 'https://xxx.com',
lang: 'ja',
googleTagManagerId: 'xxx',
}
features/
特定の機能や処理をまとめたディレクトリです。components/ui/
のコンポーネントをここで呼び出すこともあります。
構成の例を用意しました。
features/
├── _template
│ ├── api
│ │ └── getXXX.js
│ ├── components
│ │ ├── XXX.jsx
│ │ └── XXX.module.scss
│ └── hooks
│ └── useXXX.js
└── news // ニュースに関する処理やコンポーネントをまとめている
├── api
│ ├── getNewsCategory.js // カテゴリーを取得するAPI
│ └── getNewsPost.js // 投稿を取得するAPI
└── components
├── NewsCategoryList.jsx // カテゴリー一覧のコンポーネント
├── NewsCategoryList.module.scss
├── NewsPostList.jsx // 投稿一覧のコンポーネント
└── NewsPostList.module.scss
features/_template/
は複製用のテンプレートです。このフォルダを複製して、新しくセットを作成します。
基本的に3つのフォルダから構成されます。
-
api
- 関連するAPIを配置
-
getXXX
やremoveXXX
など、どのような処理かイメージできる命名にする - 1処理1ファイル(これはプロジェクトによって変えても良いかも)
- 使わない場合はフォルダごと削除
-
components
- 関連するコンポーネントとCSSを配置
- ここで作ったコンポーネントを、
components/page/
で呼び出す - 関連するファイルが多くなりそうなら、この中でさらにフォルダ分けしても良い
-
hooks
- 関連するカスタムフックを配置
-
useXXX
のように、先頭にuse
を付ける - 使わない場合はフォルダごと削除
lib/
外部ライブラリに関するファイルを配置するディレクトリです。
例えば、以下のファイルではヘッドレスCMSのNewtのSDKにAPIトークンなどを与えてインスタンス化し、それをfeatures/XXX/api/
で呼び出せるようにしています。
const { createClient } = require('newt-client-js');
export const newtCdnClient = createClient({
spaceUid: process.env.NEXT_PUBLIC_NEWT_SPACE_UID,
token: process.env.NEXT_PUBLIC_NEWT_API_TOKEN,
apiType: process.env.NEXT_PUBLIC_NEWT_API_TYPE,
});
pages/
components/page/
で解説した通り、pages/
ではページを生成するために各ページコンポーネントを置くだけで、中身の実態はcomponents/page/
で管理しています。
styles/
サイト全体に関するCSSを定義する場所です。
変数や関数、mixin、リセットCSS、<html>
や<body>
に対するスタイルなどを定義しています。
各コンポーネントのスタイルは前述の通り、各コンポーネントの.jsx
と同階層に配置しています。
ディレクトリ構成の解説のまとめ
ディレクトリの解説は以上になります。これまでの内容をざっくりまとめると以下のようになります。
-
pages/
はページを生成するためだけに使う- 中身の実態は
components/page/
で管理
- 中身の実態は
- UIコンポーネントは、
components/ui/
かfeatures/XXX/components/
のどちらかに作成する- 作成したコンポーネントファイル
.jsx
と同階層にCSSも配置する -
components/ui/
は汎用性を保たせたいので、margin
やwidth
などは指定しない
- 作成したコンポーネントファイル
-
styles
にはサイト全体のCSSのみを定義して、各コンポーネントのCSSは上記のルールで管理する - 特定の機能やグループは
features/
にフォルダを作り、その中に「コンポーネント」「CSS」「API」「カスタムフック」などをまとめる
また、必要に応じてこちらで解説されているconfig
やstore
のフォルダをsrc
直下に作るのも良いと思います。
・
・
・
・
・
コーディングルール
次に、コーディングルールに関して解説します。
JSX, JavaScript全般
インデントや表記の統一はESLint
とPrettier
で行っているので、JSを書くときはあまり細かく気にしないでいいと思います。
以下記事の環境構築を再現すれば、保存時に自動整形されるようになります。
参考までに設定ファイルを載せておきます。
module.exports = {
// ルールをまとめて追加
"extends": [
"next/core-web-vitals",
"plugin:import/recommended",
"plugin:import/warnings",
"prettier"
],
// 個別ルール
"rules": {
// importするファイルをアルファベット順に自動で並び替える
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
}
}
],
// importのパスにエイリアス('@/components/xxxxx')を使うとエラーになるのでそれを防ぐ
"import/no-unresolved": "off"
}
}
module.exports = {
"trailingComma": "all", // 可能な限り末尾にカンマを付ける
"tabWidth": 2, // インデントのスペースの数
"semi": true, // ステートメントの最後にセミコロンを追加
"singleQuote": true, // シングルクォートに統一
"jsxSingleQuote": true, // JSXでダブルクォートの代わりにシングルクォートを使用する
"printWidth": 100, // 折り返す行の長さ
"singleAttributePerLine": true // ひとつの属性ごとに改行する
}
JS周りで唯一ルールがあるとすれば、jsx記法のファイル(コンポーネント)の拡張子は.jsx
にすることくらいです。逆に、jsx記法以外のファイル(APIなど)は.js
にします。
こうすることで、ひと目でコンポーネントなのか、それ以外なのかが判断できるようになります。
SCSS
こちらも個人でバラツキがありそうな箇所はStylelint
で管理しているので、そのあたりはあまり気にしなくて大丈夫です。
ちなみに、こちらの記事ではプロパティの自動並び替えなどを導入しています。とてもおすすめです!
それ以外にはいくつかのルールを定めています。
rem
は使わない
rem
を使うことで、ブラウザの文字サイズを変更したらサイト上の文字サイズもそれに応じて変わるので、アクセシビリティ的に良いことは間違いありません。
しかし、rem
の理解度は人によって様々です。正しく理解している人もいれば、間違った理解をしている人もいます。
また、フォントサイズはrem
にするとして、他の余白や幅の単位はrem
にする?しない?など、ルールを統一するのも難しいです。初期開発時にはルールを守れていても、運用時に崩れることもあります。
今回は「複数人での開発」と「各々のスキルレベルがまばらでも問題ないようにする」を目指したかったので、rem
を使うのは禁止にしました。
クラス名でアンパサンド(&)は禁止(&:hoverなどはOK)
クラス名に&
を使うのは禁止とします。例を用意しました。
.parent {
&__child { ... } // NG
}
.parent {
&:hover { ... } // 擬似クラスはOK
&::before { ... } // 擬似要素もOK
}
このように、クラス名に対する&
のみ禁止になります。
&
を使うと、コンポーネントで定義したクラス名で、CSSファイルの中のクラス名を検索できないからです(&
でクラス名が省略されるため)。
CSSファイルの管理方法
ディレクトリ構成でも解説しましたが、共通CSSはsrc/styles/
にて管理し、各コンポーネントのスタイルは、そのコンポーネントの.jsx
ファイルと同階層で管理するようにします。
こうすることで、直感的にCSSを編集できるようになります。
page/xxx/xx.scss
で管理する
コンポーネントの余白はこちらもディレクトリ構成で解説済みですが、使い回すコンポーネントにはmargin
やwidth
など、使い回す際に障害になりそうなプロパティを指定するのは禁止とします。
それらのプロパティは、コンポーネントを呼び出すファイルのCSSで指定します。こうすることで、コンポーネントの再利用がしやすくなります。
最後に
この環境を作成するにあたり、こちらの記事はとても参考になったので是非皆さんにも読んでみてほしいです。
また、私の今の環境には合わなかったですが、これから流行る兆しがある?Feature-Driven Folder Structure
の構成も目を通してみてはいかがでしょうか。
Discussion