Hey all. Recently completely overhauled the front end for GuardianForge and it was quite an interesting, educational, and fun project. No tutorial here, just me explaining the process and various challenges along the way!
A Brief History
So in early 2021 I started building the MVP for GuardianForge. Hell bent on using Vue because I like to go against the grain sometimes, that’s where I started. V1 of the SaaS was entirely a SPA in Vue hosted on AWS S3 and CloudFront. When I applied for AdSense, I quickly realized that their validation service didn’t like SPAs and that I’d need to create a more static site. I started with Gridsome but after some issues getting things up and running, decided to jump back to Gatsby since React has such a huge support system. It worked fine at the time but as the apps complexity grew, I realized shoving a dynamic app into an SSG was not the best approach. So that’s when I started planning to break things out.
The New Structure
So the plan was to break the app up into three separate apps: a core app, a blog app, and a documentation app. The first was generated using create-react-app and the latter two were generated using a Gatsby WordPress template (since I use WP as my headless CMS).
When I started this rebuild, luckily I had everything written in TS so rewriting the components up wasn’t terribly difficult due to the type checking in VSCode. I started with the main app, added in react-router-dom to handle routing and slowly moved my pages over. As the pages came over, VSCode would complain about missing components so I’d move those over too and test things as I went.
I essentially did the same with the Blog and Docs sites, but that was even less work since there is significantly less complexity. Then the fun part started: trying to get everything working together in a cohesive manner.
Changes to the Architecture
I use Azure DevOps to manage my CI/CD along with AWS SAM to manage my infrastructure in AWS. I wanted to break out my one pipeline into 4 (the three front end apps + the backend) to speed up deployments and not have me rebuild the whole damn thing over a simple typo, for example. If anyone has worked with static sites in AWS, you probably know that the files themselves are stored in S3 and served up via the built in Public Website feature, or placed behind a CloudFront CDN instance. Forge used the latter of these two so I can use my own domain & HTTPS cert. Since I was building three separate apps, I needed them to be in separate folders in S3 and that was the first challenge.
My S3 bucket structure.
How could I serve up three separate static apps based on the URL Path, especially when one has literally no path? Here is what I needed to figure out:
- Any path that started with /blog should serve files in the blog folder in S3.
- Any path that started with /docs should serve files in the docs filter in S3.
- Any other scenarios should serve out of the app directory.
Now CloudFront has two different ways to serve files from S3. The built in feature allows your CF instance to return files from S3 directly without having to mess with the security of the files. The alternate method is to enable the Public Website feature of the S3 bucket and instead use that URL as the origin for the files. Gatsby specifically needed to use the URL directly as the framework doesn’t play well with files directly from S3 (an implementation detail I had forgotten that made this take WAY longer than it should have 🤦♂️).
By using CloudFront behaviors and two separate origins (direct S3 URL for the Gatsby apps, and built in S3 integration for the CRA app), I was able to pull this off with the below configuration.
Here are my CF Origins. Note how the last one has an origin path set to /app, which is the catch-all for requests.
And here are the Behaviors that coorespond to the Origins based on path.
Another benefit of this setup is that I can easily wipe the folders for each app as they are being deployed via Azure DevOps.
Sharing Components Between the Apps
One of the reasons I decided to use an SSG in the first place is because I didn’t want to copy/paste code between projects. Well through this process I finally took the time to figure out how to create a library of shared React components, which was actually much easier than I though it would be. It took me a bit to figure out the right import/export process, but once I figured that out I was able to pull this off.
In index.ts, I can export any TSX component from within my shared library.
And its easy to import those components into any of the apps like so.
Since the shared library is just part of the mono repo, I don’t have to worry about hosting it anywhere. So I can install it in all three apps using the following command in the terminal:
npm install ../shared
Now one of the trickier things was figuring out how to create a Nav & Footer to share between the main app (created with CRA) and the blog/docs sites (using Gatsby). Gatsby uses
reach-router whereas CRA uses
react-router-dom. I knew that
react-bootstrap had a way to somehow get a component to render as a different type, passed in via a prop. For example, if you want a
NavLink to render as a div instead of the default anchor, you can set it via a prop, which is pretty awesome.
In the NavLink component, here is how they are able to set the component type dynamically,
Turns out the trick to that is to add a prop that is a
React.ElementType type and literally use the named variable as your element in the TSX. I discovered this after reviewing the source for
react-bootstrap. Open source for the win!!!
Here is the code for the footer of all three apps. Note the linkComponent prop type on 43 and how its used on 55.
And here is how I'm able to pass in a component type as the linkComponent.
My Azure DevOps Pipelines
Now the last bit to this entire process is the updated pipelines I had to setup in Azure DevOps. Recall that before, everything was setup in a single pipeline with two stages, QA and Production. I essentially had to pull out the necessary build steps from the pre-existing pipeline and create new pipelines for each of the new projects. The main app & backend still force me to deploy to QA first so I can check that everything is working properly when new functionality is added, but I decided to create separate pipelines for the blogs & docs apps, one each for QA & Prod so if I write an article, for example, I can deploy straight out instead of going through my standard QA process.
Here is a list of all the Release definitions I use in Azure DevOps.
And here is essentially what each of the front end apps all do, with slight modifications based on app & environment.
Faster and more targeted deployments, and an easier to manage project that lets me add new feature & content MUCH faster. Plus I don’t have to fight against Gatsby for adding functionality to GuardianForge. I don’t fault the framework at all, I was trying to use it for something it really wasn’t designed to be used for.
GuardianForge is entirely open sourced, feel free to explore the magic that powers it here.