🐕
推しの Next.js のはじまりのスタックを紹介します🙌
推しの Next.js のはじまりのスタックを紹介します🙌
【ちょっと宣伝】
推しの Next.js のはじまりのスタックを Cursor や Claude などのコーディングエージェントがさくっとつくれるように、
スターターのプロンプトを実装しました🚀
ざっくりと紹介
ライブラリ
- リント
- Biome
- テスト
- Vitest
- CI/CD
- Husky
- GitHub Actions
- スキーマ
- Zod
- フォーム
- React Hook Form
- shadcn/ui
- データベース
- Prisma
- 認証
- Clerk
- 決済
- Stripe
- UI
- Tailwind CSS
- UI - テーマ切り替え
- next-themes
- アクセス解析
- Google Analytics
- @next/third-parties
- vanilla-cookieconsent
プラットフォーム
- アプリ
- Vercel
- ストレージ
- Vercel Blob
- データベース
- Neon
ディレクトリ構成
src/
├─ _lib/
├─ _actions/
│ └─ domain/
│ └─ todo.ts
├─ _schemas/
│ └─ domain/
│ └─ todo.ts
├─ _services/
│ ├─ app/
│ └─ domain/
│ └─ todo.ts
├─ _components/
│ ├─ ui/
│ └─ domain/
│ └─ domain/
│ ├─ form.tsx
│ └─ list.tsx
├─ _hooks/
└─ app/
├─ examples/
│ ├─ [id]/
│ │ └─ page.tsx
│ └─ page.tsx
└─ page.tsx
それぞれを選んだ理由
2025年現在においてはデファクトスタンダードっぽいものは説明の割愛をしています🙏
ライブラリ
リント
以下を選択肢にあげて検討しました。
-
ESLint+Prettier Biome
わたしは推しを選んだ理由としては、
- 設定が簡潔
- 速度が速い
というものになります。
ただし Tailwind CSS の並び順がまだ組み込みされていないため VSC Extension の Tailwind CSS IntelliSense で並べ替えをしています。
テスト
- /
CI/CD
- /
スキーマ
- /
フォーム
- /
データベース
以下を選択肢にあげて検討しました。
PrismaDrizzle ORM
わたしは推しを選んだ理由としては、
- 挙動が安定
- スキーマファイルの見やすい
というものになります。
認証
以下を選択肢にあげて検討しました。
- プロパイダーがある
Auth0Clerk
- プロパイダーがない
NextAuthBetter Auth
わたしは推しを選んだ理由としては、
- プロパイダーがある(個人情報絶対もちたくないマン)
- 価格が安い
- 開発の体験が良い
というものになります。
決済
- /
UI
- /
UI - テーマ切り替え
テーマ(ダークモードやライトモードなど)の切り替えに対応しています。
"use client";
import {
MoonIcon as DarkModeIcon,
SunIcon as RootModeIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { Button } from "@/_components/ui/button";
export default function Component() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const next = resolvedTheme === "dark" ? "light" : "dark";
return (
<Button onClick={() => setTheme(next)} variant="ghost" size="icon">
{mounted ? (
resolvedTheme === "dark" ? (
<RootModeIcon />
) : (
<DarkModeIcon />
)
) : null}
</Button>
);
}
アクセス解析
ユーザーのクッキーの許可をまって解析タグを読み込みます。
"use client";
import { GoogleAnalytics } from "@next/third-parties/google";
import { useEffect, useState } from "react";
import "vanilla-cookieconsent/dist/cookieconsent.css";
import * as CookieConsent from "vanilla-cookieconsent";
export default function Component({ gaId }: { gaId: string | undefined }) {
const [acceptedAnalytics, setAcceptedAnalytics] = useState(false);
useEffect(() => {
CookieConsent.run({
categories: {
necessary: { enabled: true, readOnly: true },
analytics: {},
},
language: {
default: "en",
translations: {
en: {
consentModal: {
title: "We use cookies",
description: "Cookie modal description",
acceptAllBtn: "Accept all",
acceptNecessaryBtn: "Reject all",
showPreferencesBtn: "Manage Individual preferences",
},
preferencesModal: {
title: "Manage cookie preferences",
acceptAllBtn: "Accept all",
acceptNecessaryBtn: "Reject all",
savePreferencesBtn: "Accept current selection",
closeIconLabel: "Close modal",
sections: [
{
title: "Somebody said ... cookies?",
description: "I want one!",
},
{
title: "Strictly Necessary cookies",
description:
"These cookies are essential for the proper functioning of the website and cannot be disabled.",
//this field will generate a toggle linked to the 'necessary' category
linkedCategory: "necessary",
},
{
title: "Performance and Analytics",
description:
"These cookies collect information about how you use our website. All of the data is anonymized and cannot be used to identify you.",
linkedCategory: "analytics",
},
{
title: "More information",
description:
'For any queries in relation to my policy on cookies and your choices, please <a href="#contact-page">contact us</a>',
},
],
},
},
},
},
onConsent() {
setAcceptedAnalytics(CookieConsent.acceptedCategory("analytics"));
},
});
}, []);
if (gaId == null) {
return;
}
return acceptedAnalytics ? <GoogleAnalytics gaId={gaId} /> : null;
}
さいごに
みなさんの推しのはじまりのスタックも知りたい……!
Discussion