Skip to content

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.tsx

Top (/)

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.tsx
  • app/(public)/layout.tsx
  • app/(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.tsx
  • app/(public)/layout.tsx
  • app/(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.tsx
  • app/(private)/layout.tsx
  • app/(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.