Black Sheep Code

In the previous post I outlined how to make use of mdx-bundler to access your frontmatter metadata. This approach requires file system access as the files are parsed at run time.

In this post, we'll we'll talk about how to achieve the same effect for use in a serverless platform, such a Netlify or AWS Lambda.

The solution involves a build time step that extracts the frontmatter to JSON files. These JSON files can then be read at runtime.

This solution is for Remix V1

This solution is for Remix V1. The reason is because V2 made changes to Remix's routing and got rid of nested routing.

Where in V1 we can organise our routing like:

/app 
   /routes 
       /posts
          blogpost1.mdx
          blogpost2.mdx
      /drafts
          draftpost1.mdx
       posts.tsx 
       some_other_page.tsx

Remix V2 got rid of the ability to use file/folder nesting to determine the URL, the equivalent solution would look like:

/app 
   /routes 

      posts.blogpost1.mdx
      posts.blogpost2.mdx
      drafts.draftpost1.mdx
      posts.tsx 
      some_other_page.tsx

For potentially hundreds of files this would get quite messy.

(However, I concede that this is potentially something I could live with).

Also note that I don't want to do a 'store the blog posts in a database' solution. This is something that is recommended in Remix's documentation but I don't agree with the reasoning. In my opinion two of the major advantages of using a Github-hosted-MDX-files solution is that you provide a convenient way of allow viewers to submit edit requests, and it provides a handy version history.

This solution uses barrel files

A barrel file is an index file that looks like this:

export {default as adding_msw_bundler_to_remix_app_2} from './adding_msw_bundler_to_remix_app_2.json';
export {default as adding_msw_bundler_to_remix_app} from './adding_msw_bundler_to_remix_app.json';
//etc

The reason we need to use barrel files is we can't do dynamic imports with Remix (though the change to using Vite might have changed this).

Maybe Remix isn't the right tool for the job.

This is all feeling a bit hacky. When I started this blog it felt 50/50 the race between NextJS and Remix, whereas in 2024 it seems like NextJS is really pulling ahead. If I were to be starting this blog today, I'd be using NextJS or perhaps Astro.

That said, this is going pretty smoothly, for what it's worth.

Starting Point - We have frontmatter in our posts

For example, here is the frontmatter for this post:

Note that this frontmatter is entirely custom, we're going to need to write handlers for it later.

Starting point - We have multiple folders of blog posts

My file structure looks like this:

/app 
   /routes 
       /posts
          blogpost1.mdx
          blogpost2.mdx
      /drafts
          draftpost1.mdx
      /test 
          testpost1.mdx
       posts.tsx 
       drafts.tsx
       test.tsx
       some_other_page.tsx

This allows me to write draft posts and see how they look, but selectively only publish the posts posts.

Create a validation schema for our custom frontmatter

Here I'm using Zod to create a validation schema:

What I've done is I've said that title, description and date created are always going to exist. I can continue extending this schema in the future, and adding optional properties.

A build time tool to generate JSON and barrel files from our frontmatter

As an aside, this code was largely generated with assistance of ChatGPT, and then I tweaked it as I want.

Now looking at it, it looks kind of gross.

But right now, I don't care enough. The code is tested, and my philosophy is that 'good code is easy to delete', the interface is fine, we can rewrite this later if we need.

Essentially the process here is:

  1. Recursively traverse our routes structure, finding the .mdx files
  2. Use the front-matter package to parse the MDX files, extracting the front-matter.
  3. Validate that the frontmatter matches our Zod schema
  4. For each MDX file, create a json file in app/generated/frontmatter matching the mdx file path
  5. Once all JSON files have been created, create a index.js barrel file in each folder.

I have a separate file that runs this code:

And we have a script in our package.json to calls this:

Example Layout Route

Here's what my posts.tsx layout route is looking like:

This is what gives a consistent look feel across all the posts.

The key things we need to implement are:

createLoaderFunction

This function creates our Remix loader function.

The logic here is a little hardcoded - it won't work for deeper levels of nesting, but essentially we use the location URL to determine the folder and filename to retrieve the metadata json from the correct barrel file.

Note for the series metadata, I also collate all of the metadata for that series in with response payload. That's how I create the series contents on a given page.

createMetaFunction

This function creates our Remix meta function.

Importantly, it does things like transforming the title and description into the correct format twitter meta tags.

FrontmatterBox

This is component is responsible for transforming any of metadata into whatever content we want for that post, series contents, 'next post in series', etc.

Creating a table of contents

We can create a table of contents on our home page using the getAllPostFrontmatter function.

Conclusions and my experience

Now that I've got this going, it works pretty well.

One thing that isn't work for me - is that I can't put images in the metadata, which is important for social sharing. However, a similar barrel file approach might work.

The rigmarole of doing this suggests that Remix might not be the right tool for the job. Astro is something worth considering, also I might look at NextJS.

For one - a blog a good use case for static site generation, which Remix doesn't support.

This solution doesn't support an automated created/updated fields - those have to maintained manually. Perhaps some git commands in the build time JSON generation could do it.

Note that the barrel files are only ever used by Remix loader and meta functions, ie. on the server, so we don't need to worry about them being needlessly pulled in the frontend. If we had thousands of posts this might start posing a problem.


Spotted an error? Edit this page with Github