Next.js
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.
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