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
View File
+251
View File
@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
namespace App\Database\Seeds;
use App\Libraries\CatalogBuilder;
use App\Models\CategoryModel;
use App\Models\GuideModel;
use App\Models\ToolModel;
use CodeIgniter\Database\Seeder;
/**
* Initial seeder: categories, the complete tool catalog and starter
* guides. Idempotent — safe to run repeatedly (tools/categories are
* upserted, existing curated edits to non-generated fields survive).
*/
final class InitialSeeder extends Seeder
{
public function run(): void
{
$this->seedCategories();
$this->seedTools();
$this->seedGuides();
}
private function seedCategories(): void
{
$categories = [
[
'slug' => 'youtube-tools', 'name' => 'YouTube Tools', 'icon' => 'play', 'sort_order' => 1,
'tagline' => 'Downloaders, converters and metadata tools for YouTube links.',
'description' => 'Everything you need for working with YouTube content: video and audio downloaders, thumbnail grabbers, subtitle extractors, transcript tools and link utilities. Paste a URL and get results in seconds.',
'seo_title' => 'Free YouTube Tools Downloaders & Converters Online',
'seo_description' => 'Free online YouTube tools: video downloader, YouTube to MP3 converter, thumbnail grabber, subtitle extractor and more. Fast, simple, no software needed.',
'intro' => "Our YouTube toolbox handles the whole workflow around videos: saving content for offline use where permitted, extracting audio, grabbing thumbnails at full resolution, exporting captions and analyzing links. Every tool works the same way — paste a URL, press one button, get your file.\n\nTools that download video or audio streams require explicit operator opt-in because platform terms evolve; metadata, thumbnails, transcripts analysis and link utilities always work out of the box.",
],
[
'slug' => 'video-tools', 'name' => 'Video Tools', 'icon' => 'film', 'sort_order' => 2,
'tagline' => 'Convert, compress, cut and edit videos right in your browser workflow.',
'description' => 'A complete video toolbox: format conversion between MP4, WebM, MOV, MKV and AVI, compression with quality control, trimming, merging, rotating, speed changes, GIF export and audio extraction.',
'seo_title' => 'Online Video Tools Convert, Compress & Edit Videos Free',
'seo_description' => 'Convert, compress, trim and edit videos online free. MP4, WebM, MKV support, no watermark, no signup — fast processing on dedicated servers.',
'intro' => "Video work shouldn't require a video editor. This collection covers the everyday jobs: converting containers so files play everywhere, compressing oversized recordings under email and upload limits, cutting highlights, fixing orientation and pulling audio tracks out.\n\nFiles upload once, queue on dedicated workers powered by FFmpeg, and come back as downloads. Nothing is published anywhere and temporary copies expire automatically.",
],
[
'slug' => 'audio-tools', 'name' => 'Audio Tools', 'icon' => 'music', 'sort_order' => 3,
'tagline' => 'Convert, trim, normalize and boost any audio format.',
'description' => 'Audio conversion between MP3, WAV, M4A, AAC, FLAC and OGG plus cutting, merging, loudness normalization and volume boosting — all in the browser.',
'seo_title' => 'Online Audio Tools Convert & Edit Audio Files Free',
'seo_description' => 'Free audio converter and editor tools: MP3, WAV, FLAC, M4A conversion, trimming, normalization and volume boost. No installation required.',
'intro' => "From podcast producers to musicians sketching ideas, these tools cover the common audio chores: converting between formats with bitrate control, cutting silence or highlights, evening out loudness to broadcast standards and rescuing quiet recordings.\n\nConversions run through FFmpeg with professional codecs — libmp3lame for MP3s, native AAC, lossless FLAC — so output quality matches desktop applications.",
],
[
'slug' => 'image-tools', 'name' => 'Image Tools', 'icon' => 'image', 'sort_order' => 4,
'tagline' => 'Compress, convert and clean up images for web and print.',
'description' => 'Image compressor, resizer and converter for JPG, PNG, WebP and AVIF; EXIF inspection and removal; favicon generation and more image utilities.',
'seo_title' => 'Free Image Tools Compress & Convert Images Online',
'seo_description' => 'Compress JPG and PNG, convert WebP and AVIF, remove EXIF data and resize images online for free. Fast browser-friendly image tools.',
'intro' => "Faster websites start with lighter images. Here you can compress photos without visible loss, convert between modern formats like WebP and AVIF, strip location-leaking EXIF data before publishing, and generate every favicon size a modern site needs.\n\nSimple operations finish entirely in your browser — no upload wait at all.",
],
[
'slug' => 'gif-tools', 'name' => 'GIF Tools', 'icon' => 'sparkles', 'sort_order' => 5,
'tagline' => 'Create, shrink and dissect animated GIFs.',
'description' => 'Make GIFs from videos or image sequences, compress them via palette optimization, resize, retime and extract individual frames.',
'seo_title' => 'GIF Tools Make, Compress & Optimize Animated GIFs',
'seo_description' => 'Create GIFs from video, compress heavy animations, resize loops and extract frames — free online GIF toolkit.',
'intro' => "GIFs are everywhere but notoriously heavy. This set builds them properly: smart palette generation from video sources, frame-sequence assembly with custom timing, then compression passes that routinely halve file size while keeping motion smooth.\n\nNeed stills instead? The frame extractor dumps numbered PNGs ready for editing.",
],
[
'slug' => 'pdf-tools', 'name' => 'PDF Tools', 'icon' => 'filetext', 'sort_order' => 6,
'tagline' => 'Merge, split, compress and convert PDF documents.',
'description' => 'PDF merger and splitter, compression presets, page extraction, metadata stripping, rasterization to JPG/PNG and text extraction.',
'seo_title' => 'Free PDF Tools Merge, Split & Compress PDFs Online',
'seo_description' => 'Merge PDFs, compress large documents, extract pages and convert PDF to JPG or text online. Free and privacy-friendly.',
'intro' => "Document plumbing made easy: combine scanned pages into one file, split contracts back apart, squeeze oversized PDFs under attachment limits, pull out single chapters, or turn pages into images for presentations.\n\nProcessing uses hardened open-source engines (qpdf, Ghostscript, Poppler) inside isolated workers. Documents are never shared and vanish after the retention window.",
],
[
'slug' => 'utilities', 'name' => 'Developer Utilities', 'icon' => 'tool', 'sort_order' => 7,
'tagline' => 'Fast little helpers: QR codes, hashes, UUIDs, encoders and lookups.',
'description' => 'QR code generator, hash digests, UUID generation, JSON formatting, URL encoding, timestamp and color conversion, Base64 image tools and media lookups.',
'seo_title' => 'Developer Utilities QR Codes, Hashes, UUIDs & More',
'seo_description' => 'Free developer utilities: QR generator, MD5/SHA hashes, UUID v4, JSON formatter, URL encoder, timestamp converter and Base64 image tools.',
'intro' => "The small tools developers reach for daily. Everything here is instant: text utilities compute entirely in your browser with zero network round-trips, while generators like QR codes produce downloadable assets server-side without logging input.\n\nNo accounts, no quotas worth mentioning, no dark patterns — just tools that work.",
],
];
foreach ($categories as $category) {
$existing = model(CategoryModel::class)->where('slug', $category['slug'])->first();
if ($existing === null) {
model(CategoryModel::class)->insert($category);
} else {
model(CategoryModel::class)->update($existing['id'], $category);
}
}
}
private function seedTools(): void
{
$categories = [];
foreach (model(CategoryModel::class)->findAll() as $row) {
$categories[$row['slug']] = (int) $row['id'];
}
$model = model(ToolModel::class);
$created = 0;
$updated = 0;
foreach (CatalogBuilder::build() as $definition) {
$categoryId = $categories[$definition['category']] ?? null;
if ($categoryId === null) {
continue;
}
$row = [
'category_id' => $categoryId,
'kind' => $definition['kind'],
'operation' => $definition['operation'],
'slug' => $definition['slug'],
'name' => $definition['name'],
'short_description' => $definition['short_description'],
'description' => $definition['description'],
'input_formats' => json_encode($definition['input_formats']),
'output_formats' => json_encode($definition['output_formats']),
'primary_input_format' => $definition['primary_input_format'],
'primary_output_format' => $definition['primary_output_format'],
'icon' => $definition['icon'],
'seo_title' => $definition['seo_title'],
'seo_description' => mb_substr($definition['seo_description'], 0, 320),
'h1' => $definition['h1'],
'intro' => $definition['intro'],
'how_to' => json_encode($definition['how_to']),
'faqs' => json_encode($definition['faqs']),
'related_slugs' => json_encode($definition['related_slugs'] ?? []),
'aliases' => json_encode($definition['aliases'] ?? []),
'requires_binaries' => json_encode($definition['requires_binaries']),
'accepts_mimes' => json_encode($definition['accepts_mimes']),
'popularity' => $definition['popularity'],
'is_featured' => $definition['is_featured'],
'status' => $definition['status'],
];
$existing = $model->where('slug', $row['slug'])->first();
if ($existing === null) {
$model->insert($row);
++$created;
} else {
// keep editorial overrides made in admin: only refresh structural fields
$model->update($existing['id'], [
'category_id' => $row['category_id'],
'kind' => $row['kind'],
'operation' => $row['operation'],
'accepts_mimes' => $row['accepts_mimes'],
'requires_binaries' => $row['requires_binaries'],
'primary_input_format' => $row['primary_input_format'],
'primary_output_format'=> $row['primary_output_format'],
'input_formats' => $row['input_formats'],
'output_formats' => $row['output_formats'],
]);
++$updated;
}
}
echo "Tools seeded: {$created} created, {$updated} refreshed." . PHP_EOL;
}
private function seedGuides(): void
{
$dir = ROOTPATH . 'content/guides';
if (! is_dir($dir)) {
echo "No guides directory found — skipping." . PHP_EOL;
return;
}
foreach (glob(rtrim($dir, '/') . '/*.md') ?: [] as $file) {
$raw = (string) file_get_contents($file);
$frontMatter = self::parseFrontMatter($raw);
if ($frontMatter === null) {
continue;
}
[$meta, $body] = $frontMatter;
$data = [
'slug' => $meta['slug'],
'title' => $meta['title'],
'excerpt' => $meta['excerpt'],
'body_md' => $body,
// front matter holds a JSON list as raw text — normalize to a real list
'tool_slugs' => json_encode(self::decodeList($meta['tools'] ?? [])),
'seo_title' => $meta['seo_title'] ?? null,
'seo_description' => $meta['seo_description'] ?? null,
'reading_minutes' => max(2, (int) ceil(str_word_count(strip_tags($body)) / 220)),
'status' => 'published',
'published_at' => date('Y-m-d H:i:s', strtotime($meta['date'] ?? 'now')),
];
$existing = model(GuideModel::class)->where('slug', $data['slug'])->first();
if ($existing === null) {
model(GuideModel::class)->insert($data);
} else {
// keep editorial fields, refresh derived ones
model(GuideModel::class)->update($existing['id'], [
'body_md' => $data['body_md'],
'tool_slugs' => $data['tool_slugs'],
'excerpt' => $data['excerpt'],
'reading_minutes' => $data['reading_minutes'],
]);
}
}
echo "Guides seeded." . PHP_EOL;
}
/** Front-matter values may be raw JSON lists or comma text. */
private static function decodeList(mixed $value): array
{
if (is_array($value)) {
return $value;
}
$decoded = json_decode((string) $value, true);
return is_array($decoded) ? $decoded : array_filter(array_map('trim', explode(',', (string) $value)));
}
/** @return array{0: array, 1: string}|null */
private static function parseFrontMatter(string $raw): ?array
{
if (! str_starts_with($raw, '---')) {
return null;
}
$end = strpos($raw, '---', 3);
if ($end === false) {
return null;
}
$head = substr($raw, 3, $end - 3);
$body = ltrim(substr($raw, $end + 3));
$meta = [];
foreach (explode("\n", trim($head)) as $line) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$meta[trim($parts[0])] = trim(trim($parts[1]), "\"'");
}
}
if (empty($meta['slug']) || empty($meta['title'])) {
return null;
}
return [$meta, $body];
}
}