A hands-on course for new frontend developers
Next.js from Zero to a Real Company Website
You know some HTML, CSS, and JavaScript. You have 14 days. You spend the first 6 learning Next.js with this course. The rest of the time, you build and deliver the website of a real company, which your trainer will hand you.
The single most important idea in this course
In Next.js, folders are your URLs. If you understand this one sentence, everything else in this course is detail.
Course contents
How to use this course
- Type every code example yourself. Do not copy-paste. Typing is how the syntax enters your fingers.
- Do every exercise before opening the solution. Being stuck for 10 minutes teaches more than reading for an hour.
- The checkboxes save automatically in your browser, so you can close this page and continue later.
- Follow the schedule below. One day at a time. One day late is fine; three days late, tell your trainer.
- Stuck for more than 15 minutes? Every chapter ends with a green WhatsApp button: it opens a chat with your trainer, with the chapter already filled in. Add your question and send.
Your schedule · 14 days total
| Days | To do | You know it's done when... |
|---|---|---|
| 1 | Chapters 01 + 02: understand Next.js, install the tools | node --version prints a number |
| 2 | Chapter 03: React (take the whole day, it is the foundation of everything) | Exercise 01 done without looking at the solution |
| 3 | Chapters 04 + 05 + 06: create the project, pages, navigation | Your practice site has 4 pages linked together |
| 4 | Chapters 07 + 08: layouts and styling | Nav and footer everywhere, written once, and it looks like a real site |
| 5 | Chapters 09 + 10: images and "use client" | The FAQ items open and close |
| 6 | Chapters 11 + 12 + 13: data, metadata, static export | npm run build passes with no errors |
| 7 to 12 | Chapter 14: THE REAL PROJECT, the company website your trainer gives you, one milestone per day | Each milestone checked before moving to the next |
| 13 | Trainer feedback, corrections, polish | The build passes and the site looks right at phone width |
| 14 | Delivery and presentation of the site | You can explain every file in the project |
The pace is intense on purpose: the course contains only what you will need for the real project. Nothing in it is optional.
Chapter 01
What is Next.js, and why does it exist?
Goal: be able to explain in one sentence what Next.js is and why a team would choose it over plain HTML files.
Start from what you know
Imagine you build a website the classic way: one HTML file per page. index.html, about.html, contact.html. It works. But three problems appear very quickly:
- Repetition. Your navigation bar and footer are copied into every single file. Change one menu item and you must edit every page. With 30 pages, you will forget one, and the site becomes inconsistent.
- No components. You have a nice "card" design used 20 times. In plain HTML, that is 20 copies of the same markup. There is no way to say "here is my Card, reuse it".
- Data is painful. If your services live in a list somewhere, you cannot loop over that list in HTML. You write each item by hand.
React solves these problems. React is a JavaScript library that lets you build a page out of reusable pieces called components, and lets you generate HTML from data. We will learn just enough React in chapter 3.
So where does Next.js fit?
React alone is only a library. It does not answer questions like: How do URLs work? How do I get more than one page? How do I make the site fast? How do I ship it to a real server? Every team used to answer these questions themselves, differently, and often badly.
Next.js is a framework built on top of React that answers all of those questions for you. It gives you:
| You need | Next.js gives you |
|---|---|
| Multiple pages with clean URLs | File-based routing: create a folder, get a URL |
| Shared navigation and footer | Layouts: write them once, every page gets them |
| A fast development experience | npm run dev with instant reload when you save |
| Fast pages for visitors | Pages are pre-built into plain HTML before anyone visits |
| Optimized images, fonts, metadata | Built-in components and configuration |
| A way to ship | One command builds a folder you can host anywhere |
The one-sentence definition
Next.js is a framework that takes React components and turns them into a complete, fast, multi-page website, with routing, layouts, and builds handled for you.
"Static" means pre-built
In this course we build a static website. Static means: when you run the build command, Next.js generates finished HTML, CSS, and JavaScript files. A visitor's browser just downloads those files. There is no database and no server code running per visitor. This makes the site extremely fast, very cheap to host, and hard to hack. It is perfect for portfolios, restaurants, agencies, landing pages, and documentation.
Check yourself: your client has a 25-page brochure site and complains that updating the footer takes an hour. Which Next.js feature fixes this?
Check yourself: is Next.js a replacement for React?
Chapter 02
Install your tools
Goal: a working Node.js installation, a code editor, and confidence in the terminal.
1. Node.js
Next.js runs on Node.js, which lets JavaScript run on your computer instead of only inside a browser. Download the LTS version (the stable one, currently 22.x) from nodejs.org and install it with the default options.
| System | How to install Node.js |
|---|---|
| macOS | Download the macOS installer (the .pkg file) from nodejs.org and run it: it automatically picks the Apple Silicon or Intel build. If you already use Homebrew, brew install node@22 also works. |
| Windows | Download the Windows installer (the .msi file) and keep the default options. |
| Linux | Simplest route: download Node from nodejs.org. You can also use your distribution's package if it is version 22 or newer. The classic trap is a distribution package that is too old. |
Open a terminal
- macOS: press
Cmd + Space(Spotlight), type "Terminal", then Enter. - Windows: Start menu, type "PowerShell" (or "Terminal" on Windows 11), then Enter.
- Linux: usually
Ctrl + Alt + T, or look for "Terminal" in the applications menu.
Then open a terminal and verify:
$ node --version
v22.11.0
$ npm --version
10.9.0If you see version numbers (yours may differ slightly), you are ready. npm came along automatically; it is the tool that downloads JavaScript packages, including Next.js itself.
2. A code editor
Use VS Code from code.visualstudio.com unless you already have a favorite. Recommended extensions: ES7+ React snippets and Prettier (auto-formats your code on save, so you never argue about spacing).
3. Terminal survival kit
You only need five commands for this entire course:
| Command | What it does |
|---|---|
cd my-folder | Move into a folder ("change directory") |
cd .. | Move up one folder |
ls (Mac/Linux) or dir (Windows) | List what is in the current folder |
npm run dev | Start the development server (you will type this daily) |
Ctrl + C | Stop whatever is running in the terminal (on macOS this really is the ctrl key, not cmd, inside the terminal) |
Most common beginner mistake
Running commands in the wrong folder. If npm run dev says something like "no such file package.json", you are not inside your project folder. Run cd my-project first, then try again.
Chapter 03
React in 30 minutes
Goal: understand components, JSX, props, state, and rendering lists. This is all the React you need for this course.
Components: functions that return HTML
A React component is simply a JavaScript function whose name starts with a capital letter and which returns something that looks like HTML. That returned syntax is called JSX.
function Welcome() {
return <h1>Hello, team!</h1>;
}Once a component exists, you can use it like a custom HTML tag: <Welcome />. This is the superpower plain HTML never had: you invent your own tags.
JSX: five rules that cover 95% of cases
- Return one single root element. If you need several, wrap them in a
<div>or in an empty wrapper<> ... </>(called a fragment). classbecomesclassName, becauseclassis a reserved word in JavaScript. AlsoforbecomeshtmlFor.- Every tag must close.
<img>becomes<img />,<br>becomes<br />. - Curly braces
{ }mean "JavaScript goes here". Inside braces you can put a variable, a calculation, or a function call. - Comments inside JSX look like
{/* this */}, not<!-- this -->.
function Greeting() {
const name = "Sara";
const hour = 14;
return (
<div className="greeting">
{/* braces = escape hatch into JavaScript */}
<h1>Hello, {name}!</h1>
<p>{hour < 12 ? "Good morning" : "Good afternoon"}</p>
</div>
);
}Props: passing data into a component
Components become truly reusable when you can pass them data, the same way HTML tags take attributes. These are called props (properties). The component receives them as one object, which we usually unpack directly:
function ProfileCard({ name, job }) {
return (
<div className="card">
<h2>{name}</h2>
<p>{job}</p>
</div>
);
}
// Used three times, with different data each time:
function Team() {
return (
<section>
<ProfileCard name="Sara" job="Designer" />
<ProfileCard name="Ali" job="Developer" />
<ProfileCard name="Lina" job="Project manager" />
</section>
);
}One card design, written once, filled with different data. This is the "no more copy-paste" promise from chapter 1, delivered.
Rendering a list with .map()
When your data lives in an array, you turn each item into JSX with the array method .map(). Each generated element needs a key prop with a unique value, so React can track the items.
const team = [
{ name: "Sara", job: "Designer" },
{ name: "Ali", job: "Developer" },
{ name: "Lina", job: "Project manager" },
];
function Team() {
return (
<section>
{team.map((person) => (
<ProfileCard key={person.name} name={person.name} job={person.job} />
))}
</section>
);
}Now adding a team member means adding one line of data, not copying a block of HTML. Read that code slowly until it makes sense; it is the pattern you will use most in real work.
State: values that change when the user interacts
Props flow in from outside and do not change. When a component needs its own changing value (a counter, an open/closed menu, an input's text), it uses state via the useState function:
"use client"; // required in Next.js, explained in chapter 10
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}Read it as: "give me a value count starting at 0, and a function setCount to change it." When setCount runs, React re-renders the component with the new value. You never touch the DOM yourself; you change the data and React updates the screen.
Think in components
Show solution
function MenuItem({ name, price }) {
return (
<li>
{name} · {price} €
</li>
);
}
const items = [
{ name: "Espresso", price: 2.5 },
{ name: "Cappuccino", price: 3.5 },
{ name: "Croissant", price: 2.0 },
];
function Menu() {
return (
<ul>
{items.map((item) => (
<MenuItem key={item.name} name={item.name} price={item.price} />
))}
</ul>
);
}If your version differs slightly but renders three list items, it is correct.
Chapter 04
Create the project, and a tour of every file
Goal: a running Next.js project on your machine, and you know what each file in it is for.
One command creates everything
$ npx create-next-app@latest my-first-siteThe installer asks a few questions. Exact wording changes between versions; answer with this table and you will match this course:
| Question | Answer | Why |
|---|---|---|
| TypeScript? | No | We learn with plain JavaScript first. TypeScript comes later in your career. |
| ESLint / linter? | Yes | It warns you about mistakes while you type. |
| Tailwind CSS? | No | We learn real CSS first so you understand what any tool does underneath. |
src/ directory? | No | Fewer folders to think about. |
| App Router? | Yes | The modern way. This whole course uses it. |
| Turbopack? | Yes | Faster dev server. Accept the default. |
| Import alias? | No | Keep the default. |
Then start the development server:
$ cd my-first-site
$ npm run dev
▲ Next.js
- Local: http://localhost:3000Open http://localhost:3000 in your browser. You should see the Next.js welcome page. localhost:3000 means "a website served by my own computer, on port 3000". Only you can see it. Keep this terminal running while you work; press Ctrl + C when you want to stop it.
Tour of the project
Two files matter today. First, app/page.js is your homepage. A page is just a React component that is default exported:
export default function Home() {
return (
<main>
<h1>My first Next.js site</h1>
<p>It is alive!</p>
</main>
);
}Save the file and look at the browser: it updates instantly, without you refreshing. This is called hot reload, and it is why the dev server stays running.
Second, app/layout.js is the frame around every page. Notice it renders {children}; that placeholder is where each page's content appears. We work with it properly in chapter 7.
The daily rhythm
Every work session from now on is: open the project folder in VS Code, open the terminal, run npm run dev, edit files, watch the browser update. That is the whole workflow.
Make it yours
Chapter 05
Pages and routing: folders become URLs
Goal: create new pages and predict exactly which URL any file will get.
The rule
Remember the hero diagram at the top of this course. Here is the complete rule:
The routing rule
Inside app/, every folder becomes a URL segment, and the folder's page.js file is what visitors see at that URL. No page.js in a folder = no page at that URL.
So to create an About page at /about, you create a folder app/about/ containing a file page.js:
export default function AboutPage() {
return (
<main>
<h1>About us</h1>
<p>We are a small team that loves the web.</p>
</main>
);
}Save, then visit localhost:3000/about. That is all. No configuration file, no route registration. The folder structure is the configuration.
Folders nest, and so do URLs: app/services/design/page.js is served at /services/design.
Click a file. The fake browser shows the URL it would create.
Special file names inside app/
Next.js reserves a few file names. Each has one job:
| File | Job |
|---|---|
page.js | The content of a page. Required for the URL to exist. |
layout.js | Shared frame around pages (chapter 7). |
not-found.js | Your custom 404 page. |
Others exist (loading.js, error.js...), but these three are all you need for this course. Any file with a different name (like hero.js or data.js) is ignored by the router: no risk of creating a URL by accident.
Grow the site to four pages
Show solution
Each page.js is a small default-exported component, like the About example above.
Chapter 06
Navigation with <Link>
Goal: connect your pages with fast, app-like navigation.
You could connect pages with a normal <a href="/about"> tag, and it would work. But a plain <a> makes the browser throw away the whole page and download the next one from scratch: you see a white flash, and it is slow.
Next.js provides a <Link> component instead. It looks and behaves like a link (visitors can right-click it, open in a new tab, and search engines follow it), but Next.js swaps the page content without a full reload, and even pre-loads pages the visitor is likely to click. Same HTML underneath, much faster experience.
import Link from "next/link";
export default function Home() {
return (
<main>
<h1>My first Next.js site</h1>
<p>
Read more <Link href="/about">about us</Link>.
</p>
</main>
);
}Reading the import line
import Link from "next/link" means: "bring the Link component from the Next.js package into this file". Any component or function you use from another file must be imported at the top. If you forget, the error says "Link is not defined": that message is your reminder.
The rule of thumb: use <Link> for pages inside your site, and a normal <a> for external websites (like a link to Instagram).
A navigation component
A nav bar is the perfect first custom component. Create a components/ folder at the project root (next to app/, not inside it) to hold reusable pieces:
import Link from "next/link";
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/contact">Contact</Link>
</nav>
);
}And use it in a page. Because Nav.js is our own file (not a package), the import path starts with dots that describe where the file is relative to the current one:
import Nav from "../components/Nav";
export default function Home() {
return (
<main>
<Nav />
<h1>My first Next.js site</h1>
</main>
);
}../ means "go up one folder": from app/ up to the project root, then into components/. Getting import paths wrong is the number one beginner error; when it happens, the error message names the path it could not find, so check the dots first.
But wait: must we now import <Nav /> into every single page? That would be the copy-paste problem again. No. The next chapter fixes it properly.
Wire it up
Chapter 07
Layouts: build the frame once
Goal: a shared header and footer on every page, written exactly once.
Open app/layout.js. Simplified, it looks like this:
import "./globals.css";
export const metadata = {
title: "My first site",
description: "Built while learning Next.js",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Here is the mental model. The layout is a picture frame; each page is the picture. When someone visits /about, Next.js takes the layout and inserts the About page exactly where {children} is. Visit /contact, and the same frame gets the Contact page instead.
children is a prop that React fills automatically with whatever is nested inside. You never call it manually; you just mark where it belongs.
Put the nav and footer in the frame
import "./globals.css";
import Nav from "../components/Nav";
export const metadata = {
title: "My first site",
description: "Built while learning Next.js",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Nav />
{children}
<footer>© 2026 My first site</footer>
</body>
</html>
);
}Now delete <Nav /> (and its import) from your homepage: it would appear twice otherwise. Click through the site. Every page has the nav and the footer, and none of the page files mention them. This is the fix for the copy-paste problem from chapter 1. The 25-page footer update that took an hour is now a one-line edit.
One last fact to remember: the root layout is the only place in the whole app with <html> and <body> tags. Pages never include them.
Check yourself: you want to add a "Closed in August" banner visible on every page. Which file do you write it in?
app/layout.js, just above {children}. One edit, every page shows it. If you thought "in every page", reread this chapter.One frame for everything
Chapter 08
Styling: global CSS and CSS Modules
Goal: know where site-wide styles go, where component styles go, and why the two are separate.
Global CSS: the site-wide rules
app/globals.css is already imported by your root layout, so anything in it applies everywhere. This is the right place for foundations: fonts, background color, link colors, resets. Replace its content with something small and readable:
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
line-height: 1.6;
color: #1f2933;
background: #ffffff;
}
main {
max-width: 720px;
margin: 0 auto;
padding: 24px;
}CSS Modules: styles that cannot leak
Here is a classic team accident: you write .card { padding: 20px } for the services page, and a month later a teammate names something else .card on the contact page. The styles collide and one page breaks mysteriously.
CSS Modules prevent this. Name a file Something.module.css and Next.js makes every class in it private to the component that imports it, by renaming classes behind the scenes to something unique like Nav_card__x7Kd2. Two files can both use .card and never collide.
.nav {
display: flex;
gap: 24px;
padding: 16px 24px;
border-bottom: 1px solid #e2e8f0;
}
.nav a {
text-decoration: none;
color: #1f2933;
font-weight: 600;
}
.nav a:hover {
color: #0e7c5b;
}import Link from "next/link";
import styles from "./Nav.module.css";
export default function Nav() {
return (
<nav className={styles.nav}>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/contact">Contact</Link>
</nav>
);
}Read the pattern: import the stylesheet as an object called styles, then use className={styles.nav}. The class name comes from JavaScript (that is why it sits in curly braces), and it maps to the .nav rule in the module file.
The team rule of thumb
Global CSS for foundations that should apply everywhere (fonts, colors, resets). A CSS Module next to each component for that component's own look. When in doubt, choose the module: leaked styles cause the most confusing bugs in team projects.
Make it look designed
Chapter 09
Images and the public/ folder
Goal: add images to the site the correct Next.js way.
The public/ folder
Anything you put in public/ is served from the root of your site, exactly as-is. A file at public/logo.png is available in the browser at /logo.png. Note that the URL does not include the word "public".
The <Image> component
You could use a plain <img> tag, but Next.js ships an upgraded <Image> component that lazy-loads images (they only download when scrolled into view) and prevents the page from jumping around while images load. That jump is a real ranking and usability problem called layout shift, and it is why width and height are required: they reserve the space in advance.
import Image from "next/image";
export default function AboutPage() {
return (
<main>
<h1>About us</h1>
<Image
src="/images/team.jpg"
alt="Our team standing in front of the office"
width={720}
height={480}
/>
</main>
);
}srcstarts with/and is the path insidepublic/.altdescribes the image for blind visitors and for search engines. Write it like you are describing the photo on the phone to someone. Purely decorative image? Usealt="".widthandheightare numbers in curly braces (they are JavaScript numbers, not strings). Use the image's real proportions.
Heads-up for chapter 13
On a normal server, <Image> also resizes and compresses images on the fly. A static export has no server, so in chapter 13 we will switch that one feature off with a single config line. Everything else about <Image> keeps working.
Add real images
Chapter 10
Server vs Client Components
Goal: know when a file needs "use client" at the top, and why most files do not.
Every component in the app/ folder is, by default, a Server Component: Next.js renders it to finished HTML ahead of time. That HTML is small and instant for the visitor. All the pages you have built so far are Server Components, and that is exactly right for a static site.
But some things can only happen in the visitor's browser: reacting to clicks, remembering state, reading the window size. A component that does these things must be a Client Component, and you mark it by writing one line at the very top of the file:
"use client";
import { useState } from "react";
export default function FaqItem({ question, answer }) {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>{question}</button>
{open && <p>{answer}</p>}
</div>
);
}Two new things in that code: setOpen(!open) flips the value between true and false, and {open && <p>...</p>} means "render this paragraph only when open is true".
The decision is mechanical
| The component... | Kind | Mark |
|---|---|---|
| shows text, images, links, lists of data | Server (default) | nothing |
uses useState, useEffect, or any onClick / onChange | Client | "use client" on line 1 |
uses browser things like window or localStorage | Client | "use client" on line 1 |
In practice you do not even need to memorize this: if you use useState in a file without the marker, Next.js stops with an error that literally tells you to add "use client". Read errors; they are usually instructions.
Keep the amber small
Good Next.js style is to keep pages as Server Components and make only the small interactive piece a Client Component. The About page stays server-rendered; only the little FaqItem inside it carries "use client". Never paste "use client" at the top of every file "just in case": you would throw away the speed Next.js gives you for free.
And yes, Client Components still work in our static export. Their HTML is pre-built like everything else, and their JavaScript runs in the visitor's browser to make them interactive.
Check yourself: a Footer showing the year, and a mobile hamburger menu that opens on tap. Which needs "use client"?
useState and onClick. The footer just displays content, so it stays a Server Component with no marker.Your first interactive component
Show solution for the bonus
<button onClick={() => setOpen(!open)}>
{open ? "-" : "+"} {question}
</button>The condition ? a : b pattern picks between two values. It is the same one from the Greeting example in chapter 3.
Chapter 11
From data to UI
Goal: keep content in one data file and render it with components, the way real sites are built.
Real websites separate content from presentation. The menu prices live in a data file; the components decide how they look. When the client says "the cappuccino is 4 euros now", you change one number, not any markup.
Create a data folder at the project root:
export const menuItems = [
{ id: 1, name: "Espresso", price: 2.5, desc: "Short and strong." },
{ id: 2, name: "Cappuccino", price: 3.5, desc: "Creamy classic." },
{ id: 3, name: "Flat White", price: 3.8, desc: "Silky and smooth." },
{ id: 4, name: "Croissant", price: 2.0, desc: "Baked every morning." },
];Note this is a named export (no default). A file can have many named exports, and you import them with curly braces matching the exact name.
Now a card component and a page that combines everything you have learned:
import styles from "./MenuCard.module.css";
export default function MenuCard({ name, price, desc }) {
return (
<article className={styles.card}>
<div className={styles.top}>
<h3>{name}</h3>
<span className={styles.price}>{price.toFixed(2)} €</span>
</div>
<p>{desc}</p>
</article>
);
}import { menuItems } from "../../data/menu";
import MenuCard from "../../components/MenuCard";
export default function MenuPage() {
return (
<main>
<h1>Our menu</h1>
{menuItems.map((item) => (
<MenuCard
key={item.id}
name={item.name}
price={item.price}
desc={item.desc}
/>
))}
</main>
);
}Trace the flow out loud, it is the core loop of modern frontend work: data file → import the array → .map() over it → one component per item → props fill the component. Every product grid, team page, and pricing table you will ever build is this same pattern with different CSS.
Why key={item.id}?
React needs a stable identity for each item in a list, so it can update the right one when data changes. Use an id from the data. Avoid using the array index as key; it misbehaves when items are reordered or removed.
The data-driven page
Chapter 12
Metadata: titles, descriptions, and the browser tab
Goal: give every page its own title and description, which Google and social networks read.
Metadata is information about the page: the title in the browser tab, the description Google shows under a search result. In Next.js you never write <head> tags by hand; you export a metadata object and Next.js generates the tags.
You already have site-wide metadata in app/layout.js. Any page can export its own, which overrides the layout's for that page:
export const metadata = {
title: "Our menu | The Little Café",
description: "Espresso, cappuccino and fresh pastries in the heart of town.",
};
export default function MenuPage() {
// ...same as before
}Rules of thumb your future SEO colleagues will thank you for:
- Every page gets a unique title, formatted like
Page name | Site name. - The description is one honest, specific sentence. It is your ad in the search results.
Name every page
Chapter 13
Static export: turn your project into a folder of files
Goal: build the site into plain HTML/CSS/JS files that can be hosted anywhere.
One config change
Tell Next.js you want a static export. Open next.config.mjs and add two settings:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
images: { unoptimized: true },
};
export default nextConfig;output: "export"means: at build time, generate a folder of finished static files.images: { unoptimized: true }is the promise from chapter 9: on-the-fly image resizing needs a server, and a static site has none, so we switch that single feature off.
Build it
$ npm run build
Route (app)
┌ ○ /
├ ○ /about
├ ○ /contact
└ ○ /menu
○ (Static) prerendered as static contentEvery ○ (Static) circle is a page that was pre-built into HTML. The result lands in a new folder called out/:
Open out/about.html in your editor and look at it. It is real, finished HTML containing your text. This is what "static" meant in chapter 1: the work happened on your machine at build time, so the visitor's browser gets instant files.
Preview and host it
To preview the built site locally:
$ npx serve outTo put it on the internet, upload the contents of out/ to any static host. Free options: drag the folder onto netlify.com/drop, or push the project to GitHub and import it into Vercel or Cloudflare Pages (they run npm run build for you on every push). No servers to manage, nothing to keep updated, nothing that can be hacked at 3 a.m.
What a static export cannot do
No login system, no database writes, no form handling on your own server (forms need an external service like Formspree, or a host feature like Netlify Forms). When a project needs those, you deploy the same Next.js app to a Node server instead of exporting, and unlock its server features. Same framework, bigger world: that is a later course.
Ship the practice site
Chapter 14 · The real project
Build a real company's website
Goal: deliver a complete static site for a real client, alone, from empty folder to exported build. This is your end-of-training deliverable.
This is no longer practice. Your trainer hands you the brief of a real company: its name, its business, its texts, its logo and its photos. Your job for days 7 to 12 is to turn that content into a finished website, applying exactly what you learned. Build it from scratch in a new project (do not reuse the practice project; starting from a blank folder is part of the job). Try to work without rereading the chapters, and treat them as your documentation when you get stuck.
The expected pages
The exact content comes from your trainer's brief, but a company site almost always follows this plan. Adapt the names if the brief asks for it:
| Page | URL | Must contain |
|---|---|---|
| Home | / | Big heading with the company name, one photo, a short introduction, a Link styled as a button to the services |
| Services | /services | The company's services or products from a data file via .map(), each as a styled card |
| About | /about | The company's story, a team section from a data file, and 3 FAQ items that open and close |
| Contact | /contact | Address, opening hours (from a data file), email, and an external map link |
Site-wide requirements (this is what you will be evaluated on):
- Nav and footer on every page, via the root layout, never repeated in a page file.
- Every component styled with its own CSS Module; only foundations in
globals.css. - Every image through
<Image>with realalttext. - Unique
metadataon every page. - Exactly one file in the whole project says
"use client"(the FAQ item). - It builds with
npm run buildand works when served fromout/.
Suggested project structure
One milestone per day
Professionals do not build everything at once. One milestone per day, verified in the browser before moving to the next. If a milestone is done early, get ahead on the next one. Check them off as you go:
From empty folder to shipped site
Bonus goals, only if you finish early
- Add a custom 404 page with
app/not-found.js. - Group the services by category - hint: two arrays, or
.filter()before.map().
How your trainer will review it
Not by pixel-perfection. The review questions are: Is the layout written once? Is content in data files? Are styles in modules? Is "use client" used exactly once and in the right place? Does the build pass? Those five answers show whether you think in Next.js.
Bonus module · Git
Git: save your work and share it, without ever losing anything
Goal: turn a project into a Git repository, make clean save points (called "commits"), send them to GitHub, and know what to type when something breaks. No terminal experience needed beyond what this course already taught you.
هذه الوحدة متوفّرة بالعربية · This lesson is also available in Arabic.
This module is a bonus, but on a real team it is not optional: it is how everyone saves and shares their code. The good news is that 90 % of the time you will only use three commands. We start with the idea, then install, then build the habit.
Git 1 · What Git is, in a single idea
You already know the idea, without realizing it. You have probably once created final_report.docx, then final_report_v2.docx, then final_report_v2_REALLYfinal.docx. That was a manual, messy, fragile way to keep versions of your work.
Git does exactly that, but properly. Think of the checkpoint saves in a video game: before a hard section, you save. If it goes wrong, you reload the last save and nothing is lost. Git gives those save points to your project. Each save is called a commit: a snapshot of all your files at one moment, with a small label describing what you just did.
The one-sentence definition
Git is a time machine for your project: it records a snapshot of your files whenever you decide, so you can always go back, compare, and work with other people without overwriting each other.
Why every professional uses it, in three concrete reasons:
- Never lose work. Once a commit is made, it is carved into history. Even if you delete a file by mistake tomorrow, yesterday's version is still there.
- Undo without stress. Broke something? You go back to the last save point that worked, exactly like reloading a game.
- Work as a team. Two people can edit the same project without emailing files around or overwriting each other's work.
Git 2 · Git is not GitHub
People mix these up all the time, so let us settle it right now. Git is the tool that runs on your computer and makes the save points. GitHub is a website where you store a copy of your project online. It is like the difference between Word (the app on your machine) and Google Drive (the online place where you store the document).
| Git | GitHub | |
|---|---|---|
| What it is | A tool on your computer | A website |
| What it does | Makes the save points (commits) and history | Keeps an online copy and shares it |
| The analogy | Word, on your machine | Google Drive, in the cloud |
| Works offline? | Yes, it is all local | No, it is a website |
You can happily use Git on its own, without GitHub. But as soon as you want a backup off your computer, or want to show your code to someone (your trainer, for example), you send your project to GitHub. Alternatives exist (GitLab, Bitbucket), they do the same thing; this course uses GitHub because it is the most common.
Git 3 · Install Git (once)
Like Node.js in chapter 2, this is a one-time install per machine.
| System | How to install Git |
|---|---|
| macOS | Git is often already there. Type git --version in the terminal: if a number appears, you are done. Otherwise macOS will offer to install the "command line tools" (Xcode Command Line Tools), accept it. With Homebrew: brew install git. |
| Windows | Download the installer at git-scm.com and run it. Keep all the default options (click "Next" all the way). It also installs "Git Bash", a handy terminal. |
| Linux | One line in the terminal, depending on your distribution: sudo apt install git (Debian/Ubuntu) or sudo dnf install git (Fedora). |
Check that it is installed:
$ git --version
git version 2.43.0A number shows: perfect. Next, two settings to do once. They tell Git your name and email, so it can "sign" your save points.
$ git config --global user.name "Your Name"
$ git config --global user.email "you@example.com"What each command does, in plain words
git config --global user.name "...": records the name that will appear on each of your save points. It is just the signature on your commits.git config --global user.email "...": same with the email. Use the same one as your future GitHub account, it is cleaner.
--global means "for all my projects on this computer", so you will not do it again.
Git 4 · The 3-step ritual: look, box, seal
This is the heart of all Git. Every time you want to save, you repeat the same little habit of three commands. Picture packing a parcel: you look at what changed, you put the changes in the box, then you close the box with a label.
The journey of one change, in 3 steps
git add puts your changes in the box, git commit seals the box and stores it on the history shelf, for good.
First step in a brand new project: tell Git to watch this folder. That too is once per project.
$ git initgit init creates a hidden .git folder in your project: that is where Git will store all the history. You will never touch it by hand. Then the 3-step ritual, which you repeat for every save:
| The step | The command | What it does, in plain words |
|---|---|---|
| 1. Look | git status | Shows what changed since the last save. Changes nothing: it is just a glance. Get into the habit of typing it before and after each step. |
| 2. Box | git add . | Puts all changes in the box (the "staging area"), ready to be saved. The dot means "everything that changed". |
| 3. Seal | git commit -m "message" | Closes the box and stores it in history, with a label (the message) describing what you just did. |
In the terminal, it really looks like this:
$ git status # what changed?
$ git add . # put everything in the box
$ git commit -m "Add the contact page" # seal with a labelThe commit message: a note to tomorrow's you
The message should say what this commit changes, in one short sentence. Write it as if finishing the sentence "This commit will...". Future you, or a teammate, will thank you when reading the history.
| Weak message | Good message |
|---|---|
"stuff" | "Add the About page" |
"asdf" | "Fix the broken menu link" |
"final changes v2" | "Make the footer full-width on mobile" |
Git 5 · Looking back: your history of save points
To see all your save points, one per line, newest to oldest:
$ git log --oneline
a1b2c3d Make the footer full-width on mobile
9f8e7d6 Add the contact page
4c5b6a7 First commitEach line is a save point: the small code at the start (for example a1b2c3d) is its unique id, followed by your message. It is your trail of breadcrumbs. Remember this idea that changes everything: once a commit is made, its contents are never lost. You can experiment, break things, try wild ideas, and always come back to a healthy line in that history.
Tip · the git log screen
If git log (without --oneline) fills the whole screen and the terminal seems "stuck" with a : at the bottom, do not panic: Git is just showing you a long list. Press the q key (for "quit") to get out and return to your normal terminal.
Git 6 · Putting your project online on GitHub
So far everything has stayed on your computer. Let us send a copy to GitHub, to keep it safe and be able to share it.
a) Create a GitHub account. Go to github.com, click "Sign up" and follow the steps (email, password, username). It is free.
b) Create an empty repository. A "repository" (or "repo") is simply your project folder, GitHub version. Once logged in:
- Click the "New" button (green), or the "+" at the top right, then "New repository".
- Give it a name, for example
my-first-site. - Leave everything else at its default. Important: do NOT tick "Add a README", nor
.gitignore, nor a license. We want a completely empty repo, because your project already exists on your machine. - Click "Create repository".
GitHub then shows a page with commands. You are looking for the "...or push an existing repository" block. Those are exactly the three commands below. Copy the address of YOUR repo (it ends in .git) and run, from your project folder:
$ git remote add origin https://github.com/you/my-first-site.git
$ git branch -M main
$ git push -u origin mainWhat each line does, in plain words
git remote add origin ...: tells Git "here is this project's online address". We nickname that addressorigin. Do it once per project.git branch -M main: names your main line of workmain, the standard name today. Do it once.git push -u origin main: "pushes" (sends) your commits to GitHub. The-u origin mainremembers the destination.
Refresh the GitHub page: your files are there. From now on, after each new commit, a single command sends your changes online:
$ git pushTwo words you will meet soon, in two sentences each:
git clonedownloads an entire project from GitHub, with all its history, into a new folder on your machine. That is how you get a team's project the first time.git pullfetches the latest changes your teammates pushed to GitHub and merges them into your copy. Do it before you start working, to begin from the most recent version.
Git 7 · The .gitignore file, in 3 lines
Some files must never go into Git: they are huge, regenerated automatically, or contain secrets. The .gitignore file is simply a list of names to ignore. The most important one for you: node_modules, that giant folder of downloaded packages (see chapter 4). It recreates itself with npm install, so sending it to GitHub would be pointless and very heavy.
Good news: when you ran create-next-app in chapter 4, a correct .gitignore was already created for you. You have nothing to do. Here it is, so you know what it looks like:
node_modules
.next
out
.env*.localTranslation: "ignore the downloaded packages, the build folders, and any secrets file". If one day git status shows you thousands of node_modules files, that is the sign a .gitignore is missing.
Git 8 · "Help, I broke something"
The four most common beginner hiccups, and the exact command that fixes each. All of these commands are safe: no risky manipulation here.
| Your situation | The fix | What it does |
|---|---|---|
| I just made a commit with a bad message | git commit --amend -m "The right message" | Rewrites the label of the very last commit. Only do this if it has not been pushed yet. |
| I want to discard my changes to a file (not committed yet) | git restore file-name | Puts that file back as it was at the last commit. (Older form you will see online: git checkout -- file-name.) |
| I committed, but I want to undo the commit and keep my work | git reset --soft HEAD~1 | Removes the last commit but leaves all your changes intact in the folder, ready to be committed again. |
The terminal is stuck on a weird screen after git log | Press q | Quits Git's text viewer and gives the terminal back to you. |
Stick to the safe commands
Searching online, you will run into git push --force and git rebase. They are powerful tools that can erase work if misused. While you are a beginner working solo, you do not need them. When in doubt, stop and ask your trainer on WhatsApp before typing a command you do not understand.
Git 9 · Branches, gently
One last idea, just so you recognize it when you see it. A branch is a parallel line of save points: a working copy where you can try a new feature without touching the main version (main). When it is ready and works, you merge the branch into main. That is how teams work together on the same project without stepping on each other: everyone on their own branch. To create one: git switch -c my-new-feature (older form: git checkout -b ...).
Let us be honest: if you are a beginner working solo on your project, you can happily stay on main for now. Branches become useful the day you join a team. Just know the word exists and what it means.
Git 10 · Guided exercise: your project on GitHub
Let us put it all together. You will take the training project from the earlier chapters, turn it into a Git repository, and send it to GitHub. Follow the boxes in order.
From local folder to shared repository
Check yourself
What is the difference between Git and GitHub?
What are the three commands of the save ritual, in order?
git status (look at what changed), git add . (put the changes in the box), then git commit -m "message" (seal the box with a label).Why must node_modules be in the .gitignore?
npm install. Sending it to GitHub would be pointless and very heavy. Good news: create-next-app already put it in the .gitignore for you.You just made a commit with the message "asdf". How do you fix just the label?
git commit --amend -m "A clear, correct message". It rewrites the last commit's message. Only do this if it has not been pushed to GitHub yet.You are a beginner working solo. Must you create branches?
main is perfectly fine for now. Branches become useful when you work as a team. You just need to know the word exists and what it means.Git cheat sheet
Once per machine
git --versioncheck Git is installedgit config --global user.name "..."your name on commitsgit config --global user.email "..."your email on commits
Once per project
git initstart watching this foldergit remote add origin ...link to the GitHub repogit branch -M mainname the main linegit push -u origin mainfirst push online
The everyday ritual
git statussee what changedgit add .put changes in the boxgit commit -m "message"seal the save pointgit pushsend to GitHub
Look and fetch
git log --onelinethe history, one line per commitqquit the git log screengit pullfetch other people's changesgit clone ...download an entire project
Fix without stress
git commit --amend -m "..."fix the last messagegit restore filediscard a file's changesgit reset --soft HEAD~1undo the last commit, keep the work
Later, on a team
git switch -c my-branchcreate a working branchgit switch maingo back to the main line- Solo and a beginner? Stay on
main, it is fine
Appendix A
Cheat sheet
Commands
npx create-next-app@latest namenew projectnpm run devdevelop at localhost:3000npm run buildbuild for productionnpx serve outpreview the static exportCtrl + Cstop the server
Routing
- Folder in
app/= URL segment page.js= the page at that URLlayout.js= shared frame, renders{children}not-found.js= custom 404
Imports you will type daily
import Link from "next/link"import Image from "next/image"import { useState } from "react"import styles from "./X.module.css"
JSX gotchas
className, notclass- One root element per return (or
<>...</>) - Self-close every tag:
<Image /> { }= JavaScript inside markupkeyon every.map()item
Server vs Client
- Default = Server Component (fast, static)
- Clicks or state? Add
"use client"line 1 - Keep client components small and low in the tree
Static export
output: "export"in next.config.mjsimages: { unoptimized: true }- Result in
out/, host anywhere
Appendix B
Common errors, decoded
| The error says | It means | Fix |
|---|---|---|
| "npm: command not found" | Node.js is not installed or the terminal was open during install | Install Node LTS, then open a fresh terminal |
| "Could not read package.json" | You are in the wrong folder | cd into the project folder first |
| "X is not defined" | You used a component or function without importing it | Add the import line at the top |
| "Module not found: Can't resolve '../components/Nav'" | The import path or file name is wrong | Count the ../ hops; check exact spelling and capitalization |
| "You're importing a component that needs useState..." | State or events in a Server Component | Add "use client" as line 1 of that file |
| "Adjacent JSX elements must be wrapped..." | Your return has two root elements | Wrap them in one <div> or <>...</> |
| "Each child in a list should have a unique 'key' prop" | A .map() without keys | Add key={item.id} to the outermost mapped element |
| Page shows 404 but the folder exists | The file inside is not named exactly page.js | Rename it; only page.js creates a URL |
| Image shows broken icon | Wrong src path | Path starts at public/: file public/images/a.jpg is src="/images/a.jpg" |
| Port 3000 already in use | An old dev server is still running | Find that terminal and Ctrl + C, or accept the offered port 3001 |
General debugging method: read the first error (later ones are usually echoes of it), note the file name and line number it mentions, and re-read that exact line. Nine times out of ten it is a typo, a missing import, or a missing closing tag.
Appendix C
Glossary
- Component
- A capitalized JavaScript function that returns JSX. The reusable building block of React.
- JSX
- The HTML-like syntax inside components. Curly braces embed JavaScript.
- Props
- Data passed into a component like HTML attributes, making it reusable with different content.
- State
- A component's own changing value, created with
useState. Changing it re-renders the component. - App Router
- Next.js's routing system where folders in
app/define URLs. - Layout
- A shared frame around pages. Renders each page where it says
{children}. - Server Component
- The default. Rendered to HTML ahead of time; no interactivity of its own.
- Client Component
- A component marked
"use client"whose JavaScript runs in the browser for interactivity. - CSS Module
- A
.module.cssfile whose class names are private to the component importing it. - Hot reload
- The dev server updating the browser instantly when you save a file.
- Static export
- Building the whole site into plain files in
out/, hostable on any static host. - Build
- The process (
npm run build) that turns your source code into optimized production files. - localhost:3000
- Your own computer serving the site during development. Only visible to you.
- npm
- The package manager that installs libraries and runs project scripts.