Guiding the Next.js Maze: An Exploration of Next.js

Next.js is a powerful framework that allows you to build web applications with ease. In this blog post, I will guide you through the maze of Next.js and show you how to effectively use its features.

Guiding the Next.js Maze: An Exploration of Next.js

Next.js is a powerful framework that allows you to build web applications with ease. It provides a wide range of features that make it easy to create dynamic and interactive web applications. In this blog post, I will guide you through the maze of Next.js and show you how to effectively use its features.

Introduction

Next.js is a React framework that provides a powerful set of tools for building web applications. It supports server-side rendering (SSR) and static site generation (SSG), making it easy to create fast and efficient web applications. Next.js also provides a wide range of features that make it easy to build dynamic and interactive web applications.

Getting Started with Next.js

To get started with Next.js, you need to install it using npm or yarn. You can create a new Next.js project using the following command:

npx create-next-app my-next-app

Or with pnpm:

pnpm create next-app my-next-app

Note: You can replace my-next-app with the name of your project.

Find more information on Next.js documentation: Installation.

Next.js Features

Next.js provides a wide range of features that make it easy to build web applications. Some of the key features of Next.js include:

  • Server-side rendering (SSR)
  • Static site generation (SSG)
  • Dynamic routing
  • API routes
  • Image optimization
  • (S)CSS support
  • TypeScript support
  • Active Caching
  • Incremental Static Regeneration (ISR)
  • Environment Variables
  • and many more...

In this blog post, we will cover the following features of Next.js:

  • Server-side rendering (SSR)
  • Static site generation (SSG)
  • SEO optimization
  • Open Graph social media metadata
  • Cache management
  • Environment variables

Version: The version of Next.js used in this blog post is 14.2.3.

Info: In this 'next' section, we will explore some of these features in more detail.

App structure:

The structure of a Next.js application is heavily reliant on the developer's preference. However, a common structure is as follows:

my-next-app
├── src/
│   ├── components/
│   ├── app/
│   |  ├── api/[webhook]/
│   |  |  ├── route.ts
│   |  ├── layout.tsx
│   |  ├── page.tsx
│   |  ├── blog/[...slug]/
|   |  |  ├── page.tsx
|   |  |  ├── _actions.ts
|   ├── lib/
|   |  ├── utils.ts
|   |  ├── env.ts
|   |  ├── db.ts

Server-side Rendering (SSR)

Server-side rendering (SSR) is a key feature of Next.js that allows you to render web pages on the server before sending them to the client. This can help improve the performance of your web application by reducing the time it takes to load pages.

Example:

// src/app/page.tsx
export default async function Page() {
  const data = await fetchData();
  return (
    <div>
      <h1>Hello, Next.js!</h1>
      <p>{data}</p>
    </div>
  );
}

By marking the page as async, Next.js will render the page on the server before sending it to the client.

This also comes with the benefit of SEO optimization, as search engines can easily crawl and index your web pages.

Client Components

However this gets tricky when you have to deal with client-side state or events such as button clicks or window events.

You can easiliy overcome this by wrapping you component with a server side rendered component:

// src/components/client-component.tsx
"use client";
 
import { useEffect, useState } from "react";
 
export default function ClientComponent() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    console.log("ClientComponent mounted");
  }, []);
 
  return (
    <div>
      <h2>Client Component</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
// src/app/page.tsx
import ClientComponent from "@/components/client-component";
 
export default async function Page() {
  return (
    <div>
      <h1>Hello, Next.js!</h1>
      <ClientComponent />
    </div>
  );
}

Static Site Generation (SSG)

Next.js offers a feature called static site generation (SSG) that allows you to generate static HTML files for your web pages at build time. This can help improve the performance of your web application by reducing the time it takes to load pages.

You can make use of this feature by using the generateStaticParams function:

// src/app/blog/[...slug]/page.tsx
 
export const generateStaticParams = async () => {
  const slugs = await fetchSlugs();
  return slugs.map((slug) => ({ params: { slug } }));
};
 
export default async function Page({ params: { slug } }) {
  const data = await fetchData(slug);
  return (
    <div>
      <h1>{slug}</h1>
      <p>{data}</p>
    </div>
  );
}

Next.js will then use the same syntax as SSR to build the static HTML files : SSG, leading to faster load times.

SEO Optimization

With Next.js, you can easily optimize your web application for search engines by using the metadata or generateMetadata function:

// src/app/blog/[...slug]/page.tsx
export const generateMetadata = async ({
  params: { slug },
}: {
  params: { slug: string };
}) => {
  const data = await fetchData(slug);
  return {
    title: data.title,
    description: data.description,
  };
};
 
export default async function Page({ params: { slug } }) {
  const data = await fetchData(slug);
  return (
    <div>
      <h1>{slug}</h1>
      <p>{data}</p>
    </div>
  );
}

This results in better SEO optimization for your web application, as search engines will be able to see the metadata and index your web pages more effectively.

Read more about SEO. And Next.js Metadata.

Open Graph Social Media Metadata

Next.js also allows you to easily add Open Graph metadata to your web pages, which can help improve the way your web pages are shared on social media platforms.

Open Graph from Dynamic Data

You can do this by using the generateMetadata function, then using the openGraph object to define the metadata for the Open Graph tags:

// src/app/blog/[...slug]/page.tsx
export const generateMetadata = async ({
  params: { slug },
}: {
  params: { slug: string };
}) => {
  const { meta_description, meta_title, ...data } = await fetchData(slug);
  return {
    title: data.title,
    description: data.description,
    openGraph: {
      title: meta_title,
      description: meta_description,
      images: [
        {
          url: `/api/og/?title=${truncate(meta_title, 50)}&description=${truncate(
            meta_description,
            120,
          )}`,
          width: 1200,
          height: 630,
          alt: meta_title,
          type: "image/png",
        },
      ],
      type: slug.split("/").length > 1 ? "article" : "website",
      url: `${env.NEXT_PUBLIC_FRONTEND_URL}/${slug}`,
    },
  };
};
 
export default async function Page({ params: { slug } }) {
  // ...
}

Open Graph API Route

This will generate the Open Graph metadata for your web pages, allowing you to easily share them on social media platforms.

You then can make this API route with your own logic to generate the Open Graph image.

Next.js provides a built-in ImageResponse object that allows you to easily generate images on the fly:

// src/app/api/og/route.tsx
// Note: This is a .tsx file
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
 
/**
 * Takes in a request with a title and description query parameter and returns an image response
 *
 * Returns an image with a width of 1200 and a height of 630
 *
 * @param request {NextRequest} - The incoming request
 * @returns {ImageResponse} - The image response
 */
export async function GET(request: NextRequest): Promise<ImageResponse> {
  const url = new URL(request.url);
  const searchParams = url.searchParams;
  const title = searchParams.get("title") || "OpenGraph";
  const description = searchParams.get("description") || "Crafted with Next.js";
 
  return new ImageResponse(
    (
      <div
        style={{
          background: "white",
          color: "black",
          padding: "1rem 2.25rem",
          display: "flex",
          width: "100%",
          height: "100%",
          textAlign: "center",
          justifyContent: "center",
          alignItems: "center",
          flexDirection: "column",
        }}>
        <h1
          style={{
            fontSize: "3rem",
            fontWeight: "bolder",
            lineHeight: "1.2",
          }}>
          {title}
        </h1>
        <p
          style={{
            color: "gray",
            fontSize: "1.25rem",
            lineHeight: "1.2",
          }}>
          {description}
        </p>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

This will create an image based on the data passed in, meaning it can generate images on the fly for your own data.

Read more about Open Graph. Or Next.js ImageResponse.

Cache Management

Next.js provides a built-in cache management system that allows you to cache data on the server and client side. This can help improve the performance of your web application by reducing the time it takes to load data.

cache function

You can make use of the cache function from React.js to cache data per request for when a function can be called more then once in a request lifecycle:

// src/app/blog/[...slug]/page.tsx
import { cache } from "react";
 
export const fetchData = cache(async (slug: string) => {
  const response = await fetch(`${API_URL}/${slug}`);
  return response.json();
});
 
// ... metadata as above
 
export default async function Page({ params: { slug } }) {
  const data = await fetchData(slug);
  return (
    <div>
      <h1>Hello, Next.js!</h1>
      <p>{data}</p>
    </div>
  );
}

This will cache the results based on the input parameter, so that the next time the function is called with the same parameter, the cached result will be returned instead of making a new request.

This is however not the only way to cache data in Next.js, you can also use the fetch function to cache requests on the lifecycle of the entire application, meaning it will be quick and easy when you need to fetch the same data again.

Read more about React cache. The cache function is as of this writing still in the experimental phase.

fetch function

Next.js has modified the fetch function to cache every request, you can opt out of this by using the no-cache or no-store option:

// src/app/page.tsx
export default async function Page() {
  const response = await fetch(`${API_URL}`, { cache: "no-store" });
  // ...
}

Read more about fetch. And what Next.js does with fetch.

To take control over your cache, use the next.tags inside the fetch function:

// src/app/page.tsx
import { cache } from "react";
 
export const fetchData = cache(async (slug: string) => {
  const response = await fetch(`${API_URL}/${slug}`, {
    next: { tags: ["x-data", `x-${slug}`] },
  });
  return response.json();
});

This will add the x-data and x-${slug} tags to the cache, allowing you to easily invalidate the cache when needed.

You can invalidate the cache by using the revalidateTag function:

import { revalidateTag } from "next";
 
revalidateTag("x-data");

Any requests being made with the x-data tag will be re-fetched and the cache will be updated.

Read more about revalidateTag. Or close related: revalidatePath. For the full overview of the Caching behavior, check out the Next.js documentation: fetching-caching-and-revalidating and Next.js documentation: Caching.

Environment Variables

Next.js allows you to easily manage environment variables in your web application. You can define environment variables in a .env file and access them in your code using the process.env object.

Combining this with zod for type checking, you can easily manage your environment variables:

// src/lib/env.ts
import { z } from "zod";
 
const envSchema = z.object({
  API_URL: z.string(),
  API_KEY: z.string(),
});
 
export const env = envSchema.parse(process.env);
// src/app/page.tsx
import { env } from "@/lib/env";
 
export default async function Page() {
  const response = await fetch(`${env.API_URL}`, {
    headers: {
      "x-api-key": env.API_KEY,
    },
  });
  // ...
}

This allows you to easily manage your environment variables and ensure that they are correctly defined and used in your web application.

Check out the Zod documentation for more information about the zod library. Check out the Next.js documentation: Environment Variables for more information about environment variables.

A cool library that does this for you is @t3-oss/env-nextjs which allows you to easily manage your environment variables in a Next.js application.

It's usage is very similar to the above example:

// src/lib/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  server: z.object({
    API_URL: z.string(),
    API_KEY: z.string(),
  }),
  client: z.object({}),
  runtimeEnv: {
    API_URL: process.env.API_URL,
    API_KEY: process.env.API_KEY,
  },
});

Conclusion

Next.js is a powerful framework that allows you to build web applications with ease. It provides a wide range of features that make it easy to create dynamic and interactive web applications. In this blog post, I have guided you through the maze of Next.js and shown you how to effectively use its features.

Some features may be more complex than others, but with practice and experience, you will be able to master the art of Next.js and build amazing web applications.