Engineering

Wildcard domain and SSR to the rescue!

The Goal

I'm one of developers who were responsible for creating a website you are reading this article on. We wanted to create a separate blog page for each AppUniter to streamline the article writing process and create a sort of private space where they could publish all their posts. The goal seemed pretty simple:

  • create a web app,
  • make it accessible on wildcard domain ([author-slug].appunite.com),
  • serve author's data and posts based on url's subdomain author-slug

Technology stack

Docker + Kubernetes - We used container orchestration system for automating deployment, scaling and load balancing. We had to configure ingress controller to make our app available at [author-slug].appunite.com domain.

Strapi - fully customisable headless CMS - each author is able to create an account, specify an author-slug (the subdomain on which their blog will be available at), fill in their personal info (like bio, avatar and social media links), specify sections they want to display on home or subpages and publish posts. We can access all this data by hitting Strapi's endpoint with author's blog author-slug.

Next.js - React framework with hybrid static and server side rendering, TypeScript support, API routes, optimised builds and more. With such a framework all we had to do was to:

  • create views for pages (home and subpages) and for posts,
  • get posts and author's info from Strapi based on author's author-slug
  • hydrate the page with fetched data

The first solution

As mentioned above Next.js has the ability to pre-generate all the pages on build time and even revalidate pages with stale data (Incremental Static Regeneration). Next's static pages are well cached and very fast by default, so the solution was clear: Let's pre-generate all the pages and posts! But...

Something ain't right gif

It's not possible to pre-render a page for each author and make all of them accessible on a root path (/), that's not how static site rendering works! Every page must have specified path: strictly defined (like /about) or dynamic (eg. /post/[slug]), so we have to find a way to generate static pages for each author at /[author-slug] path and make their content available at [author-slug].appunite.com/ using ngnix location redirecting.

After a 3-day battle (a lot of 500 and 404 errors or infinite 301 redirects) with ingress and nginx configuration, with a lot of help from one of AppUnite's devops (thank you so much Michał!). We were able to achieve the desired effect using this configuration:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: blogs-authors-ingress
  namespace: appunite
  annotations:
    kubernetes.io/ingress.class: nginx
    ingress.kubernetes.io/ssl-redirect: "true"
    ingress.kubernetes.io/secure-backends: "true"
    ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/server-snippet: |

      server_name ~^(?<author-slug>.+)\\.appunite\\.com$;

      set $proxy_upstream_name "appunite-blogs-80";
      set $proxy_host $proxy_upstream_name;

      location = / {
        proxy_pass http://upstream_balancer/$author-slug?$query_string;
        proxy_redirect off;
      }

      location ~* "^/" {
        proxy_pass http://upstream_balancer/$author-slug$uri?$query_string;
        proxy_redirect off;
      }

spec:
  rules:
    - http:
        paths:
          - path: /
            backend:
              serviceName: blogs
              servicePort: 80
  tls:
  - hosts:
    - "*.appunite.com"

What's happening here?

We declare the name of a domain whose requests we want to redirect. Using regular expression ((?<author-slug>.+)) we can access matched author's slug to map it later to the /[author-slug] path.

  • All the requests to the root path (<author-slug>.appunite.com/) gets redirected to /[author-slug] page.
  • Requests to the rest of subpages (eg. <author-slug>.appunite.com/post/[slug]) are redirected to /[author-slug]/post/[slug] (path remains the same thanks to nginx $uri and $query_string variables).

It did work! We were able to access the page at [author-slug].appunite.com, but the page was... ugh... empty!

Nginx was redirecting all the requests to /[author-slug] path and our app was not able to access any resources statically hosted by Next.js in /static or _next directories (which of course do not exist under /[author-slug] directory).

We had too define two extra locations to nginx configuration:

location ~* "^/static/(.+)" {
  proxy_pass http://upstream_balancer/static/$1?$query_string;
}

location ~* "^/_next/(.+)" {
  proxy_pass http://upstream_balancer/_next/$1?$query_string;
}

From now all the request to /static and /_next paths (and all the subdirectories and files) were no longer redirected under /[author-slug] path. Unfortunately we were not able to use Next’s Image component (because of redirects, next-image couldn't properly generate optimised images), so we had to use native <img/> tag, loosing a little bit of load performance. Besides that, all the styles, images and other assets were fetched successfully!

We’ve published the app with test author account and asked our colleagues to test the product and provide feedback. It didn’t take too much time until Karol did spot a bug in website’s routing.

Next’s client side routing was adding /[author-slug] part to each path. We’ve tried to solve this issue by moving out of client-side routing and making all the links “external” by reading current website’s host name and constructing links this way:<author-slug>.appunite.com/(subpath without [author-slug]), but for some reason the links were working properly only when we’ve refreshed the page after navigating to it.

Back up gif

It was time to take a step back and rethink the solution. In our case, static site generation, instead of performance provided a couple of bugs and so much frustration:

  • we couldn’t use Next Image
  • no client-side routing
  • complicated ingress config
  • possible navigation bugs in the future

After intensive google’ing, talking with Adam and reading this awesome article we’ve come up with a better solution!

Server side rendering with wildcard domain

Next.js has an option to render a page on the server (Server Side Rendering). Using getServerSideProps function we are able to access request object and read values as cookies, parameters and headers like Content-Type and Hostname!

Then, based on hostname (<author-slug>) we are able to fetch all the author’s content from the CMS and pass it to the page via props! We have to repeat the same process for all the subpages and blog posts, and voila!

export async function getServerSideProps(ctx: GetServerSidePropsContext) {
  const wildcard = ctx.req.headers.host?.split('.')[0];

  if (!wildcard) {
    return {
      notFound: true,
    };
  }

    try {
        //fetching content by wildcard (author-slug)
    return {
      props: { blog: { ...blog, posts, pages }, footer },
    };
  } catch {
    return {
      notFound: true,
    };
  }

}

Remember this long ingress configuration from the first solution? We don’t need that any more!

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: blogs-ingress
  namespace: appunite
  annotations:
    kubernetes.io/ingress.class: nginx
    ingress.kubernetes.io/ssl-redirect: "true"

spec:
  rules:
  - host: '*.appunite.com'
    http:
      paths:
      - path: /
        backend:
          serviceName: blogs
          servicePort: 80

  tls:
  - hosts:
    - '*.appunite.com' 

Thanks to SSR solution:

  • there were no bugs with navigation
  • we were able to use Next.js features like Image component and client-side routing
  • no more complicated nginx config
  • content changes visible instantly
  • architecture became much simpler

Maybe it’s not as fast as if it was static generated, but in our case most of the content is just text and a couple of images, so the performance is not only not bad, but it’s actually pretty good!

Pretty heigh lighthouse score

Is that possible to achieve the same result with SSG?

Next.js 12.0.0 introduced a feature called Middlewares (beta), which lets us run code before a page request is completed. Based on the user's incoming request, we can modify the response by rewriting, redirecting, adding headers, or even streaming HTML. So it turns out, that we can pre-generate all the pages on build time and then rewrite incoming request to matching page based on its Host header.

For now we are happy with server-rendered pages, but we might want to use this cool feature in the future and switch to SSG. If you'd like to learn more about this implemenation, here is very cool example shared by vercel: https://github.com/vercel/examples/tree/main/edge-functions/hostname-rewrites

Mission completed

No more bugs and our app is live!

Do I regret time lost on configuring nginx and fighting with redirects? Not a bit! I’ve learned a lot about k8s, ingress, nginx, web requests, redirecting, proxing and wildcard domains. I wouldn’t have learned that much if we had jumped straight to server-side rendering.

It seems that the first solution is not always the best. Sometimes we have to take a step back and consider other available solutions to move forward! Thanks for reading! Keep learning!