Loading blog posts...
Loading blog posts...
Loading...
Explore Next.js 15's revolutionary features including React 19 support, Turbopack, new Form component, enhanced TypeScript support, and async request APIs. Learn how to build blazing-fast web applications with the latest version.
Next.js has revolutionized how we build React applications, and with the release of Next.js 15 (and the latest 15.5), the framework reaches new heights of performance, developer experience, and functionality. Whether you're building a simple blog or a complex enterprise application, Next.js 15 provides everything you need out of the box.
In this comprehensive guide, we'll explore everything from installation to advanced features, including how to leverage Markdown rendering for blog posts like this one!
Next.js 15, released in October 2024 and continuously updated through 2025, brings groundbreaking improvements that make it the most powerful version yet.
Next.js 15.1 introduced stable support for React 19, bringing cutting-edge React features to your applications:
jsx// React 19 features now work seamlessly in Next.js 15 import { use } from 'react'; export default function UserProfile({ userPromise }) { // New 'use' hook for handling promises const user = use(userPromise); return ( <div className="profile"> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }
Turbopack, the Rust-based successor to Webpack, delivers incredible performance improvements:
bash# Enable Turbopack for development next dev --turbo # With Turbopack, you'll see speeds like: # ✓ Ready in 1.2s (previously 5s) # ✓ Fast Refresh in 50ms (previously 1s)
Benchmark Results (on Vercel app):
Traditional Webpack:
├── Initial compile: 5.2s
├── Fast Refresh: 980ms
└── Bundle time: 12.4s
Turbopack (75-95% faster):
├── Initial compile: 1.3s
├── Fast Refresh: 49ms
└── Bundle time: 3.1s
The new <Form> component extends HTML forms with Next.js superpowers:
tsximport Form from 'next/form'; export default function SearchPage() { return ( <Form action="/search"> {/* Prefetching for instant navigation */} <input name="query" placeholder="Search..." /> <button type="submit">Search</button> </Form> ); }
Features:
Next.js 15 introduces async versions of dynamic APIs for better data fetching:
typescript// Before (Next.js 14) import { cookies } from 'next/headers'; export default function Page() { const cookieStore = cookies(); const theme = cookieStore.get('theme'); return <div>Theme: {theme}</div>; } // After (Next.js 15) import { cookies } from 'next/headers'; export default async function Page() { const cookieStore = await cookies(); const theme = cookieStore.get('theme'); return <div>Theme: {theme}</div>; }
Async APIs now include:
cookies() - Read request cookiesheaders() - Access request headersparams - Dynamic route parameterssearchParams - URL search parametersThe new onRequestError hook captures errors and sends them to your observability provider:
typescript// instrumentation.ts export async function onRequestError( err: Error, request: Request, context: { routerKind: 'Pages Router' | 'App Router'; routePath: string; routeType: 'render' | 'route' | 'action' | 'middleware'; } ) { // Send to your error tracking service await fetch('https://your-error-tracker.com/api/errors', { method: 'POST', body: JSON.stringify({ message: err.message, stack: err.stack, url: request.url, route: context.routePath, type: context.routeType, timestamp: new Date().toISOString(), }), }); }
Next.js 15.5 introduces stable typed routes for compile-time type safety:
typescript// next.config.ts const config: NextConfig = { experimental: { typedRoutes: true, // Now stable! }, }; export default config;
tsximport Link from 'next/link'; export default function Navigation() { return ( <nav> {/* ✅ TypeScript knows this route exists */} <Link href="/blog/nextjs-15">Next.js Guide</Link> {/* ❌ TypeScript error: Route doesn't exist */} <Link href="/non-existent-route">Broken Link</Link> {/* ✅ Type-safe dynamic routes */} <Link href={{ pathname: '/blog/[slug]', params: { slug: 'nextjs-15' } }} > Dynamic Route </Link> </nav> ); }
Creating a new Next.js 15 project is simple:
bash# Create a new Next.js app npx create-next-app@latest my-nextjs-app # You'll be prompted with: # ✔ Would you like to use TypeScript? Yes # ✔ Would you like to use ESLint? Yes # ✔ Would you like to use Tailwind CSS? Yes # ✔ Would you like to use `src/` directory? Yes # ✔ Would you like to use App Router? Yes # ✔ Would you like to use Turbopack for `next dev`? Yes cd my-nextjs-app npm run dev
If you have an existing project:
bash# Update Next.js npm install next@latest react@latest react-dom@latest # Check for breaking changes npm run build # Enable Turbopack # Update package.json: { "scripts": { "dev": "next dev --turbo" } }
A typical Next.js 15 project looks like this:
my-nextjs-app/
├── src/
│ ├── app/ # App Router (recommended)
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Home page
│ │ ├── globals.css # Global styles
│ │ ├── blog/
│ │ │ ├── page.tsx # Blog list
│ │ │ └── [slug]/
│ │ │ └── page.tsx # Blog post
│ │ └── api/
│ │ └── hello/
│ │ └── route.ts # API endpoint
│ ├── components/ # Reusable components
│ └── lib/ # Utility functions
├── public/ # Static assets
├── content/ # Markdown files
│ └── blog/
│ └── my-post.md
├── next.config.ts # Next.js configuration
├── tsconfig.json # TypeScript config
└── package.json
One of Next.js 15's powerful features is seamless Markdown rendering. Here's how to build a blog like the one you're reading!
bashnpm install gray-matter remark remark-html npm install react-syntax-highlighter @types/react-syntax-highlighter npm install reading-time
Create markdown files in content/blog/:
markdown--- title: "My First Blog Post" excerpt: "Learn how to build a blog with Next.js 15" category: "tutorial" date: "2025-11-22" readTime: "5 min read" tags: ["Next.js", "React", "Markdown"] author: "Your Name" --- # My First Blog Post This is the content of my blog post written in **Markdown**! ## Code Example ```javascript console.log('Hello, Next.js 15!');
### Step 3: Create Markdown Parser
```typescript
// src/lib/markdown.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
const postsDirectory = path.join(process.cwd(), 'content/blog');
export interface BlogPost {
slug: string;
title: string;
excerpt: string;
category: string;
date: string;
readTime: string;
tags: string[];
author: string;
content: string;
}
export async function getPost(slug: string): Promise<BlogPost> {
const fullPath = path.join(postsDirectory, `${slug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// Parse markdown frontmatter
const { data, content } = matter(fileContents);
// Convert markdown to HTML
const processedContent = await remark()
.use(html)
.process(content);
return {
slug,
content: processedContent.toString(),
...data,
} as BlogPost;
}
export function getAllPosts(): Array<Omit<BlogPost, 'content'>> {
const fileNames = fs.readdirSync(postsDirectory);
const posts = fileNames
.filter(fileName => fileName.endsWith('.md'))
.map(fileName => {
const slug = fileName.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContents);
return {
slug,
...data,
} as Omit<BlogPost, 'content'>;
})
.sort((a, b) => (a.date > b.date ? -1 : 1));
return posts;
}
tsx// src/app/blog/page.tsx import Link from 'next/link'; import { getAllPosts } from '@/lib/markdown'; export default async function BlogPage() { const posts = getAllPosts(); return ( <div className="container mx-auto px-4 py-16"> <h1 className="text-4xl font-bold mb-8">Blog</h1> <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3"> {posts.map((post) => ( <Link key={post.slug} href={`/blog/${post.slug}`} className="group block p-6 bg-white rounded-lg shadow-lg hover:shadow-xl transition-shadow" > <div className="flex gap-2 mb-3"> {post.tags.map((tag) => ( <span key={tag} className="text-xs px-2 py-1 bg-blue-100 text-blue-800 rounded" > {tag} </span> ))} </div> <h2 className="text-2xl font-bold mb-2 group-hover:text-blue-600 transition-colors"> {post.title} </h2> <p className="text-gray-600 mb-4">{post.excerpt}</p> <div className="flex justify-between text-sm text-gray-500"> <span>{post.date}</span> <span>{post.readTime}</span> </div> </Link> ))} </div> </div> ); }
tsx// src/app/blog/[slug]/page.tsx import { notFound } from 'next/navigation'; import { getPost, getAllPosts } from '@/lib/markdown'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; export async function generateStaticParams() { const posts = getAllPosts(); return posts.map((post) => ({ slug: post.slug, })); } export default async function BlogPostPage({ params, }: { params: { slug: string }; }) { const post = await getPost(params.slug); if (!post) { notFound(); } return ( <article className="container mx-auto px-4 py-16 max-w-4xl"> {/* Header */} <header className="mb-12"> <div className="flex gap-2 mb-4"> {post.tags.map((tag) => ( <span key={tag} className="text-xs px-3 py-1 bg-blue-100 text-blue-800 rounded-full" > {tag} </span> ))} </div> <h1 className="text-5xl font-bold mb-4">{post.title}</h1> <div className="flex items-center gap-4 text-gray-600"> <span>{post.author}</span> <span>•</span> <span>{post.date}</span> <span>•</span> <span>{post.readTime}</span> </div> </header> {/* Content with Syntax Highlighting */} <div className="prose prose-lg max-w-none"> <ReactMarkdown components={{ code({ node, inline, className, children, ...props }) { const match = /language-(\w+)/.exec(className || ''); return !inline && match ? ( <SyntaxHighlighter style={vscDarkPlus} language={match[1]} PreTag="div" {...props} > {String(children).replace(/\n$/, '')} </SyntaxHighlighter> ) : ( <code className={className} {...props}> {children} </code> ); }, }} > {post.content} </ReactMarkdown> </div> </article> ); }
All components in the App Router are Server Components by default:
tsx// This runs on the server - no JavaScript sent to client! export default async function UserDashboard() { const data = await fetch('https://api.example.com/user', { cache: 'no-store', // Dynamic data }); const user = await data.json(); return ( <div> <h1>Welcome, {user.name}</h1> <UserStats stats={user.stats} /> </div> ); }
Use 'use client' for interactive components:
tsx'use client'; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> </div> ); }
typescript// Static data (generated at build time) async function getStaticData() { const res = await fetch('https://api.example.com/posts', { cache: 'force-cache', // Default behavior }); return res.json(); } // Dynamic data (fetched on each request) async function getDynamicData() { const res = await fetch('https://api.example.com/user', { cache: 'no-store', }); return res.json(); } // Revalidated data (cached with time-based revalidation) async function getRevalidatedData() { const res = await fetch('https://api.example.com/products', { next: { revalidate: 3600 }, // Revalidate every hour }); return res.json(); }
typescriptimport type { Metadata } from 'next'; export const metadata: Metadata = { title: 'Next.js 15 Guide', description: 'Learn everything about Next.js 15', openGraph: { title: 'Next.js 15 Guide', description: 'Learn everything about Next.js 15', images: ['/og-image.jpg'], }, twitter: { card: 'summary_large_image', title: 'Next.js 15 Guide', description: 'Learn everything about Next.js 15', images: ['/og-image.jpg'], }, };
tsx// app/blog/loading.tsx export default function Loading() { return ( <div className="container mx-auto px-4 py-16"> <div className="animate-pulse"> <div className="h-8 bg-gray-200 rounded w-1/4 mb-8"></div> <div className="grid gap-8 md:grid-cols-3"> {[...Array(6)].map((_, i) => ( <div key={i} className="bg-gray-100 p-6 rounded-lg"> <div className="h-4 bg-gray-200 rounded mb-4"></div> <div className="h-4 bg-gray-200 rounded mb-4"></div> <div className="h-4 bg-gray-200 rounded w-2/3"></div> </div> ))} </div> </div> </div> ); }
tsximport Image from 'next/image'; export default function Hero() { return ( <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} priority // Load immediately for above-the-fold images placeholder="blur" // Show blur while loading blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." /> ); }
tsximport { Inter, Roboto_Mono } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', }); const robotoMono = Roboto_Mono({ subsets: ['latin'], display: 'swap', }); export default function RootLayout({ children }) { return ( <html lang="en" className={inter.className}> <body>{children}</body> </html> ); }
typescript// next.config.ts const config: NextConfig = { // Enable bundle analysis bundlePagesRouterDependencies: true, // Optimize packages transpilePackages: ['@my-org/ui'], // Enable production optimizations compiler: { removeConsole: process.env.NODE_ENV === 'production', }, };
Let's test various programming languages to ensure the syntax highlighting works:
javascriptconst greet = (name) => { console.log(`Hello, ${name}!`); return `Welcome to Next.js 15`; }; // Array methods const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(n => n * 2);
pythondef fibonacci(n): """Generate Fibonacci sequence""" if n <= 1: return n else: return fibonacci(n-1) + fibonacci(n-2) # List comprehension squares = [x**2 for x in range(10)]
css.container { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; padding: 2rem; } .card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 0.5rem; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease; } .card:hover { transform: translateY(-5px); }
json{ "name": "my-nextjs-app", "version": "1.0.0", "scripts": { "dev": "next dev --turbo", "build": "next build", "start": "next start" }, "dependencies": { "next": "^15.5.0", "react": "^19.0.0" } }
bash#!/bin/bash # Deploy script echo "Deploying Next.js app..." npm run build npm run test if [ $? -eq 0 ]; then echo "✓ Build successful" npm run deploy else echo "✗ Build failed" exit 1 fi
bash# Install Vercel CLI npm i -g vercel # Deploy vercel # Production deployment vercel --prod
dockerfile# Dockerfile FROM node:18-alpine AS base # Dependencies FROM base AS deps WORKDIR /app COPY package*.json ./ RUN npm ci # Builder FROM base AS builder WORKDIR /app COPY /app/node_modules ./node_modules COPY . . RUN npm run build # Runner FROM base AS runner WORKDIR /app ENV NODE_ENV production COPY /app/public ./public COPY /app/.next/standalone ./ COPY /app/.next/static ./.next/static EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"]
bash# Build for production npm run build # Start production server npm start # Or use PM2 pm2 start npm --name "nextjs-app" -- start
bashnpm install next@latest react@latest react-dom@latest
typescript// Update dynamic API calls to async const cookieStore = await cookies(); const headersList = await headers();
json{ "scripts": { "dev": "next dev --turbo" } }
bashnpm run build npm run start
Next.js 15 represents a massive leap forward in web development. With React 19 support, Turbopack's blazing-fast performance, enhanced TypeScript support, and powerful new features like the Form component and async request APIs, it's the perfect framework for building modern web applications.
The combination of:
...makes Next.js 15 the best version yet.
Whether you're building a personal blog with Markdown rendering (like this post!), an e-commerce platform, or a complex dashboard, Next.js 15 provides all the tools you need to build fast, scalable, production-ready applications.
At Joulyan IT, we specialize in building high-performance web applications using cutting-edge technologies like Next.js 15. From planning to deployment, we help businesses leverage the latest web technologies to create exceptional user experiences.
Want to discuss your next project? Contact us to learn how we can help bring your vision to life with Next.js 15.
Last updated: November 22, 2025