Hello Remix

Ever since I saw a demo of Remix back in 2020 I was hooked. The DX - developer experience - looked amazing while also meeting the UX - user experience - I've been yearning for. I purchased a license then and tinkered around with it, but never got around to actually setting up a site. That changes today - Feb 13, 2022 - as I sit down and create my personal site.

Codebase

Initializing a remix codebase is as simple as calling an npx command and then going straight to work.

npx create-remix@latest

With the skeleton setup I added the following additional files (as of 2022-02-13 highlights don't work so imagine lines 3-5 & 7 are highlighted). blog/index.tsx is the /blog route which shows a list of all posts. blog/$slug.tsx is a parameterized route - /blog/:slug - that renders individual blog posts. blog.util.ts contains helpers for loading blog posts which will be covered further down.

app/
├── routes
│ ├── blog
│ │ ├── $slug.tsx
│ │ └── index.tsx
│ └── index.tsx
├── blog.util.ts
├── entry.client.tsx
├── entry.server.tsx
└── root.tsx

Loading Blog Posts

To keep things simple, I elected to write blog posts in Markdown. All posts are store in the blog folder; a sibling (not a child) of the app folder. The filename indicates the slug for the post.

blog/
├── hello-remix.md
└── hello-world.md

To load these I use the fs/promises library. Knowing the location of the blog folder containing our .md posts, we can iterate over all the files in the directory. Post is simply a TypeScript type to represent a post's metadata

export type Post = {
  slug: string;
  title: string;
  description: string;
  written: Date;

  tags?: string[];
};

const blogPostsPath = path.join(__dirname, "..", "..", "blog");

export async function listPosts(): Promise<Post[]> {
  let dir = await fs.readdir(blogPostsPath);
  let posts = await Promise.all(
    dir.map(async filename => {
      let file = await fs.readFile(path.join(blogPostsPath, filename));
      return {
        slug: filename.replace(/\.md$/, "")
      };
    })
  );
}

The more astute readers will notice that the return in the above doesn't "fulfill" the Post type. You might also be wondering, how do we get metadata about a post from a Markdown file?

Front-Matter

In order to include metadata in our Markdown files I use Front-Matter. Front-Matter is a block of YAML that lives at the top of a file. The front-matter library helps separate Front-Matter from the main body of a file. In the below example --- is used to define the Front-Matter boundary.

---
title: Hello Remix
description: Learning to setup a personal site using Remix
written: 2022-02-13 00:00:01

tags:
  - meta
  - remix
---

# Hello Remix

...

Parsing Front-Matter is trivial with the use of the front-matter library.

import parseFrontMatter from "front-matter";

//...

export async function listPosts(): Promise<Post[]> {
  let dir = await fs.readdir(blogPostsPath);
  let posts = await Promise.all(
    dir.map(async filename => {
      let file = await fs.readFile(path.join(blogPostsPath, filename));
      let { attributes } = parseFrontMatter(file.toString());

      invariant(
        isValidPostFrontmatter(attributes),
        `${filename} has bad meta data!`
      );

      return {
        ...attributes,
        slug: filename.replace(/\.md$/, "")
      };
    })
  );

  return posts;
}

I use the spread operator (...) on the parsed Front-Matter to add it to the return type. For TypeScript users, know that isValidPostFrontmatter (a custom function) applies type assertion to attributes.

Sorting

Sorting by date is pretty easy since we exposed the written attribute. We simply need to compare the (numeric) values of the written attributes. The + symbol in +a.written tells JS to convert the date to a number.

posts.sort((a, b) => +a.written - +b.written);
return posts;

If I ever write multiple articles on the same day, I can add a time to the written attribute in my Front-Matter to ensure correct order is maintained. For example in this post - written the same day as Hello World - I use'd the following

written: 2022-02-13 00:00:01

Footnote

I didn't spend much time reviewing/editing this article. Beginners to Remix should check out the Blog Tutorial from which much of this code is from!