Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
80
database/migrations/2026_08_13_160000_create_media_table.php
Normal file
80
database/migrations/2026_08_13_160000_create_media_table.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* What is actually known about an uploaded image.
|
||||
*
|
||||
* Until now an image was a string — a path, and nothing else. For a frontend
|
||||
* rendering server-side that is not enough to render one properly: without
|
||||
* width and height the page reflows as images arrive, and without derived
|
||||
* versions every visitor downloads a 2000px original to fill a 400px slot.
|
||||
*
|
||||
* This table is *derived*: everything in it can be rebuilt from the files on
|
||||
* disk with `php artisan media:process --all`. Nobody edits it by hand, which
|
||||
* is why the human part of an image — its alt text, which depends on where the
|
||||
* image is used — lives on the record using it instead.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('media', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Identity is (disk, path): the file is the record. Two models
|
||||
// pointing at the same upload share one row and one set of
|
||||
// derived versions.
|
||||
$table->string('disk', 40);
|
||||
$table->string('path', 500);
|
||||
|
||||
$table->string('mime', 100)->nullable();
|
||||
$table->unsignedBigInteger('size')->nullable();
|
||||
|
||||
// The pair that stops the page reflowing as images load.
|
||||
$table->unsignedInteger('width')->nullable();
|
||||
$table->unsignedInteger('height')->nullable();
|
||||
|
||||
// Tiny blurred data URI, inlined by the frontend while the real
|
||||
// image loads.
|
||||
$table->text('placeholder')->nullable();
|
||||
|
||||
// [{ "format": "webp", "width": 800, "path": "...", "size": 1234 }]
|
||||
$table->jsonb('variants')->nullable();
|
||||
|
||||
// Null means "not processed yet". The API serves the image anyway,
|
||||
// just without the extras, so an unprocessed upload degrades into
|
||||
// a plain <img> rather than a hole in the page.
|
||||
$table->timestamp('processed_at')->nullable();
|
||||
$table->text('error')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['disk', 'path']);
|
||||
$table->index('processed_at');
|
||||
});
|
||||
|
||||
// Alt text belongs to the record showing the image, not to the file:
|
||||
// the same photo can need a different description in a project page
|
||||
// than in a listing, and only the context knows which.
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->jsonb('featured_image_alt')->nullable()->after('featured_image');
|
||||
});
|
||||
|
||||
Schema::table('projects', function (Blueprint $table) {
|
||||
$table->jsonb('cover_image_alt')->nullable()->after('cover_image');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('projects', fn (Blueprint $table) => $table->dropColumn('cover_image_alt'));
|
||||
Schema::table('pages', fn (Blueprint $table) => $table->dropColumn('featured_image_alt'));
|
||||
|
||||
Schema::dropIfExists('media');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user