İçeriğe geç
nextjs

Next.js Static Export: Statik Site Oluşturma

Tarık Tunç
Next.js Static Export: Statik Site Oluşturma

Next.js Static Export: Statik Site Oluşturma

Next.js, dinamik sunucu taraflı render'ın yanı sıra tamamen statik HTML dosyaları olarak export edilebilir. Bu, sunucu gerektirmeden herhangi bir statik hosting servisinde (GitHub Pages, Netlify, S3, Cloudflare Pages) barındırılabilen hızlı ve güvenli siteler oluşturmanızı sağlar. Bu yazıda Next.js Static Export yapılandırmasını, sınırlamalarını ve deployment stratejilerini adım adım inceleyeceğiz.

Static Export Nedir?

Static Export, Next.js uygulamanızı tamamen statik HTML, CSS ve JavaScript dosyalarına dönüştürür. Build sırasında tüm sayfalar pre-render edilir ve out klasörüne çıktı olarak verilir. Sunucu tarafında çalışan hiçbir kod yoktur.

Yapılandırma

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',

  // Statik export'ta image optimization sunucu gerektirir
  images: {
    unoptimized: true,
  },

  // Opsiyonel: Trailing slash ekle
  trailingSlash: true,

  // Opsiyonel: Base path
  basePath: '/my-app',

  // Opsiyonel: Asset prefix (CDN kullanıyorsanız)
  assetPrefix: 'https://cdn.example.com',
};

module.exports = nextConfig;

Build ve Export

# Build komutu - out/ klasörüne static dosyalar oluşturur
next build

# Lokal test
npx serve out

# Klasör yapısı
out/
├── index.html
├── about.html
├── blog/
│   ├── index.html
│   ├── react-rehberi.html
│   └── nextjs-rehberi.html
├── _next/
│   └── static/
├── favicon.ico
└── robots.txt

Dinamik Route'lar ile Static Export

Dinamik route'lar ([slug]) için generateStaticParams fonksiyonu zorunludur:

// app/blog/[slug]/page.js
import { getAllPosts, getPostBySlug } from '@/lib/posts';

// Build sırasında hangi sayfaların oluşturulacağını belirle
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export async function generateMetadata({ params }) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default async function BlogPost({ params }) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Markdown/MDX ile Blog

// lib/posts.js
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/posts');

export function getAllPosts() {
  const fileNames = fs.readdirSync(postsDirectory);

  return fileNames
    .filter((name) => name.endsWith('.md'))
    .map((fileName) => {
      const slug = fileName.replace(/\.md$/, '');
      const fullPath = path.join(postsDirectory, fileName);
      const fileContents = fs.readFileSync(fullPath, 'utf8');
      const { data, content } = matter(fileContents);

      return {
        slug,
        title: data.title,
        date: data.date,
        excerpt: data.excerpt || '',
        content,
      };
    })
    .sort((a, b) => new Date(b.date) - new Date(a.date));
}

export async function getPostBySlug(slug) {
  const fullPath = path.join(postsDirectory, `${slug}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  const { data, content } = matter(fileContents);
  const processedContent = await remark().use(html).process(content);

  return {
    slug,
    title: data.title,
    date: data.date,
    content: processedContent.toString(),
  };
}

Desteklenmeyen Özellikler

Static Export modunda aşağıdaki özellikler kullanılamaz:

Özellik Durum Alternatif
Server Components (dinamik) Sınırlı Build-time data fetching
Route Handlers (dinamik) Kullanılamaz Harici API servisi
Server Actions Kullanılamaz Client-side API çağrıları
Middleware Kullanılamaz CDN edge rules
ISR / Revalidation Kullanılamaz Rebuild tetikleme
Image Optimization Sunucu yok unoptimized: true veya harici loader
Draft Mode Kullanılamaz Ayrı preview ortamı
Uyarı: cookies(), headers(), searchParams (dinamik) ve redirect() (server-side) gibi dinamik fonksiyonlar static export'ta kullanılamaz. Bu fonksiyonları kullanan sayfalar build sırasında hata verir.

GitHub Pages Deployment

# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: out

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
    steps:
      - uses: actions/deploy-pages@v4
        id: deployment
// next.config.js - GitHub Pages için
const nextConfig = {
  output: 'export',
  basePath: '/repo-name',
  images: { unoptimized: true },
};

Cloudflare Pages Deployment

# Cloudflare Pages yapılandırması
# Build command: next build
# Output directory: out
# Node.js version: 20

# veya wrangler CLI ile
npx wrangler pages deploy out --project-name=my-site

Custom Image Loader

// lib/image-loader.js
export default function customLoader({ src, width, quality }) {
  // Cloudinary örneği
  return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;
}

// next.config.js
const nextConfig = {
  output: 'export',
  images: {
    loader: 'custom',
    loaderFile: './lib/image-loader.js',
  },
};

Client-Side Data Fetching

Static export'ta dinamik veri için client-side fetching kullanılır:

'use client';

import { useState, useEffect } from 'react';

export function DynamicComments({ postId }) {
  const [comments, setComments] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`https://api.example.com/posts/${postId}/comments`)
      .then((res) => res.json())
      .then((data) => {
        setComments(data);
        setLoading(false);
      });
  }, [postId]);

  if (loading) return <p>Yorumlar yükleniyor...</p>;

  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  );
}
İpucu: Static Export, blog, dokümantasyon, portfolio ve landing page gibi içerik ağırlıklı siteler için idealdir. Kullanıcı etkileşimi gerektiren dinamik uygulamalar (e-ticaret, dashboard) için sunucu taraflı render daha uygun olabilir.

Sonuç

Next.js Static Export, sunucu gerektirmeyen, hızlı ve güvenli statik siteler oluşturmanın güçlü bir yoludur. output: 'export' yapılandırması ile tüm sayfalarınız build sırasında pre-render edilir. generateStaticParams ile dinamik route'ları, Markdown/MDX ile blog içeriklerini ve client-side fetching ile dinamik verileri yönetebilirsiniz. GitHub Pages, Cloudflare Pages, Netlify gibi ücretsiz hosting servislerinde barındırarak maliyet etkin çözümler üretebilirsiniz.

Kaynaklar

İlgili Yazılar