Oğuzhan Yılmaz
29 Ağu 2023
14 dk.
Paylaş

Next.js ile Statik Sayfalar Oluşturmak

Next.js ile Statik Sayfalar Oluşturmak

Herkese selam, bu yazımda Next.js ile birlilte statik sayfalar oluşturma ve bunları istediğimiz zaman nasıl güncelleyebileceğimizden bahsedeceğim. Burada kod ile örnek verirken TypeScript ile birlikte vereceğim.

Kendimize öncelikle bir problem ya da ihtiyaç belirleyelim ve bunlar üzerinden gidelim. Bir şirketin web uygulamasını yaptığımızı varsayalım. Göstermek istediğimiz içerikler arasında bazı statik sayfalarımız olacak ve bir de blog kısmımız olacak.

Problemler:

  1. Sayfalarımız hızlı yüklensin, yani statik olsun istiyoruz.
  2. Bir panelimizin olduğunu ve yazılarımızı buradan yazdığımızı düşünürsek bir yazıyı güncelle, yeni yazı ekleme ya da herhangi bir yazıyı silmek istediğimizde oluşan statik sayfalarımız tekrardan oluşturulsun istiyoruz.
  3. Yukarıdaki maddede blog kısmı için olduğu gibi ayda yılda bir güncellenen başka sayfalarımızın da olduğunu varsayalım. Bunların çalışma mantığını biraz daha farklı yapacağız.

Bu yazıda öğreneceklerimiz:

  1. Dinamik olarak gelen içeriği statik sayfalar ile ziyaretçiye çok hızlı bir şekilde sunmak.
  2. Statik sayfalarımızı belli zaman aralıklarında otomatik olarak güncellemek.
  3. İsteğe bağlı olarak statik sayfalarımızı build time aşamasında oluşturmak.
  4. İsteğe bağlı olarak statik sayfayı sadece o sayfaya gidildiği aşamada oluşturmak.
  5. Oluşturulmuş ya da oluşturulmamış bir statik sayfayı istediğimiz zaman api yardımı ile oluşturmak ya da tekrardan oluşturarak güncellemek.

Bunları yaparken Next.js'nin en etkin kullanacağımız özellikleri şunlar olacak:

getStaticProps kullanmak

Bu sayfa en temelde bir hakkımızda sayfası olabilir. Sadece içerik olarak değil, yani bir blog sayfası olarak düşünmeden bir çok alan için bilgiyi apiden alıp bu sayfada gösterebiliriz. Örnek olarak şirketi anlatan bir yazı, misyon, vizyon, projeler, tamamlanan proje sayısı, devam eden proje sayısı, ekip arkadaşları...

Bir açıklama yazısı, misyon ve ekip arkadaşlarının bilgilerini alacağımızı varsayalım ve devam edelim.

Bu sayfa için sadece getStaticProps kullanmamız yeterli olacaktır. Backend servislerimizden ya da bir CMS API'ından verimizi çeken getAbout adında bir servisimiz olsun ve bize şu tipte bir response dönsün.

type EmployeeType = {
  firstName: string;
  lastName: string;
  linkedin: string;
  image: string;
};

type AboutContentResponseType = {
  content: string;
  mission: string;
  team: EmployeeType[];
};

Şimdi about sayfamızı oluşturalım:

import React from "react";
import { NextPage } from "next";

type AboutPagePageProps = {};

const AboutPagePage: NextPage<AboutPagePageProps> = (props) => {
  return <div>AboutPagePage</div>;
};

export default AboutPagePage;

Bu sayfa aslında bu şekilde statik olarak çalışacaktır ancak içeriğimizi bu şekilde dinamik olarak alamayız. Bunun için bir api call yapmamız lazım. Bunu client tarafında useEffect kullanarak yapabiliriz ancak bu ilk açılışta sayfanın boş olarak yüklenmesi anlamına gelir. Bu süreçte backend'e bağlı olarak bekleme süremiz artabilir ve SEO için iyi şeyler söyleyemeyiz.

Bunu yapmayın:

import React, { useEffect, useState } from "react";
import { NextPage } from "next";

type AboutPagePageProps = {};

const AboutPagePage: NextPage<AboutPagePageProps> = (props) => {
  const [about, setAbout] = useState(null);
  useEffect(() => {
    getAbout().then(setAbout);
  }, []);
  return (
    <div>
      <p>{about.content}</p>
      <p>{about.mission}</p>
      <ul>
        {about.team.map((member) => (
          <li key={`${member.firstName}-${member.lastName}`}>
            {/* bla bla bla */}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default AboutPagePage;

Bunu yapın:

import React from "react";
import { GetStaticProps, NextPage } from "next";
import type { AboutContentResponseType } from "@/types/ServiceTypes";

type AboutPagePageProps = {
  about: AboutContentResponseType;
};

const AboutPagePage: NextPage<AboutPagePageProps> = ({ about }) => {
  return (
    <div>
      <p>{about.content}</p>
      <p>{about.mission}</p>
      <ul>
        {about.team.map((member) => (
          <li key={`${member.firstName}-${member.lastName}`}>
            {/* bla bla bla */}
          </li>
        ))}
      </ul>
    </div>
  );
};

export const getStaticProps: GetStaticProps<AboutPagePageProps> = async () => {
  const about = await getAbout();
  return { props: { about } };
};

export default AboutPagePage;

Yukarıdaki örnekte projemizi deploy ederken, yani build time anında getAbout servisimiz çalışacak ve statik olarak oluşturulacaktır. Artık bu sayfaya ne zaman girersek girelim tekrardan bir api call atmayacağız ve sayfamız statik olarak çok hızlı bir şekilde açılacaktır. Yani şu şekilde çalışacaktır:

getStaticProps

Ancak bir sorunumuz var 🤔 Bu içerik güncellenirse biz hala eskisini göstermeye devam edeceğim.

Neyse ki bu sorunu çözmek çok kolay.

getStaticProps revalidate özelliği

getStaticProps içerisine ekleyeceğimiz tek şey şu olacak:

revalidate: number

Burada revalidate adında vereceğimiz parametre aslında saniye cinsinden bir zaman. Hemen son durumuna bakalım:

export const getStaticProps: GetStaticProps<AboutPagePageProps> = async () => {
  const about = await getAbout();
  return { props: { about }, revalidate: 60 * 60 };
};

Bu aslında şu demek, bu sayfa statik olsun ancak sen bunu minimum 1 saatte bir güncelle. Minimum derken ne demek istiyorum, tam 1 saat olduğunda güncellemiyor mu? Hayır, güncellemiyor.

Hadi önce buradaki mantığa bir bakalım sonra nasıl çalıştığını anlayalım, getStaticProps'un buradaki çalışma mantığı aslında şu şekilde:

getStaticProps with revalidate

Şimdi biz revalidate parametremize 1 saat(60 * 60 saniye) vermiştik. Buradaki zaman doğrusunda da 1 saatlik dilimlere ayırdık. Burada ilk işlemimiz build işlemi ve bu aşamada görüldüğü gibi sayfamız oluşturuluyor. Daha sonra 1 saat süre geçmeden birinci ziyaret gerçekleşiyor. Bu ziyaretçi normal olarak build time anında oluşturulan sayfayı görüyor. Biraz zaman geçtikten sonra, neredeyse 2 saat olacak kadar, ikinci ziyaret gerçekleşiyor. Build time zamanından 1 saatten fazla geçmesine rağmen bu gelen ziyaretçi de aynı sayfayı görüyor. E hani biz süre olarak 1 saat vermiştik ve o süreyi de geçtik, neden güncel halini görmüyor diye sorabilirsiniz. Bu süre aşımı olduktan sonra ilk ziyaret sırasnda Next.js arkaplanda bu sayfayı tekrar oluşturuyor ancak ziyaretçiyi bekletmemek adına varolan sayfamızı göstermeye devam ediyor. Hemen peşine üçüncü bir ziyaretçi geliyor, işte bu ziyaretçi artık ikinci ziyaretçi sırasında oluşturulan sayfayı görmeye başlayacak. Bu arada ikinci ziyaretçinin hemen bir kaç saniye sonra sayfasını yenilerse o da güncellenmiş safyayı görebilir tabi ki.

Bu iş aslında çok maliyeti olan bir şey değil o yüzden bu süreyi sayfanıza göre istediğiniz gibi değiştirebilirsiniz. 1 saniye bile yapmanız herhangi bir sorun yaratmaz.

Şimdi diğer işlemlere geçelim, yani blog sayfalarımıza.

Şu şekilde sayfalarımız olsun:

  • /blog

    • index.tsx
    • [slug].tsx

/blog adresini ve /blog/[slug] adreslerini karşılayan sayfalarımız. Biliyorsunuz ki [slug] yazan yere her şey gelebilir.

Yukarıdaki örneklere göre hemen blog sayfamızı hızlıca benzer şekilde oluşturalım:

import React from "react";
import { GetStaticProps, NextPage } from "next";
import { getBlogs } from "@/services/app";

export type BlogType = {
  id: string;
  title: string;
  excerpt: string;
  image: string;
  author: string;
  date: string;
};
type BlogsPageProps = {
  blogs: BlogType[];
};

const BlogsPage: NextPage<BlogsPageProps> = ({ blogs }) => {
  return (
    <div>
      {blogs.map((blog) => (
        <article key={blog.id}>
          <h2>{blog.title}</h2>
          <p>{blog.excerpt}</p>
          <span>{blog.author}</span>
          <time dateTime={blog.date}>{blog.date}</time>
        </article>
      ))}
    </div>
  );
};

export const getStaticProps: GetStaticProps<BlogsPageProps> = async () => {
  const blogs = await getBlogs();
  return { props: { blogs }, revalidate: 60 * 60 };
};

export default BlogsPage;

Bu örnekte yine saatte bir kez güncellenecek şekilde blog yazılarımızı listelediğimiz sayfayı oluşturduk. Benzer iş olduğu için burada detaylara girmiyorum.

Şimdi asıl iş blog yazılarımızın olduğu sayfaları oluşturmak. Bu aşamada diğer sayfalara göre biraz daha farklı yöntemler izleyeceğiz.

Bu sayfamız dinamik bir path olduğu için getStaticProps ile birlikte getStaticPaths kullanmak zorundayız.

getStaticPaths kullanmak

Öncelikle oluşturacağımız sayfaya bir bakalım:

import React from "react";
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import { getBlogs, getBlog } from "@/services/app";

type BlogDetailType = {
  id: string;
  slug: string;
  title: string;
  excerpt: string;
  content: string;
  image: string;
  author: string;
  date: string;
};
type BlogDetailPageProps = {
  blog: BlogDetailType[];
};

const BlogDetailPage: NextPage<BlogDetailPageProps> = ({ blog }) => {
  return (
    <main>
      <img src={blog.image} alt={blog.title} />
      <h1>{blog.title}</h1>
      <span>{blog.author}</span>
      <time dateTime={blog.date}>{blog.date}</time>
      <p>{blog.excerpt}</p>
      <div>{blog.content}</div>
    </main>
  );
};

export const getStaticProps: GetStaticProps<BlogDetailPageProps> = async ({
  params,
}) => {
  const blog = await getBlog(params.slug);
  return { props: { blog }, revalidate: 60 * 60 };
};

export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
  const blogs = await getBlogs();
  const paths = blogs.map((blog) => ({
    params: { slug: blog.slug },
  }));
  return { paths, fallback: "blocking" };
};

export default BlogDetailPage;

Şimdi burada ne yaptığımızı inceleyelim. getStaticPaths öncelikle ne işe yarıyor?

getStaticProps kullanımını zaten öğrendik. İlk örneklerde kullanırken tek bir sayfa oluşturuyorduk ve parametre vs almıyorduk. Ancak artık burası blog sayfası, dinamik bir dosya. Bu dosyada bir çok blog yazısı göstereceğiz. getStaticPaths de tam olarak bize burada lazım oluyor, hangi sayfaları göstereceğimizi bize bu söyleyecek.

Dikkat ederseniz getStaticPaths içinde bir api call atıyoruz ve blog yazılarımızı alıyoruz. Burada döneceğimiz değer şu şekilde olmalı:

{
  paths: {
    params: {slug: string}
  }[],
  fallback: boolean | "blocking"
}

paths dediğimiz alan bir array dönmek zorunda ve içerisinde de params diye bir alan olmalı. Bunun içinde de bizde şu an sadece slug var. Bunu tabi biz kafamıza göre vermedik. Bu değeri dosya adımıza göre gönderiyoruz. Bizim de hatırlarsanız dosyamız ve dosya yolu şu şekildeydi: /blog/[slug].tsx. Siz burada başka bir alan daha göndermek isterseniz slug haricinde gönderemezsiniz. Alt alta dinamik path oluşturduğunuz durumda ancak bunu yapabilirsiniz. fallback'in ne olduğuna geleceğiz ancak şimdilik buradan devam edelim.

getStaticPaths nasıl çalışıyor?

getStaticPaths build time anında sadece bir kere çalışacak. Döndüğümüz paths array'inin içindeki her bir elemanı getStaticProps'a verecek. Burada her bir eleman zaten görüldüğü üzere params değerini içeriyor. Yani biz artık getStaticProps için bir obje alıyoruz ve bu obje içerisinde de params mevcut. Sormadan cevaplayayım hemen, evet bu da params olmak zorunda. Artık params içerisinde bizim slug değerimize erişmiş olduk. Bu slug değeri için blog yazımızı çekebilir ve prop olarak sayfamıza geçebiliriz. Bu aşamayı zaten yapmıştık.

Şöyle bir detay var bunu atlamayalım. getStaticPaths içinde gönderdiğimiz array boş bile olsa biz bu sayfada getStaticProps içerisinde params'a ve onun içindeki slug değerine ulaşabiliyoruz. getStaticPaths'i burada verdiğimiz değerler için önceden oluşturmak için kullanıyoruz aslında. Kısa bir örnek vermek gerekirse 100 tane blog yazımız vardı ve biz 100 tane slug değeri döndük. Yarın slug değeriz abc olan yeni bir yazı eklendi, getStaticPaths tekrar çalışmayacak ancak biz /blog/abc sayfasına gittiğimiz zaman params.slug dediğimizde abc değerini alıyor olacağız.

getStaticPaths isFallback parametresi

Şimdi gelelim az önce bahsettiğimiz fallback değerine. Bu değerimiz ya string tipinde blocking olacak ya da bir boolean değer olacak.

fallback değerimiz false olursa getStaticPaths ile getStaticProps'a döndürülmeyen tüm dinamik sayfalarımız 404 hatası alacaktır.

fallback değerimiz true değerini çok fazla statik sayfamız olduğunda kullanmak mantıklıdır. Diyelim ki 100.000 tane statik sayfamız var ancak biz bunların hepsini getStaticPaths ile verirsek uygulamamızın build zamanı çok uzayacaktır, Hatta pipeline muhtemelen timeout olacaktır. Bu gibi durumlarda bir kısım, yapabiliyorsak en önemli gördüğümüz 1.000 tane kadar içeriğin slug değerlerini getStaticPaths ile veririz ve fallback'i de true olarak geçeriz. Bu bize şunu sağlayacak aslında, build time anında 1.000 tane sayfayı oluşturacak, diğer içerikler için bize 404 hatası vermeyecek. Onun yerine sayfayı açacak. Aklınıza şu soru gelmiş olabilir, tamam sayfayı açtı ama bu sayfamız bizim oluşturulmamıştı ne gösteriyoruz burada? Burada bir hook kullanmak durumunda kalıyoruz: useRouter. Şu şekilde tanımlamasını yaptık diyelim:

const router = useRouter();

Bunu içinde bizim bu yaşadığımız durumu karşılayan bir değerimiz var: isFallback.

Sayfada içeriğimizi göstermeden önce bir kontrol koyacağız ve diyeceğiz ki: Eğer fallback durumunda ise bir skeleton view göster. Bunu blog sayfamız için şu şekilde gösterebiliriz:

const BlogDetailPage: NextPage<BlogDetailPageProps> = ({ blog }) => {
  const router = useRouter();
  if (router.isFallback) {
    return <div>Loading...</div>;
  }
  return (
    <main>
      <img src={blog.image} alt={blog.title} />
      <h1>{blog.title}</h1>
      <span>{blog.author}</span>
      <time dateTime={blog.date}>{blog.date}</time>
      <p>{blog.excerpt}</p>
      <div>{blog.content}</div>
    </main>
  );
};

Sıra geldi fallback parametremizin blocking değerine sahip olduğu senaryoya. Bu durumda getStaticPaths ile yolladığımız pathler build time anında oluşturulur, bu aşamada oluşturulmayan sayfalara gelen istekler ise getServerSideProps gibi çalışır. Sayfayı açıp bir skeleton view, yani bir bekleme ekranı göstermek yerine sayfa açılmasını bloklar, veriyi çeker ve ondan sonra sayfayı açar. Burada da artık statik sayfalarımızın sayısı 1 artmış olur. Bir sonraki ziyaretlerde bekletme yapmaz.

getServerSideProps'un ne olduğuna bakmak için: getServerSideProps

Son olarak statik sayfaların revalidate durumlarıyla alakalı bir konuya daha değinmek istiyorum. Diyelim ki statik sayfamızı revalidate 1 gün olacak şekilde ayarladık, çünkü çok nadir güncellenecek sayfalardan biri olsun. Oldu da içerik güncellendiği anda biz de sayfamızı güncellemek istedik ancak tekrardan bir build etmek ve bunu deploy almak bir tık zahmetli olabiliyor. Bu durumlarda ne yapacağımıza gelin bakalım.

Statik bir sayfayı manuel olarak tekrardan oluşturmak

pages klasörü altında Next.js'in biliyorsunuz ki kendisi için ayırdığı bir klasör var: api. Burada hemen bir endpoint oluşturacağız, ancak bunu parametrik yapacağımız için şu şekilde oluşturmalıyız: [slug].ts. Tabii ki buraya yine istediğiniz adı verebilirsiniz. Oluşturduğumuz dosyanın tam yolunu ben şu şekilde belirledim: /pages/api/revalidate/[slug].ts.

Dosyamızı şu şekilde yazabiliriz:

import { routes } from "@/constants/routes";
import { apiResponse } from "@/utils/apiResponse";
import { NextApiRequest, NextApiResponse } from "next";

export default async function (req: NextApiRequest, res: NextApiResponse) {
  const q = req.query;
  const slug = q.slug?.toString()!;
  try {
    await res.revalidate(`/blog/${slug}`);
    return res.json(apiResponse.success("Revalidate success"));
  } catch (error) {
    return res.json(apiResponse.error("Revalidate failed"));
  }
}

Parametre olarak bize gelen req içerisinde query'den slug değerimizi alıyoruz. Bu aslında yine dosya adımızdan geliyor. Daha sonra yapacağımız şey ise res içerisinden revalidate metodunu çalıştırmak olacak. Buna vereceğimiz path bir getStaticProps içeren bir sayfa olmalı. Bu işlem bittiğinde statik olan sayfamız henüz zamanı gelmemiş olsa bile manuel olarak tekrardan oluşturulmuş olacak.

Aslında her şey bu kadar. Biraz uzun bir yazı oldu ancak umarım Next.js ile statik sayfalar oluştururken aklınızda sorulara cevap bulabilmişsinizdir. Eksiklerimiz ya da yanlışlarımız olduysa lütfen bize bildirin, başka bir yazıda görüşmek üzere :)

Paylaş