Home

Published

- 7 min read

Using Wildcard Subdomains as Paths in Next.js 14

img of Using Wildcard Subdomains as Paths in Next.js 14

The personalization of user experiences is key to building engaging web applications. One effective approach is through the use of personalized subdomains in user authentication systems. This technique enhances user engagement by providing each individual with a unique and customized space within a larger platform. Consider the approaches of some industry leaders:

For example:

  • Shopify: Every shop owner benefits from a distinct subdomain (e.g., storename.myshopify.com), offering a customized storefront within the unified Shopify ecosystem.
  • WordPress: Employs a multi-site installation feature, enabling each site or blog in a network to have its own subdomain (e.g., user1.wordpress.com or user2.wordpress.com).
  • Slack: Each workspace operates under a specific subdomain like companyname.slack.com, ensuring a tailored and organized environment for company communications.

Although Next.js provides excellent support for dynamic file-based routing, it lacks native support for subdomain-based dynamic routing. This gap presents a challenge for developers aiming to leverage Next.js’s dynamic routing features directly with subdomains to fetch subdomain-specific content.

For instance, a developer might want a subdomain like abc.yourdomain.com to automatically render content from a path such as app/[subdomain]/index.tsx in the Next.js structure. This would mirror how path-based routing works in Next.js, where accessing yourdomain.com/abc naturally leads to rendering the content defined in app/[subdomain]/index.tsx. Without native support for this type of subdomain routing, developers need to implement additional solutions to bridge this functionality gap.

This blog will guide you through using Caddy to handle wildcard subdomains effectively, maintaining the original subdomain format in the user’s browser while managing internal routing seamlessly.

I hope you’ll find this blogpost useful!

Initial Approach

Initially, I experimented with Next.js’s middleware feature to detect and rewrite URLs based on the user’s subdomain. For example, accessing abc.blogstreak.com would internally redirect to blogstreak.com/abc. Although functional, this method modified the URL in the browser’s address bar, which was not ideal for maintaining a seamless user experience. To preserve the original subdomain structure in the browser—ensuring a cleaner and more intuitive URL display—I transitioned to utilizing a reverse proxy setup with Caddy.

Tech stack

  • Digital Ocean for hosting
  • Caddy for automatic HTTPS and reverse proxy
    • XCaddy to build Caddy with Digital Ocean plugin.
  • Systemctl for managing services

Step-by-Step Implementation

1. Digital Ocean Setup

  • Sign up or log in to Digital Ocean.
  • Create a new Droplet. Configure it based on your application needs. The following is my droplet setting ($6/mo).

Region: Singapore OS: Ubuntu 23.10 x64 Plan: Regular SSD with 1 GB RAM, 1 CPU, 25 GB SSD storage, 1000 GB transfer Authentication: Use SSH keys for a secure connection.

Domain Configuration:

  • Purchase a domain (e.g., blogstreak.com) from a registrar like Namecheap or Porkbun.
  • In Digital Ocean, go to Manage > Networking and add your domain.
  • Set up DNS records
    • A Record for subdomains: Hostname *, points to Droplet IP
    • A Record for the main domain: Hostname @, points to Droplet IP
  • Update your domain’s nameservers to point to Digital Ocean:
    • ns1.digitalocean.com
    • ns2.digitalocean.com
    • ns3.digitalocean.com
  • ☝️NOTE: For guides specific to your domain registrar, check out this link.
  • Access and Setup Your Droplet:
  • Configure PM2 for App Management:
    • Install PM2: npm install -g pm2
    • Start your application with PM2: pm2 start npm —name “mynextjsapp” — start
    • Configure PM2 to restart on boot: pm2 startup and pm2 save

☝️NOTE: At this stage you could run your next.js app hosted on your droplet in the browser. You can do so by accessing http://youripaddress:3000.

3. Caddy Configuration for HTTPS and Reverse Proxy:

Please follow the steps on the following article to set up Caddy: Article Link

However, we’ll be switching things up a bit in terms of the Caddyfile Configurations. I’ll be elaborating this further below.

By the end of the article, you should have:

  • ✅ Digital Ocean personal access token key (API Key)
  • ✅ Built and installed Caddy
  • ✅ Installed the Digital Ocean plugin via xcaddy to interface with the Digital Ocean DNS API
  • ✅ Enabled automatic TLS with Let’s Encrypt
  • ✅ Configured systemd service so that Caddy will be launched automatically on system startup

Now, let’s move on to our specific Caddy Configuration!

☝️NOTE: Please create a Caddyfile in your droplet as stated in the tutorial article above.

Create a new block to set your email, mainly used when creating an ACME account with your CA, and is highly recommended in case there are problems with your certificates.

   {
	email youremail @email.com
}

...

I’ll be defining the block blogstreak_common to define common settings that will be used in multiple places in the configuration. tls part handles the TLS settings to enable HTTPS. It uses DigitalOcean for DNS challenges, a method to prove to the Certificate Authority that you control the domain

☝️NOTE: We’ll be using the Digital Ocean API Key we have generated earlier and set in the caddy.service.

   ...

(yourdomain_common) {
	tls {
		dns digitalocean { env.DO_AUTH_TOKEN }
  }
}

...

Configure for your main domain by defining a reverse_proxy, which sets all traffic coming to blogstreak.com to be forwarded to the local service running on port 3000 (Which is our Next.js app).

   ...

yourdomain.com {
	reverse_proxy localhost: 3000
  import yourdomain_common
}

...

Note above that we’re also using the common settings we defined earlier under blogstreak_common.

Configure your subdomain to set all subdomains of yourdomain.com, like user1.blogstreak.com or benny.blogstreak.com to serve blogstreak.com/thesubdomain/all/other/paths.

   ...

*.yourdomain.com {
   ...
}

...

@subdomain will match requests that are sent to any subdomain.

   ...

*.yourdomain.com {
  @subdomain header_regexp subdomain Host ^ (.+?) \.blogstreak\.com$
    handle @subdomain {
      ...
  }
}

...

subdomain is an arbitrary identifier used to name a matcher. A matcher is a set of conditions used to determine how Caddy handles incoming requests based on specific criteria, such as the host, path, headers, or other properties of the request.

Inside the subdomain block, there are different handles.

@notRewrite: specify that certain paths (/_next/, /api/, /favicon.ico)

should not be rewritten but should directly reverse proxy to localhost:3000

  • ☝️NOTE: This is important to preserve functionality of how Next works:
    • API Routes: The /api/ path is typically used for backend API requests.
    • Asset Handling (/_next/ and /favicon.ico): Paths like /_next/ (commonly used in Next.js applications for serving static or dynamic assets) and /favicon.ico (the small icon displayed in a browser tab) should be accessed directly using their specific paths to ensure they are loaded correctly by browsers and frameworks.

@rewriteAll: For all other paths, this rule rewrites the URL to include the subdomain at the beginning of the path before it proxies the request to the local server. For example, user1.blogstreak.com/blog becomes localhost:3000/user1/blog.

@root: If someone just visits a subdomain root like user1.blogstreak.com, it rewrites the URL to localhost:3000/user1 and then proxies it.

Again, we use the common configuration defined at the start in yourdomain_common

   *.yourdomain.com {
  @subdomain header_regexp subdomain Host ^ (.+?) \.blogstreak\.com$
    handle @subdomain {
        # Exclude specific paths from rewriting
    @notRewrite path_regexp not_rewrite ^ /(?:_next/ | api /| favicon.ico)
        handle @notRewrite {
            reverse_proxy localhost: 3000
    }

        # Rewrite all other paths to include subdomain at the beginning
    @rewriteAll path_regexp rewrite_all ^ /(.+)$
        handle @rewriteAll {
      rewrite * /{http.regexp.subdomain.1}/{ http.regexp.rewrite_all.1 }
            reverse_proxy localhost: 3000
    }

        # Handle root path specifically(if needed)
      @root path /
        handle @root {
      rewrite * /{http.regexp.subdomain.1}
            reverse_proxy localhost: 3000
    }
  }
  import yourdomain_common
}

Redirection for www.blogstreak.com

Here is the final Caddyfile:

   {
	email youremail @email.com
}

(yourdomain_common) {
	tls {
		dns digitalocean { env.DO_AUTH_TOKEN }
  }
}

yourdomain.com {
	reverse_proxy localhost: 3000
  import yourdomain_common
}

*.yourdomain.com {
  @subdomain header_regexp subdomain Host ^ (.+?) \.blogstreak\.com$
    handle @subdomain {
        # Exclude specific paths from rewriting
    @notRewrite path_regexp not_rewrite ^ /(?:_next/ | api /| favicon.ico)
        handle @notRewrite {
            reverse_proxy localhost: 3000
    }

        # Rewrite all other paths to include subdomain at the beginning
    @rewriteAll path_regexp rewrite_all ^ /(.+)$
        handle @rewriteAll {
      rewrite * /{http.regexp.subdomain.1}/{ http.regexp.rewrite_all.1 }
            reverse_proxy localhost: 3000
    }

        # Handle root path specifically(if needed)
      @root path /
        handle @root {
      rewrite * /{http.regexp.subdomain.1}
            reverse_proxy localhost: 3000
    }
  }
  import yourdomain_common
}


www.blogstreak.com {
	redir https://blogstreak.com{uri}
  import blogstreak_common
}

Notes on Next.js Code

  • Using this approach means that under the hood, we’ll get the directory-based route paths that Next provides, but it is indeed a bit more confusing. app.yourdomain.com would lead to yourdomain.com/app, hence triggering the page that is in app/[aRandomParam].

P.S. Please email egan@hey.com if you have questions or suggestions regarding this topic 🙏.

Related Posts

There are no related posts yet. 😢