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:
deepseek
2026-08-23 07:10:30 +00:00
commit beaf0e1f37
217 changed files with 19619 additions and 0 deletions
+86
View File
@@ -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 70320 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'),
]);
}
}