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,161 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
* Toolvana routing map
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
*
|
||||
* URL architecture (strongest SEO structure):
|
||||
*
|
||||
* / homepage
|
||||
* /{tool-slug} every tool at the root: /youtube-to-mp3, /jpg-to-png ...
|
||||
* /tools searchable tool directory (+ /tools/category/{cat})
|
||||
* /{category-slug} category landing pages: /youtube-tools, /video-tools ...
|
||||
* /blog, /blog/{guide-slug} guides cluster
|
||||
* /search crawlable search results page
|
||||
* /privacy /terms ... legal pages
|
||||
* /sitemap*.xml, /robots.txt SEO plumbing
|
||||
* /api/v1/... public API (noindex)
|
||||
* /admin/... admin panel (denied to robots)
|
||||
*/
|
||||
|
||||
/** @var RouteCollection $routes */
|
||||
|
||||
$routes->setAutoRoute(false);
|
||||
$routes->setDefaultNamespace('App\Controllers');
|
||||
$routes->setDefaultController('Home');
|
||||
$routes->setDefaultMethod('index');
|
||||
|
||||
// 404 override -> useful "Tool not found" page with popular tools.
|
||||
$routes->set404Override(static function () {
|
||||
return (new \App\Controllers\Errors())->notFound();
|
||||
});
|
||||
|
||||
// Health probes (unfiltered, no session, no analytics).
|
||||
$routes->get('/health', 'Health::index');
|
||||
$routes->get('/ready', 'Health::ready');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// SEO plumbing
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->get('/robots.txt', 'Seo::robots');
|
||||
$routes->get('/sitemap.xml', 'Seo::sitemapIndex');
|
||||
$routes->get('/sitemap-tools-([0-9]+).xml', 'Seo::sitemapTools/$1');
|
||||
$routes->get('/sitemap-guides.xml', 'Seo::sitemapGuides');
|
||||
$routes->get('/sitemap-categories.xml', 'Seo::sitemapCategories');
|
||||
$routes->get('/sitemap-pages.xml', 'Seo::sitemapPages');
|
||||
$routes->get('/opensearch.xml', 'Seo::openSearch');
|
||||
$routes->get('/manifest.webmanifest', 'Seo::manifest');
|
||||
$routes->get('/service-worker.js', 'Seo::serviceWorker');
|
||||
$routes->get('/og/(:segment).png', 'Seo::ogImage/$1'); // cached social cards
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Public API (internal + v1). Noindex; blocked in robots.txt.
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->group('api', ['namespace' => 'App\Controllers\Api'], static function ($routes) {
|
||||
// Browser-facing endpoints used by tool pages.
|
||||
$routes->post('jobs', 'Jobs::create');
|
||||
$routes->get('jobs/(:segment)', 'Jobs::show/$1');
|
||||
$routes->delete('jobs/(:segment)', 'Jobs::delete/$1');
|
||||
$routes->post('youtube/info', 'Youtube::info');
|
||||
$routes->post('events', 'Events::track'); // analytics beacon
|
||||
$routes->get('suggest', 'SearchApi::suggest'); // search-as-you-type
|
||||
|
||||
// Versioned developer API (token auth).
|
||||
$routes->group('v1', ['filter' => 'api-auth'], static function ($routes) {
|
||||
$routes->get('tools', 'ToolsApi::index');
|
||||
$routes->get('tools/(:segment)', 'ToolsApi::show/$1');
|
||||
$routes->post('convert', 'Jobs::create');
|
||||
$routes->get('jobs/(:segment)', 'Jobs::show/$1');
|
||||
$routes->delete('jobs/(:segment)', 'Jobs::delete/$1');
|
||||
});
|
||||
});
|
||||
|
||||
// File upload endpoint for tool pages (multipart).
|
||||
$routes->post('/upload', 'Media::upload');
|
||||
$routes->post('/jobs/start', 'Media::startJob');
|
||||
// Result download endpoint — temporary, never indexed.
|
||||
$routes->get('/download/(:segment)', 'Media::download/$1');
|
||||
$routes->get('/preview/(:segment).(:segment)', 'Media::preview/$1/$2');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Static / legal pages
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->get('/privacy', 'Pages::privacy');
|
||||
$routes->get('/terms', 'Pages::terms');
|
||||
$routes->get('/cookies', 'Pages::cookies');
|
||||
$routes->get('/dmca', 'Pages::dmca');
|
||||
$routes->get('/contact', 'Pages::contact');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Search & directory
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->get('/search', 'Search::index');
|
||||
$routes->get('/tools', 'Directory::index');
|
||||
$routes->get('/tools/popular', 'Directory::popular');
|
||||
$routes->get('/tools/recent', 'Directory::recent');
|
||||
$routes->get('/tools/a-z', 'Directory::az');
|
||||
$routes->get('/tools/category/(:segment)', 'Directory::category/$1');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Guides cluster
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->get('/blog', 'Guides::index');
|
||||
$routes->get('/blog/rss.xml', 'Guides::rss');
|
||||
$routes->get('/blog/(:segment)', 'Guides::show/$1');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Category landing pages: /youtube-tools, /video-tools, ...
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->get('/([a-z0-9]+-tools)', 'Categories::show/$1');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Admin panel (session auth, CSRF protected, noindex)
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->group('admin', ['namespace' => 'App\Controllers\Admin', 'filter' => 'admin-auth'], static function ($routes) {
|
||||
$routes->get('/', 'Dashboard::index');
|
||||
$routes->get('login', 'Auth::loginForm');
|
||||
$routes->post('login', 'Auth::attempt');
|
||||
$routes->get('logout', 'Auth::logout');
|
||||
|
||||
$routes->get('tools', 'ToolsAdmin::index');
|
||||
$routes->get('tools/new', 'ToolsAdmin::create');
|
||||
$routes->post('tools', 'ToolsAdmin::store');
|
||||
$routes->get('tools/(:num)/edit', 'ToolsAdmin::edit/$1');
|
||||
$routes->post('tools/(:num)', 'ToolsAdmin::update/$1');
|
||||
$routes->post('tools/(:num)/delete', 'ToolsAdmin::delete/$1');
|
||||
|
||||
$routes->get('categories', 'CategoriesAdmin::index');
|
||||
$routes->post('categories', 'CategoriesAdmin::store');
|
||||
$routes->get('categories/(:num)/edit', 'CategoriesAdmin::edit/$1');
|
||||
$routes->post('categories/(:num)', 'CategoriesAdmin::update/$1');
|
||||
|
||||
$routes->get('guides', 'GuidesAdmin::index');
|
||||
$routes->get('guides/new', 'GuidesAdmin::create');
|
||||
$routes->post('guides', 'GuidesAdmin::store');
|
||||
$routes->get('guides/(:num)/edit', 'GuidesAdmin::edit/$1');
|
||||
$routes->post('guides/(:num)', 'GuidesAdmin::update/$1');
|
||||
$routes->post('guides/(:num)/delete', 'GuidesAdmin::delete/$1');
|
||||
|
||||
$routes->get('seo', 'SeoAdmin::index');
|
||||
$routes->get('analytics', 'AnalyticsAdmin::index');
|
||||
$routes->get('jobs', 'JobsAdmin::index');
|
||||
$routes->post('jobs/(:segment)/retry', 'JobsAdmin::retry/$1');
|
||||
$routes->get('system', 'SystemAdmin::index');
|
||||
$routes->get('settings', 'SettingsAdmin::index');
|
||||
$routes->post('settings', 'SettingsAdmin::save');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Homepage last but one...
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->get('/', 'Home::index');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ...and the catch-all: every active tool slug lives at the root.
|
||||
// Registered last so concrete routes always win. Uppercase and
|
||||
// trailing-slash variants are canonicalized by the SeoFilter.
|
||||
// ---------------------------------------------------------------------
|
||||
$routes->get('/([a-z0-9-]+)', 'Tools::show/$1');
|
||||
Reference in New Issue
Block a user