Web開発

Next.js 15入門チュートリアル2026年版 - App Routerで作るモダンWebアプリ開発

カービー
Next.js 15入門チュートリアル2026年版 - App Routerで作るモダンWebアプリ開発
#Next.js#React#App Router#TypeScript#チュートリアル

Next.js 15とApp Routerを使ったモダンWebアプリ開発の完全ガイド。Server Actions、Streaming、最新の機能を実際にコードを書きながら学べます。

Next.js 15入門チュートリアル2026年版 - App Routerで作るモダンWebアプリ

Next.js 15がリリースされ、より高速で使いやすくなりました。この記事では、2026年最新のNext.js 15とApp Routerを使って、実際のWebアプリケーションを作りながらステップバイステップで学んでいきます。

何を作るのか?

このチュートリアルでは、ブログアプリケーションを作成します。以下の機能を実装していきます:

  • 📄 記事一覧表示
  • 📖 記事詳細表示
  • 🔍 検索機能
  • 📝 記事作成・編集(Server Actions使用)
  • 🎨 レスポンシブデザイン
  • ⚡ ストリーミング表示

前提条件

このチュートリアルを始める前に、以下の知識が必要です:

  • HTML/CSS の基礎知識
  • JavaScript(ES6+)の基礎知識
  • React の基本的な理解(useState、useEffect など)

開発環境の準備

Node.js のインストール

最新のLTSバージョン(Node.js 20.x以上)をインストールしてください。

# バージョン確認
node --version
npm --version

Next.js プロジェクトの作成

# Next.js 15 プロジェクト作成
npx create-next-app@latest blog-app --typescript --tailwind --app

# プロジェクトディレクトリに移動
cd blog-app

# 開発サーバー起動
npm run dev

ブラウザで http://localhost:3000 にアクセスして、Welcome画面が表示されることを確認してください。

プロジェクト構成の理解

Next.js 15 + App Routerの基本構成を確認しましょう:

blog-app/
├── app/                    # App Router(新)
│   ├── layout.tsx         # ルートレイアウト
│   ├── page.tsx           # ホームページ
│   ├── globals.css        # グローバルCSS
│   └── blog/              # ブログ関連ページ
├── components/            # 再利用可能なコンポーネント
├── lib/                   # ユーティリティ関数
└── public/               # 静的ファイル

ステップ1: 基本レイアウトの作成

まず、アプリケーション全体で使用するレイアウトを作成します。

app/layout.tsx の更新

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

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

export const metadata: Metadata = {
  title: 'モダンブログアプリ',
  description: 'Next.js 15で作るモダンなブログアプリケーション',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body className={inter.className}>
        <header className="bg-white shadow-sm border-b">
          <nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div className="flex justify-between items-center h-16">
              <h1 className="text-xl font-bold text-gray-900">
                モダンブログ
              </h1>
              <div className="space-x-4">
                <a href="/" className="text-gray-600 hover:text-gray-900">
                  ホーム
                </a>
                <a href="/blog" className="text-gray-600 hover:text-gray-900">
                  ブログ
                </a>
                <a href="/blog/new" className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
                  記事作成
                </a>
              </div>
            </div>
          </nav>
        </header>
        
        <main className="min-h-screen bg-gray-50">
          {children}
        </main>
        
        <footer className="bg-gray-900 text-white py-8">
          <div className="max-w-7xl mx-auto px-4 text-center">
            <p>&copy; 2026 モダンブログアプリ. All rights reserved.</p>
          </div>
        </footer>
      </body>
    </html>
  )
}

ホームページの作成

app/page.tsx を更新します:

import Link from 'next/link'

export default function HomePage() {
  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      <div className="text-center">
        <h1 className="text-4xl font-bold text-gray-900 mb-6">
          Next.js 15で作るモダンブログ
        </h1>
        <p className="text-xl text-gray-600 mb-8">
          App Router、Server Actions、Streamingを活用した
          <br />
          高速で使いやすいブログアプリケーション
        </p>
        
        <div className="space-y-4 sm:space-y-0 sm:space-x-4 sm:flex sm:justify-center">
          <Link
            href="/blog"
            className="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 font-semibold"
          >
            ブログを読む
          </Link>
          <Link
            href="/blog/new"
            className="inline-block border border-blue-600 text-blue-600 px-6 py-3 rounded-lg hover:bg-blue-50 font-semibold"
          >
            記事を書く
          </Link>
        </div>
      </div>
      
      <div className="mt-16 grid md:grid-cols-3 gap-8">
        <div className="text-center p-6 bg-white rounded-lg shadow-sm">
          <div className="text-3xl mb-4">⚡</div>
          <h3 className="text-lg font-semibold mb-2">高速パフォーマンス</h3>
          <p className="text-gray-600">
            App Routerによる最適化されたルーティングとStreaming機能
          </p>
        </div>
        
        <div className="text-center p-6 bg-white rounded-lg shadow-sm">
          <div className="text-3xl mb-4">🚀</div>
          <h3 className="text-lg font-semibold mb-2">Server Actions</h3>
          <p className="text-gray-600">
            サーバーサイドでの安全なデータ操作とフォーム処理
          </p>
        </div>
        
        <div className="text-center p-6 bg-white rounded-lg shadow-sm">
          <div className="text-3xl mb-4">📱</div>
          <h3 className="text-lg font-semibold mb-2">レスポンシブ</h3>
          <p className="text-gray-600">
            Tailwind CSSによるモバイルファーストなデザイン
          </p>
        </div>
      </div>
    </div>
  )
}

ステップ2: データ型とユーティリティの準備

ブログ記事の型定義

lib/types.ts を作成します:

export interface BlogPost {
  id: string
  title: string
  content: string
  excerpt: string
  author: string
  publishedAt: Date
  updatedAt: Date
  slug: string
  tags: string[]
}

export interface CreatePostData {
  title: string
  content: string
  excerpt: string
  author: string
  tags: string[]
}

モックデータとユーティリティ

lib/blog-data.ts を作成します:

import { BlogPost } from './types'

// モックデータ(実際のアプリではデータベースを使用)
export const mockPosts: BlogPost[] = [
  {
    id: '1',
    title: 'Next.js 15の新機能を徹底解説',
    content: `Next.js 15では多くの新機能が追加されました...`,
    excerpt: 'Next.js 15の主要な新機能をわかりやすく解説します。',
    author: 'テックライター',
    publishedAt: new Date('2026-01-15'),
    updatedAt: new Date('2026-01-15'),
    slug: 'nextjs-15-features',
    tags: ['Next.js', 'React', 'Web開発']
  },
  {
    id: '2',
    title: 'TypeScriptで型安全なReact開発',
    content: `TypeScriptを使ったReact開発のベストプラクティス...`,
    excerpt: 'TypeScriptでより安全で保守性の高いReactアプリを作る方法',
    author: 'フロントエンドエンジニア',
    publishedAt: new Date('2026-01-10'),
    updatedAt: new Date('2026-01-10'),
    slug: 'typescript-react-development',
    tags: ['TypeScript', 'React', '型安全']
  }
]

// 記事を取得する関数
export async function getPosts(): Promise<BlogPost[]> {
  // 実際のAPIコールをシミュレート
  await new Promise(resolve => setTimeout(resolve, 1000))
  return mockPosts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime())
}

// スラッグから記事を取得
export async function getPostBySlug(slug: string): Promise<BlogPost | null> {
  await new Promise(resolve => setTimeout(resolve, 500))
  return mockPosts.find(post => post.slug === slug) || null
}

// 記事を作成
export async function createPost(data: CreatePostData): Promise<BlogPost> {
  await new Promise(resolve => setTimeout(resolve, 1000))
  
  const newPost: BlogPost = {
    id: String(Date.now()),
    ...data,
    publishedAt: new Date(),
    updatedAt: new Date(),
    slug: data.title.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')
  }
  
  mockPosts.unshift(newPost)
  return newPost
}

ステップ3: ブログ一覧ページの作成

Loading UIとStreaming

app/blog/loading.tsx を作成します:

export default function BlogLoading() {
  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      <div className="animate-pulse">
        <div className="h-8 bg-gray-300 rounded w-1/3 mb-8"></div>
        
        <div className="grid gap-6">
          {[...Array(3)].map((_, i) => (
            <div key={i} className="bg-white p-6 rounded-lg shadow-sm">
              <div className="h-6 bg-gray-300 rounded w-3/4 mb-4"></div>
              <div className="space-y-2">
                <div className="h-4 bg-gray-300 rounded w-full"></div>
                <div className="h-4 bg-gray-300 rounded w-2/3"></div>
              </div>
              <div className="flex gap-2 mt-4">
                <div className="h-6 bg-gray-300 rounded w-16"></div>
                <div className="h-6 bg-gray-300 rounded w-20"></div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

ブログ一覧ページ本体

app/blog/page.tsx を作成します:

import { Suspense } from 'react'
import Link from 'next/link'
import { getPosts } from '@/lib/blog-data'

async function BlogList() {
  const posts = await getPosts()
  
  return (
    <div className="grid gap-6">
      {posts.map((post) => (
        <article key={post.id} className="bg-white p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow">
          <Link href={`/blog/${post.slug}`}>
            <h2 className="text-xl font-semibold text-gray-900 hover:text-blue-600 mb-3">
              {post.title}
            </h2>
          </Link>
          
          <p className="text-gray-600 mb-4">
            {post.excerpt}
          </p>
          
          <div className="flex items-center justify-between text-sm text-gray-500">
            <div className="flex items-center space-x-4">
              <span>by {post.author}</span>
              <span>{post.publishedAt.toLocaleDateString('ja-JP')}</span>
            </div>
            
            <div className="flex gap-2">
              {post.tags.map((tag) => (
                <span
                  key={tag}
                  className="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs"
                >
                  {tag}
                </span>
              ))}
            </div>
          </div>
        </article>
      ))}
    </div>
  )
}

export default function BlogPage() {
  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      <div className="mb-8">
        <h1 className="text-3xl font-bold text-gray-900 mb-4">
          ブログ記事一覧
        </h1>
        <p className="text-gray-600">
          最新の技術記事をお届けします
        </p>
      </div>
      
      <Suspense fallback={<BlogListSkeleton />}>
        <BlogList />
      </Suspense>
    </div>
  )
}

function BlogListSkeleton() {
  return (
    <div className="grid gap-6">
      {[...Array(3)].map((_, i) => (
        <div key={i} className="bg-white p-6 rounded-lg shadow-sm animate-pulse">
          <div className="h-6 bg-gray-300 rounded w-3/4 mb-4"></div>
          <div className="space-y-2">
            <div className="h-4 bg-gray-300 rounded w-full"></div>
            <div className="h-4 bg-gray-300 rounded w-2/3"></div>
          </div>
          <div className="flex gap-2 mt-4">
            <div className="h-6 bg-gray-300 rounded w-16"></div>
            <div className="h-6 bg-gray-300 rounded w-20"></div>
          </div>
        </div>
      ))}
    </div>
  )
}

ステップ4: 記事詳細ページの作成

動的ルーティング

app/blog/[slug]/page.tsx を作成します:

import { notFound } from 'next/navigation'
import Link from 'next/link'
import { getPostBySlug } from '@/lib/blog-data'

interface PageProps {
  params: {
    slug: string
  }
}

export default async function BlogPostPage({ params }: PageProps) {
  const post = await getPostBySlug(params.slug)
  
  if (!post) {
    notFound()
  }
  
  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      {/* パンくずナビ */}
      <nav className="mb-8">
        <Link href="/blog" className="text-blue-600 hover:text-blue-800">
          ← ブログ一覧に戻る
        </Link>
      </nav>
      
      <article>
        {/* ヘッダー */}
        <header className="mb-8">
          <h1 className="text-4xl font-bold text-gray-900 mb-4">
            {post.title}
          </h1>
          
          <div className="flex items-center space-x-4 text-gray-600 mb-4">
            <span>by {post.author}</span>
            <span>•</span>
            <time dateTime={post.publishedAt.toISOString()}>
              {post.publishedAt.toLocaleDateString('ja-JP', {
                year: 'numeric',
                month: 'long',
                day: 'numeric'
              })}
            </time>
          </div>
          
          <div className="flex gap-2">
            {post.tags.map((tag) => (
              <span
                key={tag}
                className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
              >
                {tag}
              </span>
            ))}
          </div>
        </header>
        
        {/* コンテンツ */}
        <div className="prose prose-lg max-w-none">
          <div className="bg-white p-8 rounded-lg shadow-sm">
            {post.content.split('\n').map((paragraph, index) => (
              <p key={index} className="mb-4 text-gray-700 leading-relaxed">
                {paragraph}
              </p>
            ))}
          </div>
        </div>
      </article>
    </div>
  )
}

// 404ページ
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
  return (
    <div className="max-w-4xl mx-auto py-12 px-4 text-center">
      <h2 className="text-2xl font-bold text-gray-900 mb-4">
        記事が見つかりません
      </h2>
      <p className="text-gray-600 mb-8">
        お探しの記事は削除されたか、URLが変更された可能性があります。
      </p>
      <Link
        href="/blog"
        className="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700"
      >
        ブログ一覧に戻る
      </Link>
    </div>
  )
}

ステップ5: Server Actionsで記事作成機能

Server Actions の実装

lib/actions.ts を作成します:

'use server'

import { redirect } from 'next/navigation'
import { createPost } from './blog-data'
import { CreatePostData } from './types'

export async function createBlogPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  const excerpt = formData.get('excerpt') as string
  const author = formData.get('author') as string
  const tagsString = formData.get('tags') as string
  
  // バリデーション
  if (!title || !content || !excerpt || !author) {
    throw new Error('必須フィールドが入力されていません')
  }
  
  const tags = tagsString
    .split(',')
    .map(tag => tag.trim())
    .filter(tag => tag.length > 0)
  
  const postData: CreatePostData = {
    title,
    content,
    excerpt,
    author,
    tags
  }
  
  const newPost = await createPost(postData)
  
  // 作成した記事ページにリダイレクト
  redirect(`/blog/${newPost.slug}`)
}

記事作成フォーム

app/blog/new/page.tsx を作成します:

import { createBlogPost } from '@/lib/actions'

export default function NewBlogPostPage() {
  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      <h1 className="text-3xl font-bold text-gray-900 mb-8">
        新しい記事を作成
      </h1>
      
      <form action={createBlogPost} className="bg-white p-8 rounded-lg shadow-sm">
        <div className="space-y-6">
          {/* タイトル */}
          <div>
            <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
              タイトル *
            </label>
            <input
              type="text"
              id="title"
              name="title"
              required
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="記事のタイトルを入力してください"
            />
          </div>
          
          {/* 要約 */}
          <div>
            <label htmlFor="excerpt" className="block text-sm font-medium text-gray-700 mb-2">
              要約 *
            </label>
            <textarea
              id="excerpt"
              name="excerpt"
              required
              rows={3}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="記事の要約を入力してください(一覧ページで表示されます)"
            />
          </div>
          
          {/* 本文 */}
          <div>
            <label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-2">
              本文 *
            </label>
            <textarea
              id="content"
              name="content"
              required
              rows={12}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="記事の本文を入力してください"
            />
          </div>
          
          {/* 著者 */}
          <div>
            <label htmlFor="author" className="block text-sm font-medium text-gray-700 mb-2">
              著者名 *
            </label>
            <input
              type="text"
              id="author"
              name="author"
              required
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="著者名を入力してください"
            />
          </div>
          
          {/* タグ */}
          <div>
            <label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-2">
              タグ
            </label>
            <input
              type="text"
              id="tags"
              name="tags"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              placeholder="タグをカンマ区切りで入力してください(例: React, Next.js, TypeScript)"
            />
            <p className="mt-1 text-sm text-gray-500">
              複数のタグはカンマで区切って入力してください
            </p>
          </div>
        </div>
        
        <div className="mt-8 flex gap-4">
          <button
            type="submit"
            className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 font-semibold"
          >
            記事を投稿
          </button>
          <a
            href="/blog"
            className="bg-gray-200 text-gray-700 px-6 py-3 rounded-lg hover:bg-gray-300 font-semibold"
          >
            キャンセル
          </a>
        </div>
      </form>
    </div>
  )
}

ステップ6: 検索機能の実装

検索用のServer Component

components/SearchForm.tsx を作成します:

'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function SearchForm() {
  const [query, setQuery] = useState('')
  const router = useRouter()
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (query.trim()) {
      router.push(`/blog/search?q=${encodeURIComponent(query.trim())}`)
    }
  }
  
  return (
    <form onSubmit={handleSubmit} className="mb-8">
      <div className="flex gap-2">
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="記事を検索..."
          className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button
          type="submit"
          className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
        >
          検索
        </button>
      </div>
    </form>
  )
}

検索結果ページ

app/blog/search/page.tsx を作成します:

import Link from 'next/link'
import { getPosts } from '@/lib/blog-data'

interface SearchPageProps {
  searchParams: {
    q?: string
  }
}

export default async function SearchPage({ searchParams }: SearchPageProps) {
  const query = searchParams.q || ''
  const allPosts = await getPosts()
  
  const filteredPosts = allPosts.filter(post =>
    post.title.toLowerCase().includes(query.toLowerCase()) ||
    post.content.toLowerCase().includes(query.toLowerCase()) ||
    post.excerpt.toLowerCase().includes(query.toLowerCase()) ||
    post.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase()))
  )
  
  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      <div className="mb-8">
        <h1 className="text-3xl font-bold text-gray-900 mb-4">
          検索結果
        </h1>
        {query && (
          <p className="text-gray-600">
            「{query}」の検索結果: {filteredPosts.length}件
          </p>
        )}
      </div>
      
      {filteredPosts.length === 0 ? (
        <div className="text-center py-12">
          <p className="text-gray-500 text-lg mb-4">
            検索条件に一致する記事が見つかりませんでした
          </p>
          <Link
            href="/blog"
            className="text-blue-600 hover:text-blue-800"
          >
            すべての記事を見る
          </Link>
        </div>
      ) : (
        <div className="grid gap-6">
          {filteredPosts.map((post) => (
            <article key={post.id} className="bg-white p-6 rounded-lg shadow-sm">
              <Link href={`/blog/${post.slug}`}>
                <h2 className="text-xl font-semibold text-gray-900 hover:text-blue-600 mb-3">
                  {post.title}
                </h2>
              </Link>
              
              <p className="text-gray-600 mb-4">
                {post.excerpt}
              </p>
              
              <div className="flex items-center justify-between text-sm text-gray-500">
                <div className="flex items-center space-x-4">
                  <span>by {post.author}</span>
                  <span>{post.publishedAt.toLocaleDateString('ja-JP')}</span>
                </div>
                
                <div className="flex gap-2">
                  {post.tags.map((tag) => (
                    <span
                      key={tag}
                      className="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs"
                    >
                      {tag}
                    </span>
                  ))}
                </div>
              </div>
            </article>
          ))}
        </div>
      )}
    </div>
  )
}

ブログページに検索フォームを追加

app/blog/page.tsx を更新:

import { Suspense } from 'react'
import Link from 'next/link'
import { getPosts } from '@/lib/blog-data'
import SearchForm from '@/components/SearchForm'

// ... 既存のBlogList, BlogListSkeleton コンポーネント

export default function BlogPage() {
  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      <div className="mb-8">
        <h1 className="text-3xl font-bold text-gray-900 mb-4">
          ブログ記事一覧
        </h1>
        <p className="text-gray-600">
          最新の技術記事をお届けします
        </p>
      </div>
      
      <SearchForm />
      
      <Suspense fallback={<BlogListSkeleton />}>
        <BlogList />
      </Suspense>
    </div>
  )
}

ステップ7: パフォーマンスの最適化

画像最適化

Next.js の Image コンポーネントを使用して画像を最適化します:

import Image from 'next/image'

// 記事内で画像を使用する場合
<Image
  src="/images/blog-hero.jpg"
  alt="ブログのヒーロー画像"
  width={800}
  height={400}
  className="rounded-lg"
  priority // 最初の画像の場合
/>

メタデータの最適化

各ページに適切なSEOメタデータを追加:

// app/blog/[slug]/page.tsx に追加
import type { Metadata } from 'next'

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const post = await getPostBySlug(params.slug)
  
  if (!post) {
    return {
      title: '記事が見つかりません'
    }
  }
  
  return {
    title: `${post.title} | モダンブログ`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt.toISOString(),
      authors: [post.author],
      tags: post.tags,
    },
  }
}

ステップ8: デプロイメント

Vercelでのデプロイ

# Vercel CLIをインストール
npm i -g vercel

# デプロイ
vercel

# 本番デプロイ
vercel --prod

環境変数の設定

本番環境用の環境変数を .env.production に設定:

NEXT_PUBLIC_APP_URL=https://your-app.vercel.app
DATABASE_URL=your-database-url

まとめ

このチュートリアルで学んだことを振り返ってみましょう:

✅ 習得したスキル

  • Next.js 15とApp Routerの基本
  • Server Actionsを使ったフォーム処理
  • SuspenseとStreamingによる段階的表示
  • 動的ルーティング([slug]
  • TypeScriptを使った型安全な開発
  • Tailwind CSSでのレスポンシブデザイン

🚀 次のステップ

このブログアプリをさらに発展させるために、以下の機能追加に挑戦してみてください:

  1. 認証機能 - NextAuthまたはClerkを使用
  2. データベース統合 - PrismaとPostgreSQLの導入
  3. コメント機能 - 記事へのコメント投稿
  4. 管理画面 - 記事の編集・削除機能
  5. RSS配信 - RSS/Atomフィードの生成
  6. SEO強化 - structured dataとsitemap.xml

📚 参考リソース

これで Next.js 15 を使ったモダンなWebアプリケーション開発の基礎をマスターできました!実際にコードを書きながら学ぶことで、理解が深まったのではないでしょうか。


お疲れさまでした! 🎉 ぜひ今回学んだ知識を活用して、オリジナルのWebアプリケーション開発に挑戦してみてください。