Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
/**
|
||||
* SEO quality dashboard: automated audit of the whole indexable surface.
|
||||
*
|
||||
* Critical checks (block publishing in ToolsAdmin too):
|
||||
* unique titles / descriptions, length ranges, H1 present, canonical
|
||||
* slug hygiene, FAQ presence for content tools, duplicate detection.
|
||||
*/
|
||||
final class SeoAdmin extends AdminBase
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
$tools = service('finder')->all();
|
||||
$guides = model(\App\Models\GuideModel::class)->published(1000);
|
||||
|
||||
$issues = [];
|
||||
$seenTitles = [];
|
||||
$seenDescs = [];
|
||||
|
||||
foreach ($tools as $tool) {
|
||||
$slug = $tool['slug'];
|
||||
$title = (string) $tool['seo_title'];
|
||||
$desc = (string) $tool['seo_description'];
|
||||
|
||||
if (trim($title) === '') {
|
||||
$issues[] = ['severity' => 'critical', 'page' => '/' . $slug, 'check' => 'Missing SEO title'];
|
||||
} elseif (mb_strlen($title) > 65) {
|
||||
$issues[] = ['severity' => 'warning', 'page' => '/' . $slug, 'check' => 'Title longer than 65 chars (' . mb_strlen($title) . ')'];
|
||||
}
|
||||
if (trim($desc) === '') {
|
||||
$issues[] = ['severity' => 'critical', 'page' => '/' . $slug, 'check' => 'Missing meta description'];
|
||||
} elseif (mb_strlen($desc) < 70 || mb_strlen($desc) > 320) {
|
||||
$issues[] = ['severity' => 'warning', 'page' => '/' . $slug, 'check' => 'Description outside 70–320 chars (' . mb_strlen($desc) . ')'];
|
||||
}
|
||||
if (trim((string) $tool['h1']) === '') {
|
||||
$issues[] = ['severity' => 'critical', 'page' => '/' . $slug, 'check' => 'Missing H1'];
|
||||
}
|
||||
if ($tool['faqs'] === [] && ! in_array($tool['kind'], ['text'], true)) {
|
||||
$issues[] = ['severity' => 'warning', 'page' => '/' . $slug, 'check' => 'No FAQ content'];
|
||||
}
|
||||
if ($tool['related_slugs'] === [] && count(service('relatedTools')->forTool($tool, 4)) < 3) {
|
||||
$issues[] = ['severity' => 'warning', 'page' => '/' . $slug, 'check' => 'Thin internal linking (<3 related tools)'];
|
||||
}
|
||||
if (! preg_match('/^[a-z0-9]+$/', (string) ($tool['primary_input_format'] ?? '')) && $tool['operation'] === 'convert') {
|
||||
$issues[] = ['severity' => 'warning', 'page' => '/' . $slug, 'check' => 'Converter without primary format pair'];
|
||||
}
|
||||
|
||||
$tKey = mb_strtolower($title);
|
||||
if ($tKey !== '' && isset($seenTitles[$tKey])) {
|
||||
$issues[] = ['severity' => 'critical', 'page' => '/' . $slug, 'check' => 'Duplicate title with /' . $seenTitles[$tKey]];
|
||||
}
|
||||
$seenTitles[$tKey] = $slug;
|
||||
|
||||
$dKey = mb_substr(mb_strtolower($desc), 0, 120);
|
||||
if ($dKey !== '' && isset($seenDescs[$dKey])) {
|
||||
// near-duplicate short descriptions are common across generated converters — flag only exact dupes
|
||||
if (mb_strtolower((string) $tool['short_description']) === mb_strtolower((string) $desc)) {
|
||||
$issues[] = ['severity' => 'warning', 'page' => '/' . $slug, 'check' => 'Description identical to /' . $seenDescs[$dKey]];
|
||||
}
|
||||
}
|
||||
$seenDescs[$dKey] = $slug;
|
||||
}
|
||||
|
||||
foreach ($guides as $g) {
|
||||
if ((string) $g['excerpt'] === '' || (string) $g['body_md'] === '') {
|
||||
$issues[] = ['severity' => 'critical', 'page' => '/blog/' . $g['slug'], 'check' => 'Guide missing excerpt/body'];
|
||||
}
|
||||
}
|
||||
|
||||
usort($issues, static fn ($a, $b) => [$b['severity'], $a['page']] <=> [$a['severity'], $b['page']]);
|
||||
$criticalCount = count(array_filter($issues, static fn ($i) => $i['severity'] === 'critical'));
|
||||
|
||||
return $this->render('seo_index', [
|
||||
'issues' => $issues,
|
||||
'criticalCount' => $criticalCount,
|
||||
'toolCount' => count($tools),
|
||||
'guideCount' => count($guides),
|
||||
'sitemapUrl' => base_url('/sitemap.xml'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user