SEO + AEO Overhaul — Landing & MCP Pages
Date: 2026-06-09
Surface: static#index (landing), static#mcp, static#mcp_tools, static#pricing and shared static layout
Goal: Take the public marketing surface from "invisible to search/answer engines" to a fully-optimized state — 100/100 on Lighthouse SEO and clean on Ahrefs/Semrush-style audits — and make the site first-class for AEO (citations by ChatGPT, Perplexity, Google AI Overviews, Claude).
Decisions (locked with user)
- Delivery: Plan, then implement in the same session.
- Positioning (broad, multi-cluster): all-in-one life management / personal OS + AI/MCP-controllable life app + gamified productivity + personal finance/habits/goals. Keyword targeting spans all four clusters, primary cluster = "all-in-one life OS".
- Market: Equal bilingual (pt-BR + en) — both languages get first-class hreflang, canonical, sitemap, and content.
- AI layer: Full —
llms.txt+llms-full.txt+ Markdown (.md) mirror of key pages. - Bilingual URL strategy: Query param
?lang=en/?lang=pt-br, honored byStatic::LocaleResolverat highest priority.x-default= bare URL (resolves to pt-BR default). Lowest-risk, reversible, no routing overhaul.
Current-state findings
- Production host:
applifehub.com. Default localept-BR, available[pt-BR, en]. - Locale resolved by
Static::LocaleResolver(cookie → Accept-Language → pt-BR). Same URL serves both languages dynamically → hreflang cannot work without per-language URLs. static.html.erbhead has: title, description, OG (partial), Twitter cards, GA4, favicon. Missing: canonical, valid hreflang, JSON-LD,og:locale,max-image-preview.public/robots.txteffectively empty. Nositemap.xml. Nollms.txt. No.mdmirror. No FAQ / question-headings / freshness signals.- No SEO gem (
meta-tags,sitemap_generator) installed → implement natively in ERB + a small helper + controller actions.
Architecture
A. Locale-from-URL foundation
Static::LocaleResolver: addfrom_param(highest priority) reading?lang=. Normalizept-br/pt/pt_BR→:"pt-BR",en→:en; ignore unknown. New priority: param → cookie → Accept-Language → pt-BR.- New
SeoHelper(app/helpers) with:canonical_url— absolute current URL, carrying?lang=for the non-default locale, bare for default.hreflang_alternates—{ "pt-BR" => url, "en" => url, "x-default" => bare_url }, absolute, reciprocal.localized_url(locale, path = request.path)— builds absolute URL with correct?lang=.seo_base_url—applifehub.comhost honoringAPP_HOST.
B. <head> upgrades (app/views/static/static.html.erb + _head not used by static)
- Self-referencing
<link rel="canonical">. - Correct reciprocal hreflang (
pt-BR,en,x-default) with absolute?lang=URLs. <meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1">.- OG completeness:
og:site_name,og:locale(current),og:locale:alternate(other). - A
<%= yield :structured_data %>slot rendered in<head>.
C. Structured data (JSON-LD) — one connected graph
Rendered via a partial static/_structured_data (site-wide: Organization + WebSite + SoftwareApplication) injected on every static page through the layout, plus per-page additions via content_for :structured_data:
- Organization (
@id #organization, logo,sameAssocials — placeholders ready to fill). - WebSite (
@id #website, publisher → org,inLanguage: [pt-BR, en],SearchAction). - SoftwareApplication (
name,applicationCategory: BusinessApplication,operatingSystem: Web, iOS, Android, localizeddescription,offersbuilt from real pricing vialanding_monthly_plan/landing_yearly_plan,featureList). No fabricatedaggregateRating(policy-safe; add later when real reviews exist). - index page:
FAQPage(mirrors visible FAQ),BreadcrumbList. - mcp page:
FAQPagefor MCP FAQ,BreadcrumbList,TechArticle/HowTo-free (HowTo deprecated). - pricing page:
Offers per tier. dateModifiedset to a committed constant (SeoHelper::CONTENT_UPDATED_ON) for freshness.
D. robots.txt (static public/robots.txt)
Explicitly allow reputable AI crawlers (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot, Google-Extended, Applebot-Extended), allow-all default, and reference the sitemap. (Goal = visibility/citations, so allow training+search bots.)
E. XML sitemap (dynamic)
- Route
get "sitemap.xml", to: "sitemap#index", defaults: { format: "xml" }. SitemapController#indexbuilds URLs for every public static page (/,/pricing,/mcp-docs,/mcp-tools,/terms,/privacy,/welcome,/lifehub_plan,/documentation) × 2 locales, each<url>carryingxhtml:linkhreflang alternates +<lastmod>. Cached,text/xml.- (Single sitemap; well under 50k-URL limit. Sitemap-index not needed yet.)
F. AI layer
/llms.txt— curated Markdown index (route →LlmsController#index,text/plain). H1 + blockquote summary +## Product/## MCP & AI/## Pricing/## Optionalsections linking to.mdpages, bilingual note./llms-full.txt— expanded single-file content (key page content concatenated as Markdown).- Markdown mirror —
.mdfor home, mcp, mcp-tools, pricing. Implementation: routesget "index.md",get "mcp-docs.md", etc. →MarkdownControllerrendering page content from dedicated.md.erbtemplates / i18n,Content-Type: text/markdown,Vary: Accept. Also honorAccept: text/markdownon the canonical routes (content negotiation) returning the same markdown..mdURLs arenoindexand excluded from sitemap (agent-only).
G. AEO content / copy
- FAQ section added to
index(visible<section>with question<h2>/<h3>+ concise lead-with-answer paragraphs), 6–8 Q&As covering: "What is Lifehub?", "Is it free?", "What can I track?", "How does the AI/MCP integration work?", "Is my data private?", "Does it work on mobile?", "Lifehub vs spreadsheets/Notion?". Bilingual locale keys. - MCP FAQ added to
mcppage (4–6 Q&As: what is MCP, which clients, is it secure, what can the AI do). - Lead-with-answer intro paragraph near top of each page (one self-contained sentence defining the product, keyword-rich, bilingual).
- Freshness: visible "Last updated {date}" line in footer or FAQ section + accurate
dateModifiedin JSON-LD. - Meta copy rewrite: richer, keyword-targeted
meta_title/meta_descriptionfor landing, mcp, pricing in both locales (life OS + finance + habits + gamification + AI). Keep titles ≤ ~60 chars, descriptions ≤ ~155. - Semantic/heading hygiene: ensure exactly one
<h1>per page (landing currently has an<h1>in hero AND an<h1>inside the dashboard mockup — demote the mockup one to a styled<div>/<p>to avoid duplicate H1). Add descriptivealtwhere missing; confirm hero image not lazy-loaded.
Workstream order (implementation)
- i18n URL foundation —
LocaleResolver#from_param,SeoHelper. - Head infra — canonical, hreflang, robots meta, OG completeness, structured-data slot.
- Structured data —
_structured_datapartial + per-pagecontent_for. - robots.txt rewrite.
- Sitemap controller + route + view.
- llms.txt / llms-full.txt controller + routes.
- Markdown mirror controller + routes +
Vary/content negotiation. - AEO copy — FAQ sections, lead-in, freshness, meta rewrites, H1 fix, alts (bilingual locale keys in
static.en.yml+static.pt-BR.yml). - Verification — routes load, pages render (200), JSON-LD validates structurally, no duplicate H1, existing tests green.
Out of scope (noted, not done now)
- Real
aggregateRating(needs genuine reviews). - Path-prefix locale routing (chose query-param).
- Off-site AEO (YouTube demo, Reddit presence) — content/marketing task.
- Per-tier
Productreview schema. - Core Web Vitals deep profiling (we apply the known wins: sized images, lazy-load below fold, hero not lazy; no Lighthouse lab run here).
Risks
?lang=param + canonical must not create duplicate-content loops → canonical always self-references the resolved-locale URL; default locale uses bare URL.- Content negotiation must send
Vary: Acceptto avoid cache poisoning (HTML served to MD clients). - JSON-LD
offersdepends onlanding_*_planhelpers returning data; guard for nil. - Demoting mockup
<h1>must not change visual styling.