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.
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} />}
</>
)
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,
})
}
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>
)