Tutorial that I used to build this website — Blog by Muhammad Daffa Ashdaqfillah
Dokumentasi tutorial2 yg membantu untuk membuat web ini 🎨😅

Published on June 3, 2024
Article Details
Haii, sebenernya ini ringkasan tutorial umum yang aku pakai aja buat develop website ini, ini bisa ditemui banyak di youtube atau artikel. tp mungkin di public referensi2 nya terpisah pisah-pisah, nah di sini aku mau menyatukan aja gimana caranya deploy web statis ini tp seolah seperti web dinamis (update data dari notion jd tidak perlu selalu berurusan dengan kode dan publish ulang, bisa pake routing tanpa terbatas oleh spa-single page applications, sama pake custom domain).
Hanya modal waktu aja sih, ga ada keluar material sepersenpun hehe..
Poin poin nya mulai dari:
- Domain .me gratis setahun bagi yang punya github student developer pack
- Setup untuk customize UI dengan shadcn sama aceternity
- Setup API Notion Database
- Setup Fetch Data Next.js
- Notion page renderer untuk menampilkan page seperti blog dan detail project di web ini
- Cara deployment github pages, serta cara routing react di spa (single-page applications), karna github pages ini sifatnya memang spa, hal ini perlu dilakuin supaya orang bisa akses halaman spesifik kamu hanya melalui direct link, jadi tidak perlu dari halaman awal untuk aksesnya, sama seperti kalian kalau akses halaman blog ini dari link 😅
Tech Stack
Di sini aku menggunakan tech stack seperti ini:
- React.js (vite-deploy di github pages) untuk Front-end
- Next.js (vite-deploy di vercel) untuk Fetch data API Notion
- Notion untuk menyimpan data yang ditampilkan di web
- Shacdn UI dan Aceternity UI untuk components
- Typescript untuk penulisan kode
Kenapa pakai React.js dan Next.js, karna init project ini awalnya pakai React, sudah jalan cukup banyak, pas butuh fetch API dari Notion ternyata bisanya di Next.js karna permasalahan CORS Policy. Sebenarnya lebih ke malas refactor ke Next sama mungkin kalau Next di vercel kali ya deploynya bukan di ghpages, jujur ilmuku dikit, blum paham-paham bgt kek ginian :)) jadi ya udah aku campur aja tampilannya pake react, fetch api nya pake next. Tp saran sih mungkin bisa langsung ke Next.js aja sekalian sih.. karna sebenernya semua library dan permasalahan yg aku temui larinya ke Next.js :) tp ak blum ada niat untuk refactor web ini ke sana
Setup Project React js dan Deployment gh-pages
Ikutin tutorial ini aja~
Setup Domain .me (khusus student developer pack)
Jadi Github Student Developer Pack punya previllage untuk bisa custom domain .me menggunakan namecheap, kamu bisa lihat tutorial ini aja
atau bisa langsung akses dari page Github Student Developer Pack, lalu cari namecheap
GitHub Student Developer Pack
The best developer tools, free for students. Get your GitHub Student Developer Pack now.
https://education.github.com/pack?sort=popularity&tag=Developer+tools
disitu kamu bisa dapat domain .me secara gratis tapi satu tahun aja
Lalu jangan lupa update domain di github pages nyaa dengan domain tadi

untuk artikel lebih lanjut bisa dari ini

How to link your Namecheap domain to GitHub Pages
🌐 Namecheap Sign in to your Namecheap account: Select Domain List from the left sidebar...
https://dev.to/fabriziobagala/how-to-link-your-namecheap-domain-to-github-pages-49a0
Setup Shadcn UI dan Aceternity UI
bisa langsung lihat docs nya aja, klo instalasi kedua ini memang disarankan pake Next langsung aja sih, karna kedua libary ini kiblatnya Next js, jd klo di pake di React bisa bisa aja, tp ada sedikit2 penyesuaian gitu nnti setiap mau make komponennya kayak <Link/> dsb.
Introduction
Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com/docs
Install Next.js
Install Next.js with Create Next App
https://ui.aceternity.com/docs/install-nextjs
Kalau udah setup tinggal make komponennya aja:
- shadcn tinggal install aja lewat terminal dan bisa langsung diimport
- aceternity perlu buat file dulu lalu copy code nya di components, baru bisa diimport
Desain Tampilan Web
Desain web nya dulu, baru kalau komponennya udah siap bisa ke Fetch API Notion Database, sama sekalian siapin kontainer untuk data yang mau difetch lewat API Notion
Kalau belum familiar dengan desain web pakai framework, bisa ikutin tutorial project2 langsung aja dari youtube banyakk bgt
🛠️ Fetch Notion API Database (Opsi 1)
Ini tutorial yang aku temuin setelah selesai setup project ini dan kelihatannya sih lebih gampang dari yang aku pake sekarang, kalian bisa pakai worker ini aja, karna mungkin kalau dari react ga perlu pakai setup Next js https://github.com/splitbee/notion-api-worker
Fetch Notion API Database (Opsi 2)
Nah kalau yang ini yang aku terapin sekarang karna nemu tutorial nya duluan yang ini 😅
Buat Notion Integration ke Next js, bisa ikuti tutorial ini ~
Kalau udah bisa jalan di localhost, tinggal di deploy ke vercel
Note
- Kalau full makai Next js, langsung implementasi frontendnya aja di sana, tinggal atur return api nya apa dan dipakai di pages mana.
- Kalau pakai React buat tampilan dan Next js buat fetch, di Next js nya cuma berurusan di folder API ajaa, nanti dari React fetch data ke vercel nyaa
Notion Page Renderer (seperti blog ini)
Nah kalau kalian pengen buat web yang ada Blog nya seperti ini, atau di portfolio kalian pengen ada detail Project nya atau dokumentasinya, bisa banget pakai ini, karna konsepnya ini ambil data pages dari notion untuk ditampilin di web, kalian bisa ikuti dokumentasinya di link github di bawah, di situ juga ada contoh json serta pengolahan fetch nya https://github.com/splitbee/react-notion
Deployment Next JS (kalau pake opsi 2 untuk fetch Notion)
Ikutin tutorial ini aja untuk deploy next.js ke vercel
Jangan lupa untuk simpan ENV kalian seperti secret dan database_id di setting vercelnya
Berikut contoh implementasi API yang aku gunain untuk blog ini
Bentuk Notion Databasenya
Handler API Next js
TypeScriptimport { Client } from "@notionhq/client";
import type { NextApiRequest, NextApiResponse } from "next";
import fetch from 'node-fetch';
// simpan di env kalian di setting vercel
const notionSecret = process.env.NOTION_SECRET;
const notionDatabaseId = process.env.NOTION_DATABASE_ID_BLOG;
const notion = new Client({ auth: notionSecret });
type Row = {
id: string;
properties: {
date: { id: string; date?: { start: string } };
Name: { title: { plain_text: string }[] };
Deskripsi: { rich_text?: { plain_text: string }[] };
img: { files?: { file?: { url: string } }[] };
};
};
type rowsBlog = {
id: string;
title: string;
deskripsi: string;
img_url: string;
date: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!notionSecret || !notionDatabaseId) {
throw new Error("Notion secret or database id not found");
}
try {
const response = await notion.databases.query({
database_id: notionDatabaseId,
});
const rows = response.results as unknown as Row[];
// untuk mapping ke properties Blog
const filteredData: rowsBlog[] = rows.map((row) => ({
id: row.id || "",
title: row.properties.Name.title[0].plain_text || "",
deskripsi: row.properties.Deskripsi.rich_text?.[0]?.plain_text || '',
img_url: row.properties.img.files?.[0]?.file?.url || '',
date: row.properties.date?.date?.start || "",
}));
// untuk mendapatkan konten page Blog nya
const detailedData = await Promise.all(filteredData.map(async (page) => {
const additionalData = await fetch(`https://notion-api.splitbee.io/v1/page/${page.id}`).then(res => res.json());
return {
...page,
properties: additionalData
};
}));
res.setHeader('Access-Control-Allow-Origin', '*');
return res.status(200).json(detailedData);
} catch (error) {
console.error("Error fetching data from Notion API:", error);
return res.status(500).json({ error: 'Failed to fetch data from Notion API' });
}
}
Handler di Page React js
TypeScriptinterface BlogPost {
title: string;
description: string;
date: string;
image: string;
onClick: () => void;
}
interface Blog {
// attribut blog
id: string;
title: string;
deskripsi: string;
img_url: string;
date: string;
// page blog nya
properties: BlockMapType;
}
function Blog() {
const [data, setData] = useState<Blog[]>([]);
const [isLoading, setIsLoading] = useState(true);
const location = useLocation();
const isBlogRoute = location.pathname === "/blog";
const navigate = useNavigate();
useEffect(() => {
const fetchData = async () => {
try {
// alamat API next js
const response = await axios.get(
"https://be-daf2a.vercel.app/api/notion-blog"
);
if (response.status !== 200) {
throw new Error("Notion API request failed");
}
const sortedData = sortByDate(response.data);
setData(sortedData);
} catch (error) {
console.error("Error fetching Notion data:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
// sorting date
const sortByDate = (data: Blog[]): Blog[] => {
return data.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
};
const formatDate = (dateString: string) => {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
};
return new Date(dateString).toLocaleDateString(undefined, options);
};untuk menampilkan halaman blog nya bisa langsung pakai ini, sesuai yang ada di tutorial Notion Page Renderer https://github.com/splitbee/react-notion
TypeScript<NotionRenderer blockMap={blog.properties as BlockMapType} />Tutorial Routing React.js di spa (single-page applications)
Tutorial videonya
Code nya
404.html
TypeScript<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Router</title>
<script type="text/javascript">
var pathSegmentsToKeep = 1;
var l = window.location;
l.replace(
l.protocol +
"//" +
l.hostname +
(l.port ? ":" + l.port : "") +
l.pathname
.split("/")
.slice(0, 1 + pathSegmentsToKeep)
.join("/") +
"/?/" +
l.pathname.slice(1).split("/").slice(pathSegmentsToKeep).join("/").replace(/&/g, "~and~") +
(l.search ? "&" + l.search.slice(1).replace(/&/g, "~and~") : "") +
l.hash
);
</script>
</head>
<body></body>
</html>script di head index.html
TypeScript<script type="text/javascript">
(function (l) {
if (l.search[1] === "/") {
var decoded = l.search
.slice(1)
.split("&")
.map(function (s) {
return s.replace(/~and~/g, "&");
})
.join("?");
window.history.replaceState(null, null, l.pathname.slice(0, -1) + decoded + l.hash);
}
})(window.location);
</script>Untuk referensinya bisa pakai dokumentasi github ini
Note: untuk yang menggunakan custom domain seperti .me jangan lupa mengganti var pathSegmentsToKeep = 1; di 404.html menjadi 0