@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)
@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 » --}}
@if($s['host'])
SEOptimer ↗
@endif
📋
⚡ Lancer Claude
↻
🕷️
{{-- Les 3 encadrés côte à côte : SEO · GEO · Backlink --}}
{{-- ===== 1) SEO ===== --}}
SEO
{{ $s['seo_total'] !== null ? $s['seo_total'].'/100' : '-' }}
@foreach ($axisLabels as $k => $label)
@php $av = $s['axes'][$k] ?? 0; @endphp
{{ $label }}
{{ $av }}
ⓘ
@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
📋 Prompt
⚡ Lancer Claude
↻ Auditer
👁 Détail
{{-- ===== 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
{{ $catName }}
{{ $v }}
ⓘ
@endforeach
Bots IA : {{ $g['ai_allowed'] ?? '-' }}/{{ $g['ai_total'] ?? '-' }} autorisés
📋 Prompt
⚡ Lancer Claude
↻ Auditer
👁 Détail
@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
📋 Prompt
⚡ Lancer Claude
↻ Auditer
👁 Détail
@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 » --}}
🤖 Tâches Claude Chaque « Lancer Claude » et son état · actualisé en direct
×
⚠️ Les lancements ne fonctionnent que depuis le hub local (localhost:8030) et requièrent une clé API (HUB_ANTHROPIC_API_KEY) — sinon statut « échec d'auth ».
@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