Lesson 09-Next.js Page Routing-Routing Fundamentals

Pages and Layouts

Basic Concepts of Pages and Layouts

Pages

Pages are the primary components of an application, with each page corresponding to a URL path.

Layouts

Layouts allow you to share common UI elements, such as headers, footers, or sidebars, across pages.

Organizing Pages and Layouts

File Structure

In Next.js 14, pages and layouts are typically placed in the pages directory. Each page can have its own layout file.

pages/
├── _app.js
├── _document.js
├── _error.js
├── home/
│   └── index.js
├── about/
│   └── index.js
├── blog/
│   └── [id].js
└── layouts/
    ├── header.js
    ├── footer.js
    └── defaultLayout.js

Default Layout (defaultLayout.js)

// pages/layouts/defaultLayout.js
import Header from './header';
import Footer from './footer';

export default function DefaultLayout({ children }) {
  return (
    <div>
      <Header />
      <main>{children}</main>
      <Footer />
    </div>
  );
}

Header Component (header.js)

// pages/layouts/header.js
export default function Header() {
  return (
    <header>
      <nav>
        <ul>
          <li>Home</li>
          <li>About</li>
          <li>Contact</li>
        </ul>
      </nav>
    </header>
  );
}

Footer Component (footer.js)

// pages/layouts/footer.js
export default function Footer() {
  return (
    <footer>
      <p{new Date().getFullYear()} My Website</p>
    </footer>
  );
}

Home Page (home/index.js)

// pages/home/index.js
import DefaultLayout from '../layouts/defaultLayout';

export default function HomePage() {
  return (
    <DefaultLayout>
      <h1>Welcome to Home Page</h1>
      <p>This is the home page content.</p>
    </DefaultLayout>
  );
}

About Page (about/index.js)

// pages/about/index.js
import DefaultLayout from '../layouts/defaultLayout';

export default function AboutPage() {
  return (
    <DefaultLayout>
      <h1>About Us</h1>
      <p>This is the about us page content.</p>
    </DefaultLayout>
  );
}

Dynamic Route Page (blog/[id].js)

// pages/blog/[id].js
import DefaultLayout from '../layouts/defaultLayout';

export default function BlogPost({ params }) {
  const postId = params.id;
  return (
    <DefaultLayout>
      <h1>Blog Post {postId}</h1>
      <p>This is the content of post {postId}.</p>
    </DefaultLayout>
  );
}

export async function getStaticPaths() {
  const posts = ['post1', 'post2', 'post3'];
  return {
    paths: posts.map((postId) => ({
      params: { id: postId },
    })),
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const { id } = params;
  // Fetch data for the specific post
  return {
    props: {
      id,
    },
  };
}

Summary

  • Default Layout: Use defaultLayout.js to create a basic layout that includes a header and footer.
  • Pages: Each page file can import the default layout and wrap its content with it.
  • Dynamic Routes: Use [id].js file structure to create dynamic route pages, with getStaticPaths and getStaticProps to generate static parameters and page data.

Dynamic Routes

Basic Concepts of Dynamic Routes

Dynamic routes are URLs that contain variable segments. In Next.js, you can create dynamic routes by using square brackets [ ] in the page file name.

Creating Dynamic Route Pages

To create a dynamic route page in Next.js, create a folder with square brackets in the my-app/pages directory. For example, to create a blog page where each post has a unique ID, structure it as follows:

my-app/
├── pages/
│   └── blog/
│       └── [id].js

app/pages/blog/[id].js

// app/pages/blog/[id].js
import DefaultLayout from '@/layouts/defaultLayout';

export default function BlogPost({ params }) {
  const postId = params.id;
  return (
    <DefaultLayout>
      <h1>Blog Post {postId}</h1>
      <p>This is the content of post {postId}.</p>
    </DefaultLayout>
  );
}

export async function generateStaticParams() {
  // Assume fetching post IDs from a database
  const posts = ['post1', 'post2', 'post3'];
  return posts.map((postId) => ({
    id: postId,
  }));
}

Parsing Dynamic Route Parameters

In the page component, access dynamic route parameters via the params object. In this example, params.id is the id parameter from the dynamic route.

Generating Static Parameters

The generateStaticParams function informs Next.js how to generate static parameters. It returns an array of objects, each representing a value for the dynamic parameter, ensuring Next.js pre-renders all possible dynamic route paths during build.

Generating Static Metadata

To generate specific metadata for each dynamic route, such as page titles or descriptions, use the generateMetadata function.

// app/pages/blog/[id].js
export async function generateMetadata({ params }) {
  const postId = params.id;
  return {
    title: `Post ${postId}`,
    description: `This is the description of post ${postId}.`,
  };
}

Summary

  • Dynamic Routes: Use [id].js file structure to create dynamic route pages.
  • Parse Parameters: Access dynamic route parameters via the params object.
  • Generate Static Parameters: Use generateStaticParams to generate a list of static parameters.
  • Generate Metadata: Use generateMetadata to generate metadata for each dynamic route.

Next.js provides a Link component to create links that optimize client-side navigation performance.

// pages/home/index.js
import Link from 'next/link';

export default function HomePage() {
  return (
    <div>
      <h1>Welcome to Home Page</h1>
      <p>This is the home page content.</p>
      <Link href="/about">
        <a>About Us</a>
      </Link>
    </div>
  );
}

Using the useRouter Hook

You can use the useRouter hook for programmatic navigation.

// pages/home/index.js
import { useRouter } from 'next/router';

export default function HomePage() {
  const router = useRouter();

  const handleNavigation = () => {
    router.push('/about');
  };

  return (
    <div>
      <h1>Welcome to Home Page</h1>
      <p>This is the home page content.</p>
      <button onClick={handleNavigation}>About Us</button>
    </div>
  );
}

Redirects

Using the redirect Function

In Next.js 14, you can use the redirect function to implement page redirects.

// pages/about/index.js
import { redirect } from 'next/navigation';

export default function AboutPage() {
  redirect('/home'); // Redirect to the homepage
  return null; // Return null to indicate the page will not render
}

Using next.config.js for Server-Side Redirects

You can configure server-side redirect rules in the next.config.js file.

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/old-about',
        destination: '/about',
        permanent: true,
      },
    ];
  },
};

Custom App

In Next.js 14, the Pages Router allows you to extend and customize your application using a custom app (_app.js) and custom document (_document.js). These files provide additional functionality, such as global styles, layout management, and page lifecycle hooks.

File Location

The _app.js file is located in the pages directory.

Uses

  • Global Layout: Add global layouts or components.
  • Page Lifecycle Hooks: Listen to page load and unload events.
  • State Management: Manage global state in _app.js.
// pages/_app.js
import DefaultLayout from '../layouts/defaultLayout';
import 'tailwindcss/tailwind.css'; // Import global styles

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

MyApp.getInitialProps = async (appContext) => {
  // Calls page's `getInitialProps` and fills `appProps.pageProps`
  const appProps = await App.getInitialProps(appContext);

  return { ...appProps };
};

export default MyApp;

Custom Document

File Location

The _document.js file is also located in the pages directory.

Uses

  • HTML Tags: Customize HTML <head> and <body> tags.
  • Global Styles: Add global styles.
  • SEO: Customize SEO-related meta tags.
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          <link rel="preconnect" href="https://fonts.gstatic.com" />
          <link
            href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
            rel="stylesheet"
          />
          <meta name="description" content="Generated by create next app" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

API Routes

Sending API Requests Using the fetch Function

Next.js 14 supports using the modern browser fetch function to send API requests.

// pages/home/index.js
import { useEffect, useState } from 'react';

export default function HomePage() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/data')
      .then((response) => response.json())
      .then((data) => setData(data))
      .catch((error) => console.error('Error fetching data:', error));
  }, []);

  return (
    <div>
      <h1>Welcome to Home Page</h1>
      <p>Data: {data?.message}</p>
    </div>
  );
}

Creating an API Route

Create an API route to handle requests and return data.

// pages/api/data.js
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello from the API!' });
}

Using useEffect and useState for Asynchronous Data Fetching

In client-rendered pages, use useEffect and useState to handle asynchronous data fetching.

// pages/home/index.js
import { useEffect, useState } from 'react';

export default function HomePage() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('/api/data');
      const data = await response.json();
      setData(data);
    }

    fetchData();
  }, []);

  return (
    <div>
      <h1>Welcome to Home Page</h1>
      <p>Data: {data?.message}</p>
    </div>
  );
}

Using getServerSideProps for Server-Side Data Fetching

For server-rendered pages, use the getServerSideProps function to fetch data.

// pages/home/index.js
export default function HomePage({ data }) {
  return (
    <div>
      <h1>Welcome to Home Page</h1>
      <p>Data: {data.message}</p>
    </div>
  );
}

export async function getServerSideProps() {
  const response = await fetch('/api/data');
  const data = await response.json();

  return {
    props: {
      data,
    },
  };
}

Summary

  • Using fetch: Use the fetch function in client-rendered pages to fetch data.
  • Using getServerSideProps: Use getServerSideProps in server-rendered pages to fetch data.
  • Using getStaticProps: Use getStaticProps in statically generated pages to fetch data.

Custom Errors

File Location

Custom error pages should be placed in the pages directory and named _error.js.

Uses

  • Handle 404 Errors: Display when a user visits a non-existent page.
  • Handle Other HTTP Errors: Display different error pages based on HTTP status codes.
// pages/_error.js
import Error from 'next/error';

function CustomErrorPage({ statusCode }) {
  return <Error statusCode={statusCode} />;
}

CustomErrorPage.getInitialProps = ({ res, err }) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
  return { statusCode };
};

export default CustomErrorPage;

Internationalized Routing

Internationalization Setup

Install next-i18next Package

Install the next-i18next package to support internationalization.

npm install next-i18next i18next react-i18next
# or
yarn add next-i18next i18next react-i18next

Configure next-i18next

Create a next-i18next.config.js file to configure internationalization.

// next-i18next.config.js
const path = require('path');

module.exports = {
  i18n: {
    locales: ['en', 'zh'], // Supported languages
    defaultLocale: 'en', // Default language
    localeDetection: false, // Disable automatic language detection
    localePath: path.resolve('./public/locales'), // Location for translation files
  },
  react: {
    useSuspense: false, // Disable Suspense to avoid SSR issues
  },
};

Create Translation Files

Create translation files in the public/locales directory.

public/
└── locales/
    ├── en/
    │   └── common.json
    └── zh/
        └── common.json

Example Content

// public/locales/en/common.json
{
  "welcome": "Welcome to our website!",
  "home": "Home",
  "about": "About"
}

// public/locales/zh/common.json
{
  "welcome": "欢迎来到我们的网站!",
  "home": "首页",
  "about": "关于我们"
}

Use next-i18next Component

Use the I18nextProvider component from next-i18next in _app.js.

// pages/_app.js
import { I18nextProvider } from 'next-i18next';
import { initI18next } from '../i18n';
import DefaultLayout from '../layouts/defaultLayout';

function MyApp({ Component, pageProps }) {
  const i18n = initI18next(pageProps.initialI18nStore, pageProps.locale);

  return (
    <I18nextProvider i18n={i18n}>
      <DefaultLayout>
        <Component {...pageProps} />
      </DefaultLayout>
    </I18nextProvider>
  );
}

export default MyApp;

Create i18n.js File

Create an i18n.js file to initialize i18next.

// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    detection: {
      order: ['cookie', 'htmlTag', 'path', 'subdomain'],
      caches: ['cookie'],
    },
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json',
    },
    react: {
      useSuspense: false,
    },
  });

export function initI18next(initialStore, initialLanguage) {
  i18n.init({
    resources: initialStore,
    lng: initialLanguage,
  });

  return i18n;
}

Use useTranslation Hook

Use the useTranslation hook in page components to access translated text.

// pages/home/index.js
import { useTranslation } from 'next-i18next';

export default function HomePage() {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <nav>
        <ul>
          <li><a href="/">{t('home')}</a></li>
          <li><a href="/about">{t('about')}</a></li>
        </ul>
      </nav>
    </div>
  );
}

Summary

  • Install next-i18next: Install the necessary packages.
  • Configure next-i18next: Set supported and default languages.
  • Create Translation Files: Create JSON files for each language.
  • Use I18nextProvider: Wrap the app in _app.js.
  • Use useTranslation Hook: Access translated text in page components.

Middleware

Creating Middleware

The middleware file should be placed at the same level as the pages directory and named middleware.js.

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  // Get the request URL
  const { pathname } = request.nextUrl;

  // Check if it's the login page
  if (pathname.startsWith('/login')) {
    return NextResponse.next();
  }

  // Check if the user is logged in
  const sessionCookie = request.cookies.get('session');
  if (!sessionCookie) {
    // If not logged in, redirect to the login page
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // If logged in, proceed with the request
  return NextResponse.next();
}

// Configure middleware matching rules
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

Parsing Middleware Configuration

middleware Function

This is the main middleware function, which takes a Request object as a parameter.

NextResponse Object

Use the NextResponse object to control middleware behavior, such as returning a response or redirecting.

config Object

This object specifies which paths the middleware applies to. In this example, API routes and certain static assets are excluded.

Example Explanation

In this example, a simple middleware checks if a user is logged in. If the user tries to access a non-login page without being logged in, the middleware redirects them to the login page.

Using Middleware

Once created, Next.js automatically loads and applies the middleware. You do not need to explicitly reference it in any pages or components.

Summary

  • Create middleware.js File: Place the middleware.js file at the same level as the pages directory.
  • Write Middleware Logic: Use the middleware function to handle requests.
  • Use NextResponse: Control middleware behavior with NextResponse.
  • Configure Matching Rules: Use the config object to specify middleware matching rules.
Share your love