Back to Blog
Web Development

Next.js App Router: Complete Guide for Building Modern Web Apps

Everything you need to know about Next.js 13+ App Router, with practical examples from building our app studio site.

W

Winkle Team

Indie Developers

January 5, 202512 min read
#nextjs#react#web development#tutorial
šŸ’»

Next.js App Router: Complete Guide for Building Modern Web Apps

Next.js App Router (introduced in Next.js 13) revolutionized how we build React applications. Here's everything we learned building Winkle Studios with it.

Why We Chose Next.js App Router

As an indie developer studio, we needed:

  • SEO-friendly - Server-side rendering out of the box
  • Fast - Excellent performance by default
  • DX - Great developer experience
  • Scalable - From MVP to production
  • Cost-effective - Deploys easily on Vercel

App Router delivered all of this.

App Router vs Pages Router

Key Differences:

Pages Router (Old):

pages/
ā”œā”€ā”€ index.js
ā”œā”€ā”€ blog/
│   └── [slug].js
└── api/
    └── hello.js

App Router (New):

app/
ā”œā”€ā”€ page.tsx
ā”œā”€ā”€ layout.tsx
ā”œā”€ā”€ blog/
│   ā”œā”€ā”€ page.tsx
│   └── [slug]/
│       └── page.tsx
└── api/
    └── hello/
        └── route.ts

Benefits:

āœ… React Server Components - Smaller bundles
āœ… Layouts - Shared UI that doesn't re-render
āœ… Loading UI - Built-in loading states
āœ… Error Handling - Granular error boundaries
āœ… Streaming - Progressive page rendering
āœ… Better SEO - Metadata API

Core Concepts

1. File-Based Routing

Every folder is a route:

app/
ā”œā”€ā”€ page.tsx           → /
ā”œā”€ā”€ about/
│   └── page.tsx       → /about
ā”œā”€ā”€ blog/
│   ā”œā”€ā”€ page.tsx       → /blog
│   └── [slug]/
│       └── page.tsx   → /blog/:slug
└── tools/
    └── [tool]/
        └── page.tsx   → /tools/:tool

2. Special Files

  • page.tsx - Route UI
  • layout.tsx - Shared layout
  • loading.tsx - Loading UI
  • error.tsx - Error handling
  • not-found.tsx - 404 page

3. Metadata API

Static Metadata:

// app/page.tsx
export const metadata: Metadata = {
  title: 'Winkle Studios',
  description: 'Indie app studio building useful tools',
}

Dynamic Metadata:

// app/blog/[slug]/page.tsx
export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      images: [post.image],
    },
  }
}

4. Server vs Client Components

Default: Server Components

  • Fetch data on server
  • Smaller bundle size
  • Direct database access
  • Better SEO
// app/blog/page.tsx (Server Component)
async function BlogPage() {
  const posts = await getPosts() // runs on server

  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  )
}

Client Components (use "use client")

  • Interactive UI
  • Browser APIs
  • React hooks
  • Event listeners
// components/LikeButton.tsx
"use client"

import { useState } from 'react'

export function LikeButton() {
  const [likes, setLikes] = useState(0)

  return (
    <button onClick={() => setLikes(likes + 1)}>
      Likes: {likes}
    </button>
  )
}

Real-World Examples from Our Site

1. Blog Listing Page

// app/blog/page.tsx
import { getPosts } from '@/lib/blog'
import { BlogCard } from '@/components/BlogCard'

export const metadata = {
  title: 'Blog - Winkle Studios',
  description: 'Insights on indie hacking, app development, and growth',
}

export default async function BlogPage() {
  const posts = await getPosts()
  const featured = posts.filter(p => p.featured)

  return (
    <div className="container py-16">
      <h1 className="text-5xl font-light mb-8">Blog</h1>

      {/* Featured Posts */}
      <section className="mb-16">
        <h2 className="text-2xl mb-6">Featured</h2>
        <div className="grid md:grid-cols-2 gap-8">
          {featured.map(post => (
            <BlogCard key={post.slug} post={post} featured />
          ))}
        </div>
      </section>

      {/* All Posts */}
      <section>
        <h2 className="text-2xl mb-6">All Posts</h2>
        <div className="grid md:grid-cols-3 gap-8">
          {posts.map(post => (
            <BlogCard key={post.slug} post={post} />
          ))}
        </div>
      </section>
    </div>
  )
}

2. Dynamic Blog Post Page

// app/blog/[slug]/page.tsx
import { getPost, getPosts } from '@/lib/blog'
import { notFound } from 'next/navigation'

export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export async function generateMetadata({ params }: Props) {
  const post = await getPost(params.slug)
  if (!post) return {}

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      images: [post.image],
      type: 'article',
      publishedTime: post.date,
    },
  }
}

export default async function BlogPostPage({ params }: Props) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()
  }

  return (
    <article className="prose lg:prose-xl mx-auto py-16">
      <header className="mb-8">
        <h1>{post.title}</h1>
        <div className="flex gap-4 text-sm text-gray-600">
          <time>{post.date}</time>
          <span>{post.readTime}</span>
        </div>
      </header>

      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

3. Root Layout with SEO

// app/layout.tsx
import { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  metadataBase: new URL('https://winklestudios.com'),
  title: {
    default: 'Winkle Studios - Free Tools & Apps',
    template: '%s | Winkle Studios'
  },
  description: 'Indie app studio building 10+ apps and free tools',
  keywords: ['free tools', 'web apps', 'indie developer'],
  authors: [{ name: 'Winkle Team' }],
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://winklestudios.com',
    siteName: 'Winkle Studios',
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@winklestudios',
  },
  robots: {
    index: true,
    follow: true,
  },
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Performance Optimizations

1. Image Optimization

import Image from 'next/image'

<Image
  src="/blog/post-image.jpg"
  alt="Blog post"
  width={800}
  height={600}
  priority={isFeatured}
  placeholder="blur"
  blurDataURL="data:image/..."
/>

2. Fonts Optimization

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
})

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

3. Route Groups

Organize without affecting URL:

app/
ā”œā”€ā”€ (marketing)/
│   ā”œā”€ā”€ layout.tsx
│   ā”œā”€ā”€ page.tsx
│   └── about/
│       └── page.tsx
└── (app)/
    ā”œā”€ā”€ layout.tsx
    └── dashboard/
        └── page.tsx

Common Pitfalls & Solutions

āŒ Problem: "use client" everywhere

Solution: Only use on interactive components

āŒ Problem: Fetching in client components

Solution: Fetch in server components, pass as props

āŒ Problem: Missing metadata

Solution: Use generateMetadata for all pages

āŒ Problem: Slow builds

Solution: Use generateStaticParams for dynamic routes

Deployment on Vercel

  1. Connect GitHub repo
  2. Vercel auto-detects Next.js
  3. Build and deploy automatically
  4. Get preview URLs for PRs

Build optimizations:

// next.config.js
module.exports = {
  images: {
    domains: ['your-cdn.com'],
  },
  experimental: {
    optimizeCss: true,
  },
}

Results

Our site built with App Router:

  • Lighthouse Score: 98/100
  • First Contentful Paint: 0.8s
  • Time to Interactive: 1.2s
  • SEO Score: 100/100

Conclusion

Next.js App Router is production-ready and excellent for:

  • Content-heavy sites (blogs, docs)
  • E-commerce platforms
  • Marketing websites
  • SaaS applications

The SEO benefits, performance, and DX make it our go-to framework for all web projects.

Have you tried App Router? What's your experience?


Explore our open-source projects built with Next.js.

Found this helpful? Share it!

W

About Winkle Team

We're a small indie development studio building apps, tools, and digital products. Our mission is to create useful software that helps people work better and faster.

Ready to start building?

Check out our free tools or get in touch for your next project