NextJS for building SEO friendly, full stack app

NextJS is the framework for building production React apps. Its main selling point is its ability to pre-render pages. If you do a “View page source” on a React app built with create-react-app (CRA), you will see an almost empty body. There will only be a div element into which JavaScript renders all content on the client-side. This is great for building admin pages or internal apps but not so great for building content websites that the search engines crawl. With a NextJS app, the search engines see the HTML body populated with pre-rendered content. In this article, we will explore NextJS features by building a hospital website as an example.

Product features for a Hospital website

Hospitals need a website to let people know that they are around. At a minimum, the website should have a home page that shows the location, working hours, a cover photo and maybe some client testimonials. Apart from the home page, the website also lists the doctors in the hospital. For each doctor, there will be a doctor detail page that show the availability of the doctor. The website should also have a page to book doctor appointments. The following list shows the list of pages in our hospital website.

  • / or Home page – has location, timings, photo, client testimonials,
  • /doctors – Doctors working in the hospital
  • /doctor/<slug> – More information about doctors and availability
  • /appointment – For booking appointments

Let’s use NextJS to build our hospital website.

A) New NextJS project

Creating a new NextJS app is similar to creating a new CRA (create-react-app) app:

npx create-next-app hospital-app

The above command installs react, react-dom and next packages. It creates a few default scripts – one of which is yarn dev that sets up a server at localhost:3000. When you open the browser, it shows the default Next page. We will remove everything in the home page.

Open pages/index.js and clear the home page:

import styles from '../styles/Home.module.css';

export default function Home() {
  return <div className={styles.container}>Hello world</div>;
}

Open styles/Home.module.css and clear everything in it. This leaves us with a clean project.

The rest of the article explains each NextJS feature and shows some code relevant for that feature. Of course, the code pertains to our hospital app. But we will deviate from the tutorial mode and follow more of a guide mode.

B) Routes including a dynamic route

We don’t have react-router for our NextJS app. This is because the framework ships with its own page based routing. There is a pages folder in every app. Within the pages folder, there is an index.js file. This file exports a component that forms the Home page, a special page denoted by ‘/’ route.

We can create another page (or route) by creating a new file in the pages folder. For example, if we have a doctors.js file, that file will serve the page component for the /doctors route.

Another way to create a /doctors route exists. We can create a new folder named doctors. And within that folder, we can create an index.js file that serves the page component for that route.

Now, what if we want to create a separate page for each doctor? Within the doctors folder, we can create a special file with a strange name like ‘[doctorId].js’. This file provides the page component for a dynamic route, /doctors/<some-id>.

For our hospital app, the file structure within the pages folder looks like so:

File structure for hospital app
File structure for hospital app

We will cover the api folder a bit later. The _app.js is a special file that will be covered in the next section. And there is an appointment.js file for the appointment booking page. That page will have a route: /appointment.

C) Custom _app.js with Navbar

Most websites have a navbar that allows users to navigate the site. The navigation bar is common to all pages. Within our app, we want to create a Layout page that is common to all pages. The layout page will have the navbar. NextJS has an _app.js file that is a special file that allows us to create an app-wide layout page. The default implementation of _app.js looks like so:

import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

It accepts the page component as a prop and renders it. Now, let’s create the Layout component. Where do we create that component? – pages is a special folder. So, we can’t put it there. To create other components for our app, we will create a /src folder. Within the src folder, we will create multiple folders, each folder representing a product feature. For example, we will have a folder for home page, a folder for doctors page and a folder for appointment page. But the Layout page is common to all of these features. So, we will create a common folder and put the Layout component in there.

src folder structure
src folder structure

In our Layout component, we import Link from next/link. And use that to build a navbar.

import Link from 'next/link';

export default function Layout({ children }) {
  return (
    <div>
      <ul>
        <li>
          <Link href="/">Home</Link>
        </li>
        <li>
          <Link href="/doctors">Doctors</Link>
        </li>
        <li>
          <Link href="/appointment">Appointment</Link>
        </li>
      </ul>
      {children}
    </div>
  );
}

The ul tag represents the navbar. Underneath that, we render the children that is nothing but the page component. In _app.js file, we use the Layout component like so.

import Layout from '../src/common/Layout';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

export default MyApp;

With these changes, we have a navbar as shown in the video below.

Navbar in action
Navbar in action

In the next section, we will explore how to style components using CSS modules.

D) Styles using CSS Modules

In the _app.js file, there is an import to a styles file like so:

import '../styles/globals.css';

Importing a CSS file allows us to style components using a className. For example, add the following snippet to globals.css in the styles folder.

.center {
  text-align: center;
}

To use this CSS class, open pages/index.js and specify the className to the div tag.

export default function Home() {
  return <div className="center">Home page</div>;
}

Adding the center className to the div centers the text in the screen. Apart from the home page, we can use the center CSS class in other components. There is an alternate way to style components in NextJS. This is by using a technique called CSS modules.

CSS modules provides a mechanism to specify class names that apply only to the component using it. Let me explain that.

In styles folder, open Home.module.css file and add the following style:

.title {
  color: red;
}

Open src/index.js and import Home.module.css:

import styles from '../styles/Home.module.css';

Specify the className like so:

<div className={styles.title}>Home page</div>

Note the syntax of the className. It is a bit different. The actual className that Webpack assigns is very specific to the component, something like ‘Home_title__3DjR7’. This ensures that we can specify the same CSS class title with different styles in multiple CSS modules and still have the styles scoped to their respective components.

Now, let’s style the navbar using CSS module. In src/common folder, create a new file: Layout.module.css and add the following styles.

.list li {
  display: inline-block;
  padding: 1rem;
  list-style: none;
}

Import the CSS module in Layout.js

import styles from './Layout.module.css';

Assign the CSS class ‘list’ to the unordered list element.

className={styles.list}

For our page components, we cannot create CSS modules within the pages folder. So, we create them in the styles folder. But for other components in src folder, we can create the CSS modules close to the component within the same folder itself so that it is easy to edit.

E) Update page title and meta

For SEO, we have to set a distinct page title. There are also several meta tags that are useful for sharing links in the social media. We can set all this with the help of the ‘next/head’ package.

As an example, we can set these tags in pages/doctors/index.js,

import Head from 'next/head';

export default function Doctors() {
  return (
    <div>
      <Head>
        <title>Doctors working in XYZ Hospital</title>
        <meta
          name="description"
          content="Doctors working in XYZ Hospitals are listed here."
        />
      </Head>
      <div>Doctors page</div>
    </div>
  );
}

For each page, we set the title and meta tags. But, let’s say we want to add a meta tag to all pages, how do we do that? For that, we make use of Head tag in the Layout page.

import Head from 'next/head';
...

export default function Layout({ children }) {
  return (
    <div>
      <Head>
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      </Head>
      ...
    </div>
  );
}

These head tags are pre-rendered. Click on “View page source”. We should see the tags in the HTML.

F) Optimise images

NextJS offers an image component that optimises image delivery to the browser.

import Image from 'next/image';
import Avatar from '../public/images/avatar.png';
...
<Image src={Avatar} alt="User profile" width={80} height={80} />

In the above code, we use the Image component with a fixed width and height. This ensures that the browser gets a resized image with the exact dimensions. In addition, the Node server inspects the request information, finds the browser and delivers an image format that is ideal for the browser.

Image optimisation works well with a Node server. NextJS build consists of two parts: the usual client bundle (HTML, JS, CSS) and some NodeJS server code. Optionally, we can build a static site where there is no NodeJS server code. For static site generation, we don’t need image optimisation or the Image component.

G) Static props and static paths

Consider the doctor detail page in our hospital website. When the browser receives the HTML response, it should have all the doctor details that are stored in a database. How do we get the information stored in the database to appear in our Page component?

NextJS offers a getStaticProps function that our Page module exports. Remember that we have a Doctor Detail component in a dynamic route: [doctorId].js. Within that file, export the getStaticProps function like so:

export async function getStaticProps(context) {
  const { doctorId } = context.params;
  const doctorDetail = await fetchDoctorDetailsFromDatabase(doctorId);
  if (!doctorDetail) {
    return {
      notFound: true,
    };
  }
  return {
    props: doctorDetail,
    revalidate: 3600,
  };
}  

In the above code snippet, we get the doctorId from context.params. The params property contains the dynamic route segments. In our case, when the user navigates to /doctors/a41e, the last part is the doctorId. And we receive it through the context object in the params key.

After we receive the doctorId, we query the database for doctor details. If the doctor details are found, we attach it to the props of the component. If the doctor details are not found, then we redirect the user to 404 page.

The build process calls the getStaticProps function during build time, attaches the data to the props and pre-renders the page during the build time. However, we can instruct NodeJS server to call this function at runtime. The revalidate key is a special property that tells the Node server to call this function again after 3600 seconds. This ensures that we always have the latest doctor details. This feature has a name – incremental static generation.

There is another function getStaticPaths that our build process calls when we are using dynamic routes. The build process calls this function to compute the pages that it should render at build time.

export async function getStaticPaths() {
  doctorIds = await fetchDoctorIdsFromDatabase();
  const paths = doctorIds.map((doctorId) => ({
    params: { doctorId },
  }));
  return {
    paths,
    fallback: 'blocking',
  };
}

The above code snippet gets all the doctors from the database, specifically the IDs and then returns the route params. There is also a fallback key that is set to ‘blocking’. If there is a route with an unknown doctorId, then the server will run the getStaticProps function at runtime to attach the relevant props to the page component.

H) Server side rendering

If the hospital is small and there are only a few doctors, then we can generate all the doctors page during build time. But let’s say, we have 1000 doctors, and the doctor details are constantly changing in the database, we can’t afford to generate all the doctor details during build time. In such cases, we want the server to always fetch the latest doctor information from the database for every request. To enable this, we have the getServerSideProps function.

export async function getServerSideProps(context) {
  const { doctorId } = context.params;
  const doctorDetail = await fetchDoctorDetailsFromDatabase(doctorId);
  if (!doctorDetail) {
    return {
      notFound: true,
    };
  }
  return {
    props: doctorDetail
  };
}  

The code for getServerSideProps is identical to getStaticProps except that NodeJS calls this function for every request instead of during build time. In both cases, our page component receives the doctor details as props.

I) API routes for booking appointment

So far, we have seen some examples where the NodeJS server executes our code. If we specify the getServerSideProps function, the server executes this function for every request. NextJS can help us create full-stack apps with API routes.

Going back to our hospital website, booking an appointment with the doctor is a core feature. Usually, we have a button in our web page that the user clicks. Then the client code calls an API. The API saves the booking information in the database. And the client code intimates the user about the successful transaction.

NextJS allows us to write API code within the same app. Within the page folder, there is an API folder. This folder has all the APIs that we define.

export default async function handler(req, res) {
  const { doctorId } = req.query;
  if (req.method === 'GET') {
    // get all appointments for a doctor for the next 5 days
    const appointments = await getAppointmentsFromDb(doctorId);
    res.status(200).json({ appointments });
    return;
  }
  if (req.method === 'POST') {
    const { date, time, email, phone, name } = req.body;
    // save appointment time along with patient details
    await saveAppointmentInDB({ date, time, email, phone, name });
    res.status(201).json({ message: 'ok' });
    return;
  }
  res.status(500).json({ message: 'Unknown method' });
}

Instead of exporting a page component, we define a handler function with a specific signature. The handler function accepts the request and response parameter. In our handler, we handle the GET (for getting all appointments for a doctor) and POST (for saving appointment details) methods. We write the API in a similar way we write an Express API server.

We de-structure the doctorId from the request object, query key. With the doctorId, we query the database for appointments. We return the appointments as a JSON object with a status code of 200.

The handler also saves an appointment. From our client code, we send the appointment time and patient details. The API receives that information in the request object, body key. With that information, we create an appointment object and store it in the database. If the transaction is successful, we intimate the client / user with a status code of 201 and an OK message.

J) Environment variables

Whenever we use a database, we have sensitive information like usernames, passwords, etc that we have to store in environment variables. This is because the databases developers work with are different from the databases that our customers work with. We call the first environment as development environment and the latter as production environment.

In versions of NextJS prior to version 10, to store this information, we had to tinker with the next configuration file, next.config.js. But now, we have environment files specific to each environment.

  • .env file for default environment settings
  • .env.local file for environment settings for the local system
  • .env.development file for development environment settings
  • .env.production file for production environment settings

This is similar to how environment variables are stored in a create-react-app (CRA) app.

The server code typically uses these environment variables. But, if the browser bundle needs any of the environment variables, it is possible. Prefix the environment variable with ‘NEXT_PUBLIC_’ and reference this from the browser javascript as process.env.NEXT_PUBLIC_SOME_VAR.

Another point to note is that we have to reference the variable with ‘process.env.’ prefix. Object de-structuring won’t work with environment variables.

K) Build and Deployment

NextJS offers three types of build output:

  • Static site generation
  • Static site with incremental generation
  • Full stack app

Static site generation produces a client bundle and no server side bundles.

Static site with incremental generation has a tiny server part that keeps calling getStaticProps function once for every interval as specified in the revalidation key.

Full stack app has page components that use ‘getServerSideProps’ function or apps that define API routes.

The build command is next build. To optionally create a static site, use the next export command.

Build command produces its output in a .next folder.

NextJS build output
NextJS build output

Export command has its output in an out folder.

Static site generation
Static site generation

Note that static site has a HTML page for each defined route.

Deployment of a NextJS app is very straight forward. It requires a hosting service that supports a NodeJS server. Both Vercel and Netlify are good alternatives.

In both Vercel and Netlify, we specify the github repository. Whenever a commit happens in the master branch, the build process kicks in. And it copies the entire .next folder or the out folder to a destination hosting directory. The next start command runs the server.


In this article, we have covered the core NextJS features:

  • File based routing
  • Dynamic routes
  • Customising _app.js file
  • Styles using CSS modules
  • Head component for SEO optimisation
  • Image optimisation
  • Attaching data as props during build time
  • Pre-render pages with dynamic routes
  • Attaching data as props for every server request
  • API routes
  • Environment variables
  • Build process
  • Deployment

Hope you enjoyed this article.

Related Posts

2 thoughts on “NextJS for building SEO friendly, full stack app

Leave a Reply

Your email address will not be published.