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,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\Table\TableExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
|
||||
/**
|
||||
* Guides cluster: /blog and /blog/{slug}.
|
||||
*/
|
||||
final class Guides extends BaseController
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
$guides = model(\App\Models\GuideModel::class)->published(50);
|
||||
|
||||
$this->seo->title('Guides & Tutorials – Media How-Tos')
|
||||
->description('Practical guides for converting, compressing and editing media: video, audio, images, GIFs, PDFs and YouTube workflows.')
|
||||
->canonical('/blog')
|
||||
->breadcrumbs([['label' => 'Blog']]);
|
||||
|
||||
return $this->render('guides_index', ['guides' => $guides]);
|
||||
}
|
||||
|
||||
public function show(string $slug): string
|
||||
{
|
||||
$guide = model(\App\Models\GuideModel::class)->findBySlug($slug);
|
||||
if ($guide === null) {
|
||||
return (new Errors())->notFound();
|
||||
}
|
||||
|
||||
model(\App\Models\GuideModel::class)->incrementViews((int) $guide['id']);
|
||||
|
||||
$linkedTools = [];
|
||||
foreach (json_decode((string) ($guide['tool_slugs'] ?? '[]'), true) ?: [] as $toolSlug) {
|
||||
if ($tool = service('finder')->tool((string) $toolSlug)) {
|
||||
$linkedTools[] = $tool;
|
||||
}
|
||||
}
|
||||
|
||||
$related = array_values(array_filter(
|
||||
model(\App\Models\GuideModel::class)->published(20),
|
||||
static fn (array $g): bool => $g['slug'] !== $guide['slug']
|
||||
));
|
||||
|
||||
$this->seo->title($guide['seo_title'] ?: $guide['title'])
|
||||
->description($guide['seo_description'] ?: mb_substr($guide['excerpt'], 0, 300))
|
||||
->canonical('/blog/' . $guide['slug'])
|
||||
->breadcrumbs([['label' => 'Blog', 'url' => '/blog'], ['label' => $guide['title']]])
|
||||
->article($guide['title'], (string) $guide['body_md'], (string) $guide['published_at'], config('Site')->name)
|
||||
->ogImage('/og/blog-' . $guide['slug'] . '.png');
|
||||
|
||||
return $this->render('guide', [
|
||||
'guide' => $guide,
|
||||
'html' => self::markdown((string) $guide['body_md']),
|
||||
'tools' => $linkedTools,
|
||||
'related'=> array_slice($related, 0, 4),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rss(): \CodeIgniter\HTTP\Response
|
||||
{
|
||||
$guides = model(\App\Models\GuideModel::class)->published(30);
|
||||
$site = config('Site');
|
||||
$items = '';
|
||||
foreach ($guides as $g) {
|
||||
$items .= "\n<item><title>" . htmlspecialchars((string) $g['title'], ENT_XML1)
|
||||
. "</title><link>" . base_url('/blog/' . $g['slug']) . "</link>"
|
||||
. "<description>" . htmlspecialchars((string) $g['excerpt'], ENT_XML1) . "</description>"
|
||||
. "<pubDate>" . date(DATE_RSS, strtotime((string) $g['published_at'])) . "</pubDate>"
|
||||
. "<guid>" . base_url('/blog/' . $g['slug']) . "</guid></item>";
|
||||
}
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>'
|
||||
. htmlspecialchars($site->name . ' Guides', ENT_XML1) . '</title><link>' . base_url('/blog')
|
||||
. '</link><description>' . htmlspecialchars($site->tagline, ENT_XML1) . '</description>'
|
||||
. $items . '</channel></rss>';
|
||||
|
||||
return response()->setContentType('application/rss+xml; charset=utf-8')->setBody($xml);
|
||||
}
|
||||
|
||||
private static function markdown(string $text): string
|
||||
{
|
||||
$env = new Environment(['html_input' => 'strip', 'allow_unsafe_links' => false]);
|
||||
$env->addExtension(new CommonMarkCoreExtension());
|
||||
$env->addExtension(new AutolinkExtension());
|
||||
$env->addExtension(new TableExtension());
|
||||
|
||||
return (new MarkdownConverter($env))->convert($text)->getContent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user