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!