Published on

Deploying multiple apps together on Vercel under one domain (Reviving the Blog)

12 min read
Authors

This website is primarily Next.js, with Vue.js bolted on for a single endpoint, then deployed via Vercel. Using Next.js to power the blog, and Vue.js for my résumé.

The frameworks I'm using may not match what you're using but the same rules will apply. I'm hoping you can read how I did it and apply what I learned to your own situation.

If you want to jump straight to how it was done, click here.

A little bit of history

Just to give a bit of history, the original blog was a WordPress installation that was set up in 2013.

mikedidomizio.com in 2016-03-23
It ain't much, but it's honest work.

Blogging was a popular activity, much like how everyone has a podcast today. I only made a few blog posts, most without much effort, other than my blog post about building an iPhone application.

2013 was quite a while back, especially when we're talking about the internet. I asked ChatGPT (that didn't exist then) "what are 10 software development events that happened in 2013."

  • Launch of iOS 7
  • Introduction of Android KitKat
  • GitHub reaches 10 million repositories
  • Docker 1.0 Release
  • Node.js and io.js Split
  • Launch of AngularJS 1.2
  • Rise of Continuous Delivery

and the rest happened in 2014 (oh ChatGPT), so I asked for more:

  • jQuery 2.0 Release
  • Node.js 0.10 Release
  • Launch of Bootstrap 3

A lot has changed since that time.

When I initially created the blog, using WordPress was the obvious choice. It was everywhere and it was awesome, applying plugins and themes with the click of a button to get what you needed. Although I haven't used it in forever, I'm confident that, for many purposes, WordPress is still the de facto choice.

Putting the blog on pause

In 2014, I decided to shut it down because it was just unnecessary hosting costs and I wasn't using it much. I was working a new job, I wasn't writing anything. I was still doing the occasional OSS contribution and small projects. The hosting was costing me a few bucks a month, and I felt it wasn't worth continuing, so I decided to cut it.

Transitioning to Amazon S3

I decided to remove the blog and just have my résumé up. By removing the WordPress blog, which required a database, I could just opt for a static site solution.

At that time my résumé was just HTML/CSS so moving it into an S3 bucket and pointing the domain to the bucket was a viable solution. (Note: This may be still a solution today for some things, but it did have a bit of a "cold start" problem).

Mid-2020 I moved my résumé into source control. This allowed me to have a history of changes, and allow me to revert if I needed. It was also just nice to see a progression over time as skills and items changed. It's something I suggest everyone to do to be honest.

At the start of 2021, I converted the résumé from HTML to Vue.js. I wanted to have an easy to maintain look and consistency throughout the page. Which could be done by breaking the résumé into components.

On top of automatic continuous deployment on merge, it has visual regression tests to verify it looked good on any screen size, and even checks that it is max two pages when printed.

Rekindling my passion for software development writing

I spent a bunch of time in 2023 writing documentation and guides at the company I work at, as well working on my own personal projects. By the end of 2023, I realized I had been involved in some interesting projects that unfortunately didn't gain the attention I felt they deserved. I wanted to write about these projects to give people a glimpse into my thought process, the challenges faced, how I approached problem-solving, and ultimately, the outcomes achieved. My original thought for writing was to go back to WordPress, but I haven't worked on PHP in many years. I chose to go the JavaScript route as it's something I've become more familiar with over time and work with every day. With full stack frameworks these days I could work under one language and have it as simple or complex as I needed it to be.

I thought to go with Remix for the blog, but I'm still not sure if I'm a fan of it at this time. There's some gotchas that I've hit in previous projects that weren't documented and required a bit of digging, and some things that just worked no problem with Next.js.

I use Next.js with a lot more of my projects and am more comfortable with it, so I decided to shift in that direction.

I started building out a blog site but my issues were:

  • It was not very pretty, but I looked at a few others in the industry and they have decided to keep things simple. I felt it would be okay.
  • It would lack a lot of features that I wanted to have.

but the biggest thing was I just wanted to get back to writing, and not waste time coding from scratch a blog.

I found this Next.js Tailwind Starter Blog, and it looked good and seemed to fit all my needs. My plan was to deploy it through Vercel to keep things simple.

Setting up separate projects in Vercel under one domain

For myself, the first thought was how was I going to deploy a Next.js app and Vue.js app together on Vercel under one domain?

What I wanted was:

Do I set up both projects under a monorepo to be deployed together?

I didn't really want that, I (currently) like the separation of the two projects. Two separate git histories, easy to maintain, deploy separately and if I need to change things again, it's fairly easy to do so.

Deploying with Vercel and modifying the Vercel project configuration

Vercel is a PaaS company that maintains Next.js. They allow you to use their platform to deploy, monitor, and maintain projects (not just Next.js) with a lot of ease. Things that you would once have to manually be set up all by yourself in the past.

I've used Vercel since 2020, and have deployed quite a few projects. If you've used Netlify or Heroku, it's in the same vein.

For most use cases, you would connect your Git repository to a Vercel project, apply a domain to that project, and you would be done. I wanted to have two Git projects in two different frameworks, under two different Vercel projects, continuously deployed and accessed under one domain.

Vercel has the ability to include a vercel.json project configuration file that can help with that.

Since I wanted to have Vercel point to a different deployment if a user goes to the /resume endpoint, I could accomplish this with rewrites.

This was solved pretty simply with just having the source be the endpoint I wanted, and the destination point to the other Vercel deployment in my vercel.json file:

{"source": "/resume", "destination": "https://mikedidomizio-resume.vercel.app/"}

This can be verified to be working by visiting your main deployment URL and visiting the source endpoint and verifying if that goes to the destination.

Dealing with resource issues

I was immediately hit with one issue though which is the CSS/JS not loading properly on the résumé page. The issue was that the routes for those resources were not accounted for and couldn't be found under the Next.js deployment. The simple solution would be to also forward those routes as well to the Vue.js deployment. I chose to have Vue build to differentiate these files as resume-* directories/files because css and js directories are pretty generic and I wanted the risk of conflicts.

Going from this:

app/
├── css/
│   └── app.css.{HASH}.css
├── js/
│   ├── app.{HASH}.js
│   └── chunk-vendors.{HASH}.js
└── index.html

to this:

app/
├── resume-css.app.css.{HASH}.css
├── resume-js/
│   ├── app.{HASH}.js
│   └── chunk-vendors.{HASH}.js
└── index.html

by changing the Vue.js configuration (vue.config.js) to include:

// all CSS files to be prefixed with `resume-css`
chainWebpack: (config) => {
  if (process.env.NODE_ENV === 'production') {
    config
      .plugin('extract-css')
      .tap((args) => {
        args[0].filename = 'resume-css.[name].[contenthash:8].css';
        args[0].chunkFilename = 'resume-css.[name].[contenthash:8].css';
        return args;
      })
      .end()
  }
},
// assets to be under `resume-js` directory (I just had js files but you may have other files in here and want to make the directory more generic named)
assetsDir: 'resume-js',

so that it outputs the files/directories in a fairly unique way.

The resume-* prefix is not required to do, but again I wanted to ensure there was less chance of conflicts. I had to also update the Vercel configuration to make it aware of these specific routes though the rewrites array.

{"source": "/resume-js/:match*", "destination": "https://mikedidomizio-resume.vercel.app/resume-js/:match*"},
{"source": "/resume-css:match(.*)", "destination": "https://mikedidomizio-resume.vercel.app/resume-css:match*"},

The Vercel configuration change is mandatory, but you don't have to do change the filename/directory output like I did.

Addressing Content Security Policy (CSP) Challenges

I encountered a few issues loading third party resources.

Google Fonts not loading

The next problem was that the Google fonts under the endpoint /resume were not loading properly, but were working properly under the "vercel.app" deployment URL.

I updated the vercel.json file to include CSP for that /resume endpoint

{
  "key": "Content-Security-Policy",
  "value": "default-src 'self'; font-src 'self' fonts.gstatic.com; style-src 'self' fonts.googleapis.com; img-src 'self' data:;"
}

This is just saying "only load my own fonts and styles to load, but also allow anything loaded from these Google domains." This allowed me to get around Google Fonts being blocked, which can be verified that the font loads, either visually or by the network panel of your browser. Some other options would have been nonces and hashes, but this felt like the quickest and simplest solution.

Google Analytics not working

Google Analytics (referred here on as GA) was also blocked by CSP. The main site had not been set up and the résumé GA was no longer working properly.

The résumé deployment required changes to the vercel.json file, that can be quickly taken from here. The rest of the document will be working towards GA 4.

To avoid using unsafe-inline I decided to go with a hash. This is a quick read on hashes with CSP. It tells you how to get the hash via command or through the console error which did it for me:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src-elem 'self' *.googletagmanager.com". Either the 'unsafe-inline' keyword, a hash ('sha256-NWf2QfXgstC58zeipPN/8CH5ZLaYLMFh0+dDWU0xbcY='), or a nonce ('nonce-...') is required to enable inline execution.

Modifying the content security policy to be such:

script-src-elem 'self' *.googletagmanager.com 'sha256-NWf2QfXgstC58zeipPN/8CH5ZLaYLMFh0+dDWU0xbcY='

and updating the <script> tag in the Vue.js project to include the hash:

<script integrity="sha256-NWf2QfXgstC58zeipPN/8CH5ZLaYLMFh0+dDWU0xbcY=" crossorigin="anonymous">

Note that the above example is also using subresource integrity or SRI to enforce the hash. You might find it duplicating to have it in both places, but it can serve as a good way to document which hash is for which script.

I like that approach. Anything to help me cross reference it in the future. You can verify the above works by loading up GA Real Time dashboard, visiting the endpoint in another tab and see if GA reports a new user.

Finally updating the main site next.config.js headers to use the values needed for it.

ℹ️ This is modified to be copy/pasted and not exactly what is applied to this project. If it doesn't work check out the next.config.js in this repo and let me know.

// all routes to have this CSP header
async headers() {
  return [
    {
      source: '/(.*)',
      headers: [
        {
          key: 'Content-Security-Policy',
          value: `
            default-src 'self';
            script-src 'self' https://*.googletagmanager.com;
            style-src 'self';
            img-src * blob: data: https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com
              https://*.g.doubleclick.net https://*.google.com https://*.google.com;
            font-src 'self';
          `
        },
      ],
    },
  ]
},

You can verify the above by going to any other endpoint and changing pages and verifying that GA reports the other pages.

The CSP stuff can be a bit tricky but the errors I find are pretty telling of what is going on. Read the CSP links in this post, and break down the errors that your browser reports.

Shout-Outs

content-security-policy.com is a really nice website and good resource regarding content security policy.

Maintenance

This should be fairly stable with GA 4, but it's always possible things change. Vercel/Next.js changes may require updates to continue this properly being deployed.

As of writing this the versions used in these projects are:

  • "next": "14.0.3",
  • "vue": "^2.6.11"

This post may be updated periodically but the majority of the work was done between 2024-01-01 and 2024-01-15. If significant time has passed since this has been published consider that some of it may be out of date.

Conclusion

By configuring Vercel it was fairly simple to get two separate apps working alongside together under one domain. It does take understanding what has to be done to get it to work right but once understood it worked really nicely 🎉.

What challenges have you faced with a similar setup? Share your experiences with me. Let me know on  or LinkedIn.

Subscribe to the newsletter