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:
- Sayfalarımız hızlı yüklensin, yani statik olsun istiyoruz.
- 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.
- 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:
- Dinamik olarak gelen içeriği statik sayfalar ile ziyaretçiye çok hızlı bir şekilde sunmak.
- Statik sayfalarımızı belli zaman aralıklarında otomatik olarak güncellemek.
- İsteğe bağlı olarak statik sayfalarımızı build time aşamasında oluşturmak.
- İsteğe bağlı olarak statik sayfayı sadece o sayfaya gidildiği aşamada oluşturmak.
- 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:
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:
Ş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 :)