Build a Blog Reader with Next.js
Build a multi-page blog reader app using Next.js App Router and TypeScript.
Introduction
30 Second Summary
Every time you scroll through a blog, news site, or social media feed, a web application is fetching content from a server and displaying it across multiple pages. These multi-page apps are the backbone of the modern web.
In this project, you will build a blog reader application using Next.js that fetches real posts from a live API and displays them across multiple pages with shared navigation.
What You'll Build
A Next.js blog reader app that uses Server Components to fetch posts from a public API, displays them on a styled home page, and renders individual post pages with comments using dynamic routes.
By the end of this project, you'll have:
- A Next.js project using the App Router with TypeScript and Tailwind CSS.
- A home page that fetches and displays blog posts from a live API using Server Components.
- Dynamic routes that display individual post details with comments based on the URL.
- A root layout with persistent navigation that stays in place across page transitions.
- Secret Mission: Add a Users page that lists all authors with links to their posts.
Are there any prerequisites?
Basic familiarity with HTML, CSS, and JavaScript is all you need. Some React experience is helpful but not required.
Set Up Your Development Environment
Before you can build your blog reader, you need a development environment ready to go. Next.js runs on Node.js, so that's the first thing to install.
Once Node.js is in place, you'll scaffold your project with a single command and launch a local dev server. By the end of this step, you'll see the default Next.js starter page running in your browser.
In this step, get ready to:
- Install and verify Node.js.
- Scaffold a new Next.js project with TypeScript, Tailwind CSS, and the App Router.
- Open the project in VS Code and run the dev server.
Install and verify Node.js
Node.js is the JavaScript runtime that powers Next.js. This project requires version 20 or later.
- Check if Node.js is already installed by running this command in your terminal:
Opening your terminal
Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
node --version
Look at the output and pick the tab below that matches what you see.
✔️ I see version 20 or higher
Nice, Node.js is ready. You'll see something like v22.17.0 or any number starting with 20, 22, or higher.
Skip ahead to the next substep.
ⓧ I see an older version
If your version starts with 18 or lower, you need to upgrade. Some operating systems ship with an older Node.js by default.
- Visit the official Node.js download page at https://nodejs.org.
- Download the LTS version (currently 22.x).
- Run the installer and follow the on-screen prompts.
- Close and reopen your terminal, then run node --version again to confirm the upgrade.
Still seeing the old version?
On macOS, the system may still resolve to an older Node.js even after installing a newer one. Try running /usr/local/bin/node --version to check the freshly installed binary directly.
If you have multiple versions, consider using a version manager like nvm to switch between them. Help me install the right version of Node.js
ⓧ Command not found
Node.js isn't installed yet. Let's fix that.
- Visit the official Node.js download page at https://nodejs.org.
- Download the LTS version (currently 22.x).
- Run the installer and follow the on-screen prompts.
- Close and reopen your terminal after installation.
- Verify the install by running:
node --version
You should now see a version number starting with 22 or higher.
Still getting command not found?
Make sure you fully closed and reopened your terminal after installing. The installer adds Node.js to your system path, but existing terminal sessions won't pick it up.
Help me troubleshoot my Node.js installation
Scaffold your Next.js project
Next.js provides a scaffolding tool called create-next-app that generates a ready-to-run project with all the configuration done for you.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Next.js project by running this command:
npx create-next-app@latest blog-reader
What happens when you run this?
If this is your first time running npx create-next-app, you may be asked to confirm the package download. Type y and press Enter to proceed.
The CLI will then ask if you'd like to use the recommended defaults (TypeScript, ESLint, Tailwind CSS, App Router). Press Enter to accept them.
- When prompted, press Enter to accept the recommended defaults.
The tool will install all dependencies automatically. This may take a minute depending on your internet speed.
- Once installation finishes, move into your new project folder by running:
cd blog-reader
- Confirm the project was created by running:
ls
You should see files like package.json, next.config.ts, and an app folder in the output.
Seeing an error during scaffolding?
Make sure your Node.js version is 20 or higher. Run node --version to double check.
If you see a network timeout error, check your internet connection and try the command again.
Help me fix my create-next-app error
Open the project in VS Code and start the dev server
With your project scaffolded, it's time to open it in a code editor and see it running live in your browser.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the blog-reader folder, then click Open.
What is VS Code?
VS Code is a free code editor from Microsoft. If you don't have it installed, download it from https://code.visualstudio.com.
Now let's start the development server so you can see your app in the browser.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
- Start the dev server by running this command:
npm run dev
What does this command do?
This starts the Next.js development server using Turbopack, the default bundler in Next.js 16. It watches your files for changes and refreshes the browser automatically whenever you save.
- Open your browser and go to http://localhost:3000.
You should see the default Next.js starter page with the Next.js logo and some helpful links.
Don't see the starter page?
Check your terminal for error messages. Make sure you're inside the blog-reader folder when you run npm run dev.
If port 3000 is already in use, the terminal will show you an alternative port number. Try the URL shown in your terminal output instead.
Help me get my Next.js dev server running
Your development environment is locked and loaded. Next up, you'll replace the default starter page with a real home page that fetches blog posts from an API.
Build the Home Page with Server-Side Data Fetching
Your development environment is ready, with the Next.js dev server running locally. Now it's time to build the first real page of your blog reader.
The default starter page shows placeholder content. In this step, you'll replace it with a Server Component that fetches blog posts from a live API and renders them as clickable cards.
In this step, get ready to:
- Clear out the default app/page.tsx content.
- Fetch blog posts from the JSONPlaceholder API using a Server Component.
- Display posts as styled link cards using Tailwind CSS and the Next.js Link component.
Clear out the default page content
The starter content in app/page.tsx is just a demo page. You'll replace it entirely with your own component.
- In VS Code, open app/page.tsx from the Explorer sidebar.
- Select all the content by pressing Cmd+A (macOS) or Ctrl+A (Windows).
- Delete everything so the file is completely empty.
Add a Server Component that fetches and displays posts
In the App Router, every component is a Server Component by default. This means you can use async/await directly inside a component to fetch data on the server before any HTML reaches the browser.
- Paste the following code into your empty app/page.tsx file:
import Link from "next/link";
// Define the shape of a post from the API
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// This is a Server Component — it fetches data on the server
export default async function HomePage() {
// Fetch all posts from JSONPlaceholder
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts: Post[] = await response.json();
return (
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Latest Posts</h1>
<div className="space-y-4">
{/* Render the first 20 posts as clickable cards */}
{posts.slice(0, 20).map((post) => (
<Link
key={post.id}
href={`/posts/${post.id}`}
className="block p-6 bg-white border border-gray-200 rounded-lg hover:border-gray-400 transition-colors"
>
<h2 className="text-xl font-semibold text-gray-900 mb-2 capitalize">
{post.title}
</h2>
<p className="text-gray-600 line-clamp-2">{post.body}</p>
</Link>
))}
</div>
</div>
);
}
What does this code do?
This single file covers three key patterns in Next.js:
- Server-side data fetching: The async keyword on HomePage lets you await the fetch() call directly. The data arrives before any HTML is sent to the browser.
- TypeScript interface: The Post interface tells TypeScript exactly what fields each post object contains, giving you autocomplete and error-checking.
- Navigation with Link: Each card uses the Next.js Link component to point to /posts/${post.id}. This enables client-side navigation without full page reloads.
What is a Server Component?
A Server Component runs only on the server. It never ships JavaScript to the browser for rendering. This means your API call, the response parsing, and the loop logic all stay server-side.
The browser receives only the finished HTML. This makes the page load faster and keeps your bundle size small.
- Save app/page.tsx.
- Switch to your browser at http://localhost:3000. You should see a list of blog post cards with titles and body text previews.
Don't see the post cards?
- Make sure you saved the file. Unsaved changes won't trigger a reload.
- Check that your dev server is still running in the terminal. If it stopped, run npm run dev again.
- Look for red error text in the browser or terminal. A typo in the fetch URL or a missing closing bracket is the most common cause.
Help me debug why my home page isn't showing posts
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and can see the post cards in your browser.
ⓧ I'd like to double check the full code
Here's the complete app/page.tsx file for comparison:
import Link from "next/link";
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
export default async function HomePage() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts: Post[] = await response.json();
return (
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Latest Posts</h1>
<div className="space-y-4">
{posts.slice(0, 20).map((post) => (
<Link
key={post.id}
href={`/posts/${post.id}`}
className="block p-6 bg-white border border-gray-200 rounded-lg hover:border-gray-400 transition-colors"
>
<h2 className="text-xl font-semibold text-gray-900 mb-2 capitalize">
{post.title}
</h2>
<p className="text-gray-600 line-clamp-2">{post.body}</p>
</Link>
))}
</div>
</div>
);
}
Your home page is live with real data from an API. Next up, you'll create dynamic post pages so each card links to its own detail view with comments.
Create Dynamic Post Pages
Your home page now lists 20 blog posts as clickable cards. But clicking one leads to a 404 page because the destination doesn't exist yet.
In this step, you'll create dynamic routes that generate a unique page for every post. Each page fetches the post's full content and its comments from the API, then renders them together.
In this step, get ready to:
- Create a dynamic route folder for individual posts.
- Fetch post details and comments from the API in parallel.
- Display the full post with a styled comments section.
Create the dynamic route file
Next.js uses a folder-naming convention to create dynamic routes. Wrapping a folder name in square brackets (like [id]) tells Next.js that this segment of the URL is a variable.
- In the VS Code Explorer sidebar, right-click the app folder and select New Folder.
- Name the folder posts.
- Right-click the new posts folder and create another folder inside it named [id] (include the square brackets).
- Right-click the [id] folder and select New File. Name it page.tsx.
Confirm the file path reads app/posts/[id]/page.tsx in the VS Code tab bar.
What is a dynamic route?
A dynamic route generates a page for every possible value of a URL segment. Your folder structure app/posts/[id]/page.tsx means any URL like /posts/1, /posts/2, or /posts/99 will match this page.
Next.js passes the actual value (1, 2, or 99) into your component as a parameter called id. You use that parameter to fetch the right data.
Fetch post and comment data in parallel
This page needs two pieces of data from the API: the post itself and its comments. You can fetch both at the same time using Promise.all so the page loads faster.
- In app/posts/[id]/page.tsx, paste the following code to set up the imports, type definitions, and data fetching:
import Link from "next/link";
// Define the shape of a post from the API
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// Define the shape of a comment from the API
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
// Await the params to get the post ID from the URL
const { id } = await params;
// Fetch the post and its comments at the same time
const [postResponse, commentsResponse] = await Promise.all([
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`),
fetch(`https://jsonplaceholder.typicode.com/posts/${id}/comments`),
]);
const post: Post = await postResponse.json();
const comments: Comment[] = await commentsResponse.json();
What does this code do?
The Post and Comment interfaces tell TypeScript what shape to expect from the API responses. This gives you autocomplete and catches typos at build time.
In Next.js 16, params is a Promise that must be awaited before you can access the id value. This is a change from earlier versions where params was synchronous.
Promise.all fires both fetch requests simultaneously instead of waiting for one to finish before starting the other. This cuts loading time roughly in half.
You won't see a change yet. The file is incomplete until the next chunk adds the JSX return statement.
Display the full post with comments
Now that you have the post and comments data, you need a return statement that renders them on the page.
- Directly below the last line you pasted (const comments: Comment[] = await commentsResponse.json();), add the following return statement to complete the component:
return (
<div>
<Link
href="/"
className="text-blue-600 hover:text-blue-800 mb-4 inline-block"
>
← Back to posts
</Link>
<article className="bg-white border border-gray-200 rounded-lg p-8 mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-4 capitalize">
{post.title}
</h1>
<p className="text-gray-700 leading-relaxed text-lg">{post.body}</p>
</article>
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Comments ({comments.length})
</h2>
<div className="space-y-4">
{comments.map((comment) => (
<div
key={comment.id}
className="bg-gray-50 border border-gray-200 rounded-lg p-4"
>
<div className="flex items-center gap-2 mb-2">
<span className="font-semibold text-gray-900">
{comment.name}
</span>
<span className="text-sm text-gray-500">{comment.email}</span>
</div>
<p className="text-gray-700">{comment.body}</p>
</div>
))}
</div>
</section>
</div>
);
}
What does this code do?
The Link component at the top gives readers a way to navigate back to the posts list. Next.js Link performs client-side navigation without a full page reload.
The <article> tag wraps the post's title and body. The <section> tag wraps the comments list, where each comment is rendered using .map() to loop over the array.
- Save app/posts/[id]/page.tsx.
- Switch to your browser at http://localhost:3000 and click any post card on the home page.
- You should see the post's full title at the top, its body text below, and a list of 5 comments with names and emails underneath.
- Click the Back to posts link at the top. It should take you back to the home page.
Seeing a 404 or blank page?
Check that your folder structure is exactly app/posts/[id]/page.tsx. The square brackets must be part of the folder name. A common mistake is naming it id without brackets.
If you see a TypeScript error in your terminal, make sure the return statement's closing } matches the function's opening brace. The file should have exactly one export default async function PostPage with a single return inside it.
Help me debug my dynamic route page.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete app/posts/[id]/page.tsx file. Make sure yours matches exactly:
import Link from "next/link";
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const [postResponse, commentsResponse] = await Promise.all([
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`),
fetch(`https://jsonplaceholder.typicode.com/posts/${id}/comments`),
]);
const post: Post = await postResponse.json();
const comments: Comment[] = await commentsResponse.json();
return (
<div>
<Link
href="/"
className="text-blue-600 hover:text-blue-800 mb-4 inline-block"
>
← Back to posts
</Link>
<article className="bg-white border border-gray-200 rounded-lg p-8 mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-4 capitalize">
{post.title}
</h1>
<p className="text-gray-700 leading-relaxed text-lg">{post.body}</p>
</article>
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Comments ({comments.length})
</h2>
<div className="space-y-4">
{comments.map((comment) => (
<div
key={comment.id}
className="bg-gray-50 border border-gray-200 rounded-lg p-4"
>
<div className="flex items-center gap-2 mb-2">
<span className="font-semibold text-gray-900">
{comment.name}
</span>
<span className="text-sm text-gray-500">{comment.email}</span>
</div>
<p className="text-gray-700">{comment.body}</p>
</div>
))}
</div>
</section>
</div>
);
}
Each post now has its own page with full content and comments. Next up, you'll add a shared layout with navigation so readers can move around the app without losing context.
Add a Layout with Navigation
Your blog reader now has a home page listing posts and dynamic detail pages with comments. But there's no consistent way to navigate between them.
In Next.js, a layout is shared UI that wraps all pages. It renders once and persists across page navigations without re-rendering. In this step, you'll add a navigation header to your layout and create a loading state that appears while pages fetch data.
In this step, get ready to:
- Add a navigation header to your root layout.
- Create a loading skeleton that shows while pages are fetching.
- Observe how the layout persists across page navigations.
Update the root layout with navigation
The root layout in app/layout.tsx wraps every page in your app. Right now it's the default template from create-next-app with no navigation. You'll update it to include a header with links so users can always get back to the posts list.
- In VS Code, open app/layout.tsx.
- Replace the entire file contents with the following code:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
import "./globals.css";
// Configure the Geist font family as CSS variables
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
// Page metadata displayed in the browser tab
export const metadata: Metadata = {
title: "Blog Reader",
description: "A blog reader built with Next.js",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{/* Navigation header that persists across all pages */}
<header className="border-b border-gray-200 bg-white">
<nav className="mx-auto max-w-4xl px-4 py-4 flex items-center gap-6">
<Link href="/" className="text-xl font-bold text-gray-900">
Blog Reader
</Link>
<Link
href="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
Posts
</Link>
</nav>
</header>
{/* Main content area with consistent max-width and padding */}
<main className="mx-auto max-w-4xl px-4 py-8">{children}</main>
</body>
</html>
);
}
What does this code do?
The Link import from next/link enables client-side navigation between pages without full page reloads.
The <header> contains a <nav> with two links: the bold "Blog Reader" brand link and a "Posts" link. Both point to / (your home page).
The <main> wrapper gives all page content a consistent max-width and padding. The {children} prop is where each page's content gets rendered inside the layout.
- Save app/layout.tsx.
- Return to your browser at http://localhost:3000. You should see a white navigation bar at the top with "Blog Reader" in bold on the left and a "Posts" link beside it.
Don't see the navigation header?
- Make sure you saved app/layout.tsx. Unsaved changes won't appear in the browser.
- Check that the import Link from "next/link" line is at the top of the file. Without it, the Link component will be undefined.
- Confirm that your dev server is still running in the terminal. If it stopped, run npm run dev again.
Still stuck? Ask for help.
Why use a layout instead of repeating the nav on every page?
In the App Router, layouts wrap their child pages and only render once. When you click a link, the layout stays mounted while only the page content swaps out. This means navigation is instant and you never have to duplicate header code across pages.
Create a loading state
When a Server Component fetches data, the page can take a moment to render. Next.js uses a special loading.tsx file to display a placeholder while the page loads.
- Create a new file called loading.tsx inside the app/ folder.
- Add the following code to create a skeleton loading animation:
export default function Loading() {
return (
<div className="space-y-4">
{/* Render 5 placeholder cards that pulse while loading */}
{[...Array(5)].map((_, i) => (
<div
key={i}
className="p-6 bg-white border border-gray-200 rounded-lg animate-pulse"
>
{/* Simulates a title bar */}
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
{/* Simulates a body text line */}
<div className="h-4 bg-gray-200 rounded w-full"></div>
</div>
))}
</div>
);
}
What does this code do?
This component renders 5 placeholder cards that mimic the shape of your post list. The animate-pulse class from Tailwind CSS creates a gentle pulsing animation so users know content is loading.
Next.js automatically displays this component whenever a sibling page.tsx is fetching data. No extra wiring needed.
- Save app/loading.tsx.
- In your browser, click any post card to navigate to its detail page, then click Posts in the navigation header. You may briefly see grey pulsing rectangles before the posts load in.
Loading too fast to see the skeleton?
The JSONPlaceholder API responds quickly, so the loading state may flash by. You can open your browser's DevTools, go to the Network tab, and select Slow 3G throttling to see the skeleton clearly.
Not seeing the loading skeleton at all?
- Confirm the file is named exactly loading.tsx (not Loading.tsx) and sits directly inside the app/ folder.
- Make sure the function is exported as export default function Loading(). Next.js requires the default export.
Need more help? Ask here.
Observe the layout persisting across navigation
The real power of a layout is that it never re-renders when you navigate between pages. The header stays in place while only the page content swaps out.
- In your browser, click on any post title to open the detail page.
- Watch the top of the page. The navigation header stays exactly where it is.
- Click Blog Reader or Posts in the header to return to the home page.
The navigation header persists without flickering or re-mounting. Only the content inside <main> changes.
How does this differ from traditional routing?
In a traditional multi-page site, every link triggers a full page reload. The browser tears down the entire page and rebuilds it from scratch. With the App Router, layouts stay mounted and only the changed segment re-renders. This makes navigation feel instant.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete app/layout.tsx:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Blog Reader",
description: "A blog reader built with Next.js",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<header className="border-b border-gray-200 bg-white">
<nav className="mx-auto max-w-4xl px-4 py-4 flex items-center gap-6">
<Link href="/" className="text-xl font-bold text-gray-900">
Blog Reader
</Link>
<Link
href="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
Posts
</Link>
</nav>
</header>
<main className="mx-auto max-w-4xl px-4 py-8">{children}</main>
</body>
</html>
);
}
And the complete app/loading.tsx:
export default function Loading() {
return (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="p-6 bg-white border border-gray-200 rounded-lg animate-pulse"
>
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-full"></div>
</div>
))}
</div>
);
}
Your blog reader now has a polished layout with navigation and a loading state. The app feels cohesive and professional. Next up, you'll tackle a Secret Mission to extend the app with a Users page.
Secret mission
Add a Users Page
Your blog reader shows posts, but who wrote them? Add a Users section that lists all authors with their company info, and link each user to a filtered view showing only their posts. This extends your app with a new route group and demonstrates how to filter API data with query parameters.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The blog-reader/ project folder on your Desktop.
- The Next.js dev server running at http://localhost:3000.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your blog reader app.
- Your dev server stays running at http://localhost:3000.
- Your blog-reader/ folder remains on your Desktop.
- Node.js remains installed on your machine (useful for future projects).
Pause - I'll come back to this later
Shut down the running dev server to free up memory. 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.
- To resume later, open the blog-reader/ folder in VS Code and run npm run dev again.
Delete - I don't want to use this again
Remove all project files and start fresh. This cannot be undone.
- Switch to the terminal where npm run dev is running and press Ctrl+C to stop it.
- Delete the project folder by running this command on macOS/Linux:
cd ~/Desktop
rm -rf blog-reader
On Windows, run this instead:
cd ~\Desktop
rmdir /s /q blog-reader
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see blog-reader listed.
Nice Work!
Nice Work!
Nice work! You just built a full multi-page web application with Next.js that fetches real data from a live API and renders it across multiple routes.
You've learned how to:
- Built a Next.js project using the App Router and TypeScript, fetching real API data with Server Components and zero client-side JavaScript.
- Created dynamic routes with URL parameters to display individual posts and their comments.
- Implemented a persistent root layout with shared navigation and a loading state using Tailwind CSS for styling.
- Added a Users page with filtered post views that link back to individual posts.
Ready to quiz yourself?