Routing
This template provides 3 pages(+ intercepting) based on Next.js App Router.
app
├── (private)
│ ├── layout.tsx
│ └── me
│ └── page.tsx
├── (public)
│ ├── layout.tsx
│ ├── page.tsx
│ └── signin
│ └── page.tsx
├── @dialog
│ ├── (.)create
│ │ ├── Content.tsx
│ │ ├── default.tsx
│ │ └── page.tsx
│ ├── _components
│ ├── default.tsx
│ └── page.tsx
├── layout.tsx
└── not-found.tsxTop (/)
What can you learn from this page?
- Server Components
- Server Components + Form + Server Actions
- How to use Suspense
- How to retrieve the session with nextAuth on Server Components
Affected Layouts
app/layout.tsxapp/(public)/layout.tsxapp/(public)/page.tsx
See Full Code
tsx
import Image from "next/image";
import Link from "next/link";
import { Suspense } from "react";
import { prisma } from "../_clients/prisma";
import { getSessionOrReject } from "../_utils/auth";
import { format } from "../_utils/date";
import { ItemManager } from "./_components/ItemManager";
export default async function Page() {
return (
<div className="space-y-5">
<Suspense fallback={<p>loading ...</p>}>
<Status />
</Suspense>
<Suspense fallback={<p>loading ...</p>}>
<List />
</Suspense>
</div>
);
}
async function Status() {
const session = await getSessionOrReject();
return (
<div className="flex justify-between gap-3 flex-col lg:flex-row lg:items-center">
<p className="text-gray-300">
{session?.data?.user
? `you are signed in as ${session.data.user.name} 😄`
: "you are not signed in 🥲"}
</p>
{session?.data?.user && <ItemManager />}
</div>
);
}
async function List() {
const data = await prisma.item.findMany({
include: {
user: true,
},
orderBy: {
createdAt: "desc",
},
});
return (
<ul className="space-y-4" aria-label="items">
{data.map(({ id, content, createdAt, user }) => (
<li
key={id}
className="border border-gray-600 p-4 flex justify-between items-start rounded-md"
>
<div className="flex justify-center gap-4 items-center">
{user.image && (
<Image
alt={user.name ?? "no name"}
src={user.image}
width={56}
height={56}
className="rounded-full border-2 border-gray-300"
priority
/>
)}
<Link
href={`/items/${id}`}
className="font-semibold md:text-xl break-all underline hover:text-blue-300"
>
{content}
</Link>
</div>
<span className="text-sm text-gray-300">{format(createdAt)}</span>
</li>
))}
</ul>
);
}tsx
import { Container } from "../_components/Container";
export default function Layout({ children }: LayoutProps<"/">) {
return <Container>{children}</Container>;
}tsx
import { clsx } from "clsx";
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import { Footer } from "./_components/Footer";
import { Header } from "./_components/Header";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "web app template",
description: "😸",
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL),
alternates: {
canonical: process.env.NEXT_PUBLIC_SITE_URL,
},
};
export const viewport: Viewport = {
// for mobile
maximumScale: 1,
};
export default function Layout({ children }: LayoutProps<"/">) {
return (
<html lang="en">
<body
className={clsx(
inter.className,
"bg-gray-700 text-gray-200 min-h-screen flex flex-col",
)}
>
<Header />
<main className="flex-1">{children}</main>
<Footer />
</body>
</html>
);
}SignIn (/signin)
What can you learn from this page?
- Client Components
- How to use React Hooks
- How to sign in via NextAuth
Affected Layouts
app/layout.tsxapp/(public)/layout.tsxapp/(public)/signin/page.tsx
See Full Code
tsx
"use client";
import { signIn } from "next-auth/react";
import { Button } from "../../_components/Button";
export default function SignIn() {
return (
<div className="flex flex-col items-center justify-center gap-6">
<Button onClick={() => signIn("google")}>Sign in with Google</Button>
</div>
);
}tsx
import { Container } from "../_components/Container";
export default function Layout({ children }: LayoutProps<"/">) {
return <Container>{children}</Container>;
}tsx
import { clsx } from "clsx";
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import { Footer } from "./_components/Footer";
import { Header } from "./_components/Header";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "web app template",
description: "😸",
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL),
alternates: {
canonical: process.env.NEXT_PUBLIC_SITE_URL,
},
};
export const viewport: Viewport = {
// for mobile
maximumScale: 1,
};
export default function Layout({ children }: LayoutProps<"/">) {
return (
<html lang="en">
<body
className={clsx(
inter.className,
"bg-gray-700 text-gray-200 min-h-screen flex flex-col",
)}
>
<Header />
<main className="flex-1">{children}</main>
<Footer />
</body>
</html>
);
}Me (/me)
What can you learn from this page?
- Client Components
- Client Components + Form +
useActionState+ Server Actions - How to retrieve the session with nextAuth on Client Components
- Handling NotFound
Affected Layouts
app/layout.tsxapp/(private)/layout.tsxapp/(private)/me/page.tsx
See Full Code
tsx
import { notFound } from "next/navigation";
import { getSessionOrReject } from "../../_utils/auth";
import { UpdateMyInfo } from "./_components/UpdateMyInfo";
export default async function Page() {
const session = await getSessionOrReject();
if (!session.success) {
notFound();
}
const { user } = session.data;
return <UpdateMyInfo name={user.name ?? ""} />;
}tsx
import { Container } from "../_components/Container";
export default function Layout({ children }: LayoutProps<"/">) {
return <Container>{children}</Container>;
}tsx
import { clsx } from "clsx";
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import { Footer } from "./_components/Footer";
import { Header } from "./_components/Header";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "web app template",
description: "😸",
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL),
alternates: {
canonical: process.env.NEXT_PUBLIC_SITE_URL,
},
};
export const viewport: Viewport = {
// for mobile
maximumScale: 1,
};
export default function Layout({ children }: LayoutProps<"/">) {
return (
<html lang="en">
<body
className={clsx(
inter.className,
"bg-gray-700 text-gray-200 min-h-screen flex flex-col",
)}
>
<Header />
<main className="flex-1">{children}</main>
<Footer />
</body>
</html>
);
}Why use Route Groups?
app
├── (private)
└── (public)The App Router inherits the parent's layout, making it challenging to differentiate the UI between logged-in and non-logged-in users. While it is possible to create separate directories, this approach results in the paths being reflected in the URL (e.g., /signed-in, /no-signed-in), which may not be the desired outcome.
Route Groups allow you to create directories without exposing them as paths, providing a flexible solution to this issue. It is recommended to adopt this structure from the beginning.