Naar de inhoud

Scanproblemen: het register met probleemcodes

Elk probleem dat de Pro-scan meldt, heeft een stabiele probleemcode uit één register: Rankbeam\Seo\Pro\Scanning\IssueRegistry. Scanners bedenken zelf geen codes; ze maken elke melding via IssueRegistry::make(), dat de ernst en het veld uit het register overneemt en ongedefinieerde codes weigert. De catalogus hieronder vormt daarmee een vast contract voor dashboards, exports en de Pro-score. Deze gebruiken codes in plaats van meldingsteksten te ontleden. De gratis seo:audit gebruikt een eigen metadataregister in de core, met een beperkter bereik en enkele andere hreflang-codes.

Elke code bevat:

  • id — de stabiele tekenreeks die wordt opgeslagen als seo_scan_issues.issue_type.
  • severitycritical, warning of notice; vast per code. Als verschillende ernstniveaus nodig zijn, splitsen we de code.
  • field — het betreffende seo_meta-veld, of page voor bevindingen op paginaniveau.
  • execution class — wat nodig is om het probleem vast te stellen; zie hieronder.
  • evidence — de sleutels in de context-array van het probleem.

Uitvoeringsklassen

Elke controle behoort tot precies één van drie klassen, op basis van wat hij nodig heeft:

KlasseBenodigdWie de controle kan uitvoeren
metadataHet model en de core-resolver; geen pagina ophalenDe modelscan (PageScanner); de gratis seo:audit bevat een deel van de metadatacontroles
renderedDe HTML die de pagina serveert, via een kernelverzoek binnen het proces of extern ophalenDe URL-scan (UrlScanner)
networkEen uitgaand verzoek om een afzonderlijk doel te controleren, zoals een canonical die naar een andere pagina verwijstDe URL-scan, altijd via de SsrfGuard

Daarom kan een gratis audit binnen het proces nooit hetzelfde dekken als de volledige Pro-scan: alleen metadata-codes zijn te berekenen zonder een pagina te renderen. Alleen de Pro-pipeline haalt gerenderde HTML op en controleert canonical-doelen via het netwerk. Filter het register op klasse met IssueRegistry::byClass(IssueRegistry::EXEC_METADATA).

Metadatacodes

PageScanner stelt deze vast op basis van het model en de resolver. De gerenderde URL-scan meldt ook missing_title, missing_description en de lengtecodes door de geserveerde <head> te meten: dezelfde codes, dezelfde betekenis.

CodeErnstVeldBewijsBetekenis
missing_titlecriticaltitleGeen titel en geen berekenbare terugvalwaarde.
missing_descriptionwarningdescriptionGeen metabeschrijving en geen berekenbare terugvalwaarde.
missing_og_imagenoticeog_imageGeen Open Graph-afbeelding en geen berekenbare terugvalwaarde.
missing_focus_keywordnoticefocus_keywordsGeen focuszoekwoord ingesteld.
duplicate_titlewarningtitletitle, duplicate_urlsDe titel wordt ook op andere pagina's in dezelfde locale gebruikt.
duplicate_descriptionwarningdescriptiondescription, duplicate_urlsDe beschrijving wordt ook op andere pagina's in dezelfde locale gebruikt.
title_too_longwarningtitlelength, max, scriptDe bepaalde titel overschrijdt de aanbeveling voor het schrift: 60 voor Latijns, circa 30 voor CJK.
title_too_shortnoticetitlelength, min, scriptDe bepaalde titel blijft onder de ondergrens voor het schrift: 30 voor Latijns, circa 15 voor CJK.
description_too_longwarningdescriptionlength, max, scriptDe bepaalde beschrijving overschrijdt de aanbeveling voor het schrift: 160 / circa 80.
description_too_shortnoticedescriptionlength, min, scriptDe bepaalde beschrijving blijft onder de ondergrens voor het schrift: 70 / circa 35.
robots_conflict_indexingcriticalrobotsrobotsDe robotsinstructie bevat zowel index als noindex.
robots_conflict_followingwarningrobotsrobotsDe robotsinstructie bevat zowel follow als nofollow.
noindex_warningwarningrobotsrobots, canonical, page_url, shipping_signalEen pagina met een canonical naar zichzelf is noindex. Een aanwijzing om te beoordelen, geen bewijs dat de pagina geïndexeerd moet worden. Zowel de modelscan als de gerenderde URL-scan meldt dit.
invalid_canonicalcriticalcanonicalcanonicalDe canonical-waarde is geen geldige URL.
cross_domain_canonicalwarningcanonicalcanonical, page_urlDe canonical verwijst naar een andere host dan de pagina.
shared_canonicalnoticecanonicalcanonicalMeerdere pagina's geven dezelfde canonical op.
insecure_canonicalwarningcanonicalcanonicalEen http://-canonical op een https-site.
hreflang_invalid_codewarningalternatesinvalid_codesEen hreflang-alternatief gebruikt een waarde die noch x-default noch een geldige BCP-47-taalcode is.
hreflang_missing_self_referencewarningalternateslocale, page_urlEr zijn alternatieven opgegeven, maar geen ervan verwijst naar de eigen locale van de pagina: de zelfverwijzende hreflang ontbreekt.
hreflang_duplicate_codewarningalternatesduplicate_codesDezelfde hreflang-code verwijst naar meer dan één URL, waardoor de groep dubbelzinnig is.
hreflang_missing_x_defaultnoticealternateslanguagesEen meertalige hreflang-groep heeft geen x-default-terugvaloptie.
aeo_missing_authornoticeschemaEen artikel in de gestructureerde gegevens van de pagina heeft geen auteursentiteit. Auteurschap of herkomst is niet expliciet vastgelegd in het schema.
aeo_article_missing_datenoticeschemaEen artikel in de gestructureerde gegevens van de pagina heeft geen publicatie- of wijzigingsdatum. De tijdlijn is niet expliciet vastgelegd in het schema.

De lengtegrenzen komen uit het lengtebeleid per schrift van de core, sinds Pro 2.33: 60/160 voor Latijnse tekst, circa 30/80 voor CJK. De telling gebruikt grafemen, zodat de scan niet afwijkt van de tekentellers in de editor. De ondergrenzen — titel 30 en beschrijving 70 voor Latijns, ongeveer de helft voor CJK — zijn de grenzen waaronder de scan onvoldoende optimalisatie signaleert. De contextsleutel script noemt de gebruikte categorie. De lengte wordt gemeten op de bepaalde titel of beschrijving: de waarde die daadwerkelijk wordt gerenderd, inclusief terugvalwaarden en titelachtervoegsel.

De hreflang_*-codes valideren de opgegeven hreflang-alternatieven van een pagina, gelezen uit alternates van de resolver: ongeldige of dubbele codes, een ontbrekende zelfverwijzing en een ontbrekende x-default in een meertalige groep. Ze worden alleen uitgevoerd als de pagina alternatieven opgeeft. Deze metadatacontroles controleren geen wederzijdse verwijzingen tussen pagina's. De optionele netwerkcontrole hieronder haalt daarvoor de andere pagina op.

De aeo_*-codes zijn signalen voor geschiktheid voor antwoordmachines (AEO): is de artikelinhoud van de pagina herkenbaar in de gestructureerde gegevens? Ze lezen de bepaalde JSON-LD-graaf en worden alleen gemeld als die gestructureerde gegevens van een artikeltype bevat, zoals Article, BlogPosting of NewsArticle, zonder author-entiteit voor expliciet auteurschap of herkomst, of zonder datePublished / dateModified voor een expliciete tijdlijn. Een pagina zonder artikel krijgt deze meldingen nooit. Ze worden bestuurd door seo-pro.scan.checks.aeo, standaard aan, en komen overeen met de gratis seo:audit.

missing_focus_keyword heeft een inschakelvoorwaarde

De melding over een ontbrekend focuszoekwoord verschijnt alleen als de focuszoekwoordworkflow in de core is ingeschakeld: seo.keywords.enabled, standaard false. Als die uitstaat, meldt de scan geen ontbrekend focuszoekwoord. Het gratis seo:audit-commando en de Filament-editor gebruiken dezelfde core-instelling. Scan, audit en editorherinnering komen daardoor altijd overeen: er is maar één schakelaar.

Codes uit gerenderde HTML

UrlScanner stelt deze vast uit de geserveerde HTML. Voor doelen op dezelfde host gebeurt dat met een kernelverzoek binnen het proces, zonder uitgaand verkeer; externe doelen worden via een beveiligd verzoek opgehaald.

CodeErnstVeldBewijsBetekenis
http_errorcriticalpagestatusDe URL gaf een 4xx/5xx-status terug.
empty_responsecriticalpageDe URL gaf een leeg antwoord terug.
missing_canonicalnoticecanonicalGeen <link rel="canonical"> in de gerenderde head.
noindex_pagenoticerobotsrobotsDe gerenderde pagina is noindex, ter informatie. Een noindex-pagina die ook een canonical naar zichzelf heeft, krijgt in plaats daarvan noindex_warning, dat meetelt in de score.
missing_h1noticepageGeen <h1>-kop.
multiple_h1noticepagecountMeer dan één <h1>, ter informatie.
missing_image_altwarningpagecount, total, sampleInhoudelijke afbeeldingen missen een alt-attribuut. Expliciet alt="" geldt als decoratief en wordt niet gemeld.
thin_contentnoticepageword_count, threshold, segmenterDe hoofdtekst blijft onder het ingestelde aantal woorden. De tokenizer van de checklist telt woorden via witruimte voor schriften met spaties, en via ICU-woordenboeksegmentatie (segmenter: intl, vereist ext-intl) voor Chinees, Japans en Thais. Een Japans artikel van 400 woorden telt dus niet als één 'woord'.
mixed_contentwarningpagecount, samplehttp://-subresources op een https-pagina.
html_lang_missingnoticepageGeen <html lang>, of een lege waarde. Ondersteunende technologie kan daardoor een ongeschikte stem kiezen.
html_lang_invalidnoticepagedeclaredDe waarde van lang is geen BCP-47-tag: english, en_US met een underscore, jp.
html_lang_mismatchwarningpagedeclared, declared_script, detected_scriptHet schrift van de zichtbare hoofdtekst past niet bij de opgegeven taal, bijvoorbeeld lang="en" op een Japanse pagina of lang="ru" bij Latijnse tekst. Alleen controle op schrift: een verkeerde taal binnen hetzelfde Latijnse schrift zou een gok zijn, en de scan gokt niet. Vereist minstens 40 letters hoofdtekst.

Netwerkcodes

UrlScanner stelt deze alleen vast als de bijbehorende optionele instelling aanstaat: seo-pro.scan.url_checks.check_canonical_target voor het canonical-doel en check_hreflang_reciprocity voor hreflang-alternatieven. Elk doel wordt via de SsrfGuard opgehaald, met toegestane URL-protocollen en hosts, weigering van privé-IP-adressen en limieten voor redirects, tijd en omvang. Redirects worden niet gevolgd, zodat een doorverwijzende canonical zichtbaar blijft. Een canonical of alternatief dat naar de pagina zelf verwijst, wordt overgeslagen: die pagina is zojuist al opgehaald.

CodeErnstVeldBewijsBetekenis
blocked_urlnoticepagereasonEen doel is door de SsrfGuard geweigerd voordat een HTTP-verzoek plaatsvond.
canonical_target_brokencriticalcanonicalcanonical, statusDe canonical verwijst naar een pagina die een HTTP-fout teruggeeft.
canonical_target_redirectwarningcanonicalcanonical, status, locationDe canonical verwijst naar een pagina die doorstuurt; gebruik de uiteindelijke URL.
canonical_target_noindexwarningcanonicalcanonicalDe canonical verwijst naar een pagina die zelf noindex is.
canonical_target_blockednoticecanonicalcanonical, reasonHet canonical-doel kon niet worden geverifieerd: geweigerd door de beveiliging of niet op te lossen.
hreflang_not_reciprocalwarningalternateshreflang, href, statusEen opgegeven alternatief verwijst niet terug naar de pagina. Het hreflang-paar kan worden genegeerd; dit maakt de vertaling op zichzelf niet onindexeerbaar.
hreflang_target_unverifiednoticealternateshreflang, href, reasonHet alternatief kon niet worden opgehaald door weigering, foutstatus, redirect of overschrijding van de omvanglimiet. Wederkerigheid is dus niet gecontroleerd. Ontbrekend bewijs, geen vastgesteld gebrek.

De controle op wederkerigheid haalt maximaal hreflang_max_alternates doelen per pagina op, standaard 10, inclusief x-default. Dubbele doelen en de pagina zelf worden overgeslagen. De hreflang_*-metadatacodes op modelniveau valideren de opgegeven lijst; deze crawl is de controle die de andere pagina nodig heeft.

Elk netwerkpad hier gebruikt de gedeelde SsrfGuard. Zie SECURITY.md voor het dreigingsmodel en de toelichting op het resterende TOCTOU-risico.

Hoe codes de score bepalen

De Pro SEO-score is 100 − een vaste aftrek per meetellend probleem, gewogen op basis van de ernst hierboven. De meeste codes tellen mee. Enkele zijn bewust uitgesloten: missing_focus_keyword als advies, noindex_page en multiple_h1 ter informatie, blocked_url / canonical_target_blocked / hreflang_target_unverified omdat 'niet kunnen controleren' geen gebrek is, en hreflang_*, html_lang_* en aeo_* als adviessignalen die voorlopig buiten de score blijven. De scorepagina bevat de volledige lijst van meetellende codes en de aftrek per code.

Levenscyclus van een probleem

Een probleem is meer dan een rij die alleen bestaat zolang het gebrek aanwezig is. Het heeft een levenscyclus: een scan werkt bestaande problemen bij in plaats van ze te wissen en opnieuw aan te maken. Elk probleem heeft een stabiele identiteit: het doel — scannable_type + scannable_id voor een model, of url voor een route- of sitemapdoel — plus issue_type. Elke code wordt per doel hoogstens eenmaal per scan gemeld. Codes met meerdere gevallen, zoals missing_image_alt, mixed_content en hreflang_*, bundelen die in één rij met count / sample. De identiteit is daardoor uniek.

Bij elke scan geldt voor elk doel:

  • Een bevinding zonder bestaande rij wordt aangemaakt als open en krijgt detected_at.
  • Een bevinding die overeenkomt met een bestaande open rij krijgt bijgewerkt bewijs en behoudt zijn oorspronkelijke detected_at. De eerste waarneming blijft zo bewaard en wordt niet bij elke scan opnieuw ingesteld.
  • Een open probleem dat een afgeronde controle niet meer vindt, wordt fixed, met resolved_at vastgelegd. De rij wordt bewaard, zodat een echte oplossing geregistreerd blijft.
  • Een fixed probleem dat terugkomt, wordt in dezelfde rij heropend: een terugval, met een nieuwe detected_at.
  • Een probleem dat een gebruiker in het dashboard op ignored heeft gezet, blijft ongemoeid.
StatusBetekenisIngesteld door
openMomenteel aanwezig.De scan, nieuw of opnieuw gevonden
fixedWas aanwezig, wordt niet meer gevonden.Automatisch door de volgende scan die het niet terugvindt
ignoredDoor een gebruiker genegeerd; telt niet mee bij openstaande problemen of in de score.De actie Negeren in het dashboard

Doordat oplossingen nu worden vastgelegd, kan het rapport in je eigen huisstijl werkelijke aantallen opgeloste en nieuwe problemen over een periode tonen, in plaats van verschillen tussen rapportmomentopnamen. Alle onderdelen die openstaande problemen tellen — het dashboard, het seo-pro:scan-status-commando en de score — filteren op open. Bewaarde fixed-rijen verhogen die aantallen dus nooit. Opgeloste rijen worden toegeschreven aan de uitvoering die ze oploste en vervallen volgens de normale bewaartermijn voor scans.

Configuratie

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
],

De maximale antwoordomvang voor beveiligde ophaalverzoeken is seo-pro.http.max_response_bytes, standaard 2 MB. De scan binnen het proces voor dezelfde host heeft deze limiet niet.

Compatibiliteit bij hernoemde probleemcodes

De eerdere code robots_conflict had twee ernstniveaus en is gesplitst, zodat elke code precies één ernstniveau heeft:

Oude codeNieuwe codeErnst
robots_conflict (index + noindex)robots_conflict_indexingcritical
robots_conflict (follow + nofollow)robots_conflict_followingwarning

Als je robots_conflict hebt opgeslagen of erop filtert, stap dan over op de twee nieuwe codes.

rankbeam/laravel-seo is uitgebracht onder de MIT-licentie.