Dev Blogs

Next-ifying (opens in a new tab)

Reed Moseng


I had just built a simple API using Next.js (opens in a new tab). This showed me how easily I could get a site up using the Next/Vercel (opens in a new tab) power couple, and I wondered how easily I could get my app’s homepage up using this tech. I was also thinking about how I’ve wanted the ability to post blogs for Shorts or Pants for a while.

My approach

How do I update my existing project (which was vanilla HTML, hosted on Firebase (opens in a new tab)) to use Next & Vercel?

I wouldn’t recommend the approach I took, but it did force me to learn a lot of details I might have missed otherwise. I opened up my existing project and a new blank Next app (opens in a new tab) side by side in VS Code (opens in a new tab), and did the following:

  1. Move current pages (HTML) into a new src/pages directory
  2. Move over all Next related config files from the blank app
  3. Delete all Firebase related files in the current project

Then, I ran npm run dev and to absolutely nobody’s surprise, it didn’t work. I should have done this instead:

  1. Set aside everything in my current project into a folder
  2. Start a new branch (and check it out)
  3. Delete everything in the root folder of the repo
  4. Create a blank app using create-next-app (opens in a new tab)
  5. Deploy that on Vercel (not going to the actual domain quite yet)
  6. Only then: slowly add in my old pages and config.

But that’s not what I did. So I had all kinds of config issues to resolve before getting the first deploy to work.

What even is React?

My main problem though, was not understanding how different React (opens in a new tab) and JSX/TSX (opens in a new tab) are from plain HTML. At first I thought I could just paste HTML into the return portion of the TSX file, in between the <> and the </>, but that was definitely not the case (can you tell I’ve never used React, lol). I had a lot of minor changes I needed to make just to get a very simple HTML page working in React. I hadn’t read about the three rules of JSX (opens in a new tab) quite yet. I figured out which changes to make by finding some examples and referencing the output from this HTML to JSX converter (opens in a new tab). Also, all of my widgets and scripts were broken, so I commented those out. I could have saved a bunch of time by going through the awesome Learn Next.js (opens in a new tab) docs from the start, but I learned a lot stumbling around in the dark like this too.


I also got warnings that I should be using Image (next/image (opens in a new tab)) tags instead of img tags. I tried that, and quickly realized Image requires both width and height to be explicitly set. This is a problem because before I was using a fixed height and "auto" for the width so it would preserve the aspect ratio and determine the width from that. I looked around and struggled a bit to get this same behavior with the Image tag, but didn’t like the complexity/one-offness of any of the solutions I found. My site only has two images, so I stuck to img.


It also took me a minute to figure out which styles should live in global.css and which should live in module-specific css files (like Home.module.css). I learned from this error (opens in a new tab) that you can’t have styles for tags like body, h1, etc in module-specific CSS files (I guess those are called impure selectors…).

I eventually got it working, and the home page looked identical to the old one (woohoo!).

Getting /privacy.html working

But I couldn’t deploy publicly yet. My privacy policy page wasn’t working, and both my iOS app and its App Store listing (opens in a new tab) have a specific link to (opens in a new tab) for the privacy policy. If this isn’t working, it’s possible my updates won’t get approved (assuming Apple does a quick automated check on the validity of those URLs in the review process). More importantly, my users won’t be able read my privacy policy. So, next up, I needed to fix this.

I use iubenda (opens in a new tab) for my privacy policy because I don’t have time to write one myself. Previously, I was embedding the policy in an HTML file using the JS blob (opens in a new tab) they provided. That wasn’t quite working, and also felt unsatisfying with this fancy new stack (I knew there must be a better way). iubenda’s docs mentioned they also have an api for grabbing the HTML content of the policy. This is unauthenticated (you don't need to be logged in to access it), and just returns HTML (not nested in JSON or anything). So it appeared to me like I could just grab that and display that content for this page.


This seemed like a super simple opportunity to try SSR (server-side rendering) (opens in a new tab) for the first time, which is one of the main features of Next.js. This was a thing I’d been hearing everyone talk about, but had no use for myself (I don’t have a web app). It is a concept of generating and returning the HTML of a webpage on the server when a website is requested, rather than first returning empty HTML that only once loaded triggers API calls that need to finish returning before the content can be populated with data. This allows you to return a webpage fully rendered with data from APIs, rather than having two stages of loading. You make the API calls immediately when the user requests the page and just return the page with the content included. This is a burden I’m not used to having on the server (since it isn’t really possible with native apps), but for web, it’s perfect (remember though, I’m a web noob).

So I checked out the docs (opens in a new tab), and it seemed like I could just swap out the url in the fetch call with my privacy policy URL from iubenda. That didn’t work. I kept getting type errors for the props. It took me a while to realize that though I was using typescript and TSX files, the docs seemed to assume I was using javascript. I found some forum posts that helped me figure this out, but the docs I didn’t find at the time describe it more clearly (opens in a new tab).

And then I read the docs more and realized I don’t actually need SSR at all… Static site generation (SSG) actually made more sense, since this content will not vary at all between requests. As I understand (or misunderstand), this still uses the server to generate the HTML, but it does it at build time so that it can be reused for each request, rather than generating identical content every time someone loads the page. SSR is ideal for cases where data is likely to vary between requests (which would be true most pages of a web app, which this is not). Oh and this is just a summary of their summary (opens in a new tab) of Static Generation vs SSR.

Switching from SSR to Static Generation was as simple as using getStaticProps instead of getServerSideProps (for the most part).

Rendering the retrieved HTML

One final thing I had to figure out was how to grab the raw HTML content and render it on that page. Most examples were using await res.json(), but this response was pure HTML, and it took me a minute to figure out that await res.text() was what I was looking for. And though it sounds dangerous, dangerouslySetInnerHTML sounded like the way to just render HTML without anybody needing to ask any questions. So here’s what I ended up with (only 24 lines!):

import type { InferGetStaticPropsType, GetStaticProps } from 'next'
export const getStaticProps: GetStaticProps<{ privacyText: string }> = async( 
) => {
    const privacyResponse = await fetch(
    const privacyText = await privacyResponse.text();
    return {
        props: {
export default function Privacy({ privacyText }: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
        <p dangerouslySetInnerHTML={{ __html: privacyText }}></p>

It works, nice!

Adorning the privacy policy with style

One totally unexpected (but probably predictable) benefit of this method was that the HTML was styled with all my globals.css styles! Background, text, and links all looked just like my homepage. This was super refreshing, as I never liked how the privacy policy was rendered before. It used styling totally specific to iubenda, and actually had some readability issues for deeply nested headings (which had microscopic text).

So I took this opportunity to take a pass on all the styling in this file and adjust as needed. That pesky tiny text, it was h6… So yeah, I increased the font size quite a bit for that. And all these changes were in my global CSS config, so any additional pages I add would adhere automatically (which I love).

Redirecting /privacy.html to /privacy

Now I had my privacy policy rendering all nice and good, but the hardcoded link (/privacy.html) still didn’t work. This new page is located at pages/privacy/index.tsx, which is accessible via /privacy, but not /privacy.html. I could have just updated those hardcoded links to not have the extension, but I wanted a solution that was compatible with including or excluding the extension and getting you to the right place, for all pages. I also figured this was a common thing to run into, so I looked around and started trying things.

This took a while, as I wasn’t sure whether this was a Next config, a Vercel config, or something in between, but I eventually found the cleanURLs (opens in a new tab) Vercel config option. I did a redeploy and that worked. However, this caused some other weird issues later on so I ended up removing it and instead adding a simple redirect to my next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  redirects() {
    return [
        source: '/privacy.html',
        destination: '/privacy',
        permanent: true
module.exports = nextConfig

You can check out the final result at either:

Getting widgets working

On my old homepage, I had two widgets: ko-fi (opens in a new tab) (you could be the very first to by me a coffee, lol), and Help Scout (opens in a new tab) (which I use for customer support).

Help Scout was definitely the priority, so I tackled that first. This turned out to be as simple as turning my script tags into Script (next/script) tags (following these instructions (opens in a new tab)), and putting them into my Head. I decided to push the ko-fi widget work until later and instead get the site deployed.

Deploying to Vercel

After all those shenanigans, I finally had my site to the state it was at before starting this project. It’s moments like this where you really question if you just wasted a bunch of time, but I think it was absolutely worth it, if only for all that I learned. Plus, there’s this weird thing that happens when you improve your developer experience: you develop more 🤯.

Oh, and deploying to production with Vercel was as easy as committing to main and pushing (which never gets old).

Undeploying from Firebase

After deploying the site, it worked using my Vercel deployment link, but (opens in a new tab) was going to a Firebase error page. My old setup used Firebase hosting and GitHub actions to deploy the site on PRs and pushes/merges to main. I had removed all the config and deployed the new site, but technically the old deployment was still active.

I (wrongly, I guess) assumed that I could just disable the deployment from the Firebase console, but that was not the case. I had to check out the commit that still had my Firebase config, and run firebase hosting:disable (full credit to this SO answer (opens in a new tab)).

Updating the domains

I also had to update some domain settings on Vercel and Google domains (as is expected whenever switching your hosting provider). And with that, (opens in a new tab) was fully Next-ified!

Get Shorts or Pants for iPhone and iPad (opens in a new tab)

Sign up for the newsletter (opens in a new tab)

Thanks for reading!