Aller au contenu

Problèmes de scan : le registre des codes ​

Chaque problème signalé par le scan Pro possède un code stable issu d’un registre unique, Rankbeam\Seo\Pro\Scanning\IssueRegistry. Les scanners n’inventent jamais de code à la volée : ils construisent chaque problème avec IssueRegistry::make(), qui applique la gravité et le champ du registre et rejette les codes non définis. Le catalogue ci-dessous constitue donc un contrat : tableaux de bord, exports et score Pro lisent les codes plutôt que d’analyser les messages. Le seo:audit gratuit utilise son propre registre de métadonnées dans le cœur, avec un périmètre plus restreint et certains codes hreflang différents.

Chaque code porte :

  • id : chaîne stable enregistrée dans seo_scan_issues.issue_type.
  • severity : critical, warning ou notice, fixe par code. Un code est scindé si nécessaire plutôt que de lui attribuer plusieurs gravités.
  • field : champ seo_meta concerné, ou page pour les problèmes de page.
  • classe d’exécution : données nécessaires à sa détection, ci-dessous.
  • preuves : clés du tableau context du problème.

Classes d’exécution ​

Chaque contrôle appartient à exactement une de ces classes selon ses besoins :

ClasseNécessiteExécution
metadataModèle et résolveur du cœur, sans récupération de pageScan de modèle, PageScanner ; le seo:audit gratuit couvre une partie de ces contrôles
renderedHTML servi par la page, requête interne au noyau ou récupération externeScan d’URL, UrlScanner
networkRequête sortante pour valider une cible distincte, par exemple une canonique vers une autre pageScan d’URL, toujours via SsrfGuard

Un audit gratuit dans le processus ne peut donc pas couvrir tout le scan Pro. Seuls les codes metadata sont calculables sans rendre une page ; la chaîne Pro récupère le HTML rendu et valide les cibles canoniques sur le réseau. Filtrez le registre par classe avec IssueRegistry::byClass(IssueRegistry::EXEC_METADATA).

Codes de métadonnées ​

Détectés depuis le modèle et le résolveur par PageScanner. missing_title, missing_description et les codes de longueur sont aussi émis par le scan d’URL rendue, qui mesure le <head> servi : mêmes codes, même sens.

CodeGravitéChampPreuvesSignification
missing_titlecriticaltitle—Aucun titre ni repli calculable.
missing_descriptionwarningdescription—Aucune meta description ni repli calculable.
missing_og_imagenoticeog_image—Aucune image Open Graph ni repli calculable.
missing_focus_keywordnoticefocus_keywords—Aucun mot-clé principal défini.
duplicate_titlewarningtitletitle, duplicate_urlsTitre réutilisé sur d’autres pages de la même locale.
duplicate_descriptionwarningdescriptiondescription, duplicate_urlsDescription réutilisée sur d’autres pages de la même locale.
title_too_longwarningtitlelength, max, scriptTitre résolu au-dessus de la longueur conseillée pour l’écriture : 60 en latin, environ 30 en CJK.
title_too_shortnoticetitlelength, min, scriptTitre résolu sous le seuil de l’écriture : 30 en latin, environ 15 en CJK.
description_too_longwarningdescriptionlength, max, scriptDescription résolue au-dessus de la longueur conseillée : 160 / environ 80.
description_too_shortnoticedescriptionlength, min, scriptDescription résolue sous le seuil : 70 / environ 35.
robots_conflict_indexingcriticalrobotsrobotsDirective robots contenant à la fois index et noindex.
robots_conflict_followingwarningrobotsrobotsDirective robots contenant à la fois follow et nofollow.
noindex_warningwarningrobotsrobots, canonical, page_url, shipping_signalPage noindex avec canonique autoréférente : heuristique à examiner, pas preuve qu’elle doit être indexée. Émis sur les scans de modèle et d’URL rendue.
invalid_canonicalcriticalcanonicalcanonicalValeur canonique qui n’est pas une URL valide.
cross_domain_canonicalwarningcanonicalcanonical, page_urlCanonique vers un autre hôte que celui de la page.
shared_canonicalnoticecanonicalcanonicalPlusieurs pages déclarent la même canonique.
insecure_canonicalwarningcanonicalcanonicalCanonique http:// sur un site https.
hreflang_invalid_codewarningalternatesinvalid_codesVariante hreflang dont la valeur n’est ni x-default ni un code de langue BCP-47 valide.
hreflang_missing_self_referencewarningalternateslocale, page_urlVariantes déclarées, mais aucune ne référence la locale de la page elle-même.
hreflang_duplicate_codewarningalternatesduplicate_codesMême code hreflang associé à plusieurs URL, groupe ambigu.
hreflang_missing_x_defaultnoticealternateslanguagesGroupe hreflang multilingue sans repli x-default.
aeo_missing_authornoticeschema—Article dans les données structurées sans entité auteur ; attribution ou provenance non explicite dans le schéma.
aeo_article_missing_datenoticeschema—Article dans les données structurées sans date de publication/modification ; chronologie non explicite dans le schéma.

Les seuils de longueur proviennent de la politique du cœur par système d’écriture, utilisée depuis Pro 2.33 : 60/160 pour le latin, environ 30/80 pour le CJK, comptés en graphèmes. Le scan reste ainsi cohérent avec les compteurs de l’éditeur. Les bornes inférieures, titre 30 et description 70 en latin, environ la moitié en CJK, sont les seuils de sous-optimisation du scan. La clé script indique la catégorie appliquée. La mesure porte sur le titre et la description résolus, réellement rendus, replis et suffixe de titre compris.

Les codes hreflang_* valident les variantes déclarées dans alternates du résolveur : codes invalides ou en double, autoréférence absente et absence de x-default dans un groupe multilingue. Ils ne s’exécutent que si la page déclare des variantes. Ces contrôles de métadonnées ne vérifient pas la réciprocité entre pages ; le contrôle réseau facultatif ci-dessous récupère l’autre page.

Les codes aeo_* sont des signaux de préparation aux réponses, AEO : le contenu de l’article est-il explicite dans les données structurées ? Ils lisent le graphe JSON-LD résolu et se déclenchent uniquement s’il déclare un type article, Article, BlogPosting, NewsArticle, etc., sans entité author ou sans datePublished / dateModified. Une page sans article n’est jamais signalée. Ils dépendent de seo-pro.scan.checks.aeo, activé par défaut, et correspondent aux contrôles du seo:audit gratuit.

missing_focus_keyword dépend d’une activation

La remarque sur le mot-clé principal ne se déclenche que si son suivi est activé dans le cœur : seo.keywords.enabled, false par défaut. Sinon, le scan ne signale pas une page sans mot-clé. Le seo:audit gratuit et l’éditeur Filament lisent le même indicateur. Scan, audit et rappel de l’éditeur restent donc cohérents, avec une seule condition d’activation.

Codes du rendu ​

Détectés par UrlScanner dans le HTML servi : requête interne au noyau pour le même hôte, sans trafic sortant, ou récupération protégée pour une cible externe.

CodeGravitéChampPreuvesSignification
http_errorcriticalpagestatusL’URL renvoie un statut 4xx/5xx.
empty_responsecriticalpage—L’URL renvoie un corps vide.
missing_canonicalnoticecanonical—Aucun <link rel="canonical"> dans le head rendu.
noindex_pagenoticerobotsrobotsPage rendue noindex, information. Avec une canonique autoréférente, elle produit plutôt noindex_warning, pris en compte dans le score.
missing_h1noticepage—Aucun titre <h1>.
multiple_h1noticepagecountPlusieurs <h1>, information.
missing_image_altwarningpagecount, total, sampleImages de contenu sans attribut alt. Un alt="" explicite est considéré comme décoratif et n’est pas signalé.
thin_contentnoticepageword_count, threshold, segmenterTexte sous le nombre de mots configuré. Le tokenizer de la liste de contrôle utilise les espaces pour les écritures espacées et la segmentation par dictionnaire ICU, segmenter: intl, avec ext-intl, pour chinois, japonais et thaï. Un article japonais de 400 mots ne devient donc pas un seul « mot ».
mixed_contentwarningpagecount, sampleSous-ressources http:// dans une page https.
html_lang_missingnoticepage—<html lang> absent ou vide ; une technologie d’assistance peut choisir une voix inadaptée.
html_lang_invalidnoticepagedeclaredlang n’est pas une balise BCP-47 valide : english, en_US avec trait de soulignement, jp.
html_lang_mismatchwarningpagedeclared, declared_script, detected_scriptÉcriture du corps incompatible avec la langue déclarée, par exemple lang="en" sur une page japonaise ou lang="ru" sur du texte latin. Contrôle au niveau de l’écriture seulement : le scan ne devine pas une autre langue latine. Nécessite au moins 40 lettres de contenu.

Codes réseau ​

Détectés par UrlScanner uniquement avec l’option correspondante : seo-pro.scan.url_checks.check_canonical_target pour la cible canonique, check_hreflang_reciprocity pour les variantes hreflang. Chaque cible passe par SsrfGuard : protocoles autorisés, périmètre d’hôtes, rejet des IP privées et budgets de redirection, temps et taille. Les redirections ne sont pas suivies, pour rendre visible une cible canonique qui redirige. Canonique et variante autoréférentes sont ignorées, puisque la page vient d’être récupérée.

CodeGravitéChampPreuvesSignification
blocked_urlnoticepagereasonCible refusée par SsrfGuard avant tout appel HTTP.
canonical_target_brokencriticalcanonicalcanonical, statusCanonique vers une page renvoyant une erreur HTTP.
canonical_target_redirectwarningcanonicalcanonical, status, locationCanonique vers une page qui redirige ; utiliser l’URL finale.
canonical_target_noindexwarningcanonicalcanonicalCanonique vers une page elle-même noindex.
canonical_target_blockednoticecanonicalcanonical, reasonCible canonique non vérifiable, refus du garde ou résolution impossible.
hreflang_not_reciprocalwarningalternateshreflang, href, statusUne variante déclarée ne référence pas la page en retour. La paire hreflang peut être ignorée ; cela ne rend pas en soi la traduction non indexable.
hreflang_target_unverifiednoticealternateshreflang, href, reasonVariante non récupérable : refus, statut d’erreur, redirection ou dépassement de taille. Réciprocité non vérifiée ; absence de preuve, pas défaut confirmé.

La réciprocité récupère au maximum hreflang_max_alternates cibles par page, 10 par défaut, x-default compris, hors doublons et page elle-même. Les codes de métadonnées hreflang_* ci-dessus valident la liste déclarée ; cette exploration est le contrôle qui nécessite l’autre page.

Tous ces chemins réseau réutilisent SsrfGuard. Consultez SECURITY.md pour le modèle de menace et la limite résiduelle TOCTOU.

Contribution des codes au score ​

Le score SEO Pro vaut 100 − une pénalité fixe par problème comptabilisé, pondérée selon les gravités ci-dessus. La plupart des codes comptent. Certains sont volontairement exclus : missing_focus_keyword, conseil ; noindex_page et multiple_h1, informations ; blocked_url, canonical_target_blocked et hreflang_target_unverified, impossibilité de vérifier plutôt que défaut ; enfin les codes hreflang_*, html_lang_* et aeo_*, signaux de conseil exclus du score pour le moment. La page du score donne la liste complète et la pénalité de chaque code.

Cycle de vie des problèmes ​

Un problème n’est pas une ligne temporaire supprimée lorsque le défaut disparaît. Le scan réconcilie les problèmes de chaque cible au lieu de les supprimer et recréer. Chaque problème possède une identité stable : cible, scannable_type + scannable_id pour un modèle ou url pour une route/sitemap, et issue_type. Chaque code est émis au plus une fois par cible et par scan. Les codes regroupant plusieurs occurrences, missing_image_alt, mixed_content, hreflang_*, etc., les réunissent dans une ligne avec count / sample, ce qui garantit l’unicité.

À chaque scan, pour chaque cible :

  • un résultat sans ligne existante est créé en open, avec detected_at ;
  • un résultat correspondant à une ligne ouverte actualise les preuves et conserve le detected_at initial : la première observation reste stable au lieu d’être réinitialisée ;
  • un problème ouvert qu’un contrôle terminé ne retrouve plus devient fixed, avec resolved_at. La ligne est conservée, pas supprimée, pour enregistrer la correction ;
  • un problème fixed qui revient est rouvert sur la même ligne, avec nouveau detected_at : c’est une régression ;
  • un problème marqué ignored par un utilisateur dans le tableau de bord reste inchangé.
StatutSignificationDéfini par
openActuellement présent.Scan, nouveau ou toujours détecté
fixedÉtait présent, n’est plus détecté.Scan automatique suivant qui ne le retrouve plus
ignoredMasqué par un utilisateur ; exclu des comptes ouverts et du score.Action Ignore du tableau de bord

Les corrections étant conservées, le rapport en marque blanche peut afficher de vrais nombres de problèmes corrigés et nouveaux sur une période, plutôt qu’une simple différence d’instantanés de rapports. Les consommateurs des comptes ouverts, tableau de bord, seo-pro:scan-status et score, filtrent tous sur open. Les lignes fixed conservées ne les gonflent donc pas. Ces lignes sont rattachées à l’exécution qui les a résolues et suivent la rétention normale des scans.

Configuration ​

php
// config/seo-pro.php → 'scan'
'url_checks' => [
    'enabled' => true,
    'crawl_external' => false,             // fetch external URL targets (guarded)
    'check_canonical_target' => false,     // EXEC_NETWORK canonical validation (guarded)
    'check_hreflang_reciprocity' => false, // EXEC_NETWORK hreflang link-back crawl (guarded)
    'hreflang_max_alternates' => 10,       // targets fetched per page by that crawl
],
'checks' => [
    'length' => true,            // title/description length (metadata + rendered)
    'rendered_content' => true,  // H1 / alt / thin content / mixed content / html lang
],
'content' => [
    'min_word_count' => 200,     // thin_content threshold
    'evidence_sample' => 5,      // max example URLs stored per issue
],

Le budget de taille des réponses récupérées sous protection est seo-pro.http.max_response_bytes, 2 Mo par défaut. Le scan interne au processus sur le même hôte n’a pas ce plafond.

Compatibilité : renommage d’un code ​

L’ancien code unique robots_conflict, qui pouvait avoir deux gravités, a été scindé pour donner une seule gravité à chaque code :

Ancien codeNouveau codeGravité
robots_conflict, index + noindexrobots_conflict_indexingcritical
robots_conflict, follow + nofollowrobots_conflict_followingwarning

Si vous stockez ou filtrez robots_conflict, passez aux deux nouveaux codes.

rankbeam/laravel-seo est distribué sous licence MIT.