Frontend has become complicated. Gone are the days where you cram everything into a single-page app with a giant bundle.
This blogpost is adapted from talks I gave at Cloudflare Tech Talks in 2023, you can find the original slides: here → (or at the end)
Today, we have a number of frameworks, libraries, and tools to pick from — Next.js, Astro, Svelte, Docusaurus, or Vue. That's just the tip of the iceberg.
There are so many tools you could use to build a frontend: Vite, Snowpack, Webpack, Rollup, Parcel, Gatsby, Nuxt, Sapper, Eleventy, Hugo, Jekyll, VuePress, Gridsome, Scully, and the list goes on.
In our case (Indent), we use Next.js for our marketing site and Docusaurus for our docs both hosted on Vercel which enables us to have niceties like per-branch deployment previews and multiplayer comments so anyone on our team can contribute.
For our main app, we also use Next.js, so why not consolidate it with our marketing site? Well, we have to follow compliance and security requirements from our customers like allowing customers to self-host and strict code review controls.
The common problem with splitting into separate apps is that you have to manage routing between them.
You can't just a regular next/link
or Link
component from react-router-dom
to link to a page in another app. You have to use routing-aware links that check if it's the linked route is part of the current app, another Next.js zone, or a different framework.
And maybe one of the worst parts is separate domains for each app. How many times have you gone to company.com/docs/deep/link
, just to get redirected to docs.company.com
index page?
It's a terrible experience.
Instead, we use Cloudflare Workers to route between the apps and merge into one canonical domain: indent.com
indent.com└── / — Next.js for marketing — Vercel└── /docs — Docusaurus for docs — Vercel└── /home — Next.js for product — Google Cloud
The worker code starts by adding security headers to every response:
addEventListener('fetch', event =>event.respondWith(handleRequest(event)))async function handleRequest(event) {try {if (event.request.method === 'GET') {let response = await serveAsset(event)return await addSecurityHeaders(response)}...
In Next.js, we also have to add an asset prefix:
/** @type {import('next').NextConfig} */const isProd = process.env.NODE_ENV === 'production'const assetUrl = process.env.VERCEL_URL || 'my.vercel.app'const assetPrefix = isProd ? `https://${assetUrl}` : ''module.exports = {assetPrefix}
And then we can serve the asset from the correct deployment:
async function serveAsset(event) {const url = new URL(event.request.url)let { pathname, search } = urllet cache = caches.defaultlet response = await cache.match(event.request)if (response && response.status < 400) {return response}if (shouldServeDashboard(pathname)) {// Remove assetPrefix for dashboard Next.js App} else if (shouldServeDocs(pathname)) {// Remove assetPrefix for docs Docsaurus 2.0 site} else {// Serve marketing Next.js App}if (response.status < 400) {const res = response.clone()event.waitUntil(cache.put(event.request, res))}return response}
Have any questions?
Feel free to reach out (@fouadmatin on X) or check out the slides!