Files
landing-api/database/migrations/2026_08_13_160000_create_media_table.php
Josep Piguillem 3897113313 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>
2026-08-19 17:55:27 +02:00

81 lines
3.1 KiB
PHP

<?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');
}
};