feat(web): SEO foundation — sitemap, robots & canonical/OG defaults#551
Conversation
release: 1.2.2
…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
There was a problem hiding this comment.
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.tsligne 48 —response.total ?? 0: le fallback crée un piège silencieux : sitotalest absent à l'exécution,urls.length >= 0est toujours vrai et la boucle s'arrête après la première page (100 truemains au lieu de tous). Puisquetotal: numberest un champ requis dansLeaderboardResponse, il suffit de supprimer le?? 0pour que le code exprime fidèlement le contrat. Détail en commentaire inline.
There was a problem hiding this comment.
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.tsligne 48 —response.total ?? 0: le fallback crée un piège silencieux : sitotalest absent à l'exécution,urls.length >= 0est toujours vrai et la boucle s'arrête après la première page. Puisquetotal: numberest un champ requis dansLeaderboardResponse, 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.
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.
There was a problem hiding this comment.
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é :
-
Ambiguïté du slug (
urls.ts:43) — Le séparateur-entregameNameettagLinepeut exister dansgameName. S'assurer que la route/truemains/[slug].vueparse le slug de façon cohérente (dernier tiret ou séparateur dédié). -
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. Untry/catchinterne à la boucle permettrait de conserver les résultats partiels. -
Cache sur l'endpoint source (
urls.ts:54) —/__sitemap__/urlsest directement accessible et peut déclencher jusqu'à 100 appels backend par hit. Ajouter un headerCache-Controlcô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.
There was a problem hiding this comment.
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
ogImageest justifiée et explicitement documentée — bon soin. - L'URL de production hardcodée (
https://truemain.lol) avec l'échappatoireNUXT_PUBLIC_SITE_URLest le pattern recommandé parnuxt-site-config.
Logique de pagination (truemainUrls)
- Le cap
MAX_TRUEMAIN_PAGES = 100borne 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.totalest correcte. - Le
try/catchsur 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 avecLeaderboardRow.vue,TruemainsPanel.vueet les autres composants de l'app : convention respectée.
Sécurité / abus
- Les
$fetchutilisent des chemins relatifs (/api/...) : pas de risque SSRF. - Le
Cache-Controlsur/__sitemap__/urlslimite efficacement les appels répétés (voir commentaire inline pour suggestion d'ajouterpublic).
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 :
response.rows ?? []— fallback mort,rowsest non-optionnel dans le type.Cache-Control— ajouterpublicpour 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.
There was a problem hiding this comment.
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-Controlsur/__sitemap__/urlsest documentée et cohérente avec la double couche de cache denuxt-sitemap. - L'URL de prod hardcodée est overridable via
NUXT_PUBLIC_SITE_URL, ce que nuxt-site-config lit automatiquement.
Suggestions (non bloquantes)
SitemapUrllocal (ligne 19-21) — Le type est redéfini localement ; importerSitemapUrldepuis#sitemap/typesévite une désynchronisation silencieuse si le module évolue.- Slug ambigu (ligne 52) — Le séparateur
-entregameNameettagLinen'est pas encodé parencodeURIComponent, ce qui rend le slug potentiellement ambigu sigameNamecontient des tirets. Hérité de la convention de routage existante, mais à garder en tête. - Rate-limit applicatif (ligne 69) —
Cache-Control: publicprotè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.
Import SitemapUrl from #sitemap/types instead of redeclaring a local subset, so the source stays aligned with the module's contract.
There was a problem hiding this comment.
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èletotal, 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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__/urlsne 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 = 100prévient une boucle incontrôlée siresponse.totalest 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) :
truemainUrls().catch(() => [])redondant (ligne 75) — la fonction ne propage jamais d'exception ; le.catchexterne est du code mort. Voir commentaire inline.- 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.
There was a problem hiding this comment.
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.tsligne 48 —response.rowsest accédé hors dutry/catch. Une réponse API malformée ferait remonter uneTypeErrorjusqu'au.catch(() => [])deloadSitemapUrls, effaçant les pages déjà collectées (inverse du but documenté dans lecatch). Simple correctif :const rows = response?.rows ?? [](commentaire inline posté). -
Pas de
lastmoddans les URLs du sitemap — optionnel, mais ajouterlastmod: 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 `?? []`.
There was a problem hiding this comment.
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.rowslu dans letry(fix983afc4correctement 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__/urlsne 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 = 100avec justificatif en commentaire — borne correcte. NUXT_PUBLIC_SITE_URLcomme override : staging/preview ne polluent pas Search Console.- Suppression du
robots.txtstatique cohérente avec la délégation ànuxt-robots.
Aucun point bloquant. ✓
What
Sets up the SEO foundation so
truemain.lolcan be submitted to Google Search Console and crawled properly. Installs@nuxtjs/seoand wires the core surfaces.Changes
@nuxtjs/seowithsiteconfig (urlhttps://truemain.lol, name, description). The url is the prod default and is overridable per environment viaNUXT_PUBLIC_SITE_URL.server/routes/__sitemap__/urls.tsenumerates 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.nuxt-robots(with theSitemap:reference and non-prod indexing protection); the staticpublic/robots.txtis removed.riot.txtis kept.seo-utilsinjects<link rel=canonical>,og:*andtwitter:cardautomatically from the existing per-pageuseSeoMeta()titles/descriptions./dev/*playground routes excluded from the sitemap.Verification
Verified against the production build (
nuxt build+node .output/server/index.mjs):/robots.txt→ indexable, referenceshttps://truemain.lol/sitemap.xml/sitemap.xml→ static pages + one entry per champion;/devabsent (0 matches)canonical,og:title/description/url/site_name,twitter:cardnuxt buildpassesManual follow-up (owner)
https://truemain.lol/sitemap.xmlin Search Console.Closes #550