Tech Stack Review: Zakiego-v4

Tech Stack Review: Zakiego-v4

M. Zakyuddin Munziri

M. Zakyuddin Munziri

@zakiego

Originally written in Bahasa Indonesia.

Introduction

This site is my 4th personal website (https://v4.zakiego.com). Through this article, I will briefly review the technologies used to build this website.

  1. Tailwind UI
  2. Keystatic
  3. Zod
  4. tRPC

Discussion

Tailwind UI

For Tailwind UI users, this site should look familiar, because indeed, this site was built using a template from Tailwind UI. The template name is Spotlight (https://tailwindui.com/templates/spotlight). For this reason, this project's repository will remain private.

Side note, if it's someone else's work, mention who the original creator is. Credibility is about self-respect.

Although using a template, there were many things I needed to customize, which took about two weeks.

Why use a template?

The main reason is lack of time, as daily time is already drained by work. Of course, if I had to build from scratch, I could only use weekends.

Unfortunately, I've dedicated weekends for rest and exploring new things. In the end, a template was the solution. I wanted to focus more on business logic, while leaving the appearance to Tailwind UI. 😉

Some might ask, is Tailwind UI worth it? How much does it cost and how do you buy it? I'll discuss that another time. Please remind me. 😜

Keystatic

This library is almost certainly still unfamiliar, because it's still very young. I decided to use Keystatic as a CMS because:

  1. I didn't want to manage a database. Since this is just a personal and simple project, the monthly database cost feels quite heavy. If using a free database like Supabase, I feel my Supabase is already messy, let that be my experimentation lab. 🤦🏻
  2. I didn't want to write in raw markdown. I think it's too tedious just to edit one letter or add a property, having to manually edit markdown.

Based on these 2 reasons, Keystatic solves them by:

  • All files are stored in the same repository, no database needed. 

As you can see, one content can contain two files: json for properties, and mdoc for the content body (--usually in md format, but for some reason Keystatic uses mdoc).

// index.json
{
  "title": "2021 Year in Review",
  "draft": false,
  "image": "/images/articles/2021/image.webp",
  "category": "contemplation",
  "date": "2021-12-30",
  "summary": "One word that best describes this year and the word that keeps echoing in my head. In this article, I will try to summarize what I learned throughout the year."
}
# content.mdoc

**Growing.**

One word that best describes this year and the word that keeps echoing in my head. In this article, I will try to summarize what I learned throughout the year. The arrangement is not based on chronological order, but starting from what I consider most important, then onwards.
  • As mentioned before, essentially it's the same, we'll be writing in markdown format, only the writing process is assisted by an editor, not in raw markdown form.

Besides these two things, we can also edit content in production after deployment. Later, the edits will be pushed to our GitHub repository (– this process is handled by Keystatic). Of course, it takes time to rebuild.

Nothing is perfect, Keystatic also has its drawbacks. So far, all content handled by Keystatic can only be fetched during the build process. This means the web will be fully static. Or if using Next.js, you can only use getStaticProps, without revalidate, and certainly not getServerSideProps.

But for me, that trade-off is not a problem, because a blog doesn't have to update that quickly. Build time only takes about one minute.

tRPC and Zod

It could be said that using tRPC on this website is actually over-engineering. But, that's fine, because personal projects are meant for exploring many things. 😋 

Without tRPC, the function to read data from Keystatic could be made simple, requiring only one key function:

const keystaticReader = createReader(process.cwd(), keystaticConfig);

The keystaticReader function can then be used directly.

But, considering the many data restructurings, I chose to use tRPC so all that processing is done in one place. For example, here are two routers below.

const keystaticRouter = createTRPCRouter({
  profile: publicProcedure.query(async () => {
    const data = await keystaticReader.singletons.profile.read();

    const parsed = keystaticValidation.profile.parse(data);

    return parsed;
  }),

  highlightArticles: publicProcedure.query(async () => {
    const { articles } =
      await keystaticReader.singletons.highlightArticles.readOrThrow();

    const getArticle = articles.map(async (slug) => {
      const data = await keystaticReader.collections.articles.readOrThrow(
        slug as string,
      );

      const content = await data.content();

      const render = {
        ...data,
        slug,
        content,
      };

      const parsed = keystaticValidation.articles.parse(render);

      return parsed;
    });
  }),
});

The route from keystaticRouter.profile is still simple, because it only contains reading and validation processes using Zod. By the way, here is the validation schema using Zod on keystaticValidation.profile:

profile: z.object({
  avatar: z.string(),
  name: z.string(),
  headline: z.string(),
  biography: z.string(),
  images: z.array(
    z.object({
      title: z.string(),
      image: z.string(),
    }),
  ),
}),

Moving on, keystaticRouter.highlightArticles is more complex, because it has two functions: getting the list of highlighted articles and getting the articles themselves. After getting the articles, there's a process of extracting content, restructuring data, and finally validation.

Actually, Keystatic itself already has validation features, but I feel Keystatic's validation is not strict enough, so I decided to re-validate with Zod. This validation is done so that when there's missing data, for example an unfilled date, I can immediately get an error warning.

That validation schema is placed in one file as a single source of truth.

Conclusion

To close, please allow me to quote this tweet from @azamuddin91 🤣.

More Articles

I Stopped Digging Through Logs

I Stopped Digging Through Logs

Debugging changed when I stopped reading logs manually and started using AI agents to correlate errors across observability data - faster root cause, fewer dead ends.

Speed Was Never the Hard Part in CI CD

Speed Was Never the Hard Part in CI CD

Fast pipelines don't eliminate shipping fear. Confidence comes from safe rollbacks, feature flags, and systems that behave predictably when things go wrong.