Mark Ku's Blog

Background

To rebuild our German e-commerce site, I did a thorough evaluation and landed on Next.js 13.4.3, the latest stable release at the time. The new SEO metadata API and server-side component model were highly appealing, and since most future Next.js features are being built around the App Router, choosing it was a no-brainer.

1. Server Components

1.1 Everything inside the app folder is a Server Component by default

  • By default, all components on Next.js 13 inside the app folder are Server Components. Server Components cannot use client-side features such as useState, useEffect, etc.
  • For third-party components that require client features, the current solution is to create a wrapper component and omit the 'use client' directive from the wrapper itself.

1.2 Benefits of Server Components

Server Components render on the server and ship only the JavaScript bundle that the client actually needs. Unnecessary JS is never downloaded, which reduces network payload and improves performance.

1.3 Limitations

Cannot use useState, useReducer, other Hooks, useEffect, or useLayoutEffect.

1.4 Opting into a Client Component

Add 'use client' at the top of every file that needs client-side features:

"use client";
import xxx
...

2. New useRouter

import { useRouter, useParams, usePathname ,useSearchParams } from 'next/navigation';

2.1 The new useRouter no longer exposes locale

const { locale } = useRouter(); 

2.2 Routing

2.2.1 Folder structure

The new convention makes it easier to distinguish pages from components at a glance:

URLPages RouterApp Router
//page/index.tsx/app/page.tsx
/about-us/page/about-us.tsx/app/about-us/page.tsx

P.S. The appendix includes a PowerShell script to quickly migrate from the pages folder to the app folder.

2.2.2 API Routes

PageRouteResult
app/page.jsapp/route.jsConflict
app/page.jsapp/api/route.jsValid
app/[user]/page.jsapp/api/route.jsValid
app/products/api/route.ts
import { NextResponse } from 'next/server';
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const id = searchParams.get('id');
  const res = await fetch(`https://data.mongodb-api.com/product/${id}`, {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY,
    },
  });
  const product = await res.json();
 
  return NextResponse.json({ product });
}

2.3 Changes to Data Fetching

2.3.1 No more getStaticProps or getServerSideProps

async func getData() {
  const res = await fetch ("https://api.xxx.com/...");
  return res.json();
}

export default async function About() {
  const name = await getData();
  return "...";
}

2.3.2 The new use hook can unwrap async results

use(getData(id))

2.3.3 Server-side fetch is extended with caching by default

The framework extends the native fetch API on the server side and adds built-in caching. The official recommendation is to avoid wrapping fetch inside Client Components.

2.3.4 Rendering strategies via revalidate

  • SSG: pages are statically generated by default.
  • SSR: declare the component as async, set cache: 'no-store' / no-cache, or set revalidate to 0 to opt into dynamic server-side rendering.
  • CSR: components inside 'use client' that fetch data inside useEffect.
  • ISR: set revalidate in the fetch call, or declare it in page.tsx:
app/page.tsx
export const revalidate = 60; // revalidate this page every 60 seconds

P.S. The old pages/api/revalidate webhook still works, but it must live in the pages folder.

3. New Metadata API — replaces next/head

Per-page SEO metadata is now much easier to manage.

3.1 Static Metadata

import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: '...',
  description: '...',
};
 
export default function Page() {}

3.2 Dynamic Metadata

import { Metadata, ResolvingMetadata } from 'next';
 
type Props = {
  params: { id: string };
  searchParams: { [key: string]: string | string[] | undefined };
};
 
export async function generateMetadata(
  { params, searchParams }: Props,
  parent?: ResolvingMetadata,
): Promise<Metadata> {
  // read route params
  const id = params.id;
 
  // fetch data
  const product = await fetch(`https://.../${id}`).then((res) => res.json());
 
  // optionally access and extend (rather than replace) parent metadata
  const previousImages = (await parent).openGraph?.images || [];
 
  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  };
}
 
export default function Page({ params, searchParams }: Props) {}

4. Packages Affected by the Upgrade

Right after upgrading, you'll notice that most i18n packages no longer work, and the new useRouter doesn't expose locale either. After trying several packages, this one worked best:

Related article

import { useLocale } from 'next-intl';

const locale = useLocale();

4.2 React Query setup

Related article

4.4 Context API setup

Related article

5. Turbopack (still in Beta)

Worth keeping an eye on: Next.js 13 ships a new JavaScript bundler called Turbopack, billed as the successor to Webpack. Turbopack is written in Rust and claims to be 700x faster than Webpack (and 10x faster than Vite).

6. Conclusion

Next.js updates at a pace that's honestly surprising — a new stable version can drop overnight. Every major release causes a bit of friction, but the upgrade usually takes a day or two, and old patterns are typically preserved alongside the new ones.

The App Router changes are significant and a bit painful, but they formally usher in the era of Server Components.

Appendix — Quick Migration PowerShell Scripts

1. Move all pages from the pages folder into the app folder

# Get a reference to all tsx files in the src\pages directory.
$files = Get-ChildItem -Path "src\pages" -Filter "*.tsx"

# For each file, create a new directory in src\app with the same name as the file (without extension),
# move the file to the new directory, rename it to page.tsx, and prepend 'use client;' to it.
foreach ($file in $files) {
    # Create new directory.
    $newDir = New-Item -Path "src\app\$($file.BaseName)\" -ItemType Directory

    # Move and rename the file.
    $newFile = Move-Item -Path $file.FullName -Destination "$($newDir.FullName)\page.tsx" -PassThru

    # Add 'use client;' to the beginning of the file.
    $content = Get-Content -Path $newFile.FullName
    $newContent = 'use client;' + "`n" + $content
    Set-Content -Path $newFile.FullName -Value $newContent
}

pause

2. Bulk-convert components to Client Components

# Get a reference to all tsx files in the src\pages directory.
$files = Get-ChildItem -Path "src\pages" -Filter "*.tsx"

# For each file, create a new directory in src\app with the same name as the file (without extension),
# move the file to the new directory, rename it to page.tsx, and prepend 'use client;' to it.
foreach ($file in $files) {
    # Create new directory.
    $newDir = New-Item -Path "src\app\$($file.BaseName)\" -ItemType Directory

    # Move and rename the file.
    $newFile = Move-Item -Path $file.FullName -Destination "$($newDir.FullName)\page.tsx" -PassThru

    # Add 'use client;' to the beginning of the file.
    $content = Get-Content -Path $newFile.FullName
    $newContent = 'use client;' + "`n" + $content
    Set-Content -Path $newFile.FullName -Value $newContent
}

pause

3. Merge all JSON files in a folder into a single JSON file

# 初始化空的 Hashtable
$combinedJson = @{}

# 指定資料夾路徑
$folderPath = 'C:\path\to\json\files'

# 尋找所有 .json 檔案
Get-ChildItem -Path $folderPath -Filter *.json | ForEach-Object {
    # 獲取檔案名稱並移除 .json 副檔名
    $propertyName = $_.BaseName

    # 讀取 JSON 檔案內容
    $content = Get-Content $_.FullName | ConvertFrom-Json

    # 將 JSON 內容加入到 combinedJson 中,使用檔案名稱作為屬性
    $combinedJson[$propertyName] = $content
}

# 將合併的 JSON 轉換為字符串並寫入到一個新的 JSON 檔案
$combinedJson | ConvertTo-Json | Set-Content -Path "$folderPath\combined.json"

Author

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。Read More

Found this useful?

The author's free tools, daily podcasts and newsletter are all here.

Mark Ku · This article is licensed under CC BY 4.0. Credit the author and link back to the original when reusing it.

Comments

Subscribe to Newsletter

Subscribe to get new posts delivered instantly — never miss a tech share.

By submitting, you agree to receive emails. You can anytime.

Popular Posts

View all
Mark Ku
··602

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution
Mark Ku
··490

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.
Mark Ku
··333

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki
Mark Ku
··264

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning
Mark Ku
··221

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1
Mark Ku
··215

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Setting Up Samba on Ubuntu to Share Folders with Windows 11
NEXTJS 13.3.4 昇級踩坑筆記,Server side component 時代來臨 - migrate page route to app route - Mark Ku's Tech Notes