Membuat blog sederhana dengan Next.Js dan Sanity.io

6 Jan 2021 ยท 17 menit baca

Link berhasil di copy

Mengawali tahun 2021 dengan membuat sebuah blog pribadi adalah hal yang bijak. Memiliki blog pribadi akan membuat kita lebih produktif dengan banyak menulis. Kontenya bisa pengalaman pribadi atau pekerjaan sehari-hari. Tak perlu ragu untuk memulai, apalagi malu. Menulislah untuk dirimu sendiri, kalau kebetulan ada orang lain membacanya, ya tidak masalah, siapa tahu dia nemu ide ๐Ÿ˜„

Kali ini Saya akan membuat tutorial untuk membuat sebuah blog sederhana, menggunakan Next.Js dan Sanity.io. Untuk styling memakai TailwindCSS. Kemudian, blog ini nanti tidak akan bikin pusing dengan bayar, semua gratis. Blog nantinya akan di deploy di vercel, dan studio (content editor sanity) akan di host di servernya Sanity.io sendiri, gratis, tinggal fokus menulis saja (kecuali mau nambah membeli domain pribadi).

Untuk dapat mengikuti tutorial ini, paling tidak kamu bisa menggunakan ReactJs dan mengerti CSS. Berikut adalah repository yang kusiapkan agar lebih mudah, silahkan fork lalu clone, karena untuk deploying ke vercel nanti perlu repository sendiri. Gunakan branch practice jika ingin mengikuti tutorial dari awal, sedang branch main adalah production branch yang sudah selesai dan di deploy disini. Ok, kita mulai!

Teknologi

  • NextJs adalah pengembangan dari React untuk membuat website hybrid yang support server side rendering dan static site generation (SSG). Untuk membuat blog kali ini kita akan memanfaatkan fitur SSG dari NextJs sehingga hasil build blog nanti adalah file HTML biasa.
  • Sanity.io adalah platform untuk mengelola konten (seperti CMS) yang memiliki editor open source yang bisa di kustomisasi dengan ReactJs. Project Sanity sendiri bisa di host di cloud ataupun di server miliknya sendiri, dengan gratis, dan quota yang nggak habis-habis.
  • TailwindCSS adalah framework CSS baru dengan paradigma utility first, menyediakan banyak class yang bisa digunakan untuk pengembangan aplikasi dengan cepat

Kick Off

Untuk mengawali, setelah clone repository blog diatas, install semua dependensi projek.

$ git clone https://github.com/muhsalaa/simple-next-blog
$ cd simple-next-blog && yarn

Setelah semua dependensi terinstall, kamu bisa menjalankan development server dengan perintah yarn dev, atau melakukan production build dan menjalankanya di local server dengan perintah yarn build && yarn start. Tentu keduanya berjalan dengan data dummy yang sudah ada sebelumnya. Target kita di tutorial ini agar blog dapat berjalan dengan data real yang tersimpan di server dengan bantuan Sanity.io.

Lanjut, kita akan menginstall Sanity CLI untuk mulai membuat studio untuk blog ini. Jalankan perintah berikut diterminal.

$ npm install -g @sanity/cli && sanity init

Setelahnya akan ada beberapa prompt untuk login (bisa memakai google atau github) dan konfigurasi projek. Pilih konfigurasi default dan template projek blog. Ketika selesai akan seperti gambar berikut.

Prompt

Kemudian kita coba menjalankan studio kita dengan perintah sanity start pada root projek yang baru saja dibuat. Lalu kita dapat membuka studio kita di http://localhost:3333. Kamu akan diminta untuk autentikasi lagi, gunakanlah akun yang kamu gunakan untuk login diawal.

Apabila login berhasil, kamu akan masuk ke studio milikmu yang dapat kamu custom sesuai kebutuhan.

Studio

Setup Sanity

Kini kita akan melakukan setup Sanity agar dapat digunakan mengisi artikel untuk blog. Jika kamu membuka file dummy.js pada folder projek blog, struktur data yang kita butuhkan adalah seperti ini.

[
  {
    _id: "5ff18dd9bb19da8432468a31",
    title: "incididunt laboris eu anim dolore nisi",
    thumbnail: "https://picsum.photos/500",
    slug: "the-slug",
    teaser: "Culpa magna cillum exercitation non commodo sit. Labore ex ...",
    tags: ["tech", "programming"],
    author: "Herrera Grimes",
    mtr: 10,
    content: "some long text ...",
    created_at: "Sat Jan 13 2018 13:13:18 GMT+0700 (Western Indonesia Time)",
  },
  // ...
];

Selanjutnya kita akan mengedit schema yang ada pada projek Sanity. Template blog yang kita pilih saat konfigurasi projek akan memberikan schema seperti gambar dibawah (abaikan folder atau file lain, fokus kita hanya pada folder schemas)

Project structure

Ada 5 schema pada folder schemas. Masing-masingnya memiliki peran sebagai berikut:

  • author berisi konfigurasi untuk form pengisian data pengarang artikel
  • blockContent berisi konfigurasi untuk field rich text yang digunakan di konfigurasi artikel
  • category adalah konfigurasi untuk kategori/tags untuk artikel
  • post adalah konfigurasi untuk form pengisian artikel
  • schema adalah konfigurasi utama yang akan merangkai semua konfigurasi sebelumnya menjadi satu skema utuh yang akan ditampilkan di studio.

Kita awali dengan menyesuaikan konfigurasi untuk author, rubahlah file menjadi seperti kode berikut.

// author.js

export default {
  name: "author",
  title: "Author",
  type: "document",
  fields: [
    {
      name: "name",
      title: "Name",
      type: "string",
    },
  ],
  preview: {
    select: {
      title: "name",
    },
  },
};

Dibandingkan konfigurasi awal, kita menghapus properti slug, image, dan bio dari fields , juga properti media dari preview. Jika melihat komponen ArticleCard di blog, memang tidak memerlukan slug, foto pengarang ataupun bio. Sedangkan properti media pada preview berfungsi untuk memberi gambar pada daftar pengarang di studio, yang perlu dihapus juga karena fields image juga sudah dihapus. Nanti hanya akan ada field name di studio.

Post Author

Selanjutnya kita merubah konfigurasi category dan post

// category.js

export default {
  name: "category",
  title: "Category",
  type: "document",
  fields: [
    {
      name: "title",
      title: "Title",
      type: "string",
    },
  ],
};
// post.js

export default {
  name: "post",
  title: "Post",
  type: "document",
  fields: [
    {
      name: "title",
      title: "Title",
      type: "string",
    },
    {
      name: "slug",
      title: "Slug",
      type: "slug",
      options: {
        source: "title",
        maxLength: 96,
      },
    },
    {
      name: "teaser",
      title: "Teaser",
      type: "text",
    },
    {
      name: "author",
      title: "Author",
      type: "reference",
      to: { type: "author" },
    },
    {
      name: "mtr",
      title: "Minutes to Read",
      type: "number",
    },
    {
      name: "thumbnail",
      title: "Thumbnail",
      type: "image",
      options: {
        hotspot: true,
      },
    },
    {
      name: "tags",
      title: "Tags",
      type: "array",
      of: [{ type: "reference", to: { type: "category" } }],
    },
    {
      name: "created_at",
      title: "Created at",
      type: "datetime",
    },
    {
      name: "content",
      title: "Content",
      type: "blockContent",
    },
  ],
  preview: {
    select: {
      title: "title",
      author: "author.name",
      media: "thumbnail",
    },
    prepare(selection) {
      const { author } = selection;
      return Object.assign({}, selection, {
        subtitle: author && `by ${author}`,
      });
    },
  },
};

Setelah semua selesai, simpan perubahan yang kamu lakukan, dan lihat apakah tampilan di local studio sudah berubah. Jika belum lakukan refresh manual karena kadang perubahan agak telat.

Sebelum dapat benar-benar menambahkan artikel, kita perlu men-deploy studio kita ke hosting yang diinginkan. Pada dasarnya Sanity adalah Single Page Application yang dibuat dengan React. Jadi kita bisa deploy ke Netlify, Vercel, Digital Ocean, dan sebagainya. Namun, Sanity memberikan solusi mudah dan gratis, yang akan kita pilih, untuk melakukan deploy ke hosting mereka dengan satu baris perintah di root projek studio mu.

$ sanity deploy

Sanity CLI akan melakukan proses building dan deploying studio kita. Nanti ada prompt untuk memasukkan nama studio-hostname. Masukkan saja nama yang kamu sukai. Setelah sukses, kamu akan mendapat link studio yang sudah siap digunakan.

Deploying

Kini kita sudah bisa menambahkan artikel untuk nantinya di tampilkan di blog kita. Mulailah dengan mengisi category dan author karena kedua data tersebut akan digunakan saat membuat post. Untuk pengisian artikel pada form post, kukira cukup mudah, kamu bisa isi sesuai kehendakmu untuk nanti kita coba fetch dari blog.

Integrasi dengan blog

Baik, studio kita sudah jadi dan sudah di deploy. Saya juga sudah menambahkan beberapa author dan category, dan membuat dua buah artikel contoh. Sekarang saatnya kita mengintegrasikan data yang sudah kita buat di studio dengan blog.

Pertama kita install dulu beberapa dependensi yang dibutuhkan. @sanity/client untuk fetching data, dan @sanity/block-content-to-react untuk parsing rich text konten blog kita ke komponen React.

$ yarn add @sanity/client @sanity/block-content-to-react

Setelah instalasi selesai, buatlah file config.js berisi konfigurasi projek dan dataset agar kita dapat melakukan fetching ke data store Sanity. Lalu file .env.local untuk menyimpan data-data sensitif, seperti projectId, yang bisa kamu dapatkan di dashboard akun Sanity-mu.

# .env.local

PROJECT_ID=sebuah_project_id
DATASET=sebuah_data_set
// config.js

const sanityClient = require("@sanity/client");

export const client = sanityClient({
  projectId: process.env.PROJECT_ID,
  dataset: process.env.DATASET,
  token: "",
  useCdn: false,
});

Konfigurasi sudah selesai. Sekarang kita tinggal membuat query untuk mengambil data dari data store Sanity. Proses ini mirip seperti saat kita melakukan query ke database MySQL atau MongoDB, hanya bahasa query nya saja yang berbeda.

Pada dashboard projek Sanity.io, kamu dapat melihat berbagai informasi seperti plan dan sisa kuota penggunaan gratis bulananmu (yang berdasarkan pengalamanku belum pernah habis). Lalu ada juga berbagai menu untuk mengatur dataset yang dipakai, menambah member projek (ya, kita bisa berkolaborasi untuk membuat konten dengan maksimal 3 member untuk plan gratis), dan mengatur autentikasi untuk akses data.

Sanity menggunakan Graph-Relational Object Queries (GROQ), bahasa query buatan Sanity, yang menurutku cukup mudah untuk dipahami. Kamu bisa melihat dasar penggunaanya disini.

Blog yang kita buat memiliki dua halaman yang akan menggunakan data, yaitu / atau home yang berisi daftar artikel, dan /articles/[slug] berisi detail artikel. Semua halaman dapat dilihat di folder pages pada projek blog. NextJS melakukan routing berdasarkan file yang ada di folder tersebut.

Untuk halaman home, kita memerlukan data yang diperlukan untuk di map pada komponen ArticleCard. Tambahkan variabel query dengan value objek berisi query.

// config.js

// ...

export const query = {
  home: `*[_type=="post"]|order(created_at desc)
    {
      _id,
      title,
      teaser,
      created_at,
      "slug": slug.current,
      mtr,
      "author": author->name,
      "thumbnail": thumbnail.asset->url,
      "tags": tags[]->title
    }`,
};

Query diatas cukup mudah dibaca apabila sering bersinggungan dengan database query. Singkatnya, kita mengambil data dari dataset post, diurutkan dari yang tanggal pembuatanya paling baru. Kemudian data spesifik yang diambil ada didalam curly bracket.

Beberapa properti mungkin kurang intuitif, seperti slug, author, thumbnail dan tags. Untuk slug, data yang tersimpan di store berbentuk objek, dengan properti current yang menyimpan string slug sebenarnya. Author dan tags menggunakan tanda panah karena tipe data mereka merupakan referensi ke dataset lain. thumbnail yang merupakan gambar, memiliki bentuk data gabungan tiga data sebelumnya. Lebih jelasnya kamu bisa membaca dokumentasi tentang GROQ.

Next, edit halaman pages/index.js untuk integrasi dengan data store kita, menggunakan modul @sanity/client yang sebelumnya di install.

import { ArticleCard, Layout } from "../components";
import { client, query } from "../config";

export async function getStaticProps() {
  const post = await client.fetch(query.home);

  return {
    props: { data: post },
    revalidate: 1,
  };
}

export default function Home({ data }) {
  return (
    <Layout>
      <div className="grid md:grid-cols-2 xl:grid-cols-3 gap-6">
        {data.map((article) => (
          <ArticleCard key={article._id} post={article} />
        ))}
      </div>
    </Layout>
  );
}

Kita akan bahas perbaris. Pada line ke-2 Saya mengimport client dan query dari config yang sudah dibuat.

Line ke-4 sampai 11, Saya membuat fungsi getStaticProps yang merupakan requirement Next.js untuk melakukan static generation halaman. Fungsi ini, jika di export dari suatu halaman, maka halaman itu akan di pre-render saat build time dengan data yang dikembalikan olehnya.

Didalam fungsi diatas Saya melakukan fetching ke data store, dan return value dari fungsi ini adalah sebuah objek dengan properti props berisi data hasil fetching, dan revalidate yang merupakan konfigurasi berapa waktu dalam detik yang diinginkan sebelum halaman melakukan revalidasi data.

Line ke-17 sampai 19 Saya melakukan map pada props data yang dikembalikan dari fungsi getStaticProps ke komponen ArticleCard, yaitu kartu berisi detail artikel yang kita lihat di halaman utama blog.

Static generation berarti halaman di build diawal menjadi file static html, css dan javascript untuk setiap halamanya. Berbeda dengan React, yang hasil buildnya hanya sebuah file index.html, yang nantinya ketika dibuka di browser akan menjalankan script dan mengisi konten halaman.

Lalu bagaimana halaman akan diupdate jika di database terjadi perubahan data, sedangkan file kita static? Next.Js memberikan konfigurasi revalidate pada return value dari fungsi getStaticProps, yang akan melakukan page regeneration setelah jangka waktu yang ditentukan. Jadi misal kita melakukan edit pada artikel kita, saat membuka halaman yang memuat artikel tersebut, awalnya takkan terjadi perubahan. Tunggu beberapa detik lalu refresh, maka halaman sudah mendapatkan perubahanya.

Selanjutnya kita edit komponen ArticleCard agar ketika link nya di klik akan menuju halaman sesuai dengan slug artikel, ganti href jadi seperti dibawah.

  // components/ArticleCard/index.js

  // ...
  <Link href={`/articles/${slug}`}>
    <button className="flex items-center text-xs text-gray-50 bg-gray-700 py-1 px-2 rounded-full ml-auto focus:outline-none">
      Read article
      <FiArrowRight className="ml-1" />
    </button>
  </Link>
  // ..
}

Sekarang kita beralih ke halaman detail artikel yang akan menampilkan konten secara utuh. Kita buat dulu query-nya di file config.js.

// config.js

// ...

export const query = {
  // ...

  articleSlug: `*[_type=="post"]
    {
      "slug": slug.current,
    }`,
  articleDetail: `*[_type=="post" && slug.current == $slug]
    {
      title,
      content[]{
        ...,
        asset-> {
          ...,
        }
      },
      created_at,
      "thumbnail": thumbnail.asset->url,
      mtr,
      "author": author->name,
      "tags": tags[]->title
    }`,
};

Ada sedikit yang berbeda dari query diatas, pada articleDetail, didalam square bracket selain menentukan dataset post, Saya juga membuat filter untuk hanya mengambil data artikel yang slug-nya sesuai dengan parameter saat di fetch nanti. Selanjutnya kita edit file pages/articles/[slug].js agar dapat menampilakn detail artikel.

import Image from "next/image";
import { FiCalendar, FiClock, FiLoader } from "react-icons/fi";
import { useRouter } from "next/router";
import BlockContent from "@sanity/block-content-to-react";

import { client, query } from "../../config";
import { Layout } from "../../components";
import { shortDateFormat } from "../../helpers/date";

export async function getStaticPaths() {
  const post = await client.fetch(query.articleSlug);

  const paths = post.map(({ slug }) => ({
    params: { slug },
  }));

  return {
    paths,
    fallback: true,
  };
}

export async function getStaticProps({ params }) {
  const article = await client.fetch(query.articleDetail, {
    slug: params.slug,
  });

  return {
    props: {
      article: article[0],
    },
    revalidate: 1,
  };
}

export default function About({ article }) {
  const router = useRouter();

  if (router.isFallback) {
    return (
      <Layout>
        <div className="w-full h-96 flex items-center justify-center">
          <FiLoader className="text-5xl text-black animate-spin" />
        </div>
      </Layout>
    );
  }

  return (
    <Layout>
      <h1 className="font-bold text-xl md:text-2xl">{article.title}</h1>
      <div className="flex items-center tracking-tighter mb-2 text-sm md:text-lg text-gray-700">
        <p className="underline mr-3">{article.author}</p>
        <div className="flex items-center mr-3">
          <FiClock className="mr-1.5" /> {article.mtr} min
        </div>
        <div className="flex items-center">
          <FiCalendar className="mr-1.5" />{" "}
          {shortDateFormat(article.created_at)}
        </div>
      </div>
      <div className="prose md:prose-lg prose-blue max-w-none font-body">
        <div className="my-2 md:my-4">
          <Image
            src={article.thumbnail}
            width="640"
            height="320"
            alt="blog-photos"
            layout="responsive"
          />
        </div>
        <BlockContent blocks={article.content} />
      </div>
    </Layout>
  );
}

Baik, kita bahas per baris lagi. Line 3-4 Saya mengimport satu modul dan komponen, useRouter untuk mengecek apakah route yang dituju sudah di generate atau belum, agar kalau belum bisa menampilkan loader. Sedangkan komponen BlockContent berguna untuk merender konten agar bisa ditampilkan.

Line 10-21 Saya membuat fungsi getStaticPaths yang jika ada di halaman yang dinamis, akan pre-render semua halaman yang ada. Didalamnya Saya melakukan fetch untuk mendapatkan semua slug artikel yang kupunya, kemudian mapping datanya menjadi sebuah array yang berisi path dari semua artikel. Properti fallback yang di set true akan membuat path baru yang dimasukkan tidak mengarah ke halaman 404, tapi ke halaman fallback.

Misalkan hasil generation di awal ada dua slug, /artikel-satu dan /artikel-dua, maka jika kita membuka /artikel-tiga yang belum ada, kita akan diarahkan ke fallback page, yang kalau dilihat dari kode di atas adalah icon loader. Saat berada di fallback page, Next akan melakukan fetch untuk slug itu. Jika ditemukan, maka halaman asli akan muncul, dan setelahnya tidak akan masuk halaman fallback lagi.

Terkahir, setelah semua data yang kita dapat dari data store ditaruh di tiap bagian komponen yang sesuai, buat file next.config.js di root projek. Kita perlu menuliskan base url CDN gambar untuk optimasi menggunakan komponen next/image.

module.exports = {
  images: {
    domains: ["cdn.sanity.io"],
  },
};

Selesai sudah, blog kita sudah jadi dan siap di deploy ๐Ÿ˜„

Deploying blog ke Vercel

Untuk deploying blog ke Vercel caranya cukup mudah. Pertama daftarkan akunmu (Gunakan github agar tidak perlu menghubungkan ulang dengan repository). Selanjutnya import projek yang ingin kamu deploy dengan menekan tombol New Project di dashboard akun.

Vercel import

Setelah projek yang akan di deploy dipilih, lanjut dengan memilih tipe akun yang akan digunakan. Gunakan yang personal. Vercel juga menyediakan akun team/pro berbayar, yang memiliki lebih banyak keuntungan.

Vercel scope

Selanjutnya, isikan environment variable yang dibutuhkan. Masukkan data yang sebelumnya kamu taruh di file .env.local disini, dan klik deploy.

Vercel environment variable

Nanti kamu akan melihat proses building blog kita yang berjalan di vercel, dan jika semua proses berhasil, kamu akan melihat alert berisi gambar blog mu yang sudah berhasil di deploy. Kamu bisa mengunjunginya atau menuju ke dashboard aplikasi.

Deploy success

Kukira itu saja yang bisa ku bagi pada kesempatan kali ini. Semoga bermanfaat dan membuat kamu mau menulis dan berbagi apa saja. Kalau kamu memiliki pertanyaan, bisa kontak Saya di Twitter. Terimakasih.

Emot's Space ยฉ 2024