logo

Next.js

Last Updated: 2023-09-23

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"]
}