next-meta
v0.4.0
Published
A composable React component for managing meta tags, Open Graph, and X/Twitter cards in Next.js applications.
Maintainers
Readme
🗣️ next-meta
A composable React component for managing meta tags, Open Graph, and Twitter cards in Next.js applications.
NOTE: This package is for use with Next.js' Pages Router. Some App Router helpers are in the works and will live here in the future as well.
Why next-meta?
Managing meta tags, Open Graph, and Twitter cards in Next.js applications can be tedious and error-prone. next-meta provides a simple, composable solution that:
- Reduces boilerplate code for meta tag management
- Ensures consistent meta tag structure across your application
- Provides TypeScript support out of the box
- Handles all major social media platforms (Open Graph, Twitter Cards, Pinterest)
- Supports dynamic meta tags based on page content
- Maintains SEO best practices
- Pairs with
react-structuredfor Schema.org structured data
Install
Via npm
npm install next-metaVia Yarn
yarn add next-metaHow to use
Basic Setup
Setting defaults within the Next.js App with MetaProvider.
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { MetaProvider } from 'next-meta'
// eslint-disable-next-line @typescript-eslint/ban-types
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
const BASE_URL = 'https://test.com'
const SITE_NAME = 'Example Site'
const DEFAULT_TITLE = 'An example title for using next-meta in your _app file.'
const DEFAULT_DESCRIPTION =
'Hopefully this makes things a little easier with adding good meta/og tags to your site.'
const DEFAULT_IMAGE = {
url: '/social-share.png',
alt: 'Default social share image',
width: 1200,
height: 630,
}
function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page)
const { asPath: metaUrl } = useRouter()
return (
<>
<Head>
<link rel="icon" type="image/png" href="/favicon.ico" />
</Head>
<MetaProvider
baseUrl={BASE_URL}
canonical={metaUrl}
description={DEFAULT_DESCRIPTION}
images={[DEFAULT_IMAGE]}
siteName={SITE_NAME}
title={DEFAULT_TITLE}
twitter={{
card: 'summary_large_image',
site: '@exampleSite',
}}
url={metaUrl}
>
{getLayout(<Component {...pageProps} />)}
</MetaProvider>
</>
)
}
export default CustomAppPage-Specific Meta Tags
Specify page-specific meta tags with PageMeta. It renders through
next/head, so it should not be wrapped in another Head component.
import { PageMeta } from 'next-meta'
const ExamplePage = () => {
return (
<>
<PageMeta
images={[
{
url: '/share/about-social.png',
alt: 'About page social share image',
width: 1200,
height: 630,
},
]}
title="About"
siteName="Example Site"
url="/about"
/>
{/* ...page code... */}
</>
)
}Advanced Usage
Here's an example showing more advanced features:
import { PageMeta } from 'next-meta'
const BlogPost = ({ post }) => {
return (
<>
<PageMeta
title={post.title}
description={post.excerpt}
images={[
{
url: post.featuredImage,
alt: post.featuredImageAlt,
width: 1200,
height: 630,
},
]}
url={`/blog/${post.slug}`}
twitter={{
card: 'summary_large_image',
creator: '@authorHandle',
}}
audio={
post.audioUrl
? [{ url: post.audioUrl, type: 'audio/mpeg' }]
: undefined
}
videos={
post.videoUrl
? [
{
url: post.videoUrl,
type: 'video/mp4',
width: 1920,
height: 1080,
},
]
: undefined
}
locale="en_US"
determiner="the"
article={{
author: post.authors,
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
section: 'Technology',
tag: post.tags,
}}
/>
{/* ...post content... */}
</>
)
}Schema.org Structured Data
next-meta focuses on meta tags, Open Graph, and social card metadata. For
Schema.org structured data, use
react-structured alongside
next-meta. When both packages are installed, React 16.14 or newer is required by
their combined peer dependency ranges.
npm install react-structuredyarn add react-structuredPages Router
Render the structured-data component alongside PageMeta. Do not nest it inside
PageMeta or next/head: Next.js requires scripts inside Head to be direct
children, while JSON-LD is valid in the document body.
import { PageMeta } from 'next-meta'
import { Schema } from 'react-structured'
const BlogPost = ({ post, nonce }) => {
const articleUrl = new URL(
`/blog/${post.slug}`,
'https://example.com',
).toString()
return (
<>
<PageMeta
title={post.title}
description={post.excerpt}
images={[{ url: post.featuredImage }]}
canonical={articleUrl}
url={articleUrl}
/>
<Schema
id="article-jsonld"
nonce={nonce}
type="Article"
data={{
headline: post.title,
image: post.featuredImage,
mainEntityOfPage: articleUrl,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
'@type': 'Person',
name: post.author.name,
},
publisher: {
'@type': 'Organization',
name: 'My Blog',
},
}}
/>
{/* ...post content... */}
</>
)
}Give each JSON-LD script a stable, document-unique id for inspection and
testing. Pass nonce when the application's Content Security Policy requires
one; otherwise omit it. Reuse the same absolute canonical and media URLs in
metadata and structured data so they cannot disagree.
App Router
next-meta v0.4 targets Pages Router. In App Router today, use Next.js's native
Metadata API and continue rendering react-structured in the page or layout.
The planned next-meta App Router adapter will replace only the metadata
construction in this pattern; JSON-LD will remain a sibling component.
// app/articles/example/page.tsx
import type { Metadata } from 'next'
import { Schema } from 'react-structured'
const ARTICLE_URL = 'https://example.com/articles/example'
const ARTICLE_IMAGE = 'https://example.com/articles/example/social.jpg'
export const metadata: Metadata = {
title: 'Example article',
alternates: { canonical: ARTICLE_URL },
openGraph: {
type: 'article',
url: ARTICLE_URL,
images: [ARTICLE_IMAGE],
},
}
export default function ArticlePage() {
return (
<>
<Schema
id="article-jsonld"
type="Article"
data={{
headline: 'Example article',
image: ARTICLE_IMAGE,
mainEntityOfPage: ARTICLE_URL,
}}
/>
<main>{/* ...article content... */}</main>
</>
)
}See the
react-structured README
for more Schema.org examples and TypeScript usage.
TypeScript Support
next-meta is written in TypeScript and provides type definitions out of the box. The package exports the following types:
PageMetaProps: Props for the PageMeta componentMetaProviderProps: Props for the MetaProvider componentSiteMetaProps: Deprecated compatibility alias forPageMetaPropsComposeMetaOptions: Controls repeatable metadata compositionImage: Type for image objectsVideo: Type for video objectsAudio: Type for audio objectsMetaTag: Type for additional meta tag objectsMetaContent: Type for additional meta tag contentMusicReference: Type for music song and album referencesTwitter: Type for Twitter card configurationTwitterApp: Type for Twitter app card platform configurationTwitterCard: Enum for Twitter card types
Components
PageMeta
Renders page metadata through next/head. It accepts PageMetaProps, including
custom children such as alternate feed links.
MetaProvider
Provides default PageMetaProps to descendant PageMeta components.
| Prop | Description |
| ------------------------------ | ------------------------------------------------------------------ |
| All PageMetaProps | Default metadata inherited by descendant PageMeta components. |
| skipDefaultsRender?: boolean | Provide defaults without immediately rendering a separate tag set. |
| children?: ReactNode | Application or page content that receives the defaults. |
SiteMeta (deprecated)
SiteMeta remains a compatibility alias for PageMeta during the v0.x
migration period. New code should use PageMeta.
Migrating from v0.3
v0.4 uses repeatable media arrays and groups X/Twitter settings under
twitter. The v0.3 names remain available as deprecated compatibility aliases,
but new code should use the current API:
| Deprecated v0.3 API | Current v0.4 API |
| ---------------------------------------------- | ---------------------------------------- |
| SiteMeta | PageMeta |
| SiteMetaProps | PageMetaProps |
| image or imageUrl, imageAlt, dimensions | images={[{ url, alt, width, height }]} |
| audioUrl, audioType | audio={[{ url, type }]} |
| videoUrl, videoType | videos={[{ url, type }]} |
| twitterCard, twitterCreator, twitterSite | twitter={{ card, creator, site }} |
| debug | No replacement; the option was a no-op. |
When current and deprecated image props are both supplied, images takes
precedence over image, and fields in image take precedence over their flat
deprecated equivalents. Nested twitter fields likewise take precedence over
the flat Twitter props.
Properties
next-meta supports the current Open Graph protocol basics, structured image,
audio, and video properties, and the object-type namespaces for article,
book, profile, music, video, and payment.link. For custom, uncommon,
or platform-specific meta tags, use additionalMetaTags.
Core Properties
| Prop | Description | Example |
| ---------------------------------- | -------------------------------------------------------- | ----------------------------------------------- |
| additionalMetaTags?: MetaTag[] | Additional custom meta tags. | [{ name: "robots", content: "index,follow" }] |
| baseUrl?: string | Base URL for all relative URLs. | "https://example.com" |
| canonical?: string | Canonical URL for the page. | "/blog/post-1" |
| composeMeta?: ComposeMetaOptions | Compose repeatable provider metadata into page metadata. | { images: true } |
| debug?: boolean | Deprecated no-op retained for compatibility. | true |
| description?: string | Page description for meta tags. | "Learn about our company" |
| determiner?: string | Word before object's title in a sentence. | "the" |
| locale?: string | Locale of site/page. | "en_US" |
| localeAlternates?: string[] | Alternate locales for the page. | ["en_CA", "fr_CA"] |
| siteName?: string | Site name for meta tags. | "My Blog" |
| siteNameDelimiter?: string | Delimiter between title and site name. | " - " |
| title?: string | Page title. | "About Us" |
| twitter?: Twitter | X/Twitter card overrides and configuration. | { card: "summary_large_image" } |
| type?: string | Open Graph type of the page. | "website" |
| url?: string | URL of page. | "/about" |
| pinterestDomainVerify?: string | Pinterest domain verification code. | "abc123xyz" |
Relative values for canonical, url, and media URLs are resolved against
baseUrl with the standard URL constructor. A trailing slash on a directory
base is significant: new URL('guide', 'https://example.com/docs/') resolves to
https://example.com/docs/guide.
Composing Repeatable Metadata
By default, page-level array props replace provider-level array props. To render
page-specific tags first and then append matching provider defaults, opt in with
composeMeta on MetaProvider:
<MetaProvider
skipDefaultsRender
composeMeta={{ images: true }}
images={[{ url: '/default-og.png', alt: 'Default share image' }]}
>
<PageMeta
title="Post title"
images={[{ url: '/post-og.png', alt: 'Post share image' }]}
/>
</MetaProvider>This renders the page image first, followed by the provider image. PageMeta
can override the provider policy for one page:
<PageMeta composeMeta={{ images: false }} images={[{ url: '/post-og.png' }]} />Supported composition fields are images, audio, videos,
localeAlternates, and additionalMetaTags. You can also pass
composeMeta={true} as shorthand for all supported fields. Passing an explicit
empty array, such as images={[]}, clears inherited values even when
composition is enabled.
When composing page-specific repeatable tags, skipDefaultsRender keeps the
provider defaults from rendering separately before the composed PageMeta
output.
Media Properties
| Prop | Description | Example |
| ------------------ | ------------------------------------------ | --------------------------------------------------------------------------------- |
| images?: Image[] | Array of image objects for social sharing. | [{ url: "/images/share.png", alt: "Description", width: 1200, height: 630 }] |
| audio?: Audio[] | Array of audio objects. | [{ url: "/podcast/episode1.mp3", type: "audio/mpeg", title: "Episode 1" }] |
| videos?: Video[] | Array of video objects. | [{ url: "/videos/tutorial.mp4", type: "video/mp4", width: 1920, height: 1080 }] |
Image Object
| Property | Type | Description |
| ----------- | ------------------ | ---------------------- |
| url | string | URL of the image |
| alt | string | Alt text for the image |
| secureUrl | string | HTTPS URL of the image |
| width | number \| string | Width of the image |
| height | number \| string | Height of the image |
| type | string | MIME type of the image |
Video Object
| Property | Type | Description |
| ------------- | ----------------------------------------- | ------------------------- |
| url | string | URL of the video |
| secureUrl | string | HTTPS URL of the video |
| type | string | MIME type of the video |
| width | number \| string | Width of the video |
| height | number \| string | Height of the video |
| duration | number \| string | Duration in seconds |
| actor | Array<{ name?: string; role?: string }> | Actors in the video |
| director | string \| string[] | Director(s) of the video |
| writer | string \| string[] | Writer(s) of the video |
| releaseDate | string | Release date of the video |
| tag | string \| string[] | Tags for the video |
| series | string | Series name if applicable |
Other Video Properties
Use videoOther for the og:video:other extension and its optional stream
metadata.
| Property | Type | Description |
| ------------------------------- | ------------------ | --------------------- |
| videoOther.url | string | Other video URL |
| videoOther.secureUrl | string | HTTPS other video URL |
| videoOther.type | string | Other video MIME type |
| videoOther.width | number \| string | Other video width |
| videoOther.height | number \| string | Other video height |
| videoOther.duration | number \| string | Other video duration |
| videoOther.stream.url | string | Stream URL |
| videoOther.stream.secureUrl | string | HTTPS stream URL |
| videoOther.stream.contentType | string | Stream MIME type |
| videoOther.stream.width | number \| string | Stream width |
| videoOther.stream.height | number \| string | Stream height |
| videoOther.stream.duration | number \| string | Stream duration |
Audio Object
| Property | Type | Description |
| ----------- | -------------------- | --------------------------- |
| url | string | URL of the audio file |
| secureUrl | string | HTTPS URL of the audio file |
| type | string | MIME type of the audio file |
| duration | number \| string | Duration in seconds |
| title | string | Title of the audio |
| artist | string \| string[] | Artist(s) of the audio |
| album | string | Album name |
Twitter Object
| Property | Type | Description |
| ------------- | ------------ | ------------------------------ |
| card | string | Twitter card type |
| title | string | Twitter-specific title |
| description | string | Twitter-specific description |
| site | string | Twitter username for site |
| siteId | string | Twitter numeric site ID |
| creator | string | Twitter username for author |
| creatorId | string | Twitter numeric creator ID |
| image | Image | Twitter-specific image and alt |
| app | TwitterApp | App card configuration |
| player | Player | Player card configuration |
Twitter Card Types
When using the twitter.card property, you can use one of the following values:
"summary"- Default card type"summary_large_image"- Large image card type"app"- App card type"player"- Player card type
Twitter App Object
| Property | Type | Description |
| ------------ | ------------ | ----------------------------- |
| country | string | App Store country code |
| name | string | Fallback app name |
| iPhone | TwitterApp | iPhone app card metadata |
| iPad | TwitterApp | iPad app card metadata |
| googlePlay | TwitterApp | Google Play app card metadata |
Twitter App Platform Object
| Property | Type | Description |
| -------- | -------- | ---------------------------- |
| name | string | Platform-specific app name |
| id | string | Platform-specific app ID |
| url | string | Platform-specific custom URL |
Player Object
| Property | Type | Description |
| -------- | ------------------ | -------------------- |
| url | string | URL of the player |
| width | number \| string | Width of the player |
| height | number \| string | Height of the player |
| stream | Stream | Stream configuration |
Stream Object
| Property | Type | Description |
| ------------- | -------- | -------------------------- |
| url | string | URL of the stream |
| contentType | string | Content type of the stream |
General Metadata Properties
| Prop | Type | Description |
| ---------------- | -------------------- | ---------------------------- |
| author | string \| string[] | Author(s) of the content |
| updatedTime | string | Last updated time (ISO 8601) |
| seeAlso | string \| string[] | Related URLs |
| richAttachment | boolean | Enable rich attachment |
| tag | string \| string[] | Tags for the content |
| section | string | Section of the site |
| publishedTime | string | Publication time (ISO 8601) |
| modifiedTime | string | Modification time (ISO 8601) |
| releaseDate | string | Release date |
| expirationTime | string | Expiration time (ISO 8601) |
| startTime | string | Start time (ISO 8601) |
| endTime | string | End time (ISO 8601) |
Location Properties
| Prop | Type | Description |
| --------------- | ------------------ | -------------------- |
| latitude | number \| string | Latitude coordinate |
| longitude | number \| string | Longitude coordinate |
| streetAddress | string | Street address |
| locality | string | City or locality |
| region | string | State or region |
| postalCode | string | Postal/ZIP code |
| countryName | string | Country name |
Contact Properties
| Prop | Type | Description |
| ------------- | -------- | ------------- |
| email | string | Email address |
| phoneNumber | string | Phone number |
| faxNumber | string | Fax number |
Product/Rating Properties
| Prop | Type | Description |
| --------------- | ---------------------------------------------------------------------------------- | ------------------------ |
| price | string \| number | Price of the product |
| availability | string | Availability status |
| isbn | string | ISBN for books |
| rating | { value?: number \| string; scale?: number \| string; count?: number \| string } | Rating information |
| reviewCount | number \| string | Number of reviews |
| points | number \| string | Points/rewards |
| restrictions | string \| string[] | Age/content restrictions |
| ageRating | string | Age rating |
| contentRating | string | Content rating |
Article-Specific Properties
| Prop | Type | Description |
| ------------------------ | -------------------- | ---------------------------- |
| article.author | string \| string[] | Article author(s) |
| article.publishedTime | string | Publication time (ISO 8601) |
| article.modifiedTime | string | Modification time (ISO 8601) |
| article.expirationTime | string | Expiration time (ISO 8601) |
| article.section | string | Article section |
| article.tag | string \| string[] | Article tags |
Book-Specific Properties
| Prop | Type | Description |
| ------------------ | -------------------- | -------------- |
| book.author | string \| string[] | Book author(s) |
| book.isbn | string | ISBN |
| book.releaseDate | string | Release date |
| book.tag | string \| string[] | Book tags |
Profile-Specific Properties
| Prop | Type | Description |
| ------------------- | -------- | ----------- |
| profile.firstName | string | First name |
| profile.lastName | string | Last name |
| profile.username | string | Username |
| profile.gender | string | Gender |
Music-Specific Properties
| Prop | Type | Description |
| ------------------- | ------------------------------------ | ----------------------- |
| music.duration | number \| string | Duration in seconds |
| music.album | MusicReference \| MusicReference[] | Album reference(s) |
| music.song | MusicReference \| MusicReference[] | Song reference(s) |
| music.musician | string \| string[] | Musician profile URL(s) |
| music.creator | string \| string[] | Creator profile URL(s) |
| music.releaseDate | string | Release date |
Music Reference Object
| Property | Type | Description |
| -------- | ------------------ | ----------------- |
| url | string | Song or album URL |
| disc | number \| string | Disc number |
| track | number \| string | Track number |
Payment-Specific Properties
| Prop | Type | Description |
| --------------------- | ------------------ | ------------------------ |
| payment.description | string | Payment link description |
| payment.currency | string | ISO 4217 currency code |
| payment.amount | number \| string | Payment amount |
| payment.expiresAt | string | Expiration datetime |
| payment.status | string | Payment status |
| payment.id | string | Payment identifier |
| payment.successUrl | string | Success redirect URL |
Additional Meta Tags
| Property | Type | Description |
| ----------- | ----------------------------- | ------------------------------- |
| name | string | Renders a name meta tag |
| property | string | Renders a property meta tag |
| httpEquiv | string | Renders an HTTP-equiv tag |
| itemProp | string | Renders an itemprop meta tag |
| charSet | string | Renders a charset meta tag |
| lang | string | Language for localized metadata |
| media | string | Media query for supported tags |
| scheme | string | Legacy metadata scheme |
| content | string \| number \| boolean | Meta tag content |
