All apps start from a single codebase, this is even more true for a frontend web application. When an application starts out, all you are looking for is to build an app with a framework or library of your choice. If you were someone starting out before 2021, most likely you would have just scaffolded a single-page application with something like Create React App (Long live) and just went on with your day.
Fast forward a few months, everything is going well, and customers are happy with your app, but the newly hired SEO team comes to you and says “Hey, we noticed our pages don’t rank that well on Google, we need to optimize for SEO for inbound marketing”.
Okay, very valid request from a business perspective. How do we do that? We would have to either change the framework to an SSR-enabled framework or find a way to server-render a part of the website. Both of which are headache tasks that would take some time. This is part 1 of the realization that at the very beginning, you need to be very careful about which framework and libraries you choose.
Sooner or later, you inevitably realize that different parts of the application are interfering with each other.
Problems start to arise over time with a single frontend codebase:
Now, do understand that these are extremely nice problems to have. So if you have these problems, good job. These problems aren’t caused by technical debt but rather serve as a sign that the system has outgrown itself and needs some more work to fit into the new requirements.
There exist various solutions to the aforementioned problems. The most common solution is to find a way to split your frontend into multiple parts that can be independently maintained, worked on and deployed.
It doesn’t matter how you structure it, it could be a monorepo, or it could be a poly-repo. The end result is that there is a new frontend that is served on the same domain as the old one and the users think they are browsing parts of the same application.
Some common ways to do it are:
We’ll focus on route-based app splitting in this post.
Now that we’ve decided to split our front end into marketing/public pages and dashboard/authenticated pages, let’s look at how we’ll route users.
The simplest way is to obviously have two different URLs for the apps. Examples of these would be google.com, mail.google.com, photos.google.com. Notice how the apps are split based on sub-domains, so Google Photos and GMail don’t have to share any common code but with the help of cookie-sharing across subdomains can retain the same authentication info of the user. This is how Google Search, Gmail and any other Google service can recognize that it’s you trying to access the site, without having to be one single application.
Similarly, even this site, devesh.tech and blog.devesh.tech uses this approach. The public site is a Gatsby-generated static site while the blog is a Next.js-based Static Site.
That being said, this is very simple to implement (Often a single config change at the Domain registrar level) and poses very few challenges apart from data and credential sharing, which is natively handled by the browser anyway.
The tricky part would be to serve different apps on the same domain, say google.com and google.com/photos with both coming from different source codes.
Let’s look at how we’ll route users.
There are obviously additional configurations you have to add to both the sites, otherwise, anyone could run phishing attacks on any URL (Imagine being able to serve Instagram.com or facebook.com from a different domain and the credential leaks that would cause).
There are also some nuanced considerations you would have to make at an application level when requests are rewritten, we will see them in the upcoming section.
If you go to solarladder.com, solarladder.com/design and solarladder.com/login, you will notice something interesting, the homepage’s design and speed do not match that of the Login page.
This is simply because we serve solarladder.com and solarladder.com/design from a different source than the internal post-login pages at solarladder.com. This is done via a combination of the Same-domain request Rewrites that Vercel (Our frontend host) provides us with.
On the front end, we set rewrites in our vercel.json file:
{
"rewrites": [
{
"source": "/design",
"destination": "https://solarladder-public-....com/design"
}
]
}
Since Vercel is the provider via which our Public Paths repo is also deployed, the rewrites work as expected without any additional headers and rewrite-accepting configurations from the Public Path repo.
When you rewrite a request of one path to a different page, everything stays the same and the server simply fetches the HTML of the page the rewrite points and sends it back to the browser.
With this, there comes a complication:
Scripts and Assets do not work as they point to /script-${uuid}.js
but those files are not available at solarladder.com/script-${uuid}.js
and instead are at solarladder-public-.../script-${uuid}.js
.
To fix this, Next.js and all frontend frameworks allow us to add an assetPath
prefix so when the app is built and pushed to a live environment, it picks up solarladder-public-....com
as its base path for generated assets regardless of the domain it is being rewritten to.
This fixes the problem for even Next.js optimized server images using the next/image
tag.
Make sure to do the same for static images and assets (Ones stored in the public
or static
folder, depending on your framework) you point in your public app.
This is something you’ll encounter with all SPA Frameworks simply because of how they receive requests and route all of them to the same
index.html
file.
Making the redirects and rewrites work on Vercel for the root (/) path was a nightmare, it took me 2 hours post-midnight to figure out and reverse engineer what was going on at a Vercel and the Vite framework level (We use Vite for our traditional frontend, something we switched to a couple of years ago after being in prod with CRA for 3 years).
No matter what I did, the rewrite rule:
{
"source": "/",
"destination": "https://solarladder-public-....com/home"
}
did not work. It simply opened the regular frontend homepage.
To understand why it didn’t work, we need to understand how Vite and other SPA framework’s route resolution works in general on hosts like Vercel:
To fix this issue:
index.html
file to build.html
or some other file name./build.html
in a typical SPA fashion.{ "source": "/", "destination": "https://solarladder-public-....com/home" },
{ "source": "/:path*", "destination": "/build.html" }
For logged-in users, we want a redirect to /dashboard
. In the existing site, it was simple to do on the client side as the Homepage component was configured to redirect the user to /dashboard
via React Router if the user was logged in.
To do so now required us to:
solarladder.com/dashboard
if the above cookie is present. See Vercel’s Redirects for reference./dashboard
on the client side, post which the cookie would be set for them and all subsequent visits to the site would be handled on the server-side itself.The rule looks like this:
{
...
"redirects": [
{
"source": "/",
"has": [
{
"type": "cookie",
"key": "<authorization-identifier-cookie-name>"
}
],
"permanent": false,
"destination": "/dashboard"
}
]
}
The difference between our previous homepage’s performance and our new homepage’s performance was staggering. With better SEO and insanely better design of course.
For context, our previous homepage’s performance was 27 even on a desktop!
Our current frontend dashboard is now only for logged-in users and is just that, a dashboard. It does not have to worry about any marketing and SEO pages to serve and can continue to remain an SPA and in the future, the work is decoupled between teams.