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)

  1. Delivery: Plan, then implement in the same session.
  2. 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".
  3. Market: Equal bilingual (pt-BR + en) — both languages get first-class hreflang, canonical, sitemap, and content.
  4. AI layer: Full — llms.txt + llms-full.txt + Markdown (.md) mirror of key pages.
  5. Bilingual URL strategy: Query param ?lang=en / ?lang=pt-br, honored by Static::LocaleResolver at 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 locale pt-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.erb head has: title, description, OG (partial), Twitter cards, GA4, favicon. Missing: canonical, valid hreflang, JSON-LD, og:locale, max-image-preview.
  • public/robots.txt effectively empty. No sitemap.xml. No llms.txt. No .md mirror. 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: add from_param (highest priority) reading ?lang=. Normalize pt-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_urlapplifehub.com host honoring APP_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, sameAs socials — placeholders ready to fill).
  • WebSite (@id #website, publisher → org, inLanguage: [pt-BR, en], SearchAction).
  • SoftwareApplication (name, applicationCategory: BusinessApplication, operatingSystem: Web, iOS, Android, localized description, offers built from real pricing via landing_monthly_plan/landing_yearly_plan, featureList). No fabricated aggregateRating (policy-safe; add later when real reviews exist).
  • index page: FAQPage (mirrors visible FAQ), BreadcrumbList.
  • mcp page: FAQPage for MCP FAQ, BreadcrumbList, TechArticle/HowTo-free (HowTo deprecated).
  • pricing page: Offers per tier.
  • dateModified set 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#index builds URLs for every public static page (/, /pricing, /mcp-docs, /mcp-tools, /terms, /privacy, /welcome, /lifehub_plan, /documentation) × 2 locales, each <url> carrying xhtml:link hreflang 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 / ## Optional sections linking to .md pages, bilingual note.
  • /llms-full.txt — expanded single-file content (key page content concatenated as Markdown).
  • Markdown mirror.md for home, mcp, mcp-tools, pricing. Implementation: routes get "index.md", get "mcp-docs.md", etc. → MarkdownController rendering page content from dedicated .md.erb templates / i18n, Content-Type: text/markdown, Vary: Accept. Also honor Accept: text/markdown on the canonical routes (content negotiation) returning the same markdown. .md URLs are noindex and 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 mcp page (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 dateModified in JSON-LD.
  • Meta copy rewrite: richer, keyword-targeted meta_title/meta_description for 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 descriptive alt where missing; confirm hero image not lazy-loaded.

Workstream order (implementation)

  1. i18n URL foundationLocaleResolver#from_param, SeoHelper.
  2. Head infra — canonical, hreflang, robots meta, OG completeness, structured-data slot.
  3. Structured data_structured_data partial + per-page content_for.
  4. robots.txt rewrite.
  5. Sitemap controller + route + view.
  6. llms.txt / llms-full.txt controller + routes.
  7. Markdown mirror controller + routes + Vary/content negotiation.
  8. AEO copy — FAQ sections, lead-in, freshness, meta rewrites, H1 fix, alts (bilingual locale keys in static.en.yml + static.pt-BR.yml).
  9. 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 Product review 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: Accept to avoid cache poisoning (HTML served to MD clients).
  • JSON-LD offers depends on landing_*_plan helpers returning data; guard for nil.
  • Demoting mockup <h1> must not change visual styling.