Munchable answers whether a packaged food fits the gut conditions you manage. Most of its public surface is generated: 461 pages that each answer one ingredient question, indexed at munchable.app/answers, plus eight condition guides and 38 recipes. 516 URLs in all, and everything except a handful of fixed pages comes out of data rather than out of a file somebody wrote.
Which means the title tag and the meta description are not copy anybody proofread. They are output. So I measured all of them.
The two numbers a search result can show
export const TITLE_LIMIT = 60; export const DESCRIPTION_LIMIT = 160; These are the familiar approximations of where Google cuts. The comment above them says why they are enforced rather than remembered:
* A title of 60 characters or more, or a description of 160 or more, gets cut * mid-word, and the part that gets cut is usually the part carrying the answer. That is the specific hazard of a question page. The title is the question, so the part of the description that matters is the beginning, and the part of the title that matters is all of it. A question truncated at "Is hydrogenated vegetable oi..." has spent the impression and told the reader nothing.
What 461 pages actually measure
Run over every generated page, with the brand suffix accounted for:
title min 30 median 37 max 56 (limit 60) description min 93 median 117 max 148 (limit 160) pages within 3 characters of the title limit: 0 pages within 10 characters of the description limit: 0 The shortest title on the site is munchable.app/is-rye-low-fodmap, at 30 characters including the brand. The longest is 56. Nothing is close to the edge, and that is not restraint on my part: it falls out of the phrasing. "Is X low FODMAP?" is short because the condition's search term is short, and the longest titles are the ones where a four-word ingredient meets a four-letter condition.
The descriptions are the more interesting half, because they are assembled rather than written. Each one leads with the answer and then says what to do with it:
No. Baguette is an avoid wherever it appears on the label. Scan a barcode to check a real product. That shape is deliberate: the description leads with the verdict rather than with a promise to deliver one, so the snippet is useful before the click. It is also a separately compressed string rather than the page's own opening sentence, which is what keeps the 160-character cut from ever landing mid-verdict.
The one page that loses the brand
The site appends " | Munchable" to every title through Next's title.template. That suffix is twelve characters of a sixty-character budget, and on a question page the budget is spent on the question. So the helper drops the suffix instead of truncating the subject:
return title.length + TITLE_SUFFIX.length < TITLE_LIMIT ? title : { absolute: title }; Across 461 pages, that branch is taken exactly once:
Is fully hydrogenated vegetable oil OK with BAM? 48 characters 48 + 12 = 60, which is not less than 60 So munchable.app/is-fully-hydrogenated-vegetable-oil-ok-with-bam ships a title tag with no brand in it, and its near neighbour munchable.app/is-hydrogenated-vegetable-oil-ok-with-bam keeps it, because dropping the word "fully" saves six characters. You can confirm it from the live HTML:
curl -s https://munchable.app/is-fully-hydrogenated-vegetable-oil-ok-with-bam \ | grep -o '<title>[^<]*</title>' # <title>Is fully hydrogenated vegetable oil OK with BAM?</title> One page out of 461 is a strange thing to design for, and it is the right call anyway. The alternative is a title that reads "Is fully hydrogenated vegetable oil OK with BA..." in a result where every competing line is a complete question. The brand is the most expendable word in that string: somebody who clicks will see it on the page, and somebody scanning results is matching the question.
The test has to work twice, in two different ways
A previous post covered the generated half: the test runs every page through the same functions the route calls, so the longest ingredient name is the one that decides whether the suite is green. The half I want to point at here is the hand-written pages, because that is where the clever bit is forced on you.
A server component cannot be imported under node --test. Its module pulls in React, next/navigation and the database client. So the test reads the source text instead:
const start = source.search(/export (?:const metadata|async function generateMetadata)/); const block = source.slice(start, start + 1200); const title = literal(block, 'title'); const description = literal(block, 'description'); A string-literal grep is exactly as fragile as it looks, and it fails in a specific direction: a page whose metadata comes from a constant or from a content module has no literal to find, and a test that silently finds nothing passes. So the skips are named:
const ROUTES_WITHOUT_LITERAL_META = new Set([ 'app/[question]/page.tsx', 'app/conditions/[slug]/page.tsx', 'app/recipes/[slug]/page.tsx', 'app/recipes/page.tsx', 'app/layout.tsx', 'app/admin/layout.tsx', 'app/premium/layout.tsx', 'app/support/[ticketId]/page.tsx', ]); Eight routes, each with a reason: four are checked through their content modules instead, one is the site-wide default, and three are behind a login where there is nothing for a crawler to show. Any other page with no literal metadata fails the suite rather than passing it. That is the whole trick: a test that reads source has to treat "I found nothing" as a failure, not as success.
It also has a visible cost, which the newest page shows. munchable.app/hidden-gluten-ingredients has a comment above its metadata saying that the title and description are written out as literals on purpose, because a constant would be invisible to the test. A test that reads source changes how you write the source. I think that is acceptable here, and it is worth knowing you are making the trade.
What the headroom is for
Zero pages within ten characters of the description limit looks like wasted budget, as though there is room for another clause. There is not, and the reason is the thing being measured.
A generated description is a function of the longest ingredient name in the index. Today the maximum is 148 characters. Adding one ingredient whose name is fourteen characters longer than the current record moves that number past 160, on a page that does not exist yet, with no edit to any of the copy. The slack is not spare room. It is the budget for the next word somebody puts on a label.
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
