Repository navigation
Stop querying the AEO table once per marketing page view - #420
Merged
Merged
Conversation
pg_stat_statements named marketing_aeo_questions as the top recurring reader on the production database: 7,627 calls in 24 hours, 9.7ms each, against a t4g.nano that is reporting CONNECT_TIMEOUT and an 86.8% success rate. Nothing about those answers changes between page views — they are a handful of rows an admin edits occasionally — so essentially all of that was waste. MarketingAeoSection renders on the homepage, /features, /ai, /contact, every legal page and every /f/[id], and read the table on each render. Looking for the same shape found two more: /faq pulls 60 rows on every view, and every blog article ran TWO of these — one for its path, one for its category. All of them are cached now, keyed by their arguments so pages do not share each other's answers, and under one `marketing-aeo` tag rather than a short TTL. The tag matters: the AEO console's whole promise is that one edit propagates to every matching public route immediately, and every write path — insert, update, delete, plus the blog content loop — drops the tag, so that still holds. A failed read is never cached. These readers degrade to `available: false` when the database is unreachable, which is correct for rendering and wrong to store: caching it would leave the Knowledge Center empty for an hour after a blip, during exactly the incident that caused it. The cached functions throw instead, so nothing is stored, and the catch sits outside the cache. (Same mistake I made with the social links in #419 and fixed there — worth not repeating.) This is the third instance of one pattern, after the footer's social links and /pricing's 20-second poll: an uncached Supabase read inside something that renders on every public page view. On the rest of that pg_stat_statements output, for the record: the two large DELETEs are one call each — the demo teardown from #416, already run — and family deletion is not reachable in-app except as rollback cleanup on a just-created family with no child rows, so the 93 MB cascade does not recur. The catalog introspection is Supabase Studio, and the index creations are one-off migrations. This was the only recurring application workload among them. tests/marketing-aeo-closed-loop.test.ts asserts on source text, so the renamed callers needed its pattern widened — the same way it already accepts either name for the FAQ reader. The behaviour it guards is unchanged. tsc, i18n gate, 901 files / 7,701 tests and next build all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KRUgA6hD6QgzmtpSP6TUmP
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
NewWorldVenture
marked this pull request as ready for review
September 7, 2026 20:06
NewWorldVenture
pushed a commit
that referenced
this pull request
Sep 7, 2026
main moved twice while this branch was in review, and both landed in files this branch rewrites — the footer, structured-data, the FAQ page and the marketing AEO section. One conflict, in structured-data.tsx, resolved by keeping both sides. main's comment records a decision worth preserving: sameAs deliberately does NOT take the footer's canonical-handle defaults, because a footer link that 404s is a cosmetic miss while sameAs asserts to a search engine that those accounts are this organisation. This branch's getTranslations() call is still needed beside it, so the resolution keeps the comment and the call rather than choosing. Everything else auto-merged: #419's resolveSocialLinks defaults, #420's cached AEO readers, and the seven catalogues. tsc clean, i18n gate clean on all five surfaces, no orphaned catalogue keys, 901 files / 7,712 tests pass.
NewWorldVenture
added a commit
that referenced
this pull request
Sep 7, 2026
A French visitor was getting a translated header above an English page, and a
raw catalogue key in the footer reading siteFooter.acceptableUse. This makes the
whole public site render in the reader's language, and fixes the tooling that
reported it clean the entire time.
What was showing English: the site footer (every heading and link, on every
marketing page), the Knowledge Center block, all of /mobile, all four legal
pages (123 strings), the FAQ page, and inLanguage: 'en-US' in the JSON-LD, which
told every crawler the French page was English.
Why nothing caught it — three defects in the i18n tooling, each fixed:
* walk() skipped any directory named mobile/ios/android at ANY depth. The
intent was the sibling native projects at the repo root, so app/(marketing)/mobile
— a real public page — was invisible to the gate. Root-anchored now.
* the scanner matched copy against an allowlist of property names, so every
gap had to be found by reading English off a shipped page. It now recognises
copy by VALUE, with a list of properties that are never copy — wrong in the
over-reporting direction, which is the direction to be wrong in.
* i18n-lift stored the string exactly as it sat between the quotes, so a JS
escape survived into the catalogue and 29 values held a literal backslash.
Repaired, and a catalogue test now fails on any value carrying one.
`everything` is removed from the gated set, deliberately. It was gated on a zero
from a scanner that could not see data literals, could not see props outside its
allowlist, and skipped every directory named mobile. Widening it turned that
zero into 2,343 real strings, so the promise had never been true. It goes back
when it scans clean — a surface gated while dirty teaches everyone to ignore the
gate.
Brought up to date with main twice during review, across #414, #416, #417, #419
and #420. Conflicts in site-footer, structured-data and en-US resolved by
keeping main's titleKey/labelKey naming (the shape that makes a key in a
copy field visible on sight, which is what this PR exists to fix) and main's
relocated social row, while keeping BOTH sides in structured-data: the cached
social-links reader and this branch's translations.
The merged tree then failed catalogue-key-not-rendered-raw with 20 offenders,
and the test was right: those lists were half-lifted, a key beside a literal in
the same field. The render sites were already correct, so the literals were the
defect. Nine lifted — Children's privacy, two security commitments, the GDPR /
HIPAA / CCPA labels, two aiShowcase strings and three pricing tier labels.
Footer spacing: the language pill and social row share one wrapping flex row,
and gap-y-2 left them reading as one crowded block once wrapped. gap-y-4.
Known gaps, stated rather than hidden: 293 lifted keys are en-US only and render
English by the documented fallback; 2,343 strings remain across app/(app) and
components/modules; and the FAQ answers are database rows that no amount of code
translation reaches.
All six checks green. E2E needed one re-run: 9 iPad page.goto timeouts, no
assertion failures, 395 passing at three other viewports — the same signature
seen on #412, which contained no application code at all.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The finding
pg_stat_statementsnamedmarketing_aeo_questionsthe top recurring reader on production: 7,627 calls in 24 hours, 9.7ms each, against a t4g.nano currently reportingCONNECT_TIMEOUTand an 86.8% success rate.Nothing about those answers changes between page views — they're a handful of rows an admin edits occasionally. Essentially all of that was waste.
Bigger than the one query
MarketingAeoSectionrenders on the homepage,/features,/ai,/contact, every legal page and every/f/[id], and read the table on each render. Looking for the same shape found two more:/faqpulls 60 rows on every viewAll cached now, keyed by their arguments so pages don't share each other's answers, under one
marketing-aeotag rather than a short TTL. The tag matters: the AEO console's whole promise is that one edit propagates to every matching public route immediately, so every write path — insert, update, delete, plus the blog content loop — drops it.A failed read is never cached
These readers degrade to
available: falsewhen the database is unreachable. Correct for rendering, wrong to store: caching it would leave the Knowledge Center empty for an hour after a blip, during exactly the incident that caused it. The cached functions throw instead, so nothing is stored, and the catch sits outside the cache.Same mistake I made with the social links in #419 and fixed there — worth not repeating.
On the rest of that output
Only one of the five findings was recurring application workload. Stating the others plainly rather than inventing changes for them:
DELETE FROM public.families— 93.4 MBDELETE FROM auth.users— 87.5 MBI did check whether the family cascade was worth optimising anyway — if families were deletable in-app, a cascade across 174 family-scoped tables would be a latent problem. It isn't: the only
families.delete()calls are rollback cleanups on a just-created family with no child rows. The 93 MB came from the fully-seeded demo household, deleted once.Third instance of one pattern
After the footer's social links and
/pricing's 20-second poll, this is the third uncached Supabase read inside something that renders on every public page view. That shape is the load problem.A test needed widening
tests/marketing-aeo-closed-loop.test.tsasserts on source text, so renaming the callers broke one pattern — widened the same way it already accepts either name for the FAQ reader. The behaviour it guards is unchanged, and it still fails if an article stops reading AEO for its own path.Validation
tsc --noEmitcleannode scripts/i18n-gate.mjscleannpm run buildexits 0🤖 Generated with Claude Code
https://claude.ai/code/session_01KRUgA6hD6QgzmtpSP6TUmP
Generated by Claude Code