logo

Next.js

Last Updated: 2023-10-21

app vs pages

  • app directory are Server Components by default.
  • pages directory where pages are Client Components.

You can use both /app and /pages folders, and /app folder takes precedence, and using both in the same project will cause conflict if there is similar routes in the two.

Pages Router was not designed for streaming, a cornerstone primitive in modern React.

Migration: https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app

Client Components vs Server Components

Client Components

  • Client Components can use state, effects, and event listeners, meaning they can provide immediate feedback to the user and update the UI.
  • Client Components have access to browser APIs, like geolocation or localStorage, allowing you to build UI for specific use cases.
  • add to the top of the file: 'use client'

Server Components

  • Server Components allow you to write UI that can be rendered and optionally cached on the server.
  • Static: With Static Rendering, routes are rendered at build time
  • With Dynamic Rendering, routes are rendered for each user at request time.

Markdown

Use MDX

If you use MDX, add this to every md or mdx page:

import Layout from 'path/to/Layout';
export default ({ children }) => <Layout>{children}</Layout>;

// Your markdown goes here

And add this to next.config.js:

const withMDX = require('@next/mdx')({
  extension: /\.(md|mdx)$/,
});

module.exports = withMDX({
  // Pick up MDX files in the /pages/ directory
  pageExtensions: ['js', 'jsx', 'md', 'mdx'],
});

No need to use dynamic routes.

Use Dyanmic Routes and Remark

To keep the markdown files "pure", keep the markdown files separately and use Dynamic Routes: create a file pages/[...slug].js.

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkHtml from 'remark-html';
import remarkGfm from 'remark-gfm';

export async function getStaticProps({ params }) {
  // read content using fs
  const page = getContentBySlug(params.slug);
  const markdown = await unified()
    .use(remarkParse)
    // Use Github flavor so tables and other features can be rendered correctly
    .use(remarkGfm)
    .use(remarkHtml)
    .process(page.content || '');
  const content = markdown.toString();

  return {
    props: {
      ...page,
      content,
    },
  };
}

export async function getStaticPaths() {
  const pages = getAllPages();

  return {
    paths: pages.map((page) => {
      return {
        params: {
          slug: page.slug,
        },
      };
    }),
    fallback: false,
  };
}

export default function Page(params) {
  // render the content
  return (
    <Layout>
      <div dangerouslySetInnerHTML={{ __html: params.content }} />
    </Layout>
  );
}

Note that remark is a shorthand for unified with remark-parse and remark-stringify. I.e. these 2 are equivalent:

const markdown = await remark()
  .use(...)
  .process(...)

const markdown = await unified()
  .use(remarkParse)
  .use(remarkStringify)
  .use(...)
  .process(...)
// remark source code
export const remark = unified().use(remarkParse).use(remarkStringify).freeze();

Google Analytics

Find the MEASUREMENT ID on analytics.google.com: Admin => Data Streams => Web stream details.

Use nextjs-google-analytics

$ npm i nextjs-google-analytics

Modify _app.js

import { GoogleAnalytics } from 'nextjs-google-analytics';

export default function MyApp({ Component, pageProps }) {
  return (
    <>
      <GoogleAnalytics trackPageViews gaMeasurementId="G-XXXXXXXXX" />
      <Component {...pageProps} />
    </>
  );
}

Use Script

Add this to any of the <Head>:

import Head from 'next/head';
import Script from 'next/script';

<Head>
  <Script async src="https://www.googletagmanager.com/gtag/js?id=XXXXXXXX" />
  <Script
    dangerouslySetInnerHTML={{
      __html: `
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());

        gtag('config', 'XXXXXXXX');
      `,
    }}
  />
</Head>;

Sitemap

Use nextjs-sitemap package.

Add this to next-sitemap.config.js file:

/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl: process.env.SITE_URL || 'https://example.com',
  generateRobotsTxt: true, // (optional)
  // ...other options
};

You may need to name the file next-sitemap.config.cjs instead if you have "type": "module" in your package.json.

Add this to package.json:

"scripts": {
  "postbuild": "next-sitemap"
}

If you named the file differently (e.g. next-sitemap.config.cjs):

"scripts": {
  "postbuild": "next-sitemap --config next-sitemap.config.cjs",
}

By default the generated sitemap is example.com/sitemap.xml

Image

Local image: no need to specify width and height since they are available at the build time.

import localImage from '/path/to/image.jpg';
import Image from 'next/image';

<Image src={localImage} />;

Remote image: need to specify width and height.

<Image src="http://example.com/image.png" width={400} height={500} />

How to use Node.js API vs Web API

Server-side only: Node.js API

To use path, fs, put them inside getStaticProps(), which is called at the build time on the server-side but not on the client-side:

import { promises as fs } from 'fs';
import path from 'path';

export async function getStaticProps(context) {
  const filepath = path.join(process.cwd(), 'path/to/file');
  const content = (await fs.readFile(filepath)).toString();
  return {
    props: {
      content: await Promise.all(content),
    },
  };
}

Client-side only: Web API

To use window or other Web APIs, use the useEffect hook, which only executes client-side:

import { useEffect } from 'react';

useEffect(() => {
  // You now have access to `window`
}, []);

Get Window Height

As mentioned above, window is available inside useEffect():

const [mapHeight, setMapHeight] = useState(0);

useEffect(() => {
  setMapHeight(window.innerHeight - 100);
}, []);

Deployment

Static

If your site is static, you can use next build && next export to export the static HTML. Generated files will be stored in the out folder. You can upload the folder to some cloud storage to serve the static site.

By default your pages will have .html suffix, like example.com/content/mypage.html, to remove the .html suffix, add trailingSlash: true to your next.config.js, then it actually generates files like /content/mypage/index.html, then it can be accessed by example.com/content/mypage.

Built-in Server

Otherwise Next.js has a built-in server. You can check Next.js's package.json, it depends on express.

Trouble Shooting

The default Firebase app already exists.

Solution: check if (!firebase.apps.length):

import firebase from 'firebase-admin';
import { initializeApp, cert } from 'firebase-admin/app';

if (!firebase.apps.length) {
  initializeApp({
    credential: cert(serviceAccount),
  });
}

Undefined router

The result of const router = useRouter(); may be undefined at the first rendering.

Solution: check router.isReady in useEffect:

const router = useRouter();
useEffect(() => {
  if (!router.isReady) return;
  // ...
}, [router.isReady]);

Lint

Install eslint-config-next:

$ npm i --save-dev eslint eslint-config-next

Add to your .eslintrc in your project folder:

{
  "extends": ["eslint:recommended", "next"]
}

Optimize for images and fonts

https://nextjs.org/docs/app/building-your-application/optimizing/images

font: CSS and font files are downloaded at build time and self-hosted with the rest of your static assets. No requests are sent to Google by the browser.

Variable fonts—officially known as OpenType Font Variations—remove the explicit distinctions between different weights and styles (more flexible in setting the parameters)

https://fonts.google.com/variablefonts

use roboto flex instead of roboto: https://fonts.google.com/specimen/Roboto+Flex