Setting Up Meta Tags for a NextJS Website: A Concise Guide

November 22, 2023

9 min read

Setting Up Meta Tags for a NextJS Website: A Concise Guide
Watch on YouTube

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 RadzionKit 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.

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

RadzionKit 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.

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.

// 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.

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.

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.

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>
)