🏆 Project – Build a Complete Website · Lesson 36
Project: SEO, Sitemap & Go Live
Step 1: SEO files
JS – app/sitemap.js
import { courses } from '@/lib/courses'
import { getAllPosts } from '@/lib/posts'
import { site } from '@/lib/site'
// Next.js turns this into /sitemap.xml automatically.
export default function sitemap() {
const now = new Date()
const pages = ['', '/courses', '/blog', '/about', '/contact'].map((p) => ({
url: `${site.url}${p}`,
lastModified: now,
changeFrequency: p === '' ? 'weekly' : 'monthly',
priority: p === '' ? 1 : 0.8,
}))
const coursePages = courses.map((c) => ({ url: `${site.url}/courses/${c.slug}`, lastModified: now, priority: 0.9 }))
const postPages = getAllPosts().map((p) => ({ url: `${site.url}/blog/${p.slug}`, lastModified: new Date(p.date), priority: 0.7 }))
return [...pages, ...coursePages, ...postPages]
}JS – app/robots.js
import { site } from '@/lib/site'
// Next.js turns this into /robots.txt automatically.
export default function robots() {
return {
rules: [{ userAgent: '*', allow: '/', disallow: ['/api/'] }],
sitemap: `${site.url}/sitemap.xml`,
}
}Step 2: OG image and icon
JSX – app/opengraph-image.jsx
import { ImageResponse } from 'next/og'
import { site } from '@/lib/site'
// Social share image (1200×630) generated at build time for the home page.
export const alt = `${site.name} – ${site.tagline}`
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default function OpengraphImage() {
return new ImageResponse(
(
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: 80, background: 'linear-gradient(135deg,#4f46e5,#0f172a)', color: '#fff' }}>
<div style={{ fontSize: 88, fontWeight: 800 }}>{site.name}</div>
<div style={{ fontSize: 44, marginTop: 16, opacity: 0.9 }}>{site.tagline}</div>
</div>
),
size
)
}SVG – app/icon.svg (favicon)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#4f46e5"/><text x="32" y="43" font-size="30" text-anchor="middle" fill="#fff" font-family="Arial" font-weight="700">E</text></svg>
Step 3: 404 and error pages
JSX – app/not-found.jsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="container section" style={{ textAlign: 'center' }}>
<h1>404 – Page not found</h1>
<p className="lead" style={{ margin: '0 auto' }}>The page you are looking for does not exist or has moved.</p>
<p style={{ marginTop: 20 }}><Link href="/" className="btn">Go to home page</Link></p>
</div>
)
}JSX – app/error.jsx
'use client' // error boundaries must be Client Components
export default function Error({ error, reset }) {
return (
<div className="container section" style={{ textAlign: 'center' }}>
<h1>Something went wrong</h1>
<p className="lead" style={{ margin: '0 auto' }}>Please try again. If the problem continues, contact us.</p>
<button className="btn" style={{ marginTop: 20 }} onClick={() => reset()}>Try again</button>
</div>
)
}
Step 4: Redirects
JS – proxy.js
import { NextResponse } from 'next/server'
// Proxy (called "middleware" before Next.js 16) runs before every matched request.
// Here: old URLs like /course/python-ai are permanently moved to /courses/python-ai.
export function proxy(request) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/course/')) {
const url = request.nextUrl.clone()
url.pathname = pathname.replace('/course/', '/courses/')
return NextResponse.redirect(url, 308)
}
return NextResponse.next()
}
export const config = { matcher: ['/course/:path*'] }JS – next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
// Simple permanent redirects live here (no code needed).
async redirects() {
return [{ source: '/home', destination: '/', permanent: true }]
},
images: {
// Allow optimized images in two sizes that this site actually uses.
qualities: [75, 90],
},
}
export default nextConfigStep 5: Production build
Terminal – npm run build
$ npm run build
✓ Compiled successfully
✓ Generating static pages (21/21)
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /about
├ ƒ /api/courses
├ ○ /blog
├ /blog/[slug]
│ ├ ● /blog/why-nextjs-for-websites
│ ├ ● /blog/python-vs-c-first-language
│ └ ● /blog/how-to-start-coding
├ ƒ /contact
├ ƒ /courses
├ /courses/[slug]
│ ├ ● /courses/c-programming
│ ├ ● /courses/python-ai
│ ├ ● /courses/web-development
│ └ ● [+3 more paths]
├ ○ /icon.svg
├ ○ /opengraph-image
├ ○ /robots.txt
└ ○ /sitemap.xml
ƒ Proxy (Middleware)
Step 6: Verify everything
Terminal – quick checks
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/courses/abc
404
$ curl -sI http://localhost:3000/course/python-ai | head -2
HTTP/1.1 308 Permanent Redirect
location: /courses/python-ai
$ curl -sI http://localhost:3000/home | head -2
HTTP/1.1 308 Permanent Redirect
location: /
$ curl -s http://localhost:3000/sitemap.xml | grep -c "<url>"
14
- 1Lighthouse (mobile): Performance, SEO, Accessibility, Best Practices
- 2View source on 3 pages: unique
, description, canonical - 3Rich Results Test on a course page (Course schema)
- 4Share the home URL in WhatsApp → OG image appears
- 5Submit the contact form → entry in data/messages.json
Step 7: Deploy
Follow the Deploy lesson: set SITE_URL to your domain, then deploy to Vercel or a VPS with PM2 + Nginx. After going live, submit https://your-domain/sitemap.xml in Google Search Console.
You built a complete website!
Download the complete project from the card on this page, run npm install and npm run dev, and make it yours. Next, try the Next.js interview questions.
✅ Final challenge
Turn EduSite into a site for a real local business (gym, school, clinic): change the data, colours and images, connect the form to MySQL, and deploy it on your own domain.
Frequently Asked Questions
What should I do after the site is live?
Submit sitemap.xml in Google Search Console, add the site to Google Business Profile for local searches, and publish one helpful blog post every week.
How do I add a new course later?
Add an object to lib/courses.js with an image in public/images, then rebuild and redeploy. The list, detail page, sitemap and API update automatically.
