Media Gallery
Tagixo ships a complete media management system: upload, storage abstraction, thumbnail generation, and a gallery picker that integrates directly with the visual builder. Everything described on this page lives in the core package (ccast/tagixo) and is wired up through your own Laravel controllers and routes.
If you are using an admin panel SDK, refer to the Filament or Primix SDK documentation sections for the ready-made UI surfaces.
The Media Model
The Ccast\Tagixo\Models\Media Eloquent model maps to the tgx_media table. Originals and derived crops both live in this table; crops are differentiated by a non-null parent_id.
tgx_media table schema
| Column | Type | Notes |
|---|---|---|
id |
bigint |
Auto-increment PK |
uuid |
varchar(36) |
Auto-generated on create; used in public URLs |
filename |
varchar |
Stored filename (may be randomised) |
original_filename |
varchar |
User-supplied filename at upload time |
disk |
varchar |
Laravel filesystem disk name |
path |
varchar |
Relative path within the disk |
mime_type |
varchar |
Full MIME, e.g. image/jpeg |
extension |
varchar |
Lowercase extension without dot |
size |
bigint |
File size in bytes |
width |
int|null |
Pixel width (images only) |
height |
int|null |
Pixel height (images only) |
alt_text |
text|null |
Editable accessibility label |
title |
varchar|null |
Human-readable title |
description |
text|null |
Optional long description |
folder |
varchar|null |
Slash-delimited folder path, e.g. marketing/hero |
metadata |
json|null |
Extension point for app-specific fields |
uploaded_by |
bigint|null |
FK to your users table; set to null on user delete |
parent_id |
bigint|null |
Null on originals; set to parent id on crops |
thumbs |
json|null |
Map of preset name → stored path for generated crops |
created_at |
timestamp |
|
updated_at |
timestamp |
|
deleted_at |
timestamp|null |
Soft-delete column |
Relations
// All defined on Ccast\Tagixo\Models\Media
public function uploader(): BelongsTo // → your User model
public function original(): BelongsTo // → Media (parent record)
public function crops(): HasMany // → Media records with matching parent_id
Scopes
| Scope | SQL effect |
|---|---|
originals() |
WHERE parent_id IS NULL |
images() |
WHERE mime_type LIKE 'image/%' |
videos() |
WHERE mime_type LIKE 'video/%' |
documents() |
Non-image, non-video MIME types |
inFolder(?string $folder) |
WHERE folder = ? — null matches root/no-folder items |
search(string $term) |
Matches on filename, original_filename, alt_text, title |
Boot hooks
A UUID is generated automatically on creating. On hard-delete (deleting), the model removes the physical file from the configured disk and any generated thumbnail files. The soft-delete column is available; file cleanup only fires on a force-delete.
Configuration
Publish the config once if it is not already present:
php artisan vendor:publish --tag=tagixo-config
The relevant block in config/tagixo.php:
'media_gallery' => [
'disk' => env('TAGIXO_MEDIA_DISK', 'public'),
'path' => 'media', // base folder within the disk
'max_file_size' => 20480, // kilobytes — 20 MB default
'accepted_types' => [ // MIME prefix allowlist
'image/*',
'video/*',
'application/pdf',
'application/zip',
],
'generate_thumbs' => true, // false to skip thumbnail generation entirely
'thumb_quality' => 85, // JPEG quality for generated thumbnails
'crop_presets' => [
'thumbnail' => ['width' => 150, 'height' => 150, 'fit' => 'crop'],
'4x3' => ['width' => 800, 'height' => 600, 'fit' => 'contain'],
'16x9' => ['width' => 1920, 'height' => 1080, 'fit' => 'contain'],
],
'signed_url_ttl' => 1440, // minutes; only applies to private disks
],
Disk configuration
Point TAGIXO_MEDIA_DISK at any disk defined in config/filesystems.php. The gallery works with any Laravel disk driver:
# .env — local public disk (default)
TAGIXO_MEDIA_DISK=public
# .env — S3-compatible (AWS S3, MinIO, Cloudflare R2)
TAGIXO_MEDIA_DISK=s3
// config/filesystems.php — MinIO example
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'visibility' => 'private',
],
Visibility matters. If the disk is
private(or your bucket policy blocks anonymous reads), the model issues signed temporary URLs automatically — see Signed URLs below. You do not need to branch on disk type in application code.
The Upload Endpoint
The builder's image-picker modal posts files to an upload endpoint you register in your application. You configure that URL when bootstrapping the builder (see the Standalone Integration page). A typical setup:
// routes/web.php
Route::middleware('auth')->group(function () {
Route::post('/builder/media/upload', [MediaController::class, 'upload']);
});
// app/Http/Controllers/MediaController.php
namespace App\Http\Controllers;
use Ccast\Tagixo\Media\MediaUploader;
use Illuminate\Http\Request;
class MediaController extends Controller
{
public function upload(Request $request, MediaUploader $uploader)
{
$request->validate([
'file' => ['required', 'file', 'max:' . config('tagixo.media_gallery.max_file_size')],
'folder' => ['nullable', 'string', 'max:255'],
]);
$media = $uploader->upload(
file: $request->file('file'),
disk: config('tagixo.media_gallery.disk'),
folder: $request->input('folder'),
uploadedBy: $request->user()?->id,
);
return response()->json([
'id' => $media->id,
'uuid' => $media->uuid,
'url' => $media->url(),
'filename' => $media->original_filename,
'width' => $media->width,
'height' => $media->height,
'thumbs' => $media->thumbs,
]);
}
}
MediaUploader::upload() stores the file, creates the tgx_media record, and — if generate_thumbs is enabled — dispatches thumbnail generation. The JSON response shape shown above is what the builder's picker modal expects to receive after a successful upload.
Accepted types enforcement
The accepted_types config key is also enforced server-side inside MediaUploader. An upload whose MIME type does not match any entry in the allowlist throws a Ccast\Tagixo\Exceptions\MediaTypeNotAllowedException, which you can catch and convert to a validation error response:
use Ccast\Tagixo\Exceptions\MediaTypeNotAllowedException;
try {
$media = $uploader->upload(...);
} catch (MediaTypeNotAllowedException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
Thumbnail Generation
When generate_thumbs is true, MediaUploader dispatches a queued job (Ccast\Tagixo\Jobs\GenerateMediaThumbs) immediately after the record is created. The job iterates crop_presets, resizes/crops using the Intervention Image library, stores each result on the same disk, and writes the path map to tgx_media.thumbs.
Make sure your queue worker is running:
php artisan queue:work
To generate thumbnails for previously uploaded media (e.g. after adding a new preset):
use Ccast\Tagixo\Jobs\GenerateMediaThumbs;
use Ccast\Tagixo\Models\Media;
Media::originals()->images()->each(
fn ($m) => GenerateMediaThumbs::dispatch($m)
);
Access a specific thumb URL at runtime:
$thumbPath = $media->thumbs['thumbnail'] ?? null;
if ($thumbPath) {
$url = Storage::disk($media->disk)->url($thumbPath);
}
Intervention Image is a required peer dependency. Run
composer require intervention/imageif it is not already in yourcomposer.json. Tagixo supports both Intervention Image v2 (GD/Imagick) and v3.
The Gallery Picker in the Builder
When a module exposes an image prop (type ImagePropType or any media_id slot), the builder renders a "Choose from gallery" button inside the prop drawer. Clicking it opens a gallery modal iframed from a URL you define in your bootstrap payload. The builder communicates with the modal via postMessage.
The gallery endpoint must:
- Return an HTML page that lists existing media items.
- Post a
tagixo:media-selectedmessage towindow.parentwhen the user picks an item.
Registering the gallery URL in the bootstrap endpoint
// In your builder bootstrap controller (GET /builder/bootstrap):
return response()->json([
// ... other builder config
'media' => [
'gallery_url' => route('builder.media.gallery'),
'upload_url' => route('builder.media.upload'),
],
]);
Implementing the gallery route
// routes/web.php
Route::middleware('auth')->group(function () {
Route::get('/builder/media/gallery', [MediaController::class, 'gallery']);
});
// app/Http/Controllers/MediaController.php
public function gallery(Request $request)
{
$query = Media::originals()->images()->latest();
if ($search = $request->input('search')) {
$query->search($search);
}
if ($folder = $request->input('folder')) {
$query->inFolder($folder);
}
$items = $query->paginate(40)->through(fn ($m) => [
'id' => $m->id,
'uuid' => $m->uuid,
'url' => $m->url(),
'thumb' => $m->thumbs['thumbnail'] ?? $m->url(),
'filename' => $m->original_filename,
'alt_text' => $m->alt_text,
'width' => $m->width,
'height' => $m->height,
]);
return view('builder.media-gallery', compact('items'));
}
The gallery view must call window.parent.postMessage with a specific shape when the user selects an item:
{{-- resources/views/builder/media-gallery.blade.php --}}
<script>
function selectMedia(item) {
window.parent.postMessage({
type: 'tagixo:media-selected',
media: item // the JSON object from your items collection
}, '*');
}
</script>
Each item card in the gallery view calls selectMedia({{ json_encode($item) }}) on click. The builder closes the modal and populates the image prop with the returned id, uuid, and url automatically.
Authentication. The gallery page is loaded in an iframe inside the builder canvas, using the same session cookie as the parent window. Standard
authmiddleware works without any extra token handling, provided your session cookie is not scoped to a sub-path that would block the iframe request.
Using Media Items in Rendering
Builder JSON stores a media_id (the integer PK). In your module renderer, resolve the record and call url():
use Ccast\Tagixo\Models\Media;
$media = Media::find($props['image']['media_id'] ?? null);
$src = $media?->url() ?? $props['image']['url'] ?? '';
$alt = $media?->alt_text ?? $props['image']['alt'] ?? '';
The fallback to $props['image']['url'] handles cases where the builder stored an external URL directly rather than an uploaded media item.
For bulk rendering (listing pages, galleries), eager-load to avoid N+1:
$mediaIds = collect($pageModules)
->pluck('props.image.media_id')
->filter()
->unique();
$mediaMap = Media::whereIn('id', $mediaIds)->get()->keyBy('id');
// Later in a loop:
$media = $mediaMap->get($props['image']['media_id']);
Signed URLs for Private Disks
When the configured disk has 'visibility' => 'private' (or the storage backend requires authentication to read objects), $media->url() returns a signed temporary URL with the TTL set by signed_url_ttl (default 1440 minutes / 24 hours).
// The model handles this transparently:
$url = $media->url(); // signed if disk is private, plain if public
Do not cache signed URLs beyond their TTL. If you render a page server-side and cache the output (Octane page cache, Varnish, Redis), strip media URLs from the cache key or set the cache TTL below the signing TTL. Signed URLs contain an expiry timestamp; serving an expired URL produces a 403 from the storage backend.
To customise the signing TTL per-call:
use Illuminate\Support\Facades\Storage;
$url = Storage::disk($media->disk)->temporaryUrl(
$media->path,
now()->addMinutes(30)
);
Deleting Media
Soft-delete keeps the tgx_media row and leaves the physical file intact — useful for audit trails or recovering a media reference from builder JSON:
$media->delete(); // soft-delete only
Hard-delete removes the row and the physical file from the disk:
$media->forceDelete();
Deleting a parent record with crops does not cascade automatically. Force-delete crops explicitly before or after:
$media->crops->each->forceDelete();
$media->forceDelete();
See also
- Standalone Page Builder Integration — registering the builder bootstrap, save, and preview endpoints where
media.gallery_urlandmedia.upload_urlare configured. - Module System and Catalog — how
ImagePropTypeand other media-aware prop types surface in a module definition. - Storage Model and Data Shape — how media references (
media_id,url) are stored within builder page JSON. - Custom Form Modules — adding a file-upload field to a form module, which uses the same upload endpoint.