Skip to content

Titles, Meta Descriptions, and Social Cards: The Next.js Metadata Playbook for Every Business Size

Your title tag is the ad you never wrote. Here’s how to make Next.js render one that gets clicked, whatever size you are.

John Cravey with EleviFounder11 min read

Most of the SEO work a visitor sees before they click happens in three lines of text: the title in the search result, the description under it, and the card that unfurls when someone pastes your link into Slack or LinkedIn. Get them right and you earn clicks you already ranked for. Get them wrong and you rank on page one and still lose the visit to a competitor with a sharper title. Next.js gives you a first-class Metadata API to control all three from your code, generated server-side so Google and every social scraper see the same thing your users do. This is the playbook we run on every Frontend Horizon build, retold for whatever size you are.

What the Metadata API actually does

In the Next.js App Router, you describe a page’s `<head>` with data, not by hand-writing tags. There are two ways to do it. For a page whose metadata never changes, you export a static `metadata` object. For a page whose metadata depends on data — a blog post, a product, a location page — you export an async `generateMetadata` function that fetches what it needs and returns the same shape. Next turns either one into the real `<title>`, `<meta name="description">`, canonical link, and Open Graph tags at render time.

// app/services/page.tsx — static metadata
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Services — Web, SEO, and Lead Systems | Acme Co",
  description:
    "Five services, each priced against a named outcome. See what ships in the first 30 days.",
  alternates: { canonical: "/services" },
  openGraph: {
    title: "Services that ship in 30 days — Acme Co",
    description: "Web, SEO, and lead systems for local businesses.",
    type: "website",
  },
};

The `generateMetadata` variant is where dynamic sites earn their keep. It receives the route params, so a single file at `app/blog/[slug]/page.tsx` produces a unique, accurate title and description for every post you publish — no per-page hand-editing, no stale copy.

// app/blog/[slug]/page.tsx — dynamic metadata
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: `${post.title} | Acme Co`,
    description: post.description,
    alternates: { canonical: `/blog/${slug}` },
  };
}

titleTemplate and metadataBase: set these once, in the root layout

Two settings belong in your root `layout.tsx` and pay off on every page under it. The first is a title template, so you write the page-specific part once and the brand suffix is appended automatically. The second is `metadataBase`, a single absolute origin that lets you write relative Open Graph and canonical paths everywhere else and have Next resolve them to full URLs — which is what social scrapers and search engines require.

// app/layout.tsx — root
export const metadata: Metadata = {
  metadataBase: new URL("https://acme.co"),
  title: {
    default: "Acme Co — Web, SEO, and Lead Systems",
    template: "%s | Acme Co",
  },
  description: "We build sites that get found and turn visits into booked work.",
};

Now any child page that returns `title: "Pricing"` renders as `Pricing | Acme Co`, and any `openGraph.images: ["/og/pricing.png"]` resolves to the absolute URL a scraper can actually fetch. Miss `metadataBase` and your social cards silently ship with broken relative image paths — a bug that passes every build and only shows up when someone shares your link and gets a blank rectangle.

When your URL gets pasted into a message, a DM, or a LinkedIn post, the receiving app scrapes your Open Graph tags to build the preview card. A good card lifts click-through on shared links noticeably; a missing one ships a naked URL that nobody clicks. Next gives you two ways to supply the image. The simplest is a file convention: drop an `opengraph-image.jpg` in a route folder and Next wires it up. The more powerful is generating the image from data with the `ImageResponse` constructor from `next/og`, so every blog post or product gets a unique card built from its own title.

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";

export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  return new ImageResponse(
    (
      <div style={{ display: "flex", width: "100%", height: "100%",
        alignItems: "center", justifyContent: "center", fontSize: 64,
        background: "#0B0A12", color: "white", padding: 80 }}>
        {post.title}
      </div>
    ),
  );
}

The mistakes that quietly cost you clicks

  1. Duplicate titles across pages. If every location page reads “Services | Acme Co,” Google can’t tell them apart and picks one to rank. Make the variable the subject: “Roof Repair in Plano | Acme Co.”
  2. Descriptions that repeat the title instead of selling the click. The description is ad copy. Name the outcome the searcher wants, in their words.
  3. Titles over ~60 characters. Google truncates them. Front-load the words that matter; put the brand at the end where truncation is harmless.
  4. Missing canonical on paginated or filtered routes, splitting ranking signals across URL variants.
  5. No Open Graph image, so every share is a dead link nobody clicks.

Twitter cards, favicons, and the rest of the head you don’t hand-write

Open Graph covers most social platforms, but X (Twitter) reads its own `twitter:` tags, and the Metadata API has a first-class field for them. Set `twitter.card` to `summary_large_image` and you get the big-image treatment instead of a thumbnail. You can reuse your OG title and description or write platform-specific ones. The meta tag reference on MDN is the canonical list of what a `<head>` can carry, but you rarely touch most of it directly — Next generates the charset and viewport tags for every page automatically, and the Metadata API covers the rest through typed fields.

export const metadata: Metadata = {
  title: "Pricing",
  twitter: {
    card: "summary_large_image",
    title: "Simple, outcome-based pricing — Acme Co",
    description: "No retainers you can't explain. Priced against a result.",
  },
  icons: { icon: "/favicon.ico", apple: "/apple-icon.png" },
};

Favicons follow the same file-convention idea as OG images: drop a `favicon.ico`, `icon.png`, or `apple-icon.png` in your `app` folder and Next wires the tags. That favicon is the tiny icon in the browser tab and, more importantly for SEO, the icon Google shows next to your result on mobile — a small brand touch that helps your listing stand out in a crowded results page.

A worked example: turning a weak title into clicks

Say you run a roofing company and your services page ships with `title: "Services"` and `description: "Learn about the services we offer."` That page might rank on page one for “roof repair plano” and still lose the click, because the title tells the searcher nothing and the description is filler. Here’s the rewrite. The title becomes `"Roof Repair & Replacement in Plano, TX | Acme Roofing"` — it leads with what the searcher typed, names the location, and puts the brand where truncation is harmless. The description becomes `"Same-day roof repair estimates in Plano. Licensed, insured, 4.8★ over 300 jobs. Call by 2pm for next-day service."` — it names the outcome, adds proof, and gives a reason to act now. Same ranking, same page, dramatically different click-through, and it’s two lines of code in `generateMetadata`. Multiply that across every service and location page and you’ve recovered traffic you already earned without touching your rankings at all. This is the highest-ROI SEO work most sites never do, because the tags are invisible until you go looking.

Fetch the data once, not twice

There’s a subtle performance trap in dynamic metadata: `generateMetadata` and the page component often need the same data — the post, the product, the location. Fetch it in both and you’ve doubled your database or API calls on every render. React’s `cache` function fixes this by memoizing the fetch, so calling `getPost(slug)` in both places executes the query once. Wrap your data loaders in `cache()` and the metadata layer stops being a hidden tax on your response time. It’s a small pattern with an outsized effect on data-heavy pages, and it’s the kind of thing that separates a site that feels fast from one that doesn’t.

The 30-day check: proving your metadata earned the click

Metadata is one of the few SEO changes with a fast, clean feedback loop, so close it. Thirty days after you rewrite your titles and descriptions, open the Performance report in Search Console and compare click-through rate on the pages you touched, before and after. The pages to prioritize for a rewrite are the ones ranking in positions 5 through 15 with a click-through rate below what their position should earn — those are pages Google already trusts enough to rank but whose listing isn’t compelling enough to win the click. A better title there converts existing rankings into traffic without any new links or content. It is, dollar for dollar, the cheapest traffic in SEO.

Watch for one Google behavior that surprises people: it doesn’t always use the title you wrote. If Google decides your title doesn’t match the query intent well, it may rewrite it in the results using your on-page headings. When you see that happening — the report shows impressions but your intended title isn’t what appears — the fix is usually to make your title and your H1 agree and to make both genuinely match what searchers are looking for, rather than stuffing keywords. Treat the metadata as a claim you then have to back up with the page, and Google is far more likely to honor the title you wrote. That alignment between title, heading, and intent is also exactly what the AI answer-engines reward, so the work compounds across both surfaces. Do this review once a quarter on your top twenty pages and you’ll keep finding one-line rewrites that move real traffic.

What this means for your business

The API is the same for everyone. What changes with size is how much you templatize, who owns the copy, and where the leverage is.

For agencies

Metadata is a productized deliverable you can systematize once and resell on every build. Ship a `generateMetadata` helper in your starter that enforces canonical URLs, a title template, and a data-driven OG image, so no client site launches without them. The failure mode at agency scale is inconsistency: fifty client sites, half missing canonicals, discovered only when one gets a duplicate-content penalty. Bake the rules into the template and a junior dev can’t ship them wrong. Then package “metadata + social card audit” as a fixed-scope engagement for prospects on other platforms — it’s a fast win that proves value before a bigger contract. Pair it with the render-mode choices in ISR, SSG, SSR, and Edge so metadata generation never becomes a per-request cost.

For micro businesses (1–5 people)

You don’t have an SEO team, so the win is doing the five things above once and never thinking about them again. If you or a freelancer built your site on Next.js, the entire metadata setup for a small site is an afternoon: a title template in the root layout, a real description on each of your five or six pages, a canonical on each, and one branded OG image. That’s it. The highest-leverage single move for a micro business is a specific, human title on your homepage and your top service page — not “Home,” not “Welcome,” but the thing a customer would type. If writing code isn’t your world, this is exactly the kind of scoped fix worth handing to us for a flat fee.

For small businesses (SMEs)

You have more pages than you can hand-tune — service pages, locations, maybe a blog — so the play is dynamic metadata driven by your content, not hand-written tags. Move your page copy into data (a CMS, a database, or typed content files) and let `generateMetadata` build titles and descriptions from it. That way, adding a new location or service automatically ships a correct, unique title. Watch Search Console for pages where your click-through rate lags your ranking: that gap is almost always a weak title or description you can rewrite in one line. Prioritize the pages that already rank 5–15; a better title there is the fastest traffic you’ll find.

For mid-size companies

At your scale metadata is a governance problem, not a coding problem. You have multiple teams shipping pages, and the risk is drift: an unmanaged `<title>` here, a missing canonical there, an OG image team that works out of a different system than the web team. Centralize the rules in a shared metadata module every route imports, add a CI check that fails a build when an indexable page ships without a canonical or description, and treat OG image generation as a design-system component so brand stays consistent across thousands of pages. The `ImageResponse` approach scales here precisely because it’s code: one template change restyles every card at once. This is the same discipline we describe in App Router patterns that actually scale — the metadata layer is part of the architecture, not an afterthought.

How we run this at Frontend Horizon

Every FH site ships with a title template and `metadataBase` in the root layout, a canonical on every indexable route, dynamic `generateMetadata` on all `[slug]` families, and generated OG cards. It’s not optional and it’s not a phase-two nicety — it’s in the build from the first deploy, because the clicks you lose to a weak title in month one never come back. If you want the same setup on your site, or an audit of what your current metadata is costing you, run a free discovery and we’ll show you the gaps before any sales call. Next in this series: how to make Google and AI engines actually understand your pages with structured data, and how to control what gets crawled with sitemaps and robots.txt.

Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Structured Data in Next.js: How JSON-LD Gets You Cited by Google and AI
Older post
SEO Foundations That Make You Eligible for Google AI Search
Keep reading

More from the blog

Next.js·10 min

Structured Data in Next.js: How JSON-LD Gets You Cited by Google and AI

Structured data is how you tell Google and AI what your page means, not just what it says. Here’s the Next.js way to ship it.

Next.js·10 min

Sitemaps and robots.txt in Next.js: Telling Crawlers and AI Bots What Actually Matters

A crawler’s time on your site is a budget. Sitemaps and robots.txt are how you spend it on the pages that make you money.

Next.js·10 min

Redirects in Next.js: How Not to Torch Your Rankings in a Redesign

Every redesign is a chance to lose the rankings you spent years earning. Redirects are the seatbelt. Here’s how to wear it.