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!