Skip to main content
ASoc
Tutorial

We Grepped Our Own Meta Tags. 29% of Titles Ran Long.

187 prerendered pages, checked against the build output rather than the pattern: title and description lengths, what generates them, and where the trade-off is deliberate.

The ASoc Team8 min read

A meta tag is any <meta> element or <title> in a page's <head> — most SEO-relevant work is one title tag, one description, one canonical link, and one robots directive per page, generated from route data rather than typed by hand. We stopped assuming ours were fine and grepped the actual build output instead: 54 of 187 rendered pages carry a title over 60 characters, and 43 of 186 carry a description over 160 — on a codebase that has had per-page metadata since Phase D. The measurement is more useful than the theory, so here is the audit and what it found.

What actually goes in the <head>, and where it's decided

Four tags carry the SEO weight on a page like this one, and none of them is hand-written per route:

TagWhat it doesWhere ours comes from
<title>The link text in search results and the browser tabgenerateMetadata() per route, or the root layout default
<meta name="description">The snippet under the title in resultsSame function, usually the route's own tagline or dek
<link rel="canonical">Tells Google which URL is the authoritative onealternates.canonical in the same metadata object
<meta name="robots">Index/follow instructionsDefaults to indexable; explicit noindex only on a handful of routes

The root layout sets the fallback and the pattern every inner page inherits:

// src/app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: "ASoc — Premium Tailwind CSS Templates",
    template: "%s — ASoc",
  },
  description: "Production-ready Tailwind CSS templates for React, Next.js, Vue, Angular, and HTML — admin dashboards, ecommerce, and landing pages...",
};

template: "%s — ASoc" is the part worth noticing: every inner page supplies only its own piece ("ASoc Ally", "Pricing") and Next.js appends the brand suffix once, in one file. A product page then only has to set what's actually unique:

// src/app/templates/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  const product = getProduct(slug);
  return {
    title: product.name,                    // resolves to "<name> — ASoc"
    description: product.tagline,
    alternates: { canonical: `/templates/${slug}` },
    openGraph: {
      title: product.name,
      description: product.tagline,
      images: [product.screenshots[0]],
    },
  };
}

Two details do real work here. The openGraph block is re-declared rather than inherited — Next.js replaces a parent's openGraph object wholesale when a route sets its own, so a page that only overrides title and forgets openGraph silently ships the site's social preview instead of its own. And the canonical is built from the route's own slug rather than read from a header, so it can never point at the wrong URL because of how the page happened to be requested.

The audit: grep the build, don't trust the pattern

A metadata function existing on every route tells you the mechanism is in place. It tells you nothing about whether any individual title is a good one. So we built the site and read what actually shipped:

npm run build
node -e '
const fs = require("fs"), path = require("path");
function walk(dir, out=[]) {
  for (const f of fs.readdirSync(dir, {withFileTypes:true})) {
    const p = path.join(dir, f.name);
    f.isDirectory() ? walk(p, out) : f.name.endsWith(".html") && out.push(p);
  }
  return out;
}
const files = walk(".next/server/app");
const titles = [], descs = [];
for (const f of files) {
  const html = fs.readFileSync(f, "utf8");
  const t = html.match(/<title>([^<]*)<\/title>/);
  const d = html.match(/<meta name="description" content="([^"]*)"/);
  if (t) titles.push(t[1].length);
  if (d) descs.push(d[1].length);
}
console.log("pages:", files.length, "titles:", titles.length, "descriptions:", descs.length);
console.log("titles > 60 chars:", titles.filter(l => l > 60).length);
console.log("descriptions > 160 chars:", descs.filter(l => l > 160).length);
'

The result, over 187 prerendered pages:

MetricResult
Pages with a <title>187 / 187
Pages with a description186 / 187 (the missing one is Next.js's generic error boundary, not a real route)
Title length11–85 characters, average 34.3
Titles over ~60 characters (Google's typical truncation point)54 / 187 (29%)
Description length43–237 characters, average 115.3
Descriptions over 160 characters43 / 186 (23%)

The distribution is not evenly bad — it's concentrated in one place. The shortest titles are the simplest routes: "Blog — ASoc" (11 characters), "Contact — ASoc", "Pricing — ASoc", and single-word product names like "ASoc Oak — ASoc". The longest are blog posts, because a headline written to be specific and quotable in an article ("Static Rendering in the Next.js App Router: What Silently Turns a Page Dynamic — ASoc" at 85 characters) is a worse fit for a search snippet than it is for an <h1>. That's a real trade-off, not an oversight: a title good enough to get clicked in an AI answer or a social share is not always the title that displays cleanly at 60 characters, and a blog whose whole differentiator is specific, falsifiable headlines is not going to shorten them purely to satisfy a truncation guideline. The number is worth knowing; it is not automatically a bug to fix.

Why this stays separate from the posts that already cover metadata

This site has three posts that touch the <head> already, and this one is deliberately not a fourth pass over the same ground: Open Graph image generation owns the opengraph-image.tsx route and its own generateStaticParams requirement; Product schema owns the JSON-LD that produces rich results, which title tags cannot; and the generative-engine-optimization audit covers the structured-data and robots.txt surface an AI answer engine reads. What none of them measured is the plain title-and-description tags every page has always had — the oldest, least glamorous part of on-page SEO, and the one every "meta tags" guide on the web still treats as a checklist item rather than something to go and check.

Mistakes and how they show up

MistakeSymptomFix
No title template on the root layoutEvery page hand-writes its own brand suffix, and someone eventually forgets itOne template: "%s — ASoc", set once
Overriding title but not openGraphThe page's social preview silently reverts to the site defaultRe-declare the full openGraph block per route, not just title
Canonical read from a request headerWrong URL under a proxy, a redirect, or an alternate domainBuild the canonical from the route's own known slug
Description copied from the page's first paragraphTruncated mid-sentence, or duplicated across similar pagesWrite a dedicated ~140–155 character description per route
Title optimized for the SERP aloneA blog headline that reads as generic once shortenedAccept the trade-off explicitly rather than by accident — know which pages you're doing it for
Never checking the rendered outputThe function exists but nobody has read what it actually producedGrep the build; the pattern being correct is not the same as every page being good
Missing metadataBaseRelative OG image paths resolve wrong when sharedSet it once in the root layout from the real site URL

Frequently asked questions

What's the ideal title tag length? There is no hard limit — Google truncates by pixel width, not character count, so the ~60-character rule of thumb is an approximation. Our own audit shows 29% of pages over it, and most of those are blog headlines that are doing a different job than a search snippet. Treat 60 as a flag to review, not a hard ceiling.

Does the meta description affect ranking? Not as a ranking factor — Google frequently rewrites it in the SERP anyway. It affects click-through, which is a real input to how a page performs, just not one the algorithm reads directly the way it reads title relevance.

Do I need a different <meta name="robots"> on every page? No — index everything by default and only add noindex where there's a specific reason (auth pages, thin duplicate routes, the framework spokes before they were ready). A page-by-page robots policy that isn't the exception is a sign the information architecture needs fixing, not the meta tag.

Should meta tags be generated or hand-written? Generated, from the same data that renders the page. A generateMetadata function reading product.tagline cannot drift from what's on the page the way a hand-typed description eventually will once someone edits the copy and forgets the <head>.

Templates that ship metadata already wired

ASoc Axiom is an AI-consultancy site with its own blog and FAQ section — exactly the content type where title-length trade-offs above show up in practice. ASoc Beacon is a mobile-device-management site with a companion-app section and reviews page, each needing its own distinct description rather than a repeated one. ASoc Beaker is a research-lab services site with a project showcase, where per-project metadata is the difference between one indexed page and several.

Browse the full set of Next.js landing page templates or the Tailwind landing page templates. For the social-preview image these same routes generate, see Open Graph image generation in the App Router.

Keep reading

Tutorial10 min read

next-seo Is Obsolete: 28 Native Metadata Declarations, Zero Package

next-seo is a Pages Router package the native Metadata API replaced in Next 13. Zero dependency here, 28 metadata sources across 27 pages — the full migration mapping.

Read more
Tutorial11 min read

Next.js 16 Renamed Middleware to Proxy: What Changes and What Breaks

Renaming the file is a third of the migration. The exported function has to change too, keeping both files is a build error, and the proxy runs on Node rather than the Edge.

Read more
Tutorial11 min read

A/B Testing in Next.js 16: The Proxy Recipe Costs the CDN, Not SSG

Rewriting in middleware does not make your pages dynamic — both variants stay prerendered. What it really costs is the shared cache and every request's critical path.

Read more