Our longest search snippet is 148 characters of a 160 character budget, and the 12 that are left over are why the answer is written twice

2026-11-04来源 dev.to TestingEnglish

Munchable has 461 generated pages, one per question it can answer about an ingredient and a gut condition, indexed here. Nobody writes their copy, which I have written about separately. This post is about one consequence of that: a page set nobody proofreads has to fit inside a search result by construction.

Two numbers define the box.

export const TITLE_LIMIT = 60; export const DESCRIPTION_LIMIT = 160; export const TITLE_SUFFIX = ` | ${SITE_NAME}`; 

Google cuts a title at roughly 60 characters and a description at roughly 160. Those are not hard limits in the protocol sense, they are where the text gets an ellipsis instead of an ending, and on these pages the tail of the description is the part carrying the answer. A snippet that is cut mid-verdict has spent the click and told the reader nothing.

The answer goes at the front, and it is written twice

The page's own sentence reads naturally because it is on a page:

In a packaged food, fully hydrogenated vegetable oil is flagged as an avoid when it is one of the main ingredients, and is flagged as a caution further down the label.

That sentence cannot be the description. Count what it spends before it has said anything: "In a packaged food," is 20 characters of throat clearing, and "is flagged as an avoid" spends 22 on a phrase whose informative part is one word. At the long end of the ingredient index there are names like "fully hydrogenated vegetable oil" and "milk protein concentrate", and with one of those in the subject position the page's own sentence runs to 167 characters, which is over budget before the closing instruction is added to it.

So there are two wordings of the same verdict in the code, one for the page and one for the snippet:

const ANSWER_LEAD = { good: 'is not flagged', caution: 'is flagged as a caution', avoid: 'is flagged as an avoid', unknown: 'cannot be assessed', }; /** The same verdicts in the fewest words that still say them, for the meta description. */ const META_ANSWER_LEAD = { good: 'is not flagged', caution: 'is a caution', avoid: 'is an avoid', unknown: 'cannot be assessed', }; 

Eleven characters recovered on the worst case, twice over on the pages where position matters and the sentence has to carry two readings. Then the snippet opens with the page's one-word answer rather than with the engine's phrasing, because that word is the thing a person is scanning the result for, and because it is the only part of the sentence that differs between an ingredient's sibling pages: the same food has a page per condition, and three results that open with identical words are three results nobody can tell apart.

The actual longest description in the set, measured this morning, is 148 characters:

No. Fully hydrogenated vegetable oil is an avoid as a main ingredient and is a caution lower down the label. Scan a barcode to check a real product.

Twelve characters of headroom across 461 pages. The last sentence is a constant:

/** * What the snippet asks the reader to do, sized to what is left of the 160 * characters once the longest ingredient name and the longest verdict have been * paid for. Every page carries it, so it cannot be lengthened without checking * the worst case. */ const META_CLOSE = ' Scan a barcode to check a real product.'; 

Forty characters, on every page, and the only thing standing between it and 41 is a test.

The test has to be the whole set

it('fits the snippet on every question page', () => { // The whole set, not a sample: the longest ingredient name decides this and // there is no way to know which one it is from here. assert.ok(ANSWER_PAGES.length > 100, 'expected the full question page set'); for (const ref of ANSWER_PAGES) { const page = answerPage(ref.question); checkTitle(`/${ref.question}`, page.ask); checkDescription(`/${ref.question}`, page.metaDescription); } }); 

Sampling is tempting on a set this size and it is useless here. The failure is always the extreme, and the extreme moves when somebody adds one ingredient with a long name to a data file, which is a change that looks like nothing in review. The length > 100 assertion above the loop is there because a loop over an accidentally empty array is a passing test, and a generated set is exactly the kind of thing that can arrive empty.

The title check mirrors a subtlety in the production code rather than asserting on the raw string:

function renderedTitle(title: string): string { // Mirrors resolvedTitle() in lib/seo.ts, which drops the suffix rather than // the subject when the two together will not fit. return title.length + TITLE_SUFFIX.length < TITLE_LIMIT ? title + TITLE_SUFFIX : title; } 

The brand suffix is 12 characters that no reader needs if the alternative is truncating the question. One page in the set currently crosses that line and renders with no brand name at all, which I measured in an earlier post.

The hand-written pages are checked by reading their source

The generated pages go through the functions the routes call. The hand-written ones cannot: they are server components, and importing one under node --test pulls in React, next/navigation and a database client, none of which will load there.

So that half of the test walks app/, reads all 31 of its page.tsx, layout.tsx and not-found.tsx files, and pulls the string literals out of the metadata block with a regex:

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'); 

Slicing a window after the export rather than scanning the file is deliberate: a title: on some unrelated object further down a page would otherwise be measured as if it were the page's title.

Two details make this honest rather than merely clever. First, adding a page adds a check, because the file list comes from the directory and not from an array somebody maintains. Second, a page with no literal metadata is not silently passed:

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 written beside it in the file: the first four take their metadata from the content layer and are covered by the loops above, one is the site-wide default, and three are behind a login where there is nothing for a crawler to show. Anything else with no literal metadata fails. The value of that list is not what it excludes, it is that an exclusion is a diff.

There is one honest weakness, and it is the reason this file reads oddly if you come to it cold: the metadata of a hand-written page has to be a literal in its own source, so the test can see it. A page that moved its description into a shared constant would become invisible to the regex, which is why one page near the top of the file carries a comment saying the literals are written out on purpose. A test that reads source instead of importing it buys coverage with a constraint on how the code is allowed to be written, and that trade is worth making exactly when the alternative is no coverage at all.

Check it yourself

Search for the exact title of one of these, or just open Is fully hydrogenated vegetable oil OK with BAM? and read the <meta name="description"> in the source. It is the page's answer, compressed, with 12 characters to spare, and nobody chose the wording for that page in particular.

原文:https://www.a8q9.cn/en/article/our-longest-search-snippet-is-148-characters-of-a-160-character-budget-and-the-1-217eb9.html

相关文章