Files
deepseek beaf0e1f37 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
2026-08-23 07:10:30 +00:00

106 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Libraries\FormatCatalog;
use App\Libraries\Pipeline\Youtube as YoutubeLib;
/**
* Renders every individual tool page (/{slug}).
*
* The page is 100% server-rendered HTML: H1, intro, formats, how-to and
* FAQ are all in the first response. Interactive processing loads a
* small per-kind JS module afterwards.
*/
final class Tools extends BaseController
{
public function show(string $slug): string
{
$tool = service('finder')->tool($slug);
if ($tool === null) {
return (new Errors())->notFound();
}
$related = service('relatedTools')->forTool($tool);
$conversions = service('relatedTools')->conversionsAround($tool);
$category = service('finder')->categoryById((int) $tool['category_id']);
$guides = $this->guidesFor($tool);
// capability flags for honest UX
$capabilities = [
'yt_dlp' => service('pipeline')->youtubeEnabled(),
'binaries' => $this->checkBinaries($tool),
];
$this->seo->title($tool['seo_title'])
->description($tool['seo_description'])
->canonical('/' . $tool['slug'])
->breadcrumbs([
['label' => $category['name'] ?? 'Tools', 'url' => '/' . ($category['slug'] ?? 'tools')],
['label' => $tool['name']],
])
->webApplication($tool['name'], (string) $tool['short_description'], $this->appCategory($tool))
->faqPage(is_array($tool['faqs']) ? $tool['faqs'] : [])
->howTo((string) $tool['name'], is_array($tool['how_to']) ? $tool['how_to'] : [])
->ogImage('/og/' . $tool['slug'] . '.png');
foreach (config('Site')->locales as $locale => $_label) {
if ($locale === config('Site')->defaultLocale) {
continue;
}
$this->seo->alternate($locale, '/' . $locale . '/' . $tool['slug']);
}
$this->seo->alternate('x-default', '/' . $tool['slug']);
$this->trackToolView($tool['slug']);
return $this->render('tool', [
'tool' => $tool,
'category' => $category,
'related' => $related,
'conversions' => $conversions,
'guides' => $guides,
'capabilities' => $capabilities,
]);
}
/** @return list<array> */
private function guidesFor(array $tool): array
{
$guides = model(\App\Models\GuideModel::class)->published(30);
$hits = [];
foreach ($guides as $guide) {
$linked = json_decode($guide['tool_slugs'] ?? '[]', true) ?: [];
if (in_array($tool['slug'], $linked, true)) {
$hits[] = $guide;
}
}
return array_slice($hits, 0, 3);
}
private function checkBinaries(array $tool): array
{
$ok = [];
foreach (($tool['requires_binaries'] ?? []) as $bin) {
$path = config('Site')->binaries[$bin] ?? null;
$ok[$bin] = $path !== null && is_executable($path);
}
return $ok;
}
private function appCategory(array $tool): string
{
return match (FormatCatalog::family((string) ($tool['primary_output_format'] ?? ''))) {
'video' => 'MultimediaApplication',
'audio' => 'MultimediaApplication',
'image' => 'DesignApplication',
default => 'UtilitiesApplication',
};
}
}