Agent skill
lexical-editor-image-management
Implement Lexical Editor with automatic image management using Laravel Observers. Converts base64 images to file storage, deletes unused images, and handles cleanup. Use when building WYSIWYG editors with rich content, managing media uploads in editors, implementing automatic image optimization, or setting up Observer-based storage management for rich text editors.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/lexical-editor-image-management
SKILL.md
Lexical Editor Image Management
Complete guide for implementing Lexical Editor with automatic image management using a reusable Trait approach. This skill transforms base64 images from rich text editors into optimized storage files and automatically cleans up unused images.
When to use this Skill
Use this Skill when you need to:
- Implement Lexical Editor or similar WYSIWYG editors with image uploads
- Convert base64 images to actual file storage
- Automatically delete unused images from storage
- Manage media files in rich text content
- Optimize storage usage in Laravel applications
- Build reusable image lifecycle management across multiple models
- Handle cleanup of old images when content is updated or deleted
Quick Start (Trait Approach - Recommended)
Step 1: Install Lexical Editor Package
composer require malzariey/filament-lexical-editor
Step 2: Create RichEditorMedia Model & Migration
php artisan make:model RichEditorMedia -m
Migration:
Schema::create('rich_editor_media', function (Blueprint $table) {
$table->id();
$table->string('model_type');
$table->unsignedBigInteger('model_id');
$table->string('field_name');
$table->string('file_path');
$table->string('disk')->default('public');
$table->timestamps();
$table->index(['model_type', 'model_id']);
});
Model:
class RichEditorMedia extends Model
{
protected $fillable = ['model_type', 'model_id', 'field_name', 'file_path', 'disk'];
public function model(): MorphTo
{
return $this->morphTo();
}
}
Step 3: Create HasRichEditorMedia Trait
Create app/Models/Concerns/HasRichEditorMedia.php:
See references/trait-implementation.md for complete trait code.
Step 4: Create Observer for RichEditorMedia
// app/Observers/RichEditorMediaObserver.php
class RichEditorMediaObserver
{
public function deleted(RichEditorMedia $media): void
{
if ($media->file_path && Storage::disk($media->disk)->exists($media->file_path)) {
Storage::disk($media->disk)->delete($media->file_path);
}
}
}
Register in AppServiceProvider:
RichEditorMedia::observe(RichEditorMediaObserver::class);
Step 5: Apply Trait to Model
use App\Models\Concerns\HasRichEditorMedia;
class Product extends Model
{
use HasRichEditorMedia;
protected array $richEditorFields = ['description']; // Fields using Lexical Editor
}
Step 6: Configure Filament Resource
use Malzariey\FilamentLexicalEditor\LexicalEditor;
LexicalEditor::make('description')
->label('Mô tả')
->helperText('Ảnh paste/upload sẽ tự lưu ra file, không lưu base64 vào DB.')
->columnSpanFull()
How It Works
Architecture Overview
┌─────────────────────────────────────────────────────┐
│ Lexical Editor (Filament Admin) │
│ - User paste/upload image in editor │
│ - Image embedded as base64 in HTML │
└──────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ HasRichEditorMedia Trait (Model) │
│ - bootHasRichEditorMedia() registers events │
│ - saving: convertBase64ImagesToFiles() │
│ - saved: cleanupRemovedRichEditorImages() │
│ - saved: syncRichEditorMedia() │
└──────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ File Storage (Dated directories) │
│ storage/app/public/uploads/{model}/content/Y/m/d/ │
│ - product-lexical-1734123456-abc12345.jpg │
│ - product-lexical-1734123457-def67890.png │
└──────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ RichEditorMedia Table (Tracking) │
│ - Tracks all images per model/field │
│ - Enables orphan detection & bulk cleanup │
└─────────────────────────────────────────────────────┘
Workflow Events
- updating: Snapshot old content for comparison
- saving: Convert all base64 images to storage files
- saved:
- Compare old/new content and delete unused images
- Sync tracking table with current images
- deleted/forceDeleted: Clean up all associated files
Implementation Details
Base64 Conversion Process
The trait scans content for base64 images using regex pattern:
data:image/(png|jpg|jpeg|gif|webp|svg+xml);base64,{data}
For each match:
- Extract MIME type (png, jpg, gif, webp, svg)
- Decode base64 data to binary
- Validate size (max 5MB)
- Generate unique filename:
{model}-lexical-{timestamp}-{random8}.{ext} - Save to dated directory:
uploads/{model}/content/Y/m/d/ - Replace base64 string with
/storage/...URL
Example:
<!-- Before (base64) -->
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...">
<!-- After (file URL) -->
<img src="/storage/uploads/products/content/2025/12/11/product-lexical-1734123456-abc12345.png">
Image Cleanup Strategy
The trait intelligently tracks images:
- snapshotRichEditorContent(): Captures old content before update
- extractImagePathsFromContent(): Parses HTML, data-url, and JSON formats
- cleanupRemovedRichEditorImages(): Deletes images no longer in content
This prevents accidental deletion of images still in use while cleaning up removed images.
Storage Structure
storage/app/public/
└── uploads/
├── products/
│ └── content/
│ └── 2025/12/11/
│ ├── product-lexical-1734123456-abc12345.jpg
│ └── product-lexical-1734123457-def67890.png
└── articles/
└── content/
└── 2025/12/11/
└── article-lexical-1734123458-ghi12345.jpg
Benefits of dated directory structure:
- Track which images belong to which model
- Prevent accidental deletion of shared images
- Organize cleanup commands by type
Best Practices
1. Image Naming Convention
Use descriptive prefixes for different contexts:
// Post model
$filename = 'lexical-' . time() . '-' . uniqid() . '.' . $extension;
// ServicePost model
$filename = 'service-lexical-' . time() . '-' . uniqid() . '.' . $extension;
// Custom models
$filename = 'custom-lexical-' . time() . '-' . uniqid() . '.' . $extension;
2. Directory Organization
Separate storage directories by model:
// In Observer
$path = 'uploads/content/' . $filename; // For Post
$path = 'uploads/service-content/' . $filename; // For ServicePost
$path = 'uploads/custom-content/' . $filename; // For custom models
3. Logging and Monitoring
Always log important operations:
Log::info("Converted base64 image to storage: {$filePath}");
Log::info("Deleted unused content image: {$imagePath}");
Log::error("Failed to convert base64 image: " . $e->getMessage());
4. Error Handling
Implement graceful error handling:
try {
// Convert base64 to file
$filePath = $this->saveBase64AsFile($base64Data, $extension);
} catch (\Exception $e) {
Log::error("Conversion failed: " . $e->getMessage());
// Continue processing other images
continue;
}
5. Soft Deletes
Consider using soft deletes for safety:
// In Model
use SoftDeletes;
// Allow recovery of deleted models and their images
public function restore()
{
// Restore images from backup if available
}
Advanced Usage
Multiple Image Formats
Support different image types:
$supported = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg+xml'];
// In regex pattern
preg_match_all('/data:image\/(png|jpg|jpeg|gif|webp|svg\+xml);base64,/i', $content);
Image Validation
Validate before saving:
private function validateImage(string $base64Data, string $extension): bool
{
$imageData = base64_decode($base64Data);
// Check file size (max 5MB)
if (strlen($imageData) > 5 * 1024 * 1024) {
throw new \Exception("Image exceeds 5MB limit");
}
// Verify MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, $imageData);
finfo_close($finfo);
if (!str_starts_with($mime, 'image/')) {
throw new \Exception("Invalid image file");
}
return true;
}
Automatic Resizing
Resize large images:
use Intervention\Image\Facades\Image;
private function optimizeImage(string $path): void
{
$image = Image::make(storage_path('app/public/' . $path));
// Resize if larger than 1920px
if ($image->width() > 1920) {
$image->resize(1920, null, ['aspect_ratio' => true]);
}
// Compress and save as WebP
$image->save(str_replace($path, '.webp', $path), 75);
}
Cleanup Command
Create a command to clean unused images:
php artisan make:command ImagesCleanUnused
See cleanup-command.md for complete implementation.
Common Issues and Solutions
Issue 1: Images Not Converting
Symptom: Base64 images remain in content instead of being saved
Solutions:
- Verify Observer is registered in
EventServiceProvider - Check storage permissions:
chmod -R 775 storage/app/public/ - Verify
publicdisk is configured inconfig/filesystems.php - Check logs for conversion errors:
tail -f storage/logs/laravel.log
Issue 2: Images Not Displaying
Symptom: Images saved but not visible in frontend
Solutions:
- Run storage symlink:
php artisan storage:link - Verify
storage/app/publicdirectory exists - Check file permissions:
chmod -R 755 storage/app/public/uploads/ - Verify public URL in
config/app.php
Issue 3: Orphaned Files
Symptom: Old images remain in storage after deletion
Solutions:
- Verify
deleted()event is firing: Add logging - Check
handleContentImages()logic for content extraction - Run cleanup command:
php artisan images:clean-unused --dry-run - Manually delete orphaned files: Find files modified before X days
Issue 4: Observer Not Running
Symptom: Model events not triggering
Solutions:
# Clear configuration cache
php artisan config:clear
# Clear application cache
php artisan cache:clear
# Verify Observer is registered
php artisan tinker
>>> Post::getObservableEvents()
Testing
Unit Testing
public function test_base64_converts_to_file()
{
$post = Post::create([
'name' => 'Test Post',
'content' => '<img src="data:image/png;base64,iVBORw0...">',
]);
$this->assertStringContainsString('/storage/uploads/content/', $post->content);
$this->assertStringNotContainsString('data:image/png;base64', $post->content);
}
public function test_old_image_deleted_on_update()
{
$post = Post::create(['name' => 'Test', 'image' => 'old.jpg']);
Storage::disk('public')->put('old.jpg', 'dummy content');
$post->update(['image' => 'new.jpg']);
Storage::disk('public')->assertMissing('old.jpg');
}
Feature Testing
public function test_image_cleanup_on_content_update()
{
$post = Post::create([
'content' => '<img src="/storage/uploads/content/image1.jpg">
<img src="/storage/uploads/content/image2.jpg">',
]);
$post->update([
'content' => '<img src="/storage/uploads/content/image1.jpg">',
]);
// image2.jpg should be deleted
Storage::disk('public')->assertMissing('uploads/content/image2.jpg');
}
Related Resources
- Observer Implementation Guide
- Cleanup Command Setup
- Frontend Integration
- Troubleshooting Guide
Examples
See EXAMPLES.md for:
- Complete working Observer class
- Filament Resource configuration
- Cleanup command implementation
- Real-world use cases
References
- Laravel Observers Documentation
- Filament File Upload
- Lexical Editor Official
- Malzariey Filament Lexical Editor
Validation Checklist
Before deploying to production:
- Observers registered in
EventServiceProvider - Storage permissions set correctly (775)
- Storage symlink created:
php artisan storage:link - Base64 regex pattern tested with sample content
- Image cleanup logic verified with test data
- Logging configured and monitored
- Cleanup command scheduled in Kernel.php
- Backup system in place before cleanup
- File size limits enforced
- Image format validation in place
Output Format
When implementing this Skill, I will:
- Analyze your current model structure
- Generate complete Observer class with all lifecycle hooks
- Provide Filament Resource configuration
- Create cleanup command if needed
- Set up logging and monitoring
- Include comprehensive error handling
- Provide testing examples
- Document storage structure
- Create troubleshooting guide
This ensures a production-ready image management system with automatic cleanup and error recovery.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?