Build a Project Showcase with Next.js

Build a multi-page portfolio site using Next.js, TypeScript, and Tailwind CSS.

Introduction

30 Second Summary

Every developer needs a place to show off their work. A personal portfolio site is one of the most practical things you can build, especially when it loads instantly and navigates without clunky page reloads.

In this project, you will build a multi-page project showcase website using Next.js, TypeScript, and Tailwind CSS. You will create styled pages, a shared navigation bar, and interactive project cards that expand on click.

What You'll Build

When you run your finished site, you will click between a home page, a projects page, and an about page with instant transitions, expand project cards to reveal full descriptions, and see the nav bar highlight which page you are on.

By the end of this project, you'll have:

  • A polished home page where visitors see your name and tagline, plus an about page with your background. Both pages share a navigation bar that highlights the active link.
  • Clickable project cards on a dedicated projects page. Each card expands on click to reveal a full description, then collapses when you click again.
  • Instant page transitions with no white flash or full-page reload, powered by Next.js client-side routing.
  • Secret Mission: Add dynamic routes so each project gets its own dedicated detail page at a shareable URL.

Are there any prerequisites?

This project assumes basic JavaScript knowledge. You should have Node.js and VS Code already installed on your machine.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This is your moment to commit to the project goal before any hands-on work begins.

Set Up Your Next.js Project

Your project showcase needs a home before you can start building pages. Right now you have Node.js and VS Code ready, but no project files to work with.

Next.js provides a scaffold command that creates an entire project structure for you. In this step, you will create a new Next.js project, explore what the scaffold gives you, and make your first code change to prove hot reload is working.

In this step, get ready to:
  • Scaffold a new Next.js project with TypeScript and Tailwind CSS.
  • Explore the generated folder structure and understand what each key file does.
  • Start the development server and make your first edit with hot reload.
Scaffold your Next.js project

The create-next-app CLI creates a ready-to-run project in a single command. You will use the --yes flag to skip the interactive prompts and accept the recommended defaults.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open a terminal inside VS Code by pressing Ctrl+` (the backtick key, on both macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your Next.js project by running this command:
npx create-next-app@latest project-showcase --yes

What does this command do?

npx create-next-app@latest downloads and runs the latest version of the Next.js project scaffolder. project-showcase is the name of the folder it creates.

The --yes flag skips all the interactive prompts and uses the recommended defaults: TypeScript, Tailwind CSS, ESLint, and the App Router. It also enables Turbopack for faster builds and sets up the @/* import alias.

The command takes a moment to download the template and install dependencies. You will see progress output in the terminal as packages are installed.

  • Once the scaffold finishes, move into your new project folder by running:
cd project-showcase
  • Open the project folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the project-showcase folder, and click Open.

See a trust banner?

VS Code may open the folder in Restricted Mode and show a trust banner at the top. Since you just created this project yourself, click Trust in the banner to allow VS Code to run code from this folder.

  • Open a new terminal in this workspace by pressing Ctrl+`.
  • Confirm you are inside the project by running:
ls

You should see files like package.json, next.config.ts, tsconfig.json, and folders like app/ and public/ listed in the output.

Don't see these files?

Make sure the scaffold command completed without errors. If you see a permission error, try running the npx command again. If ls shows a different folder's contents, run cd ~/Desktop/project-showcase to get to the right place.

Still stuck? Help me troubleshoot the create-next-app scaffold command.

Explore the project structure

The scaffold created a complete project with several files and folders. Look at the Explorer sidebar in VS Code to see the file tree.

What are these files?

  • app/ is where all your routes live. Every folder inside app/ can become a URL on your site.
  • app/layout.tsx is the root layout. It wraps every page in your app with shared structure like <html> and <body> tags.
  • app/page.tsx is the home page. When someone visits /, this file renders.
  • public/ holds static assets like images. Files here are served directly at the root URL.
  • next.config.ts, tsconfig.json, and package.json are configuration files for Next.js, TypeScript, and npm.
Run the dev server and make your first edit

Time to see your project running. The development server watches your files and updates the browser automatically whenever you save a change.

  • Start the development server by running:
npm run dev

What does npm run dev do?

This command starts a local development server on port 3000. It takes over the terminal, so you will not be able to type more commands in this terminal window while it is running. Keep it running for the rest of the project.

  • Open your browser and go to http://localhost:3000.

You should see the default Next.js starter page with the Next.js logo and links to documentation.

See a blank page or an error?

Check the terminal for error messages. If you see a port conflict, stop any other server running on port 3000 and try again. Make sure the terminal still shows the dev server running (it should not have returned to a command prompt).

Need more help? Help me get the Next.js dev server running.

The default page has a lot of boilerplate content. Let's replace it with something simple to prove you have full control over what appears.

  • In the VS Code Explorer sidebar, expand the app/ folder and click on page.tsx to open it.
  • Select all the code in the file by pressing Cmd+A (macOS) or Ctrl+A (Windows).
  • Delete the selected code and replace it with the following:
// Home page component
export default function Home() {
  return (
    <h1>My Project Showcase</h1>
  );
}

What does this code do?

This creates a React component called Home that returns a single <h1> heading. In Next.js, the export default function in page.tsx is the component that renders when someone visits that route.

Notice there is no 'use client' at the top. This file is a Server Component by default. It renders on the server and sends plain HTML to the browser. You will learn more about this distinction in a later step.

Before you check the browser, what do you think you will see at http://localhost:3000 after saving this file?

  • Save app/page.tsx by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Switch to your browser. The page updates automatically without a manual refresh.

You should see the text "My Project Showcase" displayed as a heading. The default boilerplate is gone. That instant update is hot reload in action.

Still seeing the old page?

Make sure you saved the file (look for the dot on the file tab in VS Code, which means unsaved changes). Try a manual refresh in your browser with Cmd+R (macOS) or Ctrl+R (Windows). Check that the terminal still shows the dev server running.

Still stuck? Help me figure out why my page changes are not appearing.

✔️ Awesome, I've got everything!

Great. Make sure you have saved app/page.tsx and that your browser shows the "My Project Showcase" heading.

ⓧ I'd like to double check the full code

Here is the complete app/page.tsx file. Make sure yours matches exactly:

export default function Home() {
  return (
    <h1>My Project Showcase</h1>
  );
}

Your Next.js project is running and responding to your edits in real time. Next up, you will build a real home page with a hero section and create a navigation bar that appears on every page.

Build Your Home Page and Layout

In Step 1, you scaffolded your Next.js project and replaced the default boilerplate with a custom heading. Your dev server is running and your site is live at http://localhost:3000.

A single heading on a blank page is not much of a showcase. You need a layout that wraps every page with shared navigation and a styled home page that gives visitors a real first impression. In this step, you will build both.

In this step, get ready to:
  • Add a navigation bar to the root layout that appears on every page.
  • Build a styled hero section on the home page.
  • Extract the navigation bar into a reusable component.
Add a navigation bar to your layout

The root layout in app/layout.tsx wraps every page in your site. Anything you place here automatically appears on every page without you needing to repeat it.

  • In VS Code, open app/layout.tsx.
  • Find the metadata object near the top of the file:
export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};
  • Replace both values so the metadata looks like this:
export const metadata: Metadata = {
  title: "My Project Showcase",
  description: "A portfolio of my projects built with Next.js",
};

Now you need to add the navigation bar. The {children} line inside the <body> tag is where page content renders.

  • Find the {children} line inside the <body> tag and replace it with the following navigation bar and content wrapper:
        <nav className="flex items-center gap-6 bg-gray-900 px-6 py-4">
          <span className="text-lg font-bold text-white">MyShowcase</span>
          <div className="flex gap-4">
            <a href="/" className="text-sm text-gray-300 hover:text-white">Home</a>
            <a href="/projects" className="text-sm text-gray-300 hover:text-white">Projects</a>
            <a href="/about" className="text-sm text-gray-300 hover:text-white">About</a>
          </div>
        </nav>
        <main className="mx-auto max-w-4xl px-6 py-10">{children}</main>

What does this code do?

The <nav> element creates a dark navigation bar with three links using plain <a> tags. Each link points to a route you will create later.

The <main> wrapper centers your page content and adds padding. Every page in your site will render inside this wrapper because the layout wraps all routes.

  • Save app/layout.tsx.
  • Switch to your browser and check http://localhost:3000. You should see a dark navigation bar at the top of the page with MyShowcase, Home, Projects, and About links. Your heading is now centered in a narrower container.

What is a layout?

In Next.js, the root layout (app/layout.tsx) wraps every page in your application. It is required and must contain <html> and <body> tags.

Anything you add inside the <body> of the layout (like your nav bar) shows up on every page automatically. The {children} prop is where the current page's content gets injected.

Don't see the navigation bar?

  • Make sure you saved app/layout.tsx after making changes.
  • Check that you replaced the {children} line with both the <nav> block and the <main> wrapper. If {children} is missing entirely, your page content will not render.
  • Confirm your dev server is still running in the terminal. If it stopped, start it again with npm run dev.

Still stuck? Help me debug my layout.tsx navigation bar.

Build a styled hero section

Your home page still shows a plain heading. Time to replace it with a proper hero section that introduces you and your work.

  • Open app/page.tsx in VS Code.
  • Replace the entire file content with the following hero section:
export default function Home() {
  return (
    <div>
      <h1 className="text-4xl font-bold">Hi, I am a developer</h1>
      <p className="mt-4 text-lg text-gray-600">
        Welcome to my project showcase. I am learning Next.js to build modern
        web applications. Browse my projects to see what I have been working on.
      </p>
      <div className="mt-8 rounded-lg bg-blue-50 p-6">
        <h2 className="text-2xl font-semibold">What is this site?</h2>
        <p className="mt-2 text-gray-700">
          This site is built with Next.js, TypeScript, and Tailwind CSS. It uses
          file-system routing, shared layouts, and both Server and Client
          Components.
        </p>
      </div>
    </div>
  );
}

What does this code do?

  • The <h1> and <p> create a hero section with your name and a short intro. Tailwind CSS classes like text-4xl and font-bold handle all the styling directly in the markup.
  • The bg-blue-50 div creates a light blue info card that describes the technologies behind the site.
  • This file has no 'use client' directive at the top. That means it is a Server Component by default. It just returns JSX with no state or event handlers needed.
  • Save app/page.tsx.
  • Check your browser. You should see a large bold heading that says Hi, I am a developer, an intro paragraph below it, and a light blue card titled What is this site? further down.

What is a Server Component?

In Next.js, every component is a Server Component by default. Server Components render on the server and send plain HTML to the browser. They cannot use React hooks like useState or attach event handlers like onClick.

Your page.tsx is a Server Component because it only returns static JSX. No interactivity means no need for the browser to run any JavaScript for this component. You will see the difference when you build interactive components later.

Don't see the styled hero?

  • Make sure you replaced the entire file content, including the export default function Home() line.
  • Check for any red squiggly lines in VS Code. A missing closing tag or bracket will prevent the page from rendering.

Need help? Why isn't my styled home page showing up?

Extract the navbar into a reusable component

Your navigation bar works, but its markup is sitting directly inside layout.tsx. As the nav grows, this file will get cluttered. Moving the nav into its own file makes it easier to maintain.

  • In the VS Code Explorer sidebar, right-click the project-showcase root folder (the one that contains app/) and select New Folder.
  • Name the folder components.
  • Right-click the new components folder and select New File.
  • Name the file Navbar.tsx.
  • Paste the following code into components/Navbar.tsx:
export default function Navbar() {
  return (
    <nav className="flex items-center gap-6 bg-gray-900 px-6 py-4">
      <span className="text-lg font-bold text-white">MyShowcase</span>
      <div className="flex gap-4">
        <a href="/" className="text-sm text-gray-300 hover:text-white">Home</a>
        <a href="/projects" className="text-sm text-gray-300 hover:text-white">Projects</a>
        <a href="/about" className="text-sm text-gray-300 hover:text-white">About</a>
      </div>
    </nav>
  );
}

What does this code do?

This is the exact same navigation markup that was previously inline in layout.tsx, now wrapped in its own exported function. It is a Server Component (no 'use client' directive) because it only renders static links.

The components/ folder is not a Next.js convention. It has no special meaning to the framework. It is just an organizational choice for reusable UI that is not a route.

  • Save components/Navbar.tsx.

Now you need to update layout.tsx to use this new component instead of the inline markup.

  • Switch back to app/layout.tsx.
  • Find the last import line near the top of the file:
import "./globals.css";
  • Add this import directly below it:
import Navbar from "@/components/Navbar";
  • Now find the inline <nav> block you added earlier (the full <nav>...</nav> element with all its children) and replace it with a single line:
        <Navbar />

What is the @/ import?

The @/ prefix is an import alias that maps to the project root. It is configured by default in the scaffold's tsconfig.json. This lets you write clean imports like @/components/Navbar instead of messy relative paths like ../../components/Navbar.

  • Save app/layout.tsx.
  • Before you check the browser, what do you expect to see? The page should look exactly the same as before. The refactor moved code into a separate file without changing any behavior.
  • Refresh your browser. Confirm the dark navigation bar with MyShowcase, Home, Projects, and About is still visible. The hero section below it is unchanged.

Seeing an error about Navbar?

  • Check that the file is named exactly Navbar.tsx (capital N) and is inside the components/ folder at the project root, not inside app/.
  • Make sure the import path in layout.tsx is exactly @/components/Navbar.
  • Verify you deleted the entire inline <nav> block and replaced it with <Navbar />. Having both will render two navigation bars.

Still stuck? Help me fix my Navbar import in layout.tsx.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full code for each file you touched in this step. Compare yours against these.

components/Navbar.tsx:

export default function Navbar() {
  return (
    <nav className="flex items-center gap-6 bg-gray-900 px-6 py-4">
      <span className="text-lg font-bold text-white">MyShowcase</span>
      <div className="flex gap-4">
        <a href="/" className="text-sm text-gray-300 hover:text-white">Home</a>
        <a href="/projects" className="text-sm text-gray-300 hover:text-white">Projects</a>
        <a href="/about" className="text-sm text-gray-300 hover:text-white">About</a>
      </div>
    </nav>
  );
}

app/page.tsx:

export default function Home() {
  return (
    <div>
      <h1 className="text-4xl font-bold">Hi, I am a developer</h1>
      <p className="mt-4 text-lg text-gray-600">
        Welcome to my project showcase. I am learning Next.js to build modern
        web applications. Browse my projects to see what I have been working on.
      </p>
      <div className="mt-8 rounded-lg bg-blue-50 p-6">
        <h2 className="text-2xl font-semibold">What is this site?</h2>
        <p className="mt-2 text-gray-700">
          This site is built with Next.js, TypeScript, and Tailwind CSS. It uses
          file-system routing, shared layouts, and both Server and Client
          Components.
        </p>
      </div>
    </div>
  );
}

app/layout.tsx has three sections to verify. Scroll up and confirm each one is present in your file, in order:

  • The import block at the top includes import Navbar from "@/components/Navbar"; after the other imports.
  • The metadata object has title: "My Project Showcase" and description: "A portfolio of my projects built with Next.js".
  • Inside the <body> tag, you have <Navbar /> followed by <main className="mx-auto max-w-4xl px-6 py-10">{children}</main>.

Your site now has a styled home page and a shared layout that wraps every page with a navigation bar. Next up, you will create the Projects and About pages and discover why these plain <a> tags in the navbar cause a problem.

Add More Pages and Fix Slow Navigation

Your home page looks great, and the Navbar already links to three pages. There is just one problem: only the Home page actually exists.

A single page does not teach you routing. In this step, you will create the Projects and About pages to see how Next.js file-system routing works. You will also discover a real limitation with how the nav bar currently handles clicks, and fix it.

In this step, get ready to:
  • Create the Projects and About pages using file-system routing.
  • Replace <a> tags with the Link component for instant navigation.
  • Add active link highlighting with usePathname.
Create the Projects and About pages

In Next.js, every folder inside app/ that contains a page.tsx file becomes a URL route. No router configuration needed.

  • In the VS Code Explorer sidebar, right-click the app folder.
  • Click New Folder and name it projects.
  • Right-click the new projects folder, click New File, and name it page.tsx.
  • Paste the following code into app/projects/page.tsx:
// Export the projects page component
export default function ProjectsPage() {
  return (
    <div>
      <h1 className="text-3xl font-bold">My Projects</h1>
      <p className="mt-2 text-gray-600">Projects coming soon.</p>
    </div>
  );
}

What does this code do?

This is a Server Component. There is no 'use client' at the top, which means Next.js renders it on the server by default. It exports a function that returns JSX, just like the Home page you already built.

  • Save app/projects/page.tsx.
  • Visit http://localhost:3000/projects in your browser. You should see the heading "My Projects" and the placeholder text.

Don't see the Projects page?

  • Make sure the file is saved and named exactly page.tsx (not projects.tsx or Page.tsx). Next.js only recognizes page.tsx as a route file.
  • Check that the file is inside app/projects/, not directly inside app/. The folder name projects creates the /projects URL segment.
  • Confirm that npm run dev is still running in your terminal.

Why isn't my /projects page showing up in Next.js?

✔️ Awesome, I've got everything!

Great. Double check you have saved app/projects/page.tsx.

ⓧ I'd like to double check the full code

export default function ProjectsPage() {
  return (
    <div>
      <h1 className="text-3xl font-bold">My Projects</h1>
      <p className="mt-2 text-gray-600">Projects coming soon.</p>
    </div>
  );
}

Now create the About page the same way.

  • Right-click the app folder in the Explorer sidebar.
  • Click New Folder and name it about.
  • Right-click the new about folder, click New File, and name it page.tsx.
  • Paste the following code into app/about/page.tsx:
// Export the about page component
export default function AboutPage() {
  return (
    <div>
      <h1 className="text-3xl font-bold">About Me</h1>
      {/* Introduction paragraph */}
      <p className="mt-4 text-gray-700">
        I am a developer with a background in PHP who is learning modern web
        technologies. I started with HTML, CSS, and JavaScript, and now I am
        exploring React and Next.js to build faster, more interactive web
        applications.
      </p>
      {/* Skills section with a styled card */}
      <div className="mt-6 rounded-lg bg-gray-50 p-6">
        <h2 className="text-xl font-semibold">Skills</h2>
        <ul className="mt-3 list-inside list-disc text-gray-700">
          <li>HTML, CSS, JavaScript</li>
          <li>PHP</li>
          <li>React and Next.js (learning)</li>
          <li>TypeScript (learning)</li>
        </ul>
      </div>
    </div>
  );
}

What does this code do?

  • The AboutPage function returns JSX with an introduction paragraph and a skills list.
  • Tailwind CSS classes like mt-6 (margin-top) and rounded-lg (rounded corners) handle all the styling directly in the markup.
  • Because this file lives at app/about/page.tsx, Next.js automatically maps it to the /about URL.
  • Save app/about/page.tsx.
  • Visit http://localhost:3000/about in your browser. You should see the "About Me" heading, the introduction paragraph, and a Skills card listing four items.

Seeing a 404 page?

  • Confirm the folder is named about (all lowercase) inside the app directory.
  • Confirm the file inside is named page.tsx exactly.

Why is my About page returning a 404 in Next.js?

✔️ Awesome, I've got everything!

Great. Double check you have saved app/about/page.tsx.

ⓧ I'd like to double check the full code

export default function AboutPage() {
  return (
    <div>
      <h1 className="text-3xl font-bold">About Me</h1>
      <p className="mt-4 text-gray-700">
        I am a developer with a background in PHP who is learning modern web
        technologies. I started with HTML, CSS, and JavaScript, and now I am
        exploring React and Next.js to build faster, more interactive web
        applications.
      </p>
      <div className="mt-6 rounded-lg bg-gray-50 p-6">
        <h2 className="text-xl font-semibold">Skills</h2>
        <ul className="mt-3 list-inside list-disc text-gray-700">
          <li>HTML, CSS, JavaScript</li>
          <li>PHP</li>
          <li>React and Next.js (learning)</li>
          <li>TypeScript (learning)</li>
        </ul>
      </div>
    </div>
  );
}

What is file-system routing?

In many frameworks, you have to write a configuration file that maps URLs to components. Next.js skips that entirely. Each folder inside app/ becomes a URL segment, and the page.tsx file inside it makes that segment accessible.

You just created app/projects/page.tsx and app/about/page.tsx. That is all Next.js needed to create the /projects and /about routes.

You now have three pages. Before you click the nav links to move between them, predict what will happen. Will navigation feel smooth, or will you notice something off?

  • Go back to http://localhost:3000 and click Projects in the nav bar.
  • Click About.
  • Click Home.

Each time you click, the screen flashes white. The browser is doing a full page reload, re-downloading the entire page from scratch. This is exactly how plain <a> tags work. That sluggish flash is the gap you will fix next.

Fix slow navigation with the Link component

Plain <a> tags tell the browser to fetch a brand-new page from the server on every click. The Link component from next/link replaces that with client-side navigation. It prefetches pages in the background and swaps content without a full reload.

  • Open components/Navbar.tsx in VS Code.
  • At the very top of the file (above the function), add this import:
import Link from "next/link";
  • Now find the three <a> tags inside the <div className="flex gap-4"> block. They look like this:
<a href="/" className="text-sm text-gray-300 hover:text-white">Home</a>
<a href="/projects" className="text-sm text-gray-300 hover:text-white">Projects</a>
<a href="/about" className="text-sm text-gray-300 hover:text-white">About</a>
  • Replace every <a with <Link and every </a> with </Link>, so the lines now look like this:
<Link href="/" className="text-sm text-gray-300 hover:text-white">Home</Link>
<Link href="/projects" className="text-sm text-gray-300 hover:text-white">Projects</Link>
<Link href="/about" className="text-sm text-gray-300 hover:text-white">About</Link>
  • Save components/Navbar.tsx.
  • Return to your browser at http://localhost:3000 and click between Home, Projects, and About.

The white flash is gone. The URL changes in the address bar, but the page transitions smoothly without reloading. That is client-side navigation in action.

Why Link instead of <a>?

A plain <a> tag tells the browser to throw away the current page and download a brand-new one from the server. The Link component from next/link intercepts the click, fetches only the new content in the background, and swaps it in. The Navbar, layout, and all shared UI stay mounted.

This is the primary way to navigate between routes in Next.js.

Still seeing a white flash?

  • Make sure you replaced all three <a> tags, including the closing </a> tags. A mix of <Link> and <a> still causes full reloads on the remaining <a> links.
  • Confirm the import at the top of the file reads import Link from "next/link" exactly.

My Next.js Link component is still causing full page reloads. Help me debug.

Add active link highlighting

Navigation is fast now, but there is no visual indicator showing which page you are on. To highlight the active link, you need to know the current URL path. Next.js provides a hook called usePathname() for exactly this.

Hooks only work in Client Components. Right now, Navbar.tsx is a Server Component (the default). You need to add the 'use client' directive at the top of the file to tell Next.js this component should run in the browser.

  • Select all the code in components/Navbar.tsx and delete it.
  • Paste in the following directive and imports:
// Mark this file as a Client Component so hooks work
"use client";

// Link for client-side navigation, usePathname to detect the current route
import Link from "next/link";
import { usePathname } from "next/navigation";

What is 'use client'?

In Next.js, every component is a Server Component by default. Server Components render on the server and send plain HTML to the browser. They cannot use hooks like useState or usePathname.

Adding 'use client' at the very top of a file (before any imports) tells Next.js to treat it as a Client Component. Client Components run in the browser, so hooks and event handlers work.

  • Directly below the imports, paste the Navbar function:
export default function Navbar() {
  const pathname = usePathname(); // Get current URL path

  const links = [
    { href: "/", label: "Home" },
    { href: "/projects", label: "Projects" },
    { href: "/about", label: "About" },
  ];

  return (
    <nav className="flex items-center gap-6 bg-gray-900 px-6 py-4">
      <span className="text-lg font-bold text-white">MyShowcase</span>
      <div className="flex gap-4">
        {links.map((link) => (
          <Link
            key={link.href}
            href={link.href}
            className={`text-sm transition-colors ${
              pathname === link.href
                ? "text-blue-400 font-semibold"
                : "text-gray-300 hover:text-white"
            }`}
          >
            {link.label}
          </Link>
        ))}
      </div>
    </nav>
  );
}

What does this code do?

  • usePathname() returns the current URL path (e.g. /projects). It updates automatically when the user navigates.
  • The links array stores all navigation items. Using .map() keeps the code clean and makes adding new links easy.
  • The className uses a ternary expression to compare pathname against each link's href. If they match, the link turns blue and bold. Otherwise, it stays gray.
  • Save components/Navbar.tsx.
  • Return to your browser and click between Home, Projects, and About. The active link should turn blue while the others stay gray.

Active link not highlighting?

  • Make sure "use client" is the very first line in the file, before any imports. If it appears after an import, Next.js ignores it.
  • Check that the href values in the links array match your folder names exactly: /, /projects, and /about.

My usePathname hook is not detecting the active page. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you have saved components/Navbar.tsx.

ⓧ I'd like to double check the full code

Scroll up and confirm your components/Navbar.tsx contains both chunks in order:

  • The "use client" directive on line 1, followed by the Link and usePathname imports.
  • The Navbar function with const pathname = usePathname(), the links array, and the .map() that renders each Link with conditional styling.

Click between all three pages one more time. Navigation is instant with no page reload. The URL changes in the address bar. The active nav link turns blue. That is file-system routing, the Link component, and usePathname all working together.

Your site now has three pages with smooth, instant navigation and a nav bar that always shows where you are. Next up, you will fill the Projects page with interactive cards that expand on click.

Create Interactive Project Cards

Your site has three pages with instant, flicker-free navigation. The Projects page, however, is just a heading with placeholder text.

You need real project data and interactive cards that expand on click. In this step, you will create a typed data file, build a ProjectCard component, and discover an important boundary between Server Components and Client Components.

In this step, get ready to:
  • Create a typed project data file in lib/projects.ts.
  • Build a ProjectCard component and render it on the Projects page.
  • Discover and fix the Server/Client Component boundary.
Create the project data file

Before you can display project cards, you need the data itself. You will store it in a dedicated lib/ folder at the project root.

Why a lib/ folder?

The lib/ folder is a developer convention for shared data and utility functions. It has no special meaning to Next.js. Only folders inside app/ become URL routes.

  • Create a lib folder at the project root (next to app/ and components/) by running this command in the VS Code terminal:
mkdir lib
  • Create a new file called projects.ts inside the lib folder and paste the following code:
export interface Project {
  slug: string;
  title: string;
  summary: string;
  description: string;
}

export const projects: Project[] = [
  {
    slug: "portfolio-site",
    title: "Portfolio Site",
    summary: "A personal portfolio built with HTML and CSS.",
    description:
      "My first web project. I built a responsive portfolio site using plain HTML and CSS to learn the fundamentals of web layout, flexbox, and media queries.",
  },
  {
    slug: "task-tracker",
    title: "Task Tracker",
    summary: "A to-do app built with vanilla JavaScript.",
    description:
      "A simple task management app that lets you add, complete, and delete tasks. Built with vanilla JavaScript and localStorage for persistence.",
  },
  {
    slug: "weather-dashboard",
    title: "Weather Dashboard",
    summary: "A weather app that fetches data from a public API.",
    description:
      "This app pulls weather data from the OpenWeatherMap API and displays current conditions and a five-day forecast. Built with JavaScript and fetch.",
  },
];

What does this code do?

  • The Project interface defines the shape of a project object using TypeScript. Every project must have a slug, title, summary, and description.
  • The projects array holds three sample project objects. Each one follows the Project interface, so TypeScript will catch any typos or missing fields.
  • Both the interface and the array are exported so other files can import them.
  • Save lib/projects.ts.
  • Confirm the file exists by running:
ls lib

You should see projects.ts listed in the output.

Don't see projects.ts?

  • Make sure you created the lib folder at the project root (inside project-showcase), not inside app/.
  • Check that your terminal is in the project-showcase directory before running ls lib.

Help me figure out why my lib/projects.ts file is missing.

✔️ Awesome, I've got everything!

Great. Make sure you have saved lib/projects.ts before moving on.

ⓧ I'd like to double check the full code

export interface Project {
  slug: string;
  title: string;
  summary: string;
  description: string;
}

export const projects: Project[] = [
  {
    slug: "portfolio-site",
    title: "Portfolio Site",
    summary: "A personal portfolio built with HTML and CSS.",
    description:
      "My first web project. I built a responsive portfolio site using plain HTML and CSS to learn the fundamentals of web layout, flexbox, and media queries.",
  },
  {
    slug: "task-tracker",
    title: "Task Tracker",
    summary: "A to-do app built with vanilla JavaScript.",
    description:
      "A simple task management app that lets you add, complete, and delete tasks. Built with vanilla JavaScript and localStorage for persistence.",
  },
  {
    slug: "weather-dashboard",
    title: "Weather Dashboard",
    summary: "A weather app that fetches data from a public API.",
    description:
      "This app pulls weather data from the OpenWeatherMap API and displays current conditions and a five-day forecast. Built with JavaScript and fetch.",
  },
];
Build the project card component

Now you have data, but no way to display it. You need a component that renders each project as a card with a title, summary, and the ability to expand on click.

  • Create a new file called ProjectCard.tsx inside your components/ folder and paste the following code:
import { useState } from "react";
import { Project } from "@/lib/projects";

// Accept a project object and display it as a card
export default function ProjectCard({ project }: { project: Project }) {
  // Track whether the card details are visible
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div onClick={() => setIsExpanded(!isExpanded)}>
      <h2>{project.title}</h2>
      <p>{project.summary}</p>
      {isExpanded && <p>{project.description}</p>}
    </div>
  );
}

What does this code do?

  • The component accepts a project prop typed with the Project interface you just created. TypeScript will ensure only valid project objects are passed in.
  • The useState hook creates an isExpanded boolean that toggles when the card is clicked.
  • When isExpanded is true, the description paragraph renders below the summary.
  • Save components/ProjectCard.tsx.
  • Now wire the card into the Projects page. Open app/projects/page.tsx and replace its entire contents with:
import { projects } from "@/lib/projects";
import ProjectCard from "@/components/ProjectCard";

export default function ProjectsPage() {
  return (
    <div>
      <h1 className="text-3xl font-bold">My Projects</h1>
      <p className="mt-2 text-gray-600">
        Click a card to expand it and see more details.
      </p>
      {/* Loop through projects and render a card for each */}
      <div className="mt-6 grid gap-4">
        {projects.map((project) => (
          <ProjectCard key={project.slug} project={project} />
        ))}
      </div>
    </div>
  );
}

What does this code do?

  • The page imports the projects array from @/lib/projects and the ProjectCard component from @/components/ProjectCard. The @/ prefix is an import alias that points to the project root.
  • The .map() call loops through every project in the array and renders a ProjectCard for each one, passing the project object as a prop.
  • The key={project.slug} prop gives React a unique identifier for each card so it can efficiently update the list.

Before you save, what do you think will happen when the browser refreshes? Will you see three project cards?

  • Save app/projects/page.tsx and check your browser at http://localhost:3000/projects.

You will see an error in the browser. The page will not load.

This error is expected

Next.js is telling you that you are using a React client hook (useState) inside a Server Component. By default, every component in the app/ directory is a Server Component. Server Components run on the server and cannot use browser-only features like state or event handlers.

Your ProjectCard.tsx file does not have the 'use client' directive, so Next.js tries to render it on the server and fails. You will fix this in the next substep.

Fix the component with 'use client'

The fix is a single directive. Adding 'use client' at the top of a file tells Next.js to run that component in the browser, where hooks and event handlers work.

  • Open components/ProjectCard.tsx and replace its entire contents with this updated version:
"use client";

import { useState } from "react";
import Link from "next/link";
import { Project } from "@/lib/projects";

export default function ProjectCard({ project }: { project: Project }) {
  const [isExpanded, setIsExpanded] = useState(false); // Toggle card details

  return (
    <div
      className="cursor-pointer rounded-lg border border-gray-200 p-6 transition-shadow hover:shadow-lg"
      onClick={() => setIsExpanded(!isExpanded)}
    >
      <h2 className="text-xl font-semibold">{project.title}</h2>
      <p className="mt-2 text-gray-600">{project.summary}</p>
      {isExpanded && ( // Show details only when card is clicked
        <div className="mt-4">
          <p className="text-gray-700">{project.description}</p>
          <Link
            href={`/projects/${project.slug}`}
            className="mt-3 inline-block text-blue-600 hover:underline"
            onClick={(e) => e.stopPropagation()}
          >
            View full page
          </Link>
        </div>
      )}
    </div>
  );
}

What changed?

  • The "use client" directive at the top marks this file as a Client Component. Now useState and onClick work because the component runs in the browser.
  • Tailwind CSS classes add borders, padding, rounded corners, and a shadow on hover so the cards look polished.
  • A Link to /projects/${project.slug} appears inside the expanded view. This link does not go anywhere yet. You will build that detail page in the Secret Mission.
  • The e.stopPropagation() call on the Link prevents the card from collapsing when you click the link.

Why does the page stay a Server Component?

Notice that app/projects/page.tsx does not have 'use client' at the top. It stays as a Server Component. Only the interactive ProjectCard is a Client Component.

This is the key pattern in Next.js: keep pages as Server Components and push interactivity down into small, focused Client Components. The page handles layout and data. The card handles clicks and state.

What do you expect to see when you save and check the browser this time?

  • Save components/ProjectCard.tsx.
  • Return to your browser at http://localhost:3000/projects.

You should see three styled project cards: Portfolio Site, Task Tracker, and Weather Dashboard. Click any card and it expands to show the description text and a "View full page" link. Click the card again and it collapses.

Cards not showing or still seeing an error?

  • Make sure "use client" is the very first line in components/ProjectCard.tsx, before any imports.
  • Check that the import path in app/projects/page.tsx matches exactly: @/components/ProjectCard.
  • Verify that lib/projects.ts exports both the Project interface and the projects array.

Help me debug why my project cards are not rendering on the Projects page.

✔️ Awesome, I've got everything!

Great. Make sure you have saved both components/ProjectCard.tsx and app/projects/page.tsx.

ⓧ I'd like to double check the full code

Here is the complete code for each file you changed in this step. Scroll up and confirm each one matches.

lib/projects.ts:

export interface Project {
  slug: string;
  title: string;
  summary: string;
  description: string;
}

export const projects: Project[] = [
  {
    slug: "portfolio-site",
    title: "Portfolio Site",
    summary: "A personal portfolio built with HTML and CSS.",
    description:
      "My first web project. I built a responsive portfolio site using plain HTML and CSS to learn the fundamentals of web layout, flexbox, and media queries.",
  },
  {
    slug: "task-tracker",
    title: "Task Tracker",
    summary: "A to-do app built with vanilla JavaScript.",
    description:
      "A simple task management app that lets you add, complete, and delete tasks. Built with vanilla JavaScript and localStorage for persistence.",
  },
  {
    slug: "weather-dashboard",
    title: "Weather Dashboard",
    summary: "A weather app that fetches data from a public API.",
    description:
      "This app pulls weather data from the OpenWeatherMap API and displays current conditions and a five-day forecast. Built with JavaScript and fetch.",
  },
];

components/ProjectCard.tsx:

"use client";

import { useState } from "react";
import Link from "next/link";
import { Project } from "@/lib/projects";

export default function ProjectCard({ project }: { project: Project }) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div
      className="cursor-pointer rounded-lg border border-gray-200 p-6 transition-shadow hover:shadow-lg"
      onClick={() => setIsExpanded(!isExpanded)}
    >
      <h2 className="text-xl font-semibold">{project.title}</h2>
      <p className="mt-2 text-gray-600">{project.summary}</p>
      {isExpanded && (
        <div className="mt-4">
          <p className="text-gray-700">{project.description}</p>
          <Link
            href={`/projects/${project.slug}`}
            className="mt-3 inline-block text-blue-600 hover:underline"
            onClick={(e) => e.stopPropagation()}
          >
            View full page
          </Link>
        </div>
      )}
    </div>
  );
}

app/projects/page.tsx:

import { projects } from "@/lib/projects";
import ProjectCard from "@/components/ProjectCard";

export default function ProjectsPage() {
  return (
    <div>
      <h1 className="text-3xl font-bold">My Projects</h1>
      <p className="mt-2 text-gray-600">
        Click a card to expand it and see more details.
      </p>
      <div className="mt-6 grid gap-4">
        {projects.map((project) => (
          <ProjectCard key={project.slug} project={project} />
        ))}
      </div>
    </div>
  );
}

Your Projects page is alive with interactive cards. Next up, you will take things further in the Secret Mission by giving each project its own dedicated detail page with a shareable URL.

Secret mission

Add Dynamic Project Detail Pages

Right now, clicking a project card expands it inline to show details. But what if each project had its own dedicated page with a full URL you could share? In this secret mission, you will create dynamic routes so every project gets a detail page at /projects/[slug].

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files, pause the dev server to free up resources, or delete everything entirely.

Resources you used:

  • The project-showcase folder on your Desktop (contains all project files and node_modules).
  • The npm run dev process running in your terminal.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with your Next.js project.

  • Your dev server stays running at http://localhost:3000. You can continue editing files and the browser will hot-reload your changes.
  • Your project-showcase folder stays on your Desktop with all your code intact.

Pause - I'll come back to this later

Shut down the running dev server to free up your terminal and system resources. Your project files stay on disk so you can pick up where you left off.

  • Switch to the terminal where npm run dev is running.
  • Press Ctrl+C to stop the dev server.
  • When you want to resume, open a terminal in the project-showcase folder and run npm run dev again.

Delete - I don't want to use this again

Remove all project files and stop the running server. This frees up disk space and fully cleans your machine.

  • Switch to the terminal where npm run dev is running and press Ctrl+C to stop the dev server.
  • Delete the project folder by running the command for your operating system.

macOS

  • Delete the project-showcase folder by running:
cd ~/Desktop
rm -rf project-showcase

Windows

  • Delete the project-showcase folder by running:
cd ~\Desktop
rmdir /s /q project-showcase
  • Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see project-showcase in the output.

Nice Work!

Nice Work!

Nice work! You just built a multi-page project showcase website with Next.js, complete with instant navigation, interactive components, and TypeScript-typed props.

What you learned:

  • Built a multi-page site using file-system routing, where folders inside app/ map directly to URL routes and page.tsx makes each route accessible.
  • Used Server Components and Client Components together, keeping pages on the server by default and adding 'use client' only where you needed hooks and event handlers.
  • Replaced plain <a> tags with the Next.js Link component for instant client-side navigation, and organized reusable code in components/ and lib/ folders.
  • Secret Mission: Created dynamic routes with [slug] folders to generate dedicated detail pages from your project data.

Ready to quiz yourself?