loading...
Cover image for Dynamic routing for Now using NextJS

Dynamic routing for Now using NextJS

robbertvancaem profile image Robbert van Caem ・4 min read

When I looked into Server Side Rendering (SSR) for React, I quickly encountered NextJS. Getting the hang of it took me some time, but since then it's become a vital part of our digital products. Quickly I came across a very basic requirement: dynamic routing for blog posts.

The objectives

  1. Informing Now about dynamic routes
  2. Fetching a blog post using Next's getInitialProps method
  3. Using a HOC to render a custom error page when data can't be found

1. Informing Now about dynamic routes

At OOGT, we deploy our applications to Now. When you're using NextJS, deploying can be as simple as running now. It also comes with a handy now dev command so that you can locally mimic your Now deployment.

Now needs some knowledge about your application, and you can provide it through a now.json file. This typically looks something like this:

// now.json
{
  "name": "dynamic-routing",
  "version": 2,
  "builds": [{ "src": "next.config.js", "use": "@now/next" }],
  "routes": [
    { "src": "/", "dest": "/index" },
    {
      "src": "/_next/static/(?:[^/]+/pages|chunks|runtime)/.+",
      "headers": { "cache-control": "immutable,max-age=31536000" }
    }
  ]
}

Here, the name is the name of your Now project. The version field is the version of Now deployments you're using. The builds fields holds some configuration about your builder, which we won't go into right now. Finally, the routes field is what we're interested in right now. We'll add a new entry to this.

// now.json
{
  "name": "dynamic-routing",
  "version": 2,
  "builds": [{ "src": "next.config.js", "use": "@now/next" }],
  "routes": [
    { "src": "/", "dest": "/index" },
    { "src": "/(?<slug>[^/]+)", "dest": "/post?slug=$slug" },
    {
      "src": "/_next/static/(?:[^/]+/pages|chunks|runtime)/.+",
      "headers": { "cache-control": "immutable,max-age=31536000" }
    }
  ]
}

What this entry will do is match any URLs after the /, and reroute them to the Post page, where slug is a property of the query object in the request context. So, the URL in the browser will be /some-blog-post-title, but under the hood it renders the post page component with a value of "some-blog-post-title" for the slug property in the query object. Sounds more difficult than it actually is:

// pages/post.js
import React from 'react';

const Post = ({ slug }) => <div>{slug}</div>;

Post.getInitialProps = async ({ query }) => {
  const { slug } = query;

  return {
    slug
  }
}

export default Post;

In the code above, the getInitialProps method provided by NextJS can be used to do some async task like data fetching. It returns an object with the props that can be used by our functional Post component.

Next up, let's use this slug to fetch a blog post!

2. Fetching a blog post

In this example, we'll pretend we have a Wordpress instance running somewhere which contains the blog posts. We're going to use the REST API to fetch a blog post. We'll use axios for the requests since this works both server and client-side.

// data/posts.js
import axios from 'axios';

export const getPost = async (slug) => {
  const response = await axios.get(`https://www.mywordpress.com/wp-json/wp/v2/posts?slug=${slug}`);

  if (!response.data.length) {
    return {
      statusCode: 404,
    };
  }

  return {
    statusCode: 200,
    post: response.data[0],
  };
};

Notice how the returned object contains a statusCode of 404 if the returned response's data object doesn't contain any items. This is an indicator that the blog post does not exist. Otherwise, we'll return a statusCode of 200 and a post property containing the actual data.

Question to self: does the Wordpress REST API actually return a statusCode of 200 for a non-existent post? It would be easier to just use the returned statusCode instead of assuming an empty response means it does not exist. Also, isn't there an easier way of fetching a single post instead of using the first item in the returned array?

Now, we can use the getPost method in our Post component

// pages/post.js
import React from 'react';

import { getPost } from '../data/posts';

const Post = ({ statusCode, post }) => {
  if (statusCode !== 200) {
    return (
      <div>
        <h1>Oops</h1>
        <p>Something has gone wrong</p>
      </div>
    );
  }

  const { title: { rendered: title }, slug } = post;

  return (
    <div>
      <h1>{title}</h1>
      <p>{slug}</p>
    </div>
  );
}

Post.getInitialProps = async (slug) => {
  const { statusCode, post } = await getPost(slug);

  return {
    statusCode,
    post,
  }
}

export default Post;

Now, the above will work just fine for this particular use case. However, imagine we have another page rendering some post that may or may not exist. We could put an if-statement in every page component checking the statusCode, but we can also use a Higher Order Component (HOC) to render an error page whenever some data fetching method returns a statusCode other than 200.

3. Using a HOC to render a custom error page

You can define a custom error page by creating a file _error.js in the pages directory. A simple error page could look something like this:

// pages/_error.js
import React from 'react';

const Error = ({ statusCode }) => {
  let errorMessage = 'An unexpected error occured';

  if (statusCode === 404) {
    errorMessage = 'Page could not be found';
  }

  return (
    <div>
      <h1>Something went wrong</h1>
      <p>{errorMessage}</p>
    </div>
  )
}

export default Error;

To render the error page I found this HOC @tneutkens mentioned which makes it very easy to reuse a custom error page.

// hoc/withError.js
import React from 'react';

import Error from '../pages/_error';

export default Component => class extends React.Component {
  static async getInitialProps(ctx) {
    const props = await Component.getInitialProps(ctx);
    const { statusCode } = props;

    return { statusCode, ...props };
  }

  render() {
    const { statusCode } = this.props;
    if (statusCode && statusCode !== 200) {
      return <Error statusCode={statusCode} {...this.props} />;
    }
    return <Component {...this.props} />;
  }
};

We can now use this for our page like this:

// pages/post.js
import React from 'react';

import withError from '../hoc/withError';
import { getPost } from '../data/posts';

const Post = ({ post }) => {
  const { title: { rendered: title }, slug } = post;

  return (
    <div>
      <h1>{title}</h1>
      <p>{slug}</p>
    </div>
  );
}

Post.getInitialProps = async (slug) => {
  const { statusCode, post } = await getPost(slug);

  return {
    statusCode,
    post,
  }
}

export default withError(Post);

Got questions or feedback?

Do you know of an easier way of doing this? Or do you have any questions? I'm not claiming to be an expert in Now/NextJS whatsoever, but perhaps I can help digging into a problem you might run into.

Don't forget to start following me here, on Medium or on Twitter!

Discussion

markdown guide
 

Hey Robbert,

Nice article. I'm wondering maybe you have an idea about deploying a monorepo to Now. I have a yarn workspaces monorepo with 2 NextJs apps inside and 1 common package.
So:

root/
  common/ - Common functions
  web/ - Nextjs app
  publisher/ - NextJS app

And I'm trying to deploy them like domain.com/ points to/web & publisher.domain.com points to /publisher apps.

(Also I am using dynamic routing for localization like domain.com/en/profile or domain.com/es)

 

Hi Davíd,

Thanks for the kind words! :-) I personally don't have any experience with Next/Now in a monorepo, but I think it should be fairly easy to do this.

In your DNS settings you'll point both publisher.domain.com and domain.com to alias.zeit.co. The recommended way is to use Zeit's nameservers, but I haven't used that yet. I stuck with adding the CNAME records.

In both your web/ and publisher/ subdirectories you'll have a now.json file with an alias that corresponds with the domain. When deploying an app you'll cd into the corresponding directory and run now from there.

You'll end up with 2 projects in Now, both using the same domain. I'm not sure if this works but maybe it'll help you into the right direction :-)