Skip to content

feat(web): SEO foundation — sitemap, robots & canonical/OG defaults#551

Merged
ilyanfraimbault merged 12 commits into
developfrom
feat/550-seo-foundation
Jun 18, 2026
Merged

feat(web): SEO foundation — sitemap, robots & canonical/OG defaults#551
ilyanfraimbault merged 12 commits into
developfrom
feat/550-seo-foundation

Conversation

@ilyanfraimbault

Copy link
Copy Markdown
Owner

What

Sets up the SEO foundation so truemain.lol can be submitted to Google Search Console and crawled properly. Installs @nuxtjs/seo and wires the core surfaces.

Changes

  • Module + site identity — register @nuxtjs/seo with site config (url https://truemain.lol, name, description). The url is the prod default and is overridable per environment via NUXT_PUBLIC_SITE_URL.
  • Dynamic sitemapserver/routes/__sitemap__/urls.ts enumerates the data-driven routes: one URL per champion (from the DDragon-backed champion list) and one per truemain profile (from the leaderboard). Each family is fetched defensively, so an unavailable upstream doesn't fail the whole sitemap. Static pages are auto-discovered.
  • robots.txt — now generated by nuxt-robots (with the Sitemap: reference and non-prod indexing protection); the static public/robots.txt is removed. riot.txt is kept.
  • Canonical + OG/Twitterseo-utils injects <link rel=canonical>, og:* and twitter:card automatically from the existing per-page useSeoMeta() titles/descriptions.
  • Exclusions/dev/* playground routes excluded from the sitemap.
  • OG-image — on-demand renderer disabled (no social artwork yet); follow-up.

Verification

Verified against the production build (nuxt build + node .output/server/index.mjs):

  • /robots.txt → indexable, references https://truemain.lol/sitemap.xml
  • /sitemap.xml → static pages + one entry per champion; /dev absent (0 matches)
  • homepage carries canonical, og:title/description/url/site_name, twitter:card
  • nuxt build passes

Truemain profile URLs populate when the backend is running; it was off locally, so that list was empty (handled gracefully).

Manual follow-up (owner)

  1. Verify domain ownership in Google Search Console (DNS TXT record).
  2. After deploy, submit https://truemain.lol/sitemap.xml in Search Console.

Closes #550

ilyanfraimbault and others added 3 commits June 14, 2026 12:21
…ults

Set up the SEO foundation so the public site can be indexed by Google.

- Register @nuxtjs/seo with site identity (url, name, description); url
  defaults to https://truemain.lol and is overridable via NUXT_PUBLIC_SITE_URL.
- Add a dynamic sitemap source (server/routes/__sitemap__/urls.ts) that
  enumerates champion routes (from the DDragon-backed list) and truemain
  profile routes (from the leaderboard), each fetched defensively.
- Let nuxt-robots generate robots.txt (with the Sitemap reference) and remove
  the static public/robots.txt; keep riot.txt.
- Exclude /dev/* playground routes from the sitemap.
- Disable the on-demand OG-image renderer (no social artwork yet); seo-utils
  still derives og:* and twitter:card from each page's useSeoMeta().

Closes #550
…ation

# Conflicts:
#	web/package-lock.json
#	web/package.json
Comment thread web/server/routes/__sitemap__/urls.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review SEO Foundation (#550)

La PR est propre et bien construite. La mise en place de @nuxtjs/seo est correcte, le fichier robots.txt statique est remplacé de façon intentionnelle par la génération dynamique, et la gestion défensive des erreurs dans le handler de sitemap (.catch(() => [])) est une bonne pratique.

Aucun point bloquant identifié.


Suggestions (non bloquantes) :

  • urls.ts ligne 48 — response.total ?? 0 : le fallback crée un piège silencieux : si total est absent à l'exécution, urls.length >= 0 est toujours vrai et la boucle s'arrête après la première page (100 truemains au lieu de tous). Puisque total: number est un champ requis dans LeaderboardResponse, il suffit de supprimer le ?? 0 pour que le code exprime fidèlement le contrat. Détail en commentaire inline.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review SEO Foundation (#550)

La PR est propre et bien construite. La mise en place de @nuxtjs/seo est correcte, le fichier robots.txt statique est remplacé de façon intentionnelle par la génération dynamique, et la gestion défensive des erreurs dans le handler de sitemap (.catch(() => [])) est une bonne pratique.

Aucun point bloquant identifié.


Suggestions (non bloquantes) :

  • urls.ts ligne 48 — response.total ?? 0 : le fallback crée un piège silencieux : si total est absent à l'exécution, urls.length >= 0 est toujours vrai et la boucle s'arrête après la première page. Puisque total: number est un champ requis dans LeaderboardResponse, il suffit de supprimer le ?? 0. Détail en commentaire inline.

The post-merge lockfile mixed sharp optional-dep versions and left nested
deps (cac/yaml/commander) unsatisfied, failing `npm ci` in CI. Regenerated
from a clean install so the lockfile matches package.json.
Comment thread web/server/routes/__sitemap__/urls.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts
The `?? 0` fallback silently capped the truemain sitemap at one page if the
backend ever returned no total (urls.length >= 0 is always true). Rely on the
typed required field so a contract violation falls back to the natural
'rows < pageSize' termination instead of under-indexing.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(web): SEO foundation — sitemap, robots & canonical/OG defaults

Vue d'ensemble

Mise en place solide et bien délimitée des fondations SEO : module @nuxtjs/seo, sitemap dynamique, migration du robots.txt statique vers le générateur, injection automatique des balises canonical/OG/Twitter. Le code est clair, la stratégie défensive sur les upstreams est correcte, et le périmètre est explicitement borné (pas d'OG-image pour l'instant).

Aucun point bloquant identifié — aucun bug, aucune régression, aucune faille de sécurité.


Suggestions (non bloquantes)

Voir les commentaires inline pour le détail. En résumé :

  1. Ambiguïté du slug (urls.ts:43) — Le séparateur - entre gameName et tagLine peut exister dans gameName. S'assurer que la route /truemains/[slug].vue parse le slug de façon cohérente (dernier tiret ou séparateur dédié).

  2. Résilience mid-pagination (urls.ts:37) — Une erreur réseau sur la page N efface les pages 1..N-1 déjà récupérées. Un try/catch interne à la boucle permettrait de conserver les résultats partiels.

  3. Cache sur l'endpoint source (urls.ts:54) — /__sitemap__/urls est directement accessible et peut déclencher jusqu'à 100 appels backend par hit. Ajouter un header Cache-Control côté réponse atténuerait un abus trivial.

- Keep already-collected truemain pages when an intermediate leaderboard page
  throws (break with the partial list) instead of letting it bubble to the
  caller's catch and wipe everything.
- Send a 1h shared-cache Cache-Control on /__sitemap__/urls so a direct hit
  can't trivially fan out MAX_TRUEMAIN_PAGES backend calls per request.
Comment thread web/server/routes/__sitemap__/urls.ts
Comment thread web/server/routes/__sitemap__/urls.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(web): SEO foundation — sitemap, robots & canonical/OG defaults

Ce que fait cette PR

Intègre @nuxtjs/seo pour poser les bases SEO : génération dynamique du robots.txt (suppression du fichier statique), sitemap auto-découvert + source dynamique pour les routes champions et truemains, et injection automatique de canonical / og:* / twitter:card via seo-utils.

Points examinés

Architecture

  • La séparation entre pages statiques (auto-découvertes) et dynamiques (/__sitemap__/urls) est propre.
  • La désactivation de ogImage est justifiée et explicitement documentée — bon soin.
  • L'URL de production hardcodée (https://truemain.lol) avec l'échappatoire NUXT_PUBLIC_SITE_URL est le pattern recommandé par nuxt-site-config.

Logique de pagination (truemainUrls)

  • Le cap MAX_TRUEMAIN_PAGES = 100 borne correctement le fan-out vers le backend (≤ 100 appels par fetch du sitemap).
  • La condition de sortie rows.length < TRUEMAIN_PAGE_SIZE || urls.length >= response.total est correcte.
  • Le try/catch sur chaque page garde les résultats partiels en cas de défaillance transiente — bon choix de dégradation gracieuse.
  • L'encodeURIComponent(slug) avec le format ${gameName}-${tagLine} est cohérent avec LeaderboardRow.vue, TruemainsPanel.vue et les autres composants de l'app : convention respectée.

Sécurité / abus

  • Les $fetch utilisent des chemins relatifs (/api/...) : pas de risque SSRF.
  • Le Cache-Control sur /__sitemap__/urls limite efficacement les appels répétés (voir commentaire inline pour suggestion d'ajouter public).

Cohérence des routes

  • /champions/${champion.championId} → route [id].vue
  • /truemains/${slug} → route [nameTag]/index.vue

Tests
Pas de nouveaux tests unitaires, ce qui est acceptable pour une couche de configuration/infrastructure. Le comportement est vérifié via nuxt build + test manuel documenté dans la description.


Suggestions (non bloquantes)

Voir commentaires inline :

  1. response.rows ?? [] — fallback mort, rows est non-optionnel dans le type.
  2. Cache-Control — ajouter public pour lever l'ambiguïté vis-à-vis des CDN stricts.

- Drop dead `?? []` on response.rows (non-optional under the type contract).
- Make Cache-Control intent explicit with `public` for strict CDNs.
Comment thread web/nuxt.config.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(web): SEO foundation — sitemap, robots & canonical/OG defaults

Aucun point bloquant. La PR pose une base SEO saine : @nuxtjs/seo correctement câblé, sitemap dynamique défensif, robots.txt délégué au module, et ogImage désactivé proprement en attendant les assets.

Points forts

  • La pagination du leaderboard est bornée (MAX_TRUEMAIN_PAGES × TRUEMAIN_PAGE_SIZE) avec un justificatif en commentaire — c'est le bon réflexe.
  • Le Promise.all + .catch(() => []) autour de chaque famille garantit qu'une upstream indisponible ne vide pas le sitemap.
  • La Cache-Control sur /__sitemap__/urls est documentée et cohérente avec la double couche de cache de nuxt-sitemap.
  • L'URL de prod hardcodée est overridable via NUXT_PUBLIC_SITE_URL, ce que nuxt-site-config lit automatiquement.

Suggestions (non bloquantes)

  1. SitemapUrl local (ligne 19-21) — Le type est redéfini localement ; importer SitemapUrl depuis #sitemap/types évite une désynchronisation silencieuse si le module évolue.
  2. Slug ambigu (ligne 52) — Le séparateur - entre gameName et tagLine n'est pas encodé par encodeURIComponent, ce qui rend le slug potentiellement ambigu si gameName contient des tirets. Hérité de la convention de routage existante, mais à garder en tête.
  3. Rate-limit applicatif (ligne 69) — Cache-Control: public protège au niveau CDN ; un accès direct à l'origine contourne cette protection. Un rate-limit H3/Nitro serait une défense en profondeur utile.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aucun point bloquant. La PR pose une base SEO saine : @nuxtjs/seo correctement câblé, sitemap dynamique défensif, robots.txt délégué au module, ogImage désactivé proprement. Suggestions non bloquantes laissées en commentaires inline.

Import SitemapUrl from #sitemap/types instead of redeclaring a local subset,
so the source stays aligned with the module's contract.
Comment thread web/server/routes/__sitemap__/urls.ts
Comment thread web/server/routes/__sitemap__/urls.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(web): SEO foundation

Vue d'ensemble

La PR met en place une fondation SEO solide : module @nuxtjs/seo, sitemap dynamique (champions + leaderboard), remplacement du robots.txt statique par nuxt-robots, et injection automatique des balises canonical/OG/Twitter depuis les useSeoMeta() existants. Le tout est bien défensif (chaque famille de sources peut échouer indépendamment) et le risque d'abus de l'endpoint /sitemap__/urls est atténué par le header Cache-Control documenté dans les commentaires.

Points bloquants

Aucun.

Suggestions (non bloquantes)

  • Pagination séquentielle dans truemainUrls() (ligne 33) — 100 pages séquentielles pourraient devenir perceptibles si le leaderboard grossit fortement. Après la première page qui révèle total, les pages restantes pourraient être parallélisées. Détails en commentaire inline.
  • Ambiguïté du séparateur - dans le slug (ligne 49) — sans conséquence pratique puisque Riot interdit les tirets dans les Riot IDs, mais mérite d'être gardé en tête si la contrainte évolue. Détails en commentaire inline.

The lockfile generated by the local npm 11.6 omitted the sharp@0.34.5 optional
platform packages that npm 11.13 (used in CI) requires, so `npm ci` failed in
CI while passing locally. Regenerated with npm 11.13.0 so the optional-dep set
matches what CI validates.
Comment thread web/server/routes/__sitemap__/urls.ts Outdated
Comment thread web/server/routes/__sitemap__/urls.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review feat(web): SEO foundation

La PR est bien structurée, documentée et défensive. Aucun point bloquant identifié.

Ce qui est solide:

  • @nuxtjs/seo configuré proprement avec site, sitemap, ogImage cohérents avec l'état actuel.
  • NUXT_PUBLIC_SITE_URL comme override par environnement — bonne pratique pour éviter que staging/preview polluent Search Console.
  • Gestion défensive dans truemainUrls() : break sur erreur intermédiaire, catch par famille dans Promise.all ; comportement dégradé documenté.
  • Cap MAX_TRUEMAIN_PAGES pour éviter une boucle infinie sur un total pathologique.
  • Suppression du robots.txt statique en faveur de nuxt-robots — évite la désynchronisation.

Suggestions (non bloquantes):

Slug gameName-tagLine (cf. commentaire inline l. 45) — Le séparateur tiret est ambigu si le game name contient un tiret. Vérifier que la convention correspond au param de route existant, ou basculer sur le séparateur canonique Riot encodé en URL (%23).

Amplification origine (cf. commentaire inline l. 61) — Le Cache-Control protège les CDN ; un bypass direct de l'origine déclenche jusqu'à 100 appels backend par requête. A garder en tête si un layer de cache n'est pas systématiquement en amont.

Wrap the champion+truemain URL build in Nitro's defineCachedFunction (1h),
mirroring server/api/static/champions.get.ts. A direct hit to /__sitemap__/urls
now reuses the cached result instead of re-fanning out up to MAX_TRUEMAIN_PAGES
backend calls, closing the low-threshold amplification vector at the origin
(not just via downstream CDNs). Caching the function rather than the handler
keeps the explicit public, s-maxage Cache-Control header intact.
Comment thread web/server/routes/__sitemap__/urls.ts
Comment thread web/server/routes/__sitemap__/urls.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(web): SEO foundation — sitemap, robots & canonical/OG defaults

L'implémentation est solide. Les points clés sont bien traités :

  • Protection contre l'amplification : defineCachedFunction + Cache-Control évitent qu'un flood sur /__sitemap__/urls ne se translate en 100 appels backend — le risque est explicitement documenté dans le code.
  • Dégradation gracieuse : chaque famille d'URLs (champions, truemains) est fetchée indépendamment ; une indisponibilité upstream n'empêche pas l'autre liste de contribuer.
  • Pagination défensive : le cap MAX_TRUEMAIN_PAGES = 100 prévient une boucle incontrôlée si response.total est pathologique.
  • Pas d'OG image : décision intentionnelle et documentée (ogImage: { enabled: false }), cohérente avec l'état actuel du projet.
  • NUXT_PUBLIC_SITE_URL : surcharge par environnement correctement commentée, évite les canonicals de prod sur staging.

Aucun point bloquant identifié. ✓


Suggestions (non bloquantes) :

  1. truemainUrls().catch(() => []) redondant (ligne 75) — la fonction ne propage jamais d'exception ; le .catch externe est du code mort. Voir commentaire inline.
  2. Encodage des slugs truemain (ligne 52) — vérifier que encodeURIComponent(slug) produit exactement le même format d'URL que le router Nuxt pour /truemains/[slug], afin d'éviter toute divergence d'URL aux yeux de Google. En pratique sans risque si les gameName ne contiennent pas de caractères spéciaux. Voir commentaire inline.

Drop 'aggregated' and 'patch by patch' from the site/home meta description
per review — keeps it concise. Applied to both nuxt.config site.description
and the homepage useSeoMeta.
Comment thread web/server/routes/__sitemap__/urls.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(web): SEO foundation

Le setup @nuxtjs/seo est bien structuré et les choix sont solides : url de prod en défaut avec override NUXT_PUBLIC_SITE_URL, cache Nitro pour limiter l'amplification de requêtes backend, exclusion cohérente de /dev/**, désactivation explicite du renderer OG image, et suppression propre du robots.txt statique au profit de nuxt-robots.

Le format de slug dans le sitemap (${gameName}-${tagLine} + encodeURIComponent) est vérifié et correspond exactement à ce que produisent LeaderboardRow.vue et TruemainsPanel.vue.


Suggestions (non bloquantes)

  • web/server/routes/__sitemap__/urls.ts ligne 48response.rows est accédé hors du try/catch. Une réponse API malformée ferait remonter une TypeError jusqu'au .catch(() => []) de loadSitemapUrls, effaçant les pages déjà collectées (inverse du but documenté dans le catch). Simple correctif : const rows = response?.rows ?? [] (commentaire inline posté).

  • Pas de lastmod dans les URLs du sitemap — optionnel, mais ajouter lastmod: new Date().toISOString() sur chaque URL aiderait Google à prioriser les re-crawls.

Move the rows access and the row loop inside the try/catch so a malformed
payload (missing `rows`, which throws on for...of) is caught alongside network
errors — the walk then stops and keeps the partial list instead of bubbling to
loadSitemapUrls's catch and discarding every page collected so far. Resolves
the gap between the inner 'keep partial' intent and the earlier removal of the
defensive `?? []`.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(web): SEO foundation — sitemap, robots & canonical/OG defaults

Le code est dans un état propre après les itérations précédentes.

Ce qui est solide :

  • response.rows lu dans le try (fix 983afc4 correctement appliqué) — la dégradation gracieuse documentée dans le commentaire est désormais effective.
  • Protection contre l'amplification : defineCachedFunction (1 h) + Cache-Control: public, max-age=3600 évitent qu'un flood sur /__sitemap__/urls ne déclenche 100 appels backend par requête.
  • Promise.all + .catch(() => []) par famille : champions et truemains sont indépendants ; une upstream défaillante ne vide pas le sitemap.
  • Cap MAX_TRUEMAIN_PAGES = 100 avec justificatif en commentaire — borne correcte.
  • NUXT_PUBLIC_SITE_URL comme override : staging/preview ne polluent pas Search Console.
  • Suppression du robots.txt statique cohérente avec la délégation à nuxt-robots.

Aucun point bloquant. ✓

@ilyanfraimbault ilyanfraimbault merged commit 774ea70 into develop Jun 18, 2026
9 checks passed
@ilyanfraimbault ilyanfraimbault deleted the feat/550-seo-foundation branch June 18, 2026 21:40
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.

feat(web): SEO foundation — sitemap, robots, canonical & OG defaults

1 participant