Every page on your website needs proper meta tags for search engines and social media. Next.js App Router provides a Metadata API that makes this straightforward and type-safe.
Static Metadata
For pages with static content, export a metadata object:
// app/about/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About Us | Your Company",
description: "Learn about our team, mission, and approach to building custom software.",
openGraph: {
title: "About Us | Your Company",
description: "Learn about our team, mission, and approach to building custom software.",
url: "https://yourdomain.com/about",
siteName: "Your Company",
images: [
{
url: "https://yourdomain.com/images/og/about.jpg",
width: 1200,
height: 630,
alt: "Your Company team",
},
],
type: "website",
},
twitter: {
card: "summary_large_image",
title: "About Us | Your Company",
description: "Learn about our team and mission.",
images: ["https://yourdomain.com/images/og/about.jpg"],
},
};
export default function AboutPage() {
return <main>...</main>;
}
Dynamic Metadata
For pages with dynamic content (blog posts, product pages), use generateMetadata:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.metaTitle,
description: post.metaDescription,
openGraph: {
title: post.metaTitle,
description: post.metaDescription,
url: `https://yourdomain.com/blog/${slug}`,
type: "article",
publishedTime: post.date,
authors: [post.author],
images: [
{
url: `https://yourdomain.com/api/og?title=${encodeURIComponent(post.title)}`,
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
title: post.metaTitle,
description: post.metaDescription,
},
};
}
Root Layout Metadata
Set default metadata in your root layout that applies to all pages:
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://yourdomain.com"),
title: {
default: "Your Company | Custom Software Development",
template: "%s | Your Company",
},
description: "Custom web design, development, and software solutions for businesses.",
openGraph: {
type: "website",
siteName: "Your Company",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
creator: "@yourhandle",
},
robots: {
index: true,
follow: true,
},
};
The title.template pattern means page-level titles like "About Us" become "About Us | Your Company".
metadataBase is critical β it resolves relative URLs in your Open Graph images to absolute URLs.
Dynamic Open Graph Images
Generate OG images dynamically using Next.js's built-in ImageResponse:
// app/api/og/route.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") ?? "Your Company";
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0a0a0a",
color: "#ffffff",
padding: "60px",
}}
>
<div style={{ fontSize: 60, fontWeight: 700, textAlign: "center" }}>
{title}
</div>
<div style={{ fontSize: 24, marginTop: 20, color: "#a3a3a3" }}>
yourdomain.com
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
}
Or use the file-based approach with opengraph-image.tsx:
// app/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "Your Company";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default function Image() {
return new ImageResponse(
(
<div style={{ /* your design */ }}>
Your Company
</div>
),
{ ...size }
);
}
Essential Meta Tags Checklist
Every Page
-
<title>: Unique, under 60 characters -
meta description: Unique, under 160 characters -
og:title: Matches or slightly differs from title -
og:description: Matches or slightly differs from description -
og:image: 1200x630px image -
og:url: Canonical URL -
og:type: "website" or "article" -
twitter:card: "summary_large_image"
Blog Posts (Additional)
-
article:published_time: ISO date -
article:author: Author name -
article:tag: Relevant tags
Robots
-
robots: "index, follow" for public pages -
robots: "noindex" for utility pages (thank you, success)
Canonical URLs
Prevent duplicate content issues:
export const metadata: Metadata = {
alternates: {
canonical: "https://yourdomain.com/about",
},
};
JSON-LD Structured Data
Add structured data alongside meta tags:
export default function AboutPage() {
const jsonLd = {
"@context": "https://schema.org",
"@type": "AboutPage",
name: "About Your Company",
description: "Learn about our team and mission.",
url: "https://yourdomain.com/about",
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<main>...</main>
</>
);
}
Testing
- Google Rich Results Test: Validate structured data
- Facebook Sharing Debugger: Preview OG tags
- Twitter Card Validator: Preview Twitter cards
- LinkedIn Post Inspector: Preview LinkedIn sharing
- Open Graph Preview: opengraph.xyz for quick checks
Need SEO Optimization?
We configure comprehensive SEO, meta tags, structured data, and OG images for every site we build. Contact us for SEO and web development services.