Victor is a full stack software engineer who loves travelling and building things. Most recently created Ewolo, a cross-platform workout logger.
Migrating to Remix.run

Over the past year I've had the pleasure of developing a full-stack application using the Remix framework and I finally took the plunge and decided to redo this blog using Remix as well.

For those that are out of the loop we have come a full-circle from the php days to program webpages using a frontend javascript framework (in this case React.js) but have them initially render on the server so that the client is presented with some content. Once the page is loaded the UI framework re-executes and runs any client-side code if required. There are multiple advantages to this approach:

  • Good for search engine optimisation (SEO): crawlers do not need to execute Javascript and can get text from the pages as is.
  • Good for slower internet connections: text is rendered first with additional functionality coming in after.
  • Potential to turn off Javascript altogether and just go back to server-side rendering a la PHP.

The obvious disadvantage of Server-side rendering (SSR) is that it puts a lot more stress on the server with regards to compute requirements as each request needs to be processed on the server before being sent out.

The original setup

The original blog was a home-rolled setup with the content being written directly as html and then compiled into static html via Mustache.js templates. The Media pages were coded separately with their Javascript code bundled using none other than the old school browserify library. A single css file was generated using Sass which bundled in the UI framework + custom styles.

While everything working decently ok, the developer experience was a a bit lacking. I even managed to integrate a task runner to run the entire build pipeline and portion out the various build tasks including live reload but it was all a bit brittle and rather outdated. For e.g. browserify itself hasn't seen much action and a lot of the d3 libraries switched over to es6 only.

The new setup

First of all, running a modern stack on a low end VPS with only 1 Gb RAM comes with its own set of challenges. I had to add export NODE_OPTIONS=--max_old_space_size=512 to .bashrc in order to let npm install even finish. However, thereafter the running application took less memory than the previous setup (most likely due to the fact that I was caching all the static html in memory for really fast page loads).

The next biggest question was whether I would be able to port over all the blog content without issues. Thanks to Remix's splat routes, I was able to program a generic route that simply loaded the right html file and use dangerouslySetInnerHTML to set the content :). Example code from blog.$.tsx:

export const loader: LoaderFunction = async function ({ request, params }) {
  const parts = getBlogFileParts(params);

  let filePath = path.join(config.blogDataDir, parts[0], `${parts.join("-")}.html`);
  const id = parts[parts.length - 1];

  let blogEntry = null;

  for (const item of config.blogJson) {
    if (item.id === id) {
      blogEntry = getBlogEntryTyped(item);
    }
  }

  if (blogEntry === null) {
    throw new Response("Not Found", { status: 404 });
  }

  let content = "";

  try {
    content = fs.readFileSync(filePath, {
      encoding: "utf-8",
    });
  } catch (err) {
    throw new Response("Not Found", { status: 404 });
  }

  return {
    content,
    blogEntry,
  };
};

export default function BlogContent() {
  const { content, blogEntry }: { content: string; blogEntry: BlogEntry } = useLoaderData();

  return (
    <>
      <div className="block blog-entry-header">
        <h5 className="title is-5">
          <Link to={getBlogLink(blogEntry.id, blogEntry.pubdate)}>{blogEntry.title}</Link>
        </h5>
        <div className="blog-entry-info">
          {getDateDisplay(blogEntry)} | By {getAuthorDisplay(blogEntry)}
        </div>
      </div>
      <div className="block">
        <div className="content" dangerouslySetInnerHTML={{ __html: content }}></div>
      </div>
    </>
  );
}

Note that I have a global JSON file that serves as a manual index for all blog entries. Keeping it in a human readable format makes it very easy to make changes and also gets rid of a very heavy database dependency which is a big win when running on low spec hardware.

Once the blog was sorted, the other things fell into place quite seamlessly. I did have some trouble with getting external Javascript to play nice with the SSR but that is for another article. Many thanks to the Remix team for creating such a cool framework!