@php // ----- Helpers SEO ----- $gradeColor = fn ($g) => match ($g) { 'A' => 'text-emerald-400 border-emerald-400/40 bg-emerald-400/10', 'B' => 'text-lime-400 border-lime-400/40 bg-lime-400/10', 'C' => 'text-amber-400 border-amber-400/40 bg-amber-400/10', 'D' => 'text-orange-400 border-orange-400/40 bg-orange-400/10', 'E' => 'text-red-400 border-red-400/40 bg-red-400/10', default => 'text-slate-500 border-slate-700 bg-slate-800/40', }; $seoBar = fn ($v) => $v >= 80 ? 'bg-emerald-400' : ($v >= 60 ? 'bg-lime-400' : ($v >= 40 ? 'bg-amber-400' : 'bg-red-500')); $dotColor = fn ($l) => $l === 'crit' ? 'bg-red-500' : ($l === 'warn' ? 'bg-amber-400' : 'bg-sky-400'); $axisLabels = ['meta' => 'Meta & contenu', 'technique' => 'Technique', 'structure' => 'Données struct.', 'perf' => 'Perf / cache', 'social' => 'Social / GEO']; // ----- Helpers GEO ----- $geoTone = fn ($v) => $v >= 85 ? 'text-emerald-400' : ($v >= 70 ? 'text-lime-400' : ($v >= 55 ? 'text-amber-400' : 'text-red-400')); $geoBar = fn ($v) => $v >= 85 ? 'bg-emerald-400' : ($v >= 70 ? 'bg-lime-400' : ($v >= 55 ? 'bg-amber-400' : 'bg-red-500')); $hint = [ 'contenu lisible sans JS (≥400 mots)' => 'Rendre le contenu clé côté serveur (SSR / pré-rendu / fallback HTML lisible) : les LLM lisent rarement le JavaScript.', 'contenu substantiel (≥600 mots)' => 'Étoffer le contenu texte (≥ 600 mots) : les IA citent les pages riches.', 'titre + description optimisés' => 'Titre clair + meta description de 120–160 caractères.', 'hiérarchie H1 + ≥2 H2' => 'Un seul H1 + au moins 2 sous-titres H2 pour structurer.', 'JSON-LD présent' => 'Ajouter des données structurées JSON-LD (schema.org).', 'entité (Org/LocalBiz)' => 'Déclarer Organization ou LocalBusiness (nom, adresse, téléphone, logo).', '≥ 3 types schema' => 'Décrire ≥ 3 types (ex. Organization + WebSite + BreadcrumbList).', 'schema Article/FAQ' => 'Structurer les contenus en Article / FAQPage (facilement citables).', 'llms.txt' => 'Publier un /llms.txt qui résume le site et guide les IA.', 'tous les bots IA autorisés' => 'Autoriser GPTBot, ClaudeBot, PerplexityBot, Google-Extended… dans robots.txt.', 'sameAs (réseaux)' => 'Lister tes profils (LinkedIn, X…) via "sameAs" dans le JSON-LD.', 'auteur + date visibles' => 'Afficher auteur + date de publication/màj (crédibilité pour la citation IA).', ]; $cats = [ 'Contenu' => ['contenu lisible sans JS (≥400 mots)', 'contenu substantiel (≥600 mots)', 'contenu approfondi (≥1500 mots)', 'titre + description optimisés', 'hiérarchie H1 + ≥2 H2'], 'Données structurées' => ['JSON-LD présent', 'entité (Org/LocalBiz)', '≥ 3 types schema', 'schema Article/FAQ', 'FAQ structurée (FAQPage)', 'fil d’Ariane (BreadcrumbList)'], 'Accès & citation IA' => ['llms.txt', 'tous les bots IA autorisés', 'sameAs (réseaux)', 'auteur + date visibles'], ]; $catScore = function (array $checks, array $labels) { $s = 0; $g = 0; foreach ($checks as $c) { if (in_array($c['label'], $labels, true)) { $s += $c['w']; if ($c['ok']) $g += $c['w']; } } return $s ? (int) round($g / $s * 100) : 0; }; @endphp
HubPilotage
@include('partials.nav', ['active' => 'referencement'])
Déconnexion

Référencement - vue par site

Une ligne = un site, avec ses trois encadrés côte à côte : SEO, GEO·IA et Backlink / Liens. Chaque encadré garde ses propres infos et actions.

@if(count($sites))
🔎
@foreach($sites as $s)@endforeach Trier Note
@foreach($sites as $s) @php $combined = ($s['seo_total'] ?? 0) + ($s['geo_score'] ?? 0); $in = $inbound[$s['key']] ?? 0; $g = $s['geo']; $L = $s['links']; @endphp
{{-- En-tête de la ligne : le site + ses 2 URLs + actions « 3 axes » --}}
{{ $s['name'] }}
global {{ $s['global_score'] }}/100 {{ $s['global_grade'] }}
@if($s['prod_url']) 🌐 Publique : {{ preg_replace('#^https?://#','',$s['prod_url']) }} ↗ @else 🌐 Publique : non déployé @endif @if($s['local_url']) 💻 Locale : {{ preg_replace('#^https?://#','',$s['local_url']) }} ↗ @endif @if(!empty($s['audited_at'])) 🕓 Audité {{ \Illuminate\Support\Carbon::parse($s['audited_at'])->locale('fr')->diffForHumans() }} @else 🕓 Jamais audité @endif
@if($s['host']) SEOptimer ↗ @endif
{{-- Les 3 encadrés côte à côte : SEO · GEO · Backlink --}}
{{-- ===== 1) SEO ===== --}}
SEO
{{ $s['seo_grade'] }}
{{ $s['seo_total'] !== null ? $s['seo_total'].'/100' : '-' }}
@foreach ($axisLabels as $k => $label) @php $av = $s['axes'][$k] ?? 0; @endphp @endforeach
@if(count($s['issues'])) @foreach (array_slice($s['issues'], 0, 3) as $iss)
{{ $iss[1] }}
@endforeach @if(count($s['issues']) > 3)
+ {{ count($s['issues']) - 3 }} autre(s)
@foreach (array_slice($s['issues'], 3) as $iss)
{{ $iss[1] }}
@endforeach
@endif @else
Aucun point bloquant détecté 🎉
@endif
{{-- ===== 2) GEO·IA ===== --}}
GEO·IA
{{ $g ? $g['score'] : '-' }}/100
@if($g)rendu {{ $g['render']==='JS' ? 'JS' : 'HTML' }}@endif
@if($g) @if($g['render']==='JS')
⚠ Rendu JavaScript : les IA lisent surtout le HTML brut. Priorité : servir le contenu clé côté serveur (SSR / pré-rendu).
@endif
@foreach($cats as $catName => $labels) @php $v = $catScore($g['checks'] ?? [], $labels); @endphp @endforeach
Bots IA : {{ $g['ai_allowed'] ?? '-' }}/{{ $g['ai_total'] ?? '-' }} autorisés
@else
GEO non encore audité.
@endif
{{-- ===== 3) Backlink / Liens ===== --}}
Backlink / Liens
{{ $s['links_score'] }}/100
@if($L)
{{ $L['internal'] }}
internes
{{ $L['external'] }}
externes
{{ $L['nofollow'] }}
nofollow
@else
Structure des liens on-page non encore auditée.
@endif
Liens ← reçus de tes sites : {{ $in }}
@if(count($s['group_links'] ?? []))
🕸️ Pointe vers : {{ collect($s['group_links'])->pluck('name')->implode(', ') }} Bien : tu transmets de l'autorité à ces sites. Vérifie que le lien retour existe (maillage réciproque).
@else
🔗 Occasion de maillage Pourquoi : un lien depuis un autre de tes sites est un backlink que tu contrôles à 100 % → autorité SEO gratuite. Comment : ajoute dans le footer / une page « nos sites » / un article, 2-3 liens vers les sites du groupe les plus proches thématiquement.
@endif
@endforeach
@else
Pas encore de données. Lance un audit depuis le hub local.
@endif {{-- Popup détail GEO --}} {{-- Popup détail SEO --}} {{-- Popup réseau de liens --}} {{-- Suivi des lancements « Lancer Claude » --}}
@php $seoData = collect($sites)->mapWithKeys(fn ($s) => [$s['key'] => [ 'name' => $s['name'], 'url' => $s['prod_url'] ?: ($s['local_url'] ?: $s['url']), 'total' => $s['seo_total'], 'grade' => $s['seo_grade'], 'axes' => $s['axes'] ?? [], 'axisChecks' => $s['axis_checks'] ?? [], 'words' => $s['word_count'], 'weight' => $s['page_weight'], 'todo' => array_map(fn ($t) => ['label' => $t['label'] ?? '', 'hint' => $t['hint'] ?? ''], $s['todo'] ?? []), 'issues' => array_map(fn ($i) => [$i[0] ?? 'info', $i[1] ?? ''], $s['issues'] ?? []), ]])->all(); $geoData = collect($sites)->mapWithKeys(fn ($s) => [$s['key'] => [ 'name' => $s['name'], 'url' => $s['url'], 'score' => $s['geo']['score'] ?? 0, 'spa' => ($s['geo']['render'] ?? '') === 'JS', 'fail' => collect($s['geo']['checks'] ?? [])->where('ok', false)->pluck('label')->values()->all(), 'checks' => array_map(fn ($c) => ['label' => $c['label'], 'ok' => $c['ok'], 'w' => $c['w'] ?? 0], $s['geo']['checks'] ?? []), ]])->all(); $hintsMap = \App\Services\SeoAudit::hints(); $liensData = collect($sites)->mapWithKeys(fn ($s) => [$s['key'] => [ 'name' => $s['name'], 'url' => $s['url'], 'host' => $s['host'], 'linked' => collect($s['group_links'] ?? [])->pluck('host')->values()->all(), ]])->all(); $groupData = collect($sites)->filter(fn ($s) => $s['host'])->map(fn ($s) => ['name' => $s['name'], 'url' => $s['url'], 'host' => $s['host']])->values()->all(); @endphp