100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Next.js App Router
70 minintermediate

Applied Project — Blog with ISR + API Routes

What You'll Build

You will build a production-shaped cricket blog that brings together every Module 3 concept into one coherent application: statically generated article pages that use Incremental Static Regeneration to stay fresh, a Route Handler exposing the posts as a real JSON API for external consumers, on-demand revalidation triggered when a post is published or edited, per-article metadata with dynamic Open Graph images for search and social, and middleware that gates an admin area. The blog will have a home page listing posts, individual article pages rendered with ISR and full SEO metadata, an API endpoint at /api/posts that external clients can read, and an admin route protected by middleware where a Server Action publishes a post and revalidates the affected pages.

This is deliberately the most complete project so far because Module 3 is where the App Router stops being a way to render pages and becomes a way to run a real content platform. Every piece you assemble here corresponds to a decision a production blog must actually make: how fresh should article pages be and at what cost, how do other systems read your content, how does a publish event propagate to every surface showing that content, how does each article present itself in search and social, and how is the authoring area kept private. By the end you will have exercised caching and revalidation strategy, the three rendering modes, Route Handlers, middleware, and metadata together, in the specific combinations that real content sites use — which is exactly the integration skill that distinguishes someone who knows the features individually from someone who can build with them.

Analogy🏏Cricket
🏏 Think of it like cricket: Building this dashboard is like running a full match-day operation rather than a single drill — the scorers, the broadcast, the substitutes, and the ground staff all working together under live conditions. Just as match day proves every department functions in concert, this project proves server fetching, mutations, streaming, and error handling work together. Just as the broadcast keeps showing the ground while a replay loads, your dashboard stays usable while the slow panel streams. Just as the team has substitutes ready for injuries, your dashboard has error boundaries ready for failures. This is the match-day rehearsal that confirms the whole Module 2 system holds up under realistic load.

Prerequisites

  • A working App Router project, or a fresh one scaffolded with create-next-app using the App Router.
  • Understanding of the caching layers and time-based versus on-demand revalidation with revalidatePath and revalidateTag.
  • Familiarity with static, dynamic, and ISR rendering and how generateStaticParams pre-renders known dynamic routes.
  • Knowledge of Route Handlers (route.js) and returning correct status codes with structured JSON.
  • Awareness of middleware for request gating and of the metadata API including generateMetadata and Open Graph images.
  • Comfort with Server Actions for mutations from the earlier components module.

Setup & Project Structure

Plan the full structure before coding so each concept has an obvious home and the relationships between them are visible. You will have a shared data layer for posts, a home page listing them, ISR article pages with metadata and dynamic Open Graph images, a public API route, an admin route group gated by middleware, and an actions file whose publish action performs on-demand revalidation. Laying this out first makes clear how a single publish event will need to invalidate the home page, the article page, and the API response together, which is the central integration challenge of the project.

Analogy🏏Cricket
🏏 Think of it like cricket: a coach lays out the match-day plan before the toss — who gathers the standings and top-scorer stats, who handles a mid-innings change, which slow specialist analysis can arrive late, and what the fallback is if a player is injured — so every job has an owner before play. Just as planning each role first makes match day run smoothly, planning your dashboard folders first gives each concept a clear home: a page that fetches standings and top scorer in parallel, an actions file holding the add-note Server Action, a slow analytics component wrapped in Suspense, and error and not-found files for safety. Just as the plan shows how batting, bowling, and fielding combine, the layout shows how server fetching, mutation, streaming, and error handling fit together. The payoff: a structure where every piece has an obvious place before you write a line.
bash
# Target structure for the blog:
# middleware.js                         \u2190 gate /admin, fast stateless check
# app/
#   page.js                             \u2190 home: list posts (ISR)
#   posts/
#     [slug]/
#       page.js                         \u2190 article: ISR + generateMetadata
#       opengraph-image.js              \u2190 dynamic per-article OG image
#   api/
#     posts/route.js                    \u2190 public GET API (cacheable)
#     posts/[slug]/route.js             \u2190 public GET single post
#   (admin)/
#     admin/
#       page.js                         \u2190 protected authoring UI
#       actions.js                      \u2190 'use server' publishPost + revalidation
# lib/
#   posts.js                            \u2190 shared data access, tagged fetches

npm run dev   # http://localhost:3000

Step 1 — Foundation

Step 1 builds the shared data layer and the ISR home page. The data module centralizes how posts are read, tagging its fetches so a single revalidateTag call can later invalidate every surface that reads posts. The home page lists published posts and is rendered with ISR — a revalidate window plus the posts tag — so it is served statically and fast for almost all visitors yet refreshes on a cadence and can be invalidated instantly on publish. Establishing the tagged data layer first is what makes the later on-demand revalidation surgical rather than a blunt full-site refresh.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 is like the analysts compiling the opposition report — multiple researchers working at once on batting, bowling, and fielding, then combining their findings into one briefing. Just as the researchers work concurrently rather than one after another, your fetches run in parallel with Promise.all. Just as the combined briefing is ready before the captain walks out, the data is resolved before the page renders. This shows why the foundation is parallel server fetching: gather everything at once, securely, before play begins.
javascript
// lib/posts.js  \u2014  shared, tagged data access
let store = [
  { slug: 'india-win-series', title: 'India clinch the series', summary: 'A clinical chase.',
    body: 'Full match report\u2026', author: 'Staff', publishedAt: '2025-01-10', published: true },
];

export async function getPosts() {
  // In a real app this would be a tagged fetch; here we simulate with the store.
  // Conceptually: fetch(url, { next: { revalidate: 300, tags: ['posts'] } })
  return store.filter((p) => p.published);
}
export async function getPost(slug) {
  return store.find((p) => p.slug === slug && p.published) ?? null;
}
export async function addPost(post) {
  store.unshift({ ...post, published: true, publishedAt: new Date().toISOString().slice(0, 10) });
}

// app/page.js  \u2014  ISR home page listing posts
import Link from 'next/link';
import { getPosts } from '../lib/posts';

export const revalidate = 300;   // ISR: regenerate at most every 5 minutes

export default async function HomePage() {
  const posts = await getPosts();
  return (
    <main>
      <h1>\ud83c\udfcf Cricket Blog</h1>
      <ul>
        {posts.map((p) => (
          <li key={p.slug}><Link href={`/posts/${p.slug}`}>{p.title}</Link> \u2014 {p.summary}</li>
        ))}
      </ul>
    </main>
  );
}

Step 2 — Core Logic

Step 2 builds the ISR article pages with full metadata and the public API. Each article page pre-renders the known posts via generateStaticParams into the Full Route Cache, carries its own revalidation so it stays fresh, and uses generateMetadata to give each article a precise title, description, canonical URL, and Open Graph image — reusing the same post fetch the page uses so it costs nothing extra. Alongside, the API route exposes the posts as structured JSON for external clients, returning correct status codes. This step delivers both the human-facing article surface and the machine-facing API surface from the same shared data.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 2 is like the captain logging a tactical note in the official book mid-innings — a direct entry that updates the team's shared record everyone then sees. Just as the note goes straight into the authoritative book, the form submission goes straight to the Server Action. Just as the updated book is immediately visible to the staff, revalidatePath makes the new note immediately visible on the page. This shows why the action is the core write logic: one direct submission records and refreshes in a single motion.
javascript
// app/posts/[slug]/page.js  \u2014  ISR article + metadata
import { notFound } from 'next/navigation';
import { getPosts, getPost } from '../../../lib/posts';

export const revalidate = 600;   // article ISR window

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ slug: p.slug }));   // pre-render known posts
}

export async function generateMetadata({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);              // deduped with the page
  if (!post) return { title: 'Post not found' };
  return {
    title: post.title,
    description: post.summary,
    alternates: { canonical: `https://cricket.example/posts/${slug}` },
    openGraph: { title: post.title, type: 'article',
                 images: [`/posts/${slug}/opengraph-image`] },
  };
}

export default async function PostPage({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();
  const jsonLd = { '@context': 'https://schema.org', '@type': 'NewsArticle',
                   headline: post.title, author: { '@type': 'Person', name: post.author },
                   datePublished: post.publishedAt };
  return (
    <article>
      <script type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

// app/api/posts/route.js  \u2014  public JSON API
import { NextResponse } from 'next/server';
import { getPosts } from '../../../lib/posts';
export async function GET() {
  const posts = await getPosts();
  return NextResponse.json({ posts }, { status: 200 });
}

Step 3 — Integration & Enhancement

Step 3 is the integration heart of the project: the admin area, its middleware gate, and the publish action that ties the whole system together with on-demand revalidation. Middleware protects the admin route with a fast stateless check, redirecting unauthenticated visitors to login without touching a database. The admin page renders an authoring form bound to a Server Action, and that action — after saving the post — calls the revalidation that refreshes the home page, the new article page, and the cached API response together, so a single publish propagates to every surface at once. This is where the dynamic Open Graph image route is added too, completing each article's social presentation.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 3 is the live match with everything running together — the scoreboard updating, a replay streaming in behind a 'pending' graphic, and substitutes ready if a fielder goes down. Just as the replay loads without freezing the broadcast, the analytics stream without freezing the dashboard. Just as substitutes contain an injury to one position, the error boundary contains a failure to one segment. This reveals why integration is the payoff: the value is in fetching, mutating, streaming, and recovering all coexisting smoothly.
javascript
// middleware.js  \u2014  gate the admin area, fast and loop-safe
import { NextResponse } from 'next/server';
export function middleware(request) {
  const token = request.cookies.get('admin')?.value;
  if (!token || token.length < 10) {              // fast stateless check, no DB
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}
export const config = { matcher: ['/admin/:path*'] };   // /login excluded \u2192 no loop

// app/(admin)/admin/actions.js  \u2014  publish + on-demand revalidation
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { addPost } from '../../../lib/posts';

export async function publishPost(formData) {
  const title = String(formData.get('title') ?? '').trim();
  const summary = String(formData.get('summary') ?? '').trim();
  if (title.length < 4) return { error: 'Title too short' };
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
  await addPost({ slug, title, summary, body: formData.get('body') ?? '', author: 'Admin' });
  // one publish refreshes every surface that shows posts:
  revalidatePath('/');                 // home list
  revalidatePath(`/posts/${slug}`);    // the new article page
  revalidateTag('posts');              // anything tagged 'posts' (incl. API reads)
  return { ok: true, slug };
}

// app/(admin)/admin/page.js  \u2014  authoring form bound to the action
import { publishPost } from './actions';
export default function AdminPage() {
  return (
    <form action={publishPost}>
      <input name="title" placeholder="Headline" />
      <input name="summary" placeholder="One-line summary" />
      <textarea name="body" placeholder="Body" />
      <button type="submit">Publish</button>
    </form>
  );
}

Step 4 — Testing & Verification

Verify each integration path deliberately rather than assuming the wiring is correct, because the value of this project is in the pieces working together and the failure modes are mostly at the seams. Confirm article pages are statically pre-rendered, that the API returns correct JSON and status codes, that publishing a post makes it appear on the home page and as its own article and in the API response without a manual rebuild, that the admin area redirects when unauthenticated, and that each article carries correct metadata and a generated Open Graph image. Checking the publish-to-every-surface propagation specifically is the most important verification, since a missing revalidation target is the classic integration bug here.

Analogy🏏Cricket
🏏 Think of it like cricket: a captain rehearses each match scenario in the nets before it counts — a quick single, a run-out call, a rejected review, an injury contingency — confirming each response works under real conditions rather than trusting it will. Just as rehearsing each scenario proves the plan holds, running your dashboard and verifying each behaviour deliberately proves the concepts work together: the fast content paints before the analytics stream in, adding a note refreshes the list without a reload, a too-short note is rejected, and the error boundary catches a thrown error. Just as testing both the smooth single and the failed review checks success and failure paths, you check both valid and invalid inputs. And just as net rehearsal turns separate skills into a coordinated performance, verifying each path proves the streaming, mutation, and error handling work in concert, not just in isolation. The payoff: confidence the whole dashboard behaves as designed.
bash
# Build and verify the full system:
npm run build
# Expect: app/posts/[slug] listed as pre-rendered (from generateStaticParams),
#         home and article routes marked as revalidating (ISR).

npm run dev
# 1. Visit /                         \u2192 lists existing posts.
# 2. Visit /posts/india-win-series   \u2192 article renders with correct <title>.
# 3. Inspect page source             \u2192 og:title, canonical, and JSON-LD present in HTML.
# 4. Visit /posts/india-win-series/opengraph-image \u2192 a generated PNG.
# 5. GET /api/posts                  \u2192 200 with { posts: [...] } JSON.
# 6. Visit /admin without the cookie  \u2192 redirected to /login.
# 7. Set the admin cookie, publish a post via the form
#                                     \u2192 it appears on /, at /posts/<slug>, AND in /api/posts
#                                        with NO rebuild (on-demand revalidation worked).

Warning: The most common bug in this project is a publish that updates one surface but not another — the article appears but the home list is stale, or the API still returns the old set. This is always a missing revalidation target. A publish must invalidate every surface that reads posts: the home path, the new article path, and the posts tag the API and lists depend on. Treat the write and its full set of revalidations as one inseparable operation.

Extension Challenge: Add an editing flow that updates an existing post and revalidates only that article's path plus the posts tag, so unrelated articles stay cached. Then add a tag-based API endpoint at /api/posts/[slug] with proper 404 handling, give the public GET API an API-key check and a Cache-Control header, and add a sitemap route that lists every post URL. Together these exercise surgical revalidation, authenticated APIs, and discovery infrastructure on top of the core build.

  • A shared, tagged data layer lets a single revalidateTag invalidate every surface that reads posts, making on-demand revalidation surgical.
  • Article pages use ISR with generateStaticParams to pre-render known posts and a revalidate window to stay fresh at static speed.
  • generateMetadata derives each article's title, description, canonical, and Open Graph image from the post data, reusing the page's deduplicated fetch.
  • A Route Handler exposes posts as structured JSON for external clients, sourced from the same shared data as the pages.
  • Middleware gates the admin area with a fast stateless check and a scoped matcher that excludes the login path to avoid redirect loops.
  • The publish Server Action ties the system together by revalidating the home path, the new article path, and the posts tag in one operation.
  • A publish must invalidate every surface that shows the content; a missing revalidation target is the classic integration failure to test for.

Submit your capstone project

Checking submission status…
Lesson 18 of 35
0% complete