Setting Up Meta Tags for a NextJS Website: A Concise Guide
Tags: nextjs, metatags, seo, pwa
Published: 2023-11-27 03:16:52

{% embed https://youtu.be/uqI6BwnIs8I %}

🐙 [GitHub](https://github.com/radzionc/reactkit)

### Understanding Meta Tags in NextJS for SEO and PWA Optimization

In this concise guide, we'll explore the essential meta tags required for a NextJS website, focusing on SEO and PWA (Progressive Web App) support. If you're transitioning from React Helmet, the `Head` component in NextJS might seem perplexing. Fear not—I'll simplify it for you. Moreover, you'll find all the reusable components and functions discussed in this article in the ReactKit repository.

### Leveraging Head Component in NextJS for Static and Dynamic Meta Tags

In NextJS, the `Head` component is utilized to insert meta tags into the page's head section. It's important to note that NextJS has two distinct `Head` components, which should not be confused. The first one is exclusively used within the `_document.tsx` file. This is where we define meta tags that remain constant across all pages and are not subject to change, as they cannot be overridden by the `Head` component used in individual pages.

```tsx
import Document, { Html, Main, NextScript, Head } from "next/document"
import { DocumentMetaTags } from "@georgian/ui/metadata/DocumentMetaTags"
import { IconMetaTags } from "icon/IconMetaTags"

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <DocumentMetaTags twitterId="@radzionc" />
          <IconMetaTags />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument
```

ReactKit offers a reusable component named `DocumentMetaTags` for defining meta tags that are consistent across all pages. This component includes links to the manifest file and a viewport meta tag. Additionally, it accepts optional props like `twitterId`, `image`, and `language`, which you can set if they are uniform for all pages. For instance, `twitterId` is typically used for Twitter cards and is less likely to vary across pages. If your site doesn't support multiple languages, you can set the `language` prop to "en". The `image` prop is particularly useful for social media sharing, as it determines the preview image of the page. For a blog, you might want to customize this image for each page. However, for websites where unique preview images are not necessary, setting it once in the document head is sufficient.

```tsx
interface DocumentMetaTagsProps {
  twitterId?: string
  image?: string
  language?: string
}

export const DocumentMetaTags = ({
  twitterId,
  image,
  language,
}: DocumentMetaTagsProps) => (
  <>
    <link rel="manifest" href="/manifest.json" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />

    {twitterId && <meta name="twitter:site" content={twitterId} />}

    {image && (
      <>
        <meta property="og:image" content={image} />
        <meta name="twitter:image:src" content={image} />
      </>
    )}

    {language && <meta httpEquiv="Content-Language" content={language} />}
  </>
)
```

### Automating Icon Meta Tags in NextJS for Enhanced PWA Support

We also utilize the `IconMetaTags` component within the document's head, as the icon remains consistent throughout the app. This component encompasses all essential icon images for your website, including those required for Progressive Web App (PWA) support. While it may appear complex, manual editing is unnecessary, thanks to the `generateIconMetaTags` function.

```tsx
// This file is generated by icon/codegen/generateIconMetaTags.ts
export const IconMetaTags = () => (
  <>
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <link rel="apple-touch-icon" href="images/icon/apple-icon-180.png" />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2048-2732.jpg"
      media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2732-2048.jpg"
      media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1668-2388.jpg"
      media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2388-1668.jpg"
      media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1536-2048.jpg"
      media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2048-1536.jpg"
      media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1668-2224.jpg"
      media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2224-1668.jpg"
      media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1620-2160.jpg"
      media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2160-1620.jpg"
      media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1290-2796.jpg"
      media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2796-1290.jpg"
      media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1179-2556.jpg"
      media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2556-1179.jpg"
      media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1284-2778.jpg"
      media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2778-1284.jpg"
      media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1170-2532.jpg"
      media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2532-1170.jpg"
      media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1125-2436.jpg"
      media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2436-1125.jpg"
      media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1242-2688.jpg"
      media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2688-1242.jpg"
      media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-828-1792.jpg"
      media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1792-828.jpg"
      media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1242-2208.jpg"
      media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-2208-1242.jpg"
      media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-750-1334.jpg"
      media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1334-750.jpg"
      media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-640-1136.jpg"
      media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
    />
    <link
      rel="apple-touch-startup-image"
      href="images/icon/apple-splash-1136-640.jpg"
      media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
    />
    <link
      rel="icon"
      type="image/png"
      sizes="196x196"
      href="images/icon/favicon-196.png"
    />
  </>
)
```

We place the function in our app's `icon/codegen` folder. This function requires a source icon, preferably in SVG format, such as `light-mode-icon.svg` located in the `icon` folder. Additionally, an icon for dark mode, `dark-mode-icon.svg`, is beneficial, especially for the splash screen. We use the `pwa-asset-generator` library to generate images and acquire the necessary meta tags. The output images are directed to the `public/images/icon` folder, and we specify the path to the `manifest.json` file, which contains various icon sizes. This function is customizable to fit specific requirements. Typically, the process involves generating icons for light mode first, then dark mode, and finally the favicon, as it should ideally have a transparent background. After `pwa-asset-generator` completes its task, we extract the meta tags from the output, eliminate duplicates, and integrate them into a component.

```tsx
import { createTsFile } from "@georgian/codegen/utils/createTsFile"
import { lightTheme } from "@georgian/ui/theme/lightTheme"
import { withoutDuplicates } from "@georgian/utils/array/withoutDuplicates"
import path from "path"
import fs from "fs"
import { generateImages } from "pwa-asset-generator"
import { darkTheme } from "@georgian/ui/theme/darkTheme"

const codeDirectory = path.resolve(__dirname, "../")

const lightModeIconPath = path.resolve(codeDirectory, "light-mode-icon.svg")
const darkModeIconPath = path.resolve(codeDirectory, "dark-mode-icon.svg")
const publicDirectory = path.resolve(__dirname, "../../public")
const iconImagesLocation = "images/icon"
const imagesOutputDirectory = path.resolve(publicDirectory, iconImagesLocation)
const manifestPath = path.resolve(publicDirectory, "manifest.json")

const generateIconMetaTags = async () => {
  const generatorOutput = [
    await generateImages(lightModeIconPath, imagesOutputDirectory, {
      manifest: manifestPath,
      background: lightTheme.colors.background.toCssValue(),
      iconOnly: true,
      pathOverride: iconImagesLocation,
    }),
  ]

  generatorOutput.push(
    await generateImages(lightModeIconPath, imagesOutputDirectory, {
      manifest: manifestPath,
      background: lightTheme.colors.background.toCssValue(),
      splashOnly: true,
      pathOverride: iconImagesLocation,
    })
  )

  if (fs.existsSync(darkModeIconPath)) {
    generatorOutput.push(
      await generateImages(darkModeIconPath, imagesOutputDirectory, {
        manifest: manifestPath,
        background: darkTheme.colors.background.toCssValue(),
        splashOnly: true,
        darkMode: true,
        pathOverride: iconImagesLocation,
      })
    )
  }
  generatorOutput.push(
    await generateImages(lightModeIconPath, imagesOutputDirectory, {
      manifest: manifestPath,
      opaque: false,
      iconOnly: true,
      favicon: true,
      type: "png",
      pathOverride: iconImagesLocation,
    })
  )

  const metaTags = withoutDuplicates(
    generatorOutput.flatMap((r) => Object.values(r.htmlMeta))
  )
    .join("")
    .replace(/>/g, "/>")

  const content = `export const IconMetaTags = () => <>${metaTags}</>`

  createTsFile({
    extension: "tsx",
    directory: codeDirectory,
    fileName: "IconMetaTags",
    content,
    generatedBy: "icon/codegen/generateIconMetaTags.ts",
  })
}

generateIconMetaTags()
```

Given that our project is structured as a monorepo with multiple packages involved in code generation, we utilize a reusable `createTsFile` function from the `codegen` package. This function formats the file content prior to saving it to the file system and appends a comment at the beginning of the file, indicating that it is auto-generated.

```tsx
import { formatCode } from "./formatCode"
import { createFile } from "./createFile"

interface CreateTsFileParams {
  extension?: "ts" | "tsx"
  directory: string
  fileName: string
  generatedBy: string
  content: string
}

export const createTsFile = async ({
  extension = "ts",
  directory,
  fileName,
  generatedBy,
  content,
}: CreateTsFileParams) => {
  const code = await formatCode({
    content: [`// This file is generated by ${generatedBy}`, content].join(
      "\n"
    ),
    extension,
  })

  createFile({
    directory,
    fileName,
    content: code,
    extension,
  })
}
```

### Utilizing the Head Component in NextJS for Page-Specific Meta Tags

Having discussed the document head, let's shift our focus to the `Head` component, which is employed on individual pages. This component allows the specification of page-specific meta tags, such as `title` and `description`, which usually vary from page to page. A crucial aspect to remember when working with NextJS's `Head` is that meta tags must be placed directly within the head; they cannot be isolated in a separate component. In contrast to the `DocumentMetaTags`, where we wrapped the meta tags in a fragment, in the `PageMetaTags` component, we have to wrap meta tags with `Head`.

```tsx
import Head from "next/head"

interface PageMetaTags {
  title?: string
  description?: string
  image?: string
  language?: string
}

export const PageMetaTags = ({
  title,
  description,
  image,
  language,
}: PageMetaTags) => (
  <Head>
    {title && (
      <>
        <title>{title}</title>
        <meta name="application-name" content={title} />
        <meta name="apple-mobile-web-app-title" content={title} />
        <meta property="og:title" content={title} />
        <meta name="twitter:title" content={title} />
      </>
    )}

    {description && (
      <>
        <meta name="description" content={description} />
        <meta property="og:description" content={description} />
        <meta name="twitter:description" content={description} />
        <meta property="og:image:alt" content={description} />
        <meta name="twitter:image:alt" content={description} />
      </>
    )}

    {image && (
      <>
        <meta property="og:image" content={image} />
        <meta name="twitter:image:src" content={image} />
      </>
    )}

    {language && <meta httpEquiv="Content-Language" content={language} />}
  </Head>
)
```