Skip to content

feat(seo): canonical origin fix, metadata completa y landing pages por lenguaje - #163

Merged
DataDave-Dev merged 3 commits into
mainfrom
feat/seo
Jul 29, 2026
Merged

feat(seo): canonical origin fix, metadata completa y landing pages por lenguaje#163
DataDave-Dev merged 3 commits into
mainfrom
feat/seo

Conversation

@DataDave-Dev

@DataDave-Dev DataDave-Dev commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Qué hace

Arregla el bug que impedía la indexación y monta el SEO completo del sitio.

El bug. Cada canonical, hreflang y entrada del sitemap se construía con
process.env.NEXT_PUBLIC_APP_URL || "https://weftmap.com". La variable no está
definida en ningún lado, así que ganaba el fallback: el sitio le decía a Google
que la versión canónica de cada página vive en un dominio que no es el deploy.
Eso suprime la indexación del sitio que sí existe. Verificar la propiedad en
Search Console no lo arregla.

Ahora el origen es una constante SITE_URL única. Deliberadamente no es una env
var: el origen no debe variar por entorno (un preview publicando su propio
canonical es un bug), y la forma env var es justamente la que dejó pasar el valor
equivocado en silencio. Migrar a dominio propio es una línea que aparece en el diff.

Sobre eso:

  • metadataBase, títulos y descripciones localizados en los 6 idiomas desde los
    diccionarios, en lugar de un único string en inglés hardcodeado.
  • buildMetadata() compone title, description, canonical, hreflang, OpenGraph y
    Twitter card en un solo cálculo, así og:url y rel=canonical no pueden divergir.
  • Imagen OG generada con next/og, más apple-icon y manifest.webmanifest.
  • JSON-LD (SoftwareApplication, Organization, FAQPage, TechArticle, BreadcrumbList)
    con el nonce del CSP por request. El FAQ se genera del mismo array que renderiza
    la sección visible, así que no puede describir contenido ausente.
  • robots.txt excluye /api y las rutas /graphs con auth, que además emiten
    noindex. El sitemap las descarta, descarta /docs (que es un redirect),
    lleva hreflang por entrada y estampa lastModified en build.
  • 42 landing pages nuevas en /[lang]/call-graph/[language], 7 lenguajes x 6 locales.
  • Java faltaba en la tabla de lenguajes y en los chips del hero, en los 6 idiomas,
    y el subtítulo seguía diciendo seis lenguajes.

Tipo

  • feat
  • fix
  • refactor
  • docs
  • test
  • chore

Checklist

  • pnpm lint pasa
  • pnpm typecheck pasa
  • pnpm test pasa (612 tests, 16 archivos)
  • pnpm build pasa (122 páginas prerenderizadas)
  • Si agrega un lenguaje, incluye su test

Verificación adicional

Check Resultado
Playwright e2e 6/6
42 rutas landing 42/42 -> 200; lenguaje desconocido -> 404
Reciprocidad hreflang + canonical 36 páginas, sin discrepancias
JSON-LD nonce coincide con el header CSP, cero violaciones
Render sin JS h1, snippet, copy, CTA y links presentes; ar en dir="rtl"

Tres cosas que solo aparecieron al implementar

og:image desaparecía de todas las rutas menos la home. Definir openGraph
en un segmento hijo reemplaza el objeto entero del padre en Next, así que la
imagen del file convention solo llegaba a la home. Ahora se declara explícita en
buildMetadata. Se habría descubierto en producción con las previews rotas.

El <script> de JSON-LD rompía la hidratación. El navegador vacía el nonce
del DOM a propósito (anti-exfiltración) y React lo lee como mismatch. Resuelto con
suppressHydrationWarning, el mismo patrón que ya usa el layout en su script de
tema. Por eso el test del nonce mira el HTML del servidor y no el DOM.

opengraph-image.tsx va bajo [lang]/, no en la raíz. En la raíz la ruta
sería /opengraph-image, sin extensión, y el matcher de proxy.ts la redirigiría
con 307 — que varios crawlers sociales no siguen. Se vería bien en el navegador y
rota en cada tarjeta social.

Desviaciones conscientes

  • Breadcrumb de landings: 2 niveles, no 3. El nivel intermedio necesitaría
    /[lang]/call-graph, que no existe. Apuntar un breadcrumb a un 404 es peor que
    acortarlo. Una página hub ahí lo restauraría y vale como trabajo aparte.
  • Las landings no son Static en la tabla de rutas. El layout lee headers()
    para el nonce del CSP, lo que marca todo el árbol como dinámico, incluidas las
    rutas que ya existían. Hacerlas estáticas significaría quitar el nonce por request:
    cambiar un control de seguridad real por un detalle de caché. Se prerenderizan
    igual y el HTML llega completo al crawler.
  • /docs fuera del sitemap — es un redirect() a /docs/introduction.
  • Los e2e corren contra pnpm dev (config existente), no contra build de producción.
    El nonce y la ausencia de unsafe-inline son idénticos en ambos; solo difiere
    unsafe-eval.

Después del merge

Queda trabajo que necesita el deploy: verificar /robots.txt y /sitemap.xml en
vivo, enviar el sitemap en Search Console anotando el baseline de impresiones,
Rich Results Test, debugger de tarjeta social y Lighthouse.

85c3a2b por sí solo es lo que desbloquea la indexación y es desplegable aparte.

Summary by CodeRabbit

  • New Features

    • Added localized landing pages for Python, JavaScript, TypeScript, Go, Rust, Java, and SQL.
    • Added structured SEO data, canonical and alternate links, social metadata, Open Graph images, and a web app manifest.
    • Added clickable language links and improved sitemap and crawler controls.
    • Added localized metadata and landing-page content across supported languages.
  • Bug Fixes

    • Authenticated and saved-graph pages are now excluded from search indexing.
  • Tests

    • Added automated coverage for SEO metadata, structured data, landing pages, sitemap output, and security policy behavior.

playwright-report/ and test-results/ are gitignored, but ESLint does not read
.gitignore, so running the e2e suite before lint floods the output with errors
from bundled report assets (3030 problems, none of them ours).
Every canonical URL, hreflang alternate and sitemap entry was built from
`process.env.NEXT_PUBLIC_APP_URL || "https://weftmap.com"`. The variable is set
nowhere, so the fallback shipped: the site told Google that the canonical
version of every page lives on a domain that is not the deployment, which
suppresses indexing of the site that actually exists.

The origin is now a single `SITE_URL` constant. It is deliberately not an
environment variable — the origin must not vary per environment (a preview
deploy publishing its own canonical is a bug), and the env-var form is what
shipped the wrong value silently in the first place. Migrating to a custom
domain is a one-line edit that shows up in a diff.

On top of that:

- metadataBase, so relative asset paths resolve absolutely.
- buildMetadata() composes title, description, canonical, hreflang, OpenGraph
  and Twitter card from one computation, so og:url and rel=canonical cannot
  drift apart. Titles and descriptions come from the i18n dictionaries in all
  six locales instead of one hardcoded English string.
- The OG image is declared explicitly rather than via Next's file convention:
  setting `openGraph` in a child segment replaces the parent's whole openGraph
  object, so a file-convention image reaches only the homepage and silently
  disappears everywhere else. The route lives under [lang]/ because at the app
  root it would be extension-less and the proxy would 307 it, which several
  social crawlers do not follow.
- JSON-LD (SoftwareApplication, Organization, FAQPage, TechArticle,
  BreadcrumbList) carrying the per-request CSP nonce. FAQ markup is generated
  from the same dictionary array the visible FAQ renders, so it cannot describe
  absent content. graphs/[id] gets none: it renders user-supplied titles.
- robots.txt disallows /api and the auth-gated /graphs routes, which also emit
  noindex. The sitemap drops them, drops the redirecting /docs, carries
  per-entry hreflang, and stamps lastModified at build time.
- Java was missing from the supported-languages copy and the hero chips in all
  six locales, and the subtitle still claimed six languages.

Titles and descriptions are length-checked by test: translations run 15-20%
longer than their English source, and 200+ strings cannot be eyeballed.
"call graph generator" is a crowded, low-intent query dominated by IDE vendors
and academic tooling. "Python call graph generator" is narrower, has clearer
intent, and Weftmap can plausibly be the best answer for it — which is the only
kind of query worth targeting from a standing start with no domain authority.

Adds /[lang]/call-graph/[language] for the seven supported analyzers, across all
six locales: 42 prerendered pages, each with its own H1, title, description and
prose, a real analyzable snippet, a CTA into /[lang]/app and links into the docs.
The homepage language table now links to them, since it is the strongest
internal link source the site has.

SQL is the deliberate exception: its analyzer emits an entity-relationship
diagram, not a call graph, so its page targets the schema query instead. Calling
it a call graph would rank for the wrong intent and bounce.

Two tests keep this honest. LANDING_LANGUAGES is checked against the analysis
registry, so an eighth language cannot ship without a landing page or an explicit
exclusion — the repo's whole pitch is that adding a language takes a few lines,
and a hand-maintained parallel list would silently fall behind. And every snippet
is run through its real analyzer, asserting at least two nodes and one edge: the
tripwire against the thin programmatic content Google's scaled-content policy
targets.

The breadcrumb has two levels rather than three. A "Call graph" middle position
would need an item URL, and /[lang]/call-graph does not exist; pointing a
breadcrumb at a 404 is worse than a shorter trail. A real hub page there would
restore it and is worth considering separately.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee45e702-64e7-4fe6-b503-59d1e1daf1af

📥 Commits

Reviewing files that changed from the base of the PR and between 37a4a90 and 3dd5187.

⛔ Files ignored due to path filters (1)
  • src/app/apple-icon.png is excluded by !**/*.png
📒 Files selected for processing (30)
  • e2e/seo.spec.ts
  • eslint.config.mjs
  • src/app/[lang]/app/page.tsx
  • src/app/[lang]/call-graph/[language]/page.tsx
  • src/app/[lang]/docs/[slug]/page.tsx
  • src/app/[lang]/graphs/[id]/page.tsx
  • src/app/[lang]/graphs/page.tsx
  • src/app/[lang]/layout.tsx
  • src/app/[lang]/opengraph-image.tsx
  • src/app/[lang]/page.tsx
  • src/app/manifest.ts
  • src/app/robots.ts
  • src/app/sitemap.test.ts
  • src/app/sitemap.ts
  • src/components/sections/Hero.tsx
  • src/components/sections/SupportedLanguages.tsx
  • src/components/seo/JsonLd.tsx
  • src/i18n/dictionaries/ar.json
  • src/i18n/dictionaries/en.json
  • src/i18n/dictionaries/es.json
  • src/i18n/dictionaries/fr.json
  • src/i18n/dictionaries/it.json
  • src/i18n/dictionaries/pt.json
  • src/lib/landing-languages.test.ts
  • src/lib/landing-languages.ts
  • src/lib/seo-lengths.test.ts
  • src/lib/seo.test.ts
  • src/lib/seo.ts
  • src/lib/structured-data.test.ts
  • src/lib/structured-data.ts

📝 Walkthrough

Walkthrough

The PR adds localized SEO metadata, JSON-LD structured data, language-specific call-graph landing pages, Open Graph and manifest routes, crawler controls, sitemap generation, and automated validation across unit, integration, and end-to-end tests.

Changes

SEO and landing pages

Layer / File(s) Summary
SEO and structured-data foundations
src/lib/seo.ts, src/lib/structured-data.ts, src/components/seo/JsonLd.tsx, src/lib/landing-languages.ts, src/i18n/dictionaries/*
Adds centralized SEO metadata, localized landing content, Schema.org builders, and nonce-protected JSON-LD rendering.
Landing-page registry and rendering
src/app/[lang]/call-graph/[language]/page.tsx, src/components/sections/SupportedLanguages.tsx, src/app/[lang]/page.tsx
Adds statically generated localized landing pages with snippets, breadcrumbs, CTAs, alternate-language links, and homepage structured data.
Route metadata and site assets
src/app/[lang]/**, src/app/manifest.ts, src/app/robots.ts, src/app/[lang]/opengraph-image.tsx
Applies shared metadata, marks graph routes non-indexable, and adds Open Graph image, manifest, and crawler configuration routes.
Static sitemap generation
src/app/sitemap.ts
Generates static localized sitemap entries for home, app, documentation, and landing routes with canonical and alternate URLs.
SEO and landing validation
e2e/seo.spec.ts, src/lib/*.test.ts, src/app/sitemap.test.ts, eslint.config.mjs
Validates metadata, JSON-LD, CSP nonces, landing coverage, sitemap output, URL formatting, and test-output lint exclusions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: yasseryg8

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/seo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DataDave-Dev
DataDave-Dev merged commit 9ee54dd into main Jul 29, 2026
1 of 3 checks passed
@DataDave-Dev
DataDave-Dev deleted the feat/seo branch July 29, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant