May 21st, 2024

Rebuilding my website using Remix, Prisma, and Markdown

Post hero for Rebuilding my website using Remix, Prisma, and Markdown

Introduction

Approaching the end of 2023 I decided I needed to refresh my website a bit. Throughout 2023 I used new tools and technologies that I really grew to appreciate. Looking at how my personal website was built at the time, I took a step back and asked myself, "How can I make this better to maintain, leaner, and more of my own?"

After posting that question to myself, I came up with the following requirements:

  • Content must be written in Markdown.
  • It has to be built with Remix.
  • No more out-of-the-box content management systems such as WordPress.
  • Flexible enough to support different types of content (writeups, blog posts, external links).
  • Version control for content.
  • Be as lightweight as possible.
  • Easy to deploy.

With that, I came up with the following tech stack:

Tech stack explanation

Remix

I started using Remix around May of 2023, about half a year after Remix joined Shopify. At first, my use of Remix was just explorative and I wanted to see how it differed from Next, which I was previously using for my website.

From a very high level view, they were not all that different as Next had introduced Layouts which are similar to Remix layouts.

But, when you start looking at the way that your routes (pages) load their data and submit forms you'll begin to realize how different the two are. Next uses api routes (/pages/api/your_api_name.ts) and Remix makes use of actions and loaders which are defined either within your route or within a resource route.

One of the biggest benefits I've found in using Remix is that their implementation of actions and loaders heavily relies on the Web API standards for Request and Response which I find much easier to follow along than any additional complexities or "features" that are baked into the API.

Prisma

I was looking for a lightweight ORM and I had used Prisma before, so naturally it was just the one I reached for first. There had been some issues that I had experienced while using Prisma previously (Support for a Union type). But, given the way that my content was going to be structured, I knew that the issues would not be a limiting factor for my personal website.

Knowing that Prisma had extensive documentation on their website and having some familiarity with it previously, I felt confident that this was a good decision to make for the ORM layer of the new build. However, if I were to approach the situation again in the future I might consider an alternative such as Drizzle ORM. Though, I'll add that I do really like the way the schema is constructed for Prisma compared to the schema for Drizzle.

Tailwind CSS

I find Tailwind to be a bit of a controversial topic in most codebases. I'll start by saying you either love it or you haven't tried it. If you're not familiar with Tailwind, I'd recommend reading the CSS section in my guide to a clean and scalable project post.

Given how much I really enjoy the flow of using Tailwind, the performance that comes as a result of minified + atomic styles, and the overall workflow, it's the styling library I chose for my project.

Object Schema Validation

In this project, I'm using yup. I was previously using zod because it had been highly praised across the community.

However, I ran into a technical "limitation" with the package which limited my ability to do conditional validation. While it's not a hard limitation, it's a limitation which I couldn't accept for my project. Before I show the example of what I was running into, I'll add that it is technically possible with zod, but the solution is not clean nor scalable.

Discussion for reference

Let's imagine a scenario where you have a data model like the following (this is the data model for posts on this website).

index.d.ts
export type Content = {
id: string
slug: string | null
externalSlug: string | null
title: string
description: string | null
imageUrl: string | null
altImageUrl: string | null
authorId: string
status: Status
type: Type
featured: boolean | null
/**
* [MarkdownSource]
*/
source: MarkdownJson.MarkdownSource
createdAt: Date
updatedAt: Date
}

The two fields here which are important are externalSlug and slug. The way I have my website set up allows me to share content that points to other websites. This is useful in the event that I post something about coffee gear or a link to another website where I have content and wanted to link viewers directly to that content.

However, if I have an externalSlug defined, I do not want to have a slug defined. Additionally, I want to ensure that when I create a new post that either an externalSlug or a slug have been provided in the form.

With zod I was limited in doing this validation as it required multiple different schemas. With yup, I was able to solve it doing the following:

content.ts
export const CONTENT_SCHEMA = object().shape(
{
...otherSchemaFields,
externalSlug: string().when('slug', {
is: (slug?: string | undefined) => !slug || slug.length === 0,
then: () => string().trim().required('Please provide a value for either slug or external slug.'),
otherwise: () => string().trim().max(0).optional(),
}),
slug: string()
.optional()
.when('externalSlug', {
is: (externalSlug?: string | undefined) =>
!externalSlug || externalSlug.length === 0,
then: () =>
string()
.trim()
.required(
'Please provide a value for either slug or external slug.',
),
otherwise: () => string().trim().max(0).nullable(),
}),
},
[['externalSlug', 'slug']],
);

With zod this implementation became much more complex as it required having multiple schemas, merging them, and then still requiring the use of superRefine which made the object validation a bit of a rats nest. While the solution for yup wasn't the ideal situation, it was still a path that worked and met the requirements.

Icon Sprites

© Quinton Chester. All rights reserved.