Landing Page & Public Site -- Pre-mortem Analysis¶
Generated: 2026-02-25 | Method: Pre-mortem Analysis (imagine failure, work backwards) Source Deep-Dive:
docs/deep-dive-landing-page-public-site.mdSetting: 6 months post-launch. SaaS product shipped with all 45 issues unresolved. The product has FAILED. Analysts: John (Product Manager), Quinn (QA Engineer)
Executive Summary¶
9 realistic failure scenarios analyzed. 3 are existential business threats. The caching bug (C1-C5) is the single most destructive defect -- it silently corrupts every dynamic page on the site and is guaranteed to manifest on the first multi-page visit. Combined with zero test coverage, zero error boundaries, dead CTA buttons, and unsanitized HTML rendering, the public site is a conversion-killing, SEO-poisoning, security-breaching liability disguised as a marketing page.
| # | Incident Name | Likelihood | Impact | Detection | Fix Time | Priority |
|---|---|---|---|---|---|---|
| 1 | The Content Shuffle | CERTAIN | CATASTROPHIC | Days-Weeks | 2 hrs | P0 |
| 2 | The Silent Storefront | CERTAIN | CATASTROPHIC | Weeks-Months | 30 min | P0 |
| 3 | The XSS Worm | HIGH | CATASTROPHIC | Hours-Days | 2 hrs | P0 |
| 4 | The Google Ghosting | HIGH | SEVERE | Weeks-Months | 1 week | P0 |
| 5 | The Legal Page Roulette | CERTAIN | SEVERE | Days | 2 hrs | P0 |
| 6 | The View Count Heist | HIGH | HIGH | Weeks | 4 hrs | P1 |
| 7 | The White Screen of Death | HIGH | HIGH | Minutes | 4 hrs | P1 |
| 8 | The Phantom Author | MEDIUM | MEDIUM | Weeks | 3 hrs | P2 |
| 9 | The Invisible Contact Form | MEDIUM | MEDIUM | Days | 1 hr | P2 |
Incident 1: "The Content Shuffle" -- Cache Poisoning Destroys Every Page¶
What Happened¶
JOHN (PM): Six weeks after launch, our support inbox exploded. Users reported that the blog listing page was "stuck" -- no matter what page they navigated to, they saw the same 9 articles. Clicking to page 2, page 3, page 4 -- all identical. Worse, visiting the blog by author "Sarah Chen" showed articles by "Marcus Wright." Visiting the category "Engineering" showed posts from "Marketing." A customer tweeted a screenshot: our Terms of Service page displayed our About page content. It went mildly viral in our industry's Slack channels.
QUINN (QA): Here's what actually happened at the code level. The very first visitor after each deployment (or cache expiration) "poisoned" every cache. When someone visited /blogs?page=1, unstable_cache stored the result under the key ["blogs"]. When the next visitor hit /blogs?page=2, the cache saw the key ["blogs"] already existed and returned page 1's data. Same story for every parameterized function:
getBlogDetails("first-post")cached under["blog-details"]. Every subsequent blog detail page --/blogs/second-post,/blogs/third-post-- returned the first post's content.getNavigationLinks("navbar")cached under["navigation-links"]. WhengetNavigationLinks("footer")was called, it returned navbar links. The footer showed the navbar menu. The bottombar showed the navbar menu.getSettingByGroup("site")cached under["settings-group"]. The contact page calledgetSettingByGroup("contact")and received site settings instead -- phone numbers, email addresses, and physical address were all missing or showed the site name instead.getDynamicPage("about")cached under["dynamic-page"]. The Terms page, Privacy Policy, and every other dynamic page all showed the About page content.
This wasn't intermittent. It was 100% reproducible after the first cache population. Every single dynamic page on the public site displayed wrong content.
Root Cause¶
C1 (getBlogs static key), C2 (getBlogDetails static key), C3 (getDynamicPage static key), C4 (getSettingByGroup static key), C5 (getNavigationLinks static key). All five unstable_cache wrappers use static cache keys that ignore their function parameters.
Business Impact¶
- User trust: Visitors see blatantly wrong content. A SaaS product that can't display its own blog correctly signals engineering incompetence.
- Content integrity: Blog authors appear to have written content they didn't write. Author reputation damage.
- Legal exposure: Terms of Service and Privacy Policy pages showing incorrect content means the company may not have legally valid published policies. If a dispute arises, "our Terms page showed the About page content for 6 weeks" is indefensible.
- SEO damage: Google indexes page 2, 3, 4 of blogs as duplicate content of page 1. Duplicate content penalties applied. Category and author pages are all duplicates. Google may de-index or severely penalize the domain.
- Conversion loss: Navigation links are wrong (footer shows navbar, creating a confusing UX). Visitors cannot find what they're looking for. Bounce rate spikes.
- Estimated magnitude: 80-95% of all dynamic page views served incorrect content during cache TTL windows. If blog drives 40% of organic traffic, and organic traffic drives 30% of signups, the compounding effect is a 25-35% reduction in total conversions for the entire period.
Detection Time¶
Days to weeks. This is the insidious part -- the site "works." Pages load fast, no errors appear in logs. The cache does exactly what it was asked to do. Detection requires someone manually checking that page 2 content differs from page 1, or that the footer links differ from the navbar links. With zero test coverage, there is no automated check. The first detection vector is user complaints, which typically take days to accumulate enough volume to trigger investigation.
Recovery Time¶
2 hours. The fix is a 1-line change in each of 5 files -- adding parameters to the cache key array. But cache invalidation after the fix requires either redeployment or manual tag-based revalidation. Full verification across all affected pages (blogs, authors, categories, dynamic pages, navigation, settings) takes the remaining time.
Prevention¶
- Include all function parameters in every
unstable_cachekey array. - Add integration tests that fetch the same endpoint with different parameters and assert different responses.
- Add a cache-key linting rule or code review checklist item: "Does every
unstable_cachekey include all varying inputs?"
Incident 2: "The Silent Storefront" -- Dead CTA Buttons Kill All Conversions¶
What Happened¶
JOHN (PM): Three months after launch, I pulled the conversion funnel analytics. We had 47,000 landing page visits. Zero clicks on "Start Free Trial." Zero clicks on "Book a Demo." Zero clicks on any pricing plan CTA. At first I thought the analytics were broken -- I asked engineering to check the event tracking. They came back and said: "There are no events because the buttons don't do anything." I clicked "Start Free Trial" myself. A pretty ripple animation played. Nothing happened. I clicked "Book a Demo." Ripple. Nothing. I clicked "Get Started" on the Professional pricing plan. Ripple. Nothing.
We had been running paid Google Ads, LinkedIn campaigns, and content marketing for three months. Every visitor who reached the landing page and was ready to convert... hit a wall. The entire marketing budget -- $45,000 in ad spend -- drove traffic to a page with zero working conversion paths.
QUINN (QA): All 7 CTA buttons across hero-section.jsx, cta-section.jsx, and pricing-section.jsx render as <button type="button"> with no href, no onClick, and no router navigation. The custom/button.jsx component renders a plain <button> -- it has no mechanism to accept a destination URL. The ui/button.jsx in pricing technically supports asChild for link wrapping, but nobody wrapped it. These buttons are decorative elements that happen to look clickable.
Root Cause¶
H1 (all 7 CTA buttons non-functional). Secondary: M13 (two different button components, neither configured for navigation).
Business Impact¶
- Direct revenue loss: 100% of landing page conversions lost. If industry-average landing page conversion rate is 2-5%, and the site received 47,000 visits, that's 940-2,350 potential signups that never happened.
- Ad spend waste: The entire paid acquisition budget produced zero conversions. At $45,000 over 3 months, this is a total write-off.
- Opportunity cost: 3 months of zero growth from the primary acquisition channel. For an early-stage SaaS, this can be fatal -- missed milestones, failed fundraising metrics, extended runway burn.
- Brand perception: Users who click "Start Free Trial" and nothing happens conclude the product is broken, abandoned, or a scam. They don't come back.
- Estimated magnitude: At $50/month average revenue per user and 2% conversion rate, 940 lost signups = $47,000/month in recurring revenue never captured. Over 3 months of compounding: $141,000+ in lost ARR.
Detection Time¶
Weeks to months. This is the terrifying one. There are no errors. No crashes. No failed network requests. Analytics show page views but no one checks for CTA click events because the team assumes the buttons work. The absence of conversions might be attributed to "we're new, we need more traffic" or "the messaging needs work." Only when someone runs a conversion funnel analysis (or personally tries to sign up via the landing page) does the root cause surface.
Recovery Time¶
30 minutes. Add href attributes or onClick handlers to 7 buttons across 3 files. Deploy. Verify.
Prevention¶
- Manual smoke test: click every button on the landing page after every deploy.
- E2E test (Playwright/Cypress): verify every CTA button navigates to the expected destination.
- Design review checklist: "Does every button have a defined action?"
Incident 3: "The XSS Worm" -- Stored Cross-Site Scripting Compromises Visitors¶
What Happened¶
QUINN (QA): A security researcher reported a stored XSS vulnerability via our blog system. Here's the attack chain they demonstrated:
- Account creation: Attacker creates a regular user account (public registration, no approval needed).
- Privilege escalation: Using the known self-role-change vulnerability in
PATCH /users/profile(documented in the API review), the attacker changes their role toadminoreditor. - Payload injection: With blog-create permissions, the attacker publishes a blog post with this content:
- Execution: Every visitor who views the blog post executes the script. The
dangerouslySetInnerHTML={{ __html: blog.content }}inblog-page-frontend.jsxrenders the<script>tag directly into the DOM with zero sanitization.
The same attack works on dynamic pages (/about, /terms, /privacy) via dynamic-page.jsx, which also uses dangerouslySetInnerHTML={{ __html: data?.description }} without sanitization.
JOHN (PM): The researcher published the vulnerability after our 90-day disclosure window expired without a fix. Within 48 hours, we detected malicious blog posts injecting cryptocurrency mining scripts and credential-harvesting fake login forms. Every visitor to those blog pages had their browser hijacked.
Root Cause¶
C6 (XSS in blog content), C7 (XSS in dynamic page content). Amplified by the known self-role-change vulnerability (PATCH /users/profile privilege escalation) that provides the attacker with the necessary permissions.
Business Impact¶
- Visitor compromise: Every visitor who viewed an infected page had scripts executed in their browser. Depending on payload: cookie theft, session hijacking, credential harvesting, cryptomining, malware distribution.
- Legal liability: Serving malicious content to visitors may trigger breach notification requirements depending on jurisdiction. GDPR Article 33 (notification within 72 hours), CCPA, etc.
- SEO blacklisting: Google Safe Browsing flags the domain as "This site may harm your computer." Chrome displays a full-page warning. Organic traffic drops to near-zero overnight.
- Reputation destruction: News coverage of "SaaS product served malware via its blog" is irrecoverable for an early-stage company.
- Customer churn: Existing paying customers who learn about the XSS vulnerability question the security of the entire platform. Churn spike of 20-40% is realistic.
- Estimated magnitude: If the malicious post ranks well (especially given SEO issues causing indexing anomalies), hundreds to thousands of visitors could be compromised before detection. Google Safe Browsing delisting takes 2-4 weeks after remediation.
Detection Time¶
Hours to days. If the attacker is subtle (exfiltrating cookies without visible DOM changes), detection requires either: (a) a security scanner that inspects served HTML, (b) a CSP violation report, or © user reports of suspicious behavior. With no CSP configured and no security monitoring, the realistic detection window is when visible damage occurs (fake login forms, site defacement) or a security researcher reports it.
Recovery Time¶
2 hours for the immediate fix (install DOMPurify, sanitize in 2 files). 2-4 weeks for full recovery including: Google Safe Browsing delisting, audit of all existing blog/page content for malicious payloads, forced password resets if cookies were stolen, security incident communication to affected users.
Prevention¶
- Sanitize all HTML rendered via
dangerouslySetInnerHTMLusing DOMPurify (server-side withisomorphic-dompurifysince these are Server Components). - Implement Content Security Policy (CSP) headers that block inline scripts.
- Fix the self-role-change vulnerability in PATCH /users/profile to prevent the privilege escalation step.
- Add backend-side HTML sanitization on blog/page content at write time (defense in depth).
Incident 4: "The Google Ghosting" -- SEO Catastrophe Kills Organic Traffic¶
What Happened¶
JOHN (PM): Six months after launch, our organic search traffic was 90% below projections. I asked the SEO consultant to audit. Their report was devastating:
-
Homepage invisible: The homepage -- our most important SEO asset -- had no
<title>, no<meta name="description">, no Open Graph tags, no structured data. Google's index showed our homepage title as "Sass Boilerplate" (the hardcoded fallback with wrong casing). It ranked for nothing. -
Duplicate content penalty: Google had indexed 47 versions of our blog listing page --
/blogs,/blogs?page=2,/blogs?page=3,/author/sarah-chen,/category/engineering-- and they ALL had identical content (due to the cache bug C1). Google flagged the entire/blogssubtree as "duplicate thin content" and de-ranked it. -
Blog detail pages all identical: All 35 blog posts indexed with the same content (the first blog ever cached, due to C2). Google treated 34 of them as duplicates and dropped them from the index. Our content marketing investment in 35 carefully written blog posts resulted in exactly 1 indexed page.
-
Author and category pages: Zero SEO metadata (M2, M3). Google indexed them with auto-generated titles like "page" (lowercase function name from L1). They ranked for nothing and diluted our domain authority.
-
404s returning 200: Deleted or moved blog posts returned HTTP 200 with a "Not Found" component (M1). Google indexed these as real pages with thin content. 12 of these phantom pages accumulated, further diluting domain authority.
-
Title corruption: When the settings API was slow or errored,
fetchMetaDatareturned a string instead of an object (H6).generateMetadataproduced titles like "Blogs | undefined", "Contact | undefined". Google indexed several pages with "undefined" in their titles. Users searching for our brand saw "undefined" in search results.
QUINN (QA): The cascading effect is what makes this catastrophic. It's not one SEO problem -- it's six simultaneous problems that compound. The cache bug alone would tank organic traffic. Combined with missing metadata, 200-status 404s, and title corruption, the domain's overall quality score in Google's eyes is rock-bottom.
Root Cause¶
H2 (no homepage SEO metadata), H6 (metadata error handler returns wrong type causing "undefined" titles), M1 (404 pages return HTTP 200), M2 (no author page SEO), M3 (no category page SEO), M4 (no contact page SEO). Amplified catastrophically by C1 and C2 (cache bugs causing duplicate content).
Business Impact¶
- Organic traffic: 90% below projections. For a content-driven SaaS, blog-sourced organic traffic is typically 40-60% of total acquisition. Losing this means the primary growth channel is dead.
- Content marketing ROI: If the team invested 200+ hours writing 35 blog posts at an average cost of $200/post, that's $7,000 in content creation with zero SEO return.
- Recovery timeline: SEO recovery after a duplicate content penalty takes 3-6 months AFTER fixes are deployed. Total time from launch to recovered organic traffic: 9-12 months.
- Paid acquisition dependency: With organic dead, all growth depends on paid channels. CAC (Customer Acquisition Cost) increases 3-5x.
- Estimated magnitude: If projected organic traffic was 10,000 visits/month at 3% conversion and $50 ARPU, lost revenue is $15,000/month for 6+ months = $90,000+ in lost ARR during the recovery window.
Detection Time¶
Weeks to months. SEO damage is slow and invisible. Traffic doesn't drop overnight -- it never materializes. The team may attribute low organic traffic to "we're a new domain, it takes time to build authority." Only a deliberate SEO audit (checking Google Search Console, running a site crawl with Screaming Frog) reveals the structural issues.
Recovery Time¶
1 week for code fixes (metadata exports, proper 404 handling, cache key fixes, error handler fix). 3-6 months for Google to re-crawl, re-index, and restore rankings. The SEO damage from months of serving duplicate content and thin pages takes significant time to recover from even after technical fixes.
Prevention¶
- Fix cache keys (C1-C5) -- eliminates the duplicate content at its source.
- Add
generateMetadataexports to all public routes. - Use
notFound()fromnext/navigationfor missing content (returns proper 404 status). - Fix
fetchMetaDataerror handler to return a valid metadata object. - Pre-launch SEO audit: crawl the site with Screaming Frog or Sitebulb, verify unique titles, proper status codes, and distinct content per URL.
Incident 5: "The Legal Page Roulette" -- Terms and Privacy Show Wrong Content¶
What Happened¶
JOHN (PM): Our legal counsel called in a panic. A potential enterprise customer had screenshot'd our Privacy Policy page as part of their vendor due diligence. The screenshot showed... our About page content. No privacy policy text at all. The enterprise deal was dead on the spot. They cited "inability to verify data handling practices" as the reason.
Separately, a user in the EU filed a GDPR complaint, arguing that we failed to provide a valid privacy policy as required by Article 13. When our legal team checked the live site, they confirmed: the /privacy URL displayed the About page content. The /terms URL also displayed the About page content. Every dynamic page showed whatever was cached first.
QUINN (QA): This is a direct consequence of C3. getDynamicPage uses the cache key ["dynamic-page"] for ALL slugs. The first dynamic page fetched (likely /about, since it's often linked in the footer) populates the cache. Every subsequent slug -- /terms, /privacy, /refund-policy -- returns the About page content. The backend has the correct content in the database; the frontend cache makes it unreachable.
Root Cause¶
C3 (getDynamicPage static cache key). The backend serves correct content per slug, but the frontend unstable_cache returns the first-fetched page's content for all slugs.
Business Impact¶
- Enterprise deal loss: Enterprise customers require verifiable legal pages as part of vendor assessment. One screenshot of incorrect content kills the deal. At enterprise ACV of \(10,000-\)50,000, each lost deal is significant.
- GDPR compliance failure: Article 13 requires a clear privacy policy. Serving About page content on the privacy URL is a compliance violation. Potential fines up to 4% of annual turnover (though enforcement on small companies is rare, the risk exists).
- Terms of Service enforceability: If the Terms page displays incorrect content, any Terms-dependent action (account termination, refund denial, etc.) may be legally unenforceable. "Your Terms page didn't show the actual terms" is a valid defense in a dispute.
- Trust erosion: Users who check legal pages and see irrelevant content lose confidence in the product's professionalism.
- Estimated magnitude: 1-3 lost enterprise deals at $25,000 ACV = \(25,000-\)75,000 in lost revenue. GDPR complaint handling costs \(5,000-\)15,000 in legal fees even without a fine.
Detection Time¶
Days. Legal pages are low-traffic but high-scrutiny. Enterprise prospects, legal teams, and privacy-conscious users check these pages. Detection occurs when someone reports the incorrect content -- likely within the first few enterprise sales conversations.
Recovery Time¶
2 hours. Fix the cache key for getDynamicPage to include the slug parameter. Redeploy. Verify all dynamic pages show correct content. No long-tail recovery needed -- once fixed, pages display correctly.
Prevention¶
- Fix C3 (include slug in cache key).
- Add a post-deploy smoke test that loads
/about,/terms, and/privacyand verifies each displays distinct content. - Legal team review of live site before launch.
Incident 6: "The View Count Heist" -- Blog Analytics Manipulation¶
What Happened¶
JOHN (PM): We launched a "Top Posts" section on the homepage, ranking blogs by view count. A competitor discovered they could inflate any blog post's view count by running a simple script:
for i in $(seq 1 100000); do
curl -s "https://our-saas.com/api/v1/public/blogs/competitor-friendly-post" > /dev/null
done
Within hours, a mediocre post had 100,000 "views" and sat at the top of our "Top Posts" ranking. Our genuinely popular content was buried. Worse, the competitor then published a LinkedIn post mocking our "inflated metrics" and questioning the integrity of our platform.
Separately, our marketing team had been reporting blog performance to investors based on view counts. The Series A deck included "Our engineering blog averages 5,000 views per post." When an investor's technical advisor pointed out the view count endpoint had no rate limiting or deduplication, the metrics were deemed unreliable. The fundraising narrative around content traction collapsed.
QUINN (QA): PublicService.getBlogByIdOrSlug increments BlogView.viewCount on every single call. No IP tracking, no session deduplication, no time-window throttling, no rate limiting. The endpoint is unauthenticated. A curl loop inflates counts at network speed. Even legitimate traffic double-counts: the frontend calls getBlogDetails twice per page load (once in generateMetadata, once in the page body -- issue M8), so every real visitor registers as 2 views minimum.
Root Cause¶
H3 (blog view count has no protection). Amplified by M8 (duplicate API calls double-counting legitimate views).
Business Impact¶
- Analytics integrity: All blog view metrics are unreliable. Marketing, investor reporting, and editorial prioritization based on view counts are invalid.
- Competitive sabotage: Competitors can manipulate ranking of "top posts" to either bury good content or promote their own preferred narrative.
- Investor trust: If fundraising materials cite blog metrics later revealed as manipulable, investor confidence in the team's technical capabilities drops.
- Estimated magnitude: Direct revenue impact is low, but the credibility damage to investor relations and marketing reporting is Medium-High. If it delays a fundraise by 3 months, the burn-rate impact could be $100,000+.
Detection Time¶
Weeks. View count inflation looks like organic traffic growth. Only statistical anomaly detection (sudden spikes, views from single IP ranges) or manual investigation reveals manipulation. With no server-side analytics beyond the raw count, there's no audit trail.
Recovery Time¶
4 hours. Implement IP-based or session-based deduplication in the BlogView increment logic. Add rate limiting on the public blog endpoint. Reset manipulated view counts. Fix M8 (deduplicate API calls) to prevent legitimate double-counting.
Prevention¶
- Add IP + time-window deduplication: only count 1 view per IP per blog per 24 hours.
- Rate limit the public blog detail endpoint (e.g., 60 requests/minute per IP).
- Fix M8 to eliminate the double API call on blog detail pages.
- Never use raw view counts for business-critical decisions without deduplication.
Incident 7: "The White Screen of Death" -- Cascading Failures on API Downtime¶
What Happened¶
QUINN (QA): The backend API experienced a 20-minute outage during a database migration. During this window, every public page on the frontend displayed a white screen -- no header, no footer, no content, no error message. Just blank white.
Here's the cascade:
- API down: Backend returns 503 for all requests.
fetch()in API modules:response.okis false. The code logs "Failed to fetch blogs" but does not return or throw (H7). Execution falls through toresponse.json().response.json()crashes: The 503 response body is an HTML error page (from the reverse proxy), not JSON.response.json()throwsSyntaxError: Unexpected token < in JSON at position 0.unstable_cachepropagates the error: The error is uncaught inside the cached function, causing the Server Component to throw.- No error boundary: There is no
error.jsfile at any level in the(main-layout)route group (M10). The unhandled error propagates to the root, and Next.js renders a blank error page. - Layout components also fail:
HeaderWrapperandFooterWrappermake API calls too. When they throw, even the shell of the page (header, footer, navigation) is gone.
Every public page -- landing page, blog, contact, about -- was a blank white screen for 20 minutes. Users thought the site was down entirely. Google's crawler hit the site during this window and indexed several pages as empty, further compounding SEO damage.
JOHN (PM): WHY wasn't there a fallback? WHY didn't the landing page -- which is 100% static content -- at least display its static sections? Because the LAYOUT throws before the page component even renders. The header/footer API calls bring down the entire page tree.
Root Cause¶
H7 (API error response falls through to .json() and crashes), M10 (no error boundary in (main-layout)). Amplified by: all layout-level data fetching (navigation, settings) using the same fragile error-handling pattern.
Business Impact¶
- Complete public site outage: Zero pages render during API downtime. For a 20-minute window, the business has no public presence.
- SEO damage: If Google crawls during the outage, it indexes empty pages. Repeated occurrences lead to quality score degradation.
- User abandonment: Visitors who see a white screen don't wait -- they leave and don't return. If the outage occurs during a traffic spike (product launch, press coverage), the damage is amplified.
- False alarm fatigue: If the backend has periodic brief outages (deployments, auto-scaling events), the frontend becomes unreliable even when the static landing page should always work.
- Estimated magnitude: A 20-minute outage during business hours with 200 concurrent visitors loses approximately 150-180 visitors who bounce immediately. If this happens monthly, the cumulative effect on brand reliability perception is significant.
Detection Time¶
Minutes. White screens are immediately visible to any visitor. Uptime monitoring (if configured) catches it instantly. This is the one failure scenario with fast detection.
Recovery Time¶
4 hours for proper fix. Immediate: restart the API (resolves the root cause). Proper fix: (1) add null return and early exit in all API modules when !response.ok, (2) add error.js boundary in (main-layout) that shows a graceful fallback, (3) wrap layout data fetching in try/catch with static fallbacks for header/footer.
Prevention¶
- Fix H7: in all 4 public API modules, add
return nullafter the error log when!response.ok. - Add
error.jsat(main-layout)/error.jswith a user-friendly error page. - Wrap
HeaderWrapperandFooterWrapperdata fetching in try/catch with hardcoded fallback navigation. - Consider static generation for the landing page (it has zero dynamic content).
Incident 8: "The Phantom Author" -- Hyphenated Names Break Author Pages¶
What Happened¶
JOHN (PM): We hired a content writer named Jean-Pierre Dupont. He published 12 excellent blog posts. When users clicked his author link, they landed on /author/jean-pierre-dupont. The page showed... zero posts. His author page was empty.
The slug-to-name conversion in author/[slug]/page.js splits on ALL hyphens and joins with spaces: "jean-pierre-dupont".split("-").join(" ") produces "jean pierre dupont". The database has the author name as "Jean-Pierre Dupont". The query ?author=jean pierre dupont matches nothing.
Meanwhile, authors with simple names like "John Smith" (/author/john-smith -> "john smith") worked fine, masking the bug for weeks until a hyphenated-name author was added.
QUINN (QA): It gets worse. The conversion is also case-sensitive depending on database collation. If the database uses a case-sensitive collation, even "john smith" might not match "John Smith". The author matching relies entirely on fragile string manipulation with no fallback to a unique identifier (no author ID, no author slug column). Any author with a hyphen, accent, apostrophe, or non-ASCII character in their name has a permanently broken author page.
Root Cause¶
H5 (author pages use fragile hyphen-split string matching instead of ID or slug).
Business Impact¶
- Content creator frustration: Authors with hyphenated names cannot share their author page. They appear to have no published content despite being prolific writers.
- Lost traffic: Author pages are a content discovery mechanism. Broken pages mean lost internal navigation and reduced page views per session.
- Cultural insensitivity: Disproportionately affects authors with non-English names (hyphenated surnames, names with accents or apostrophes). This is a bad look for a product marketed internationally.
- Estimated magnitude: Low direct revenue impact but Medium reputational and usability impact. If 20% of authors have names with special characters, 20% of author discovery paths are broken.
Detection Time¶
Weeks. Author pages are low-traffic. The bug only manifests for authors with hyphens or special characters. It may go unnoticed until a specific author reports that their page is empty.
Recovery Time¶
3 hours. Proper fix: add an authorSlug column to the User entity (or use authorId in the URL), query by ID/slug instead of reconstructed name string. Quick fix: URL-encode the full name instead of hyphen-splitting, but this creates ugly URLs and doesn't solve case sensitivity.
Prevention¶
- Use author ID or a dedicated slug column in author page URLs.
- Test author pages with names containing hyphens, apostrophes, accents, and non-Latin characters.
- Use parameterized queries with exact matching rather than string reconstruction.
Incident 9: "The Invisible Contact Form" -- Settings Cache Breaks Lead Capture¶
What Happened¶
JOHN (PM): Our sales team reported that the contact page was "broken." The page loaded, but the contact information sidebar (phone number, email, physical address) showed garbled data -- the site name where the phone number should be, the favicon URL where the email should be. The form itself worked, but the contextual information that builds trust ("Here's our phone number and address if you prefer to reach us directly") was completely wrong.
QUINN (QA): This is C4 in action. The contact page calls getSettingByGroup("contact") to fetch contact-specific settings (phone, email, address). But the root layout has already called getSettingByGroup("site") for site-wide settings (site name, logo, favicon). Because both calls share the cache key ["settings-group"], the contact page receives site settings instead of contact settings. The component then tries to display settings.contact.phone but gets settings.site.site_name instead, because the key structure maps to different fields.
The contact form submission itself works (it POSTs directly to the API), but the surrounding trust signals are all wrong. Visitors see gibberish where they expect a phone number and may abandon the form entirely.
Root Cause¶
C4 (getSettingByGroup static cache key). The contact page receives site settings instead of contact settings.
Business Impact¶
- Lead capture degradation: The contact form is described as "the primary lead capture mechanism." Visitors who see garbled contact information trust the form less. Form abandonment rate increases.
- Missed leads: Visitors who wanted to call or email directly (not use the form) cannot find valid contact information.
- Estimated magnitude: If the contact form receives 50 submissions/month and garbled info reduces that by 30%, that's 15 lost leads/month. At a 10% lead-to-customer rate and $50 ARPU, that's $75/month in lost revenue -- small individually but compounding over 6 months to $450 in direct revenue plus the harder-to-quantify trust damage.
Detection Time¶
Days. The contact page is visited by prospective customers doing research. They may notice the wrong information but simply leave without reporting it. Detection requires either the sales team checking the page or a visitor explicitly reporting the issue.
Recovery Time¶
1 hour. Fix the cache key for getSettingByGroup to include the group parameter. Redeploy. Verify contact page shows correct settings.
Prevention¶
- Fix C4 (include group in cache key).
- Smoke test that verifies the contact page displays phone/email/address correctly after every deploy.
Pre-mortem Verdict¶
The #1 Thing That Must Be Fixed Before Launch¶
The unstable_cache key bug (C1-C5). This single class of defect -- static cache keys that ignore function parameters -- is the root cause or amplifying factor in 6 out of 9 failure scenarios. It is:
- Guaranteed to manifest (100% reproducible, not probabilistic)
- Invisible to monitoring (no errors, no crashes, just wrong data)
- Affects every dynamic page (blogs, authors, categories, dynamic pages, navigation, settings)
- Trivial to fix (5 files, 1-line change each, 30 minutes total)
- Catastrophic in impact (SEO destruction, legal page corruption, content integrity loss, trust erosion)
The fix-to-impact ratio is the most extreme we have ever documented. 30 minutes of work prevents multiple months-long recovery scenarios.
Ship-Blocker List (MUST fix before launch)¶
These issues are NOT safe to ship with under any circumstances:
| Issue | Fix Time | Rationale |
|---|---|---|
| C1-C5 (unstable_cache keys) | 30 min | Every dynamic page shows wrong content. Guaranteed. |
| C6 (XSS blog content) | 15 min | Stored XSS on public pages. Security vulnerability. |
| C7 (XSS dynamic page content) | 15 min | Stored XSS on legal/content pages. Security vulnerability. |
| H1 (dead CTA buttons) | 30 min | Zero conversion paths on landing page. Business-fatal. |
| H2 (no homepage SEO) | 15 min | Most important page invisible to search engines. |
| H6 (metadata error -> "undefined") | 15 min | Titles show "undefined" in search results on any API hiccup. |
| H7 (API error falls through) | 20 min | Any API error crashes the entire page tree. |
| M1 (404 returns 200) | 15 min | Search engines index non-existent pages as real content. |
| M10 (no error boundary) | 20 min | Any uncaught error produces a white screen with no recovery. |
Total estimated fix time for all ship-blockers: ~3 hours.
Accept-and-Monitor List (can ship with active monitoring)¶
These issues have real impact but can be tolerated short-term with monitoring:
| Issue | Monitoring Required | Timeline |
|---|---|---|
| H3 (view count manipulation) | Monitor for anomalous view count spikes; do not use raw counts in investor materials | Fix within 2 weeks |
| H4 (draft content may be public) | Verify in staging whether draft posts appear in public listings; if yes, escalate to ship-blocker | Verify before launch |
| H5 (author name fragile matching) | Ensure launch authors have simple names; document the limitation | Fix within 1 month |
| M2-M4 (missing SEO on 3 pages) | Monitor Google Search Console for indexing issues on author/category/contact pages | Fix within 2 weeks |
| M5-M7 (console.log in production) | Cosmetic server-side only; no user impact | Fix within 1 month |
| M8 (duplicate API calls) | Monitor API load; if backend is under stress, prioritize | Fix within 1 month |
| M9 (annual pricing data duplication) | Ensure monthly and annual prices are consistent before launch; manual check | Fix within 2 months |
| M12 (inconsistent field names) | Verify blog images display correctly in staging | Fix within 2 weeks |
| M16 (baseUrl behind proxy) | Ensure reverse proxy forwards X-Forwarded-Proto header |
Fix within 1 month |
| M17 (useQuery import bloat) | Monitor server bundle size | Fix within 2 months |
Backlog-Safe List (can wait for future sprints)¶
These issues have minimal user/business impact and can be addressed as part of normal development:
| Issue | Rationale |
|---|---|
| M11 (catch-all route conflict) | Theoretical risk; no current conflict |
| M13 (two button components) | Code quality; no user impact |
| M14 (secondary variant defined twice) | Dead code; no user impact |
| M15 (focus rings non-functional) | Accessibility concern, not a launch blocker |
| L1 (lowercase function names) | Convention; no runtime impact |
| L2-L3 (breadcrumb raw slugs) | Minor cosmetic issue |
| L4 (category title wrong source) | Edge case with multi-category posts |
L5 (CSS typo breaka-all) |
Minor layout issue in contact page |
| L6-L7 (dead code) | Cleanup; no runtime impact |
| L8 (header skeleton CSS) | Loading state cosmetic |
| L9 (misleading function name) | Code quality; no runtime impact |
| L10 (unnecessary async) | Lint warning; no runtime impact |
| L11 (animation variants in body) | Minor performance; negligible on modern hardware |
| L12 (both images priority) | Wastes ~200KB bandwidth on initial load |
| L13 (partner logos forced black) | Design choice; no functionality impact |
| L14 (inconsistent CTA labels) | Copywriting consistency; fix with H1 button work |
Final Assessment¶
JOHN (PM): The public site has a 3-hour fix list that prevents 6 months of cascading failures. The cache bug alone would cost us more in lost SEO recovery time than the entire sprint needed to fix all ship-blockers. The dead CTA buttons would waste our entire marketing budget. The XSS vulnerabilities would end the company. There is no scenario where shipping without the ship-blocker fixes is a rational business decision.
QUINN (QA): From a testing perspective, the most alarming finding is not any individual bug -- it's that ZERO of these 45 issues would have been caught by the test suite, because the test suite does not exist. The first investment after fixing ship-blockers should be a basic E2E smoke test that: (1) loads each public page, (2) verifies non-empty, distinct content, (3) clicks every CTA button and verifies navigation, and (4) checks for <script> tags in rendered blog content. That single test file prevents incidents 1, 2, 3, and 7 from ever recurring.
Generated by Pre-mortem Analysis (John + Quinn)
Source: docs/deep-dive-landing-page-public-site.md (45 issues)
Analysis date: 2026-02-25