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
- Informing Now about dynamic routes
- Fetching a blog post using Next's
getInitialProps
method - 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!
Top comments (2)
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:
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 :-)