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.
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:
| Tag | What it does | Where ours comes from |
|---|---|---|
<title> | The link text in search results and the browser tab | generateMetadata() per route, or the root layout default |
<meta name="description"> | The snippet under the title in results | Same function, usually the route's own tagline or dek |
<link rel="canonical"> | Tells Google which URL is the authoritative one | alternates.canonical in the same metadata object |
<meta name="robots"> | Index/follow instructions | Defaults 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:
| Metric | Result |
|---|---|
Pages with a <title> | 187 / 187 |
| Pages with a description | 186 / 187 (the missing one is Next.js's generic error boundary, not a real route) |
| Title length | 11–85 characters, average 34.3 |
| Titles over ~60 characters (Google's typical truncation point) | 54 / 187 (29%) |
| Description length | 43–237 characters, average 115.3 |
| Descriptions over 160 characters | 43 / 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
| Mistake | Symptom | Fix |
|---|---|---|
| No title template on the root layout | Every page hand-writes its own brand suffix, and someone eventually forgets it | One template: "%s — ASoc", set once |
Overriding title but not openGraph | The page's social preview silently reverts to the site default | Re-declare the full openGraph block per route, not just title |
| Canonical read from a request header | Wrong URL under a proxy, a redirect, or an alternate domain | Build the canonical from the route's own known slug |
| Description copied from the page's first paragraph | Truncated mid-sentence, or duplicated across similar pages | Write a dedicated ~140–155 character description per route |
| Title optimized for the SERP alone | A blog headline that reads as generic once shortened | Accept the trade-off explicitly rather than by accident — know which pages you're doing it for |
| Never checking the rendered output | The function exists but nobody has read what it actually produced | Grep the build; the pattern being correct is not the same as every page being good |
Missing metadataBase | Relative OG image paths resolve wrong when shared | Set 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.
