Full image paths
Closed this issue · 2 comments
When I use this to upload an image everything works fine, but in the database images are saved like this:
<img src="../../../storage/project-images/example.jpg" alt="" width="800" height="530">
How can I get the images to save with the APP_URL like this:
<img src="https://mysite.com/storage/project-images/example.jpg" alt="" width="800" height="530">
I was able to fix this issue by downloading php-html-parser
This is very opinionated, but I wanted clean HTML that works well with the Tailwind CSS prose class. Check out the preview if you don't know about prose.
The code does the following:
- Optimizes the images on save using Intervention Image (Resize, convert to .jpg and compresses)
- Checks for existing images vs new images so we don't optimize the same image twice
- Renames the images based on the slug-datetime-3randomdigits
- Strips all the width and height tags from images, adds an absolute path to the images, removes any paragraph tags surrounding the img tags
- Fill in all empty alt tags from a list of random alt tags
I then created a disk in the filesystem
:
'blog-images' => [
'driver' => 'local',
'root' => storage_path('app/public/blog-projects'),
'url' => env('APP_URL') . '/storage/blog-projects',
'visibility' => 'public',
],
In the config/filament-tinyeditor.php
I removed the upload_directory
as it was creating nested directories.
'custom' => [
'plugins' => '...',
'toolbar' => '...',
// 'upload_directory' => 'null',
],
In the Filament BlogResource.php
TinyEditor::make('description')
->fileAttachmentsDisk('blog-images')
->fileAttachmentsVisibility('public')
->fileAttachmentsDirectory('')
->profile('custom')
->columnSpan('full')
->required(),
Then this is where I parse all the TinyEditor html so I can use it nicely with Tailwind CSS prose class.
CreateBlog.php
use PHPHtmlParser\Dom;
class CreateBlog extends CreateRecord
{
protected static string $resource = BlogResource::class;
protected function handleRecordCreation(array $data): Model
{
$slug = $data['slug']; // Extract slug from $data
// Run through the phases
$data['description'] = $this->processContentPhases($data['description'], $slug);
// Handle tags
if (isset($data['tags']) && is_array($data['tags'])) {
$tagIds = collect($data['tags'])->map(function ($name) {
$tag = Tag::findOrCreate($name);
return $tag->name;
});
$data['tags'] = $tagIds->toArray();
}
return Blog::create($data);
}
private function processContentPhases($htmlContent, $slug): string
{
$contentImageOptimized = $this->imageOptimize($htmlContent, $slug);
$contentImageFilledAltTags = $this->fillAltTags($contentImageOptimized);
$contentImageRemoveWidthHeight = $this->imageRemoveWidthHeight($contentImageFilledAltTags);
return $this->removeParagraphTagsAroundImg($contentImageRemoveWidthHeight);
}
private function imageOptimize($htmlContent, $slug): string
{
$dom = new Dom;
$dom->loadStr($htmlContent);
$appUrl = config('app.url') . '/storage/blog-projects/';
$safeSlug = $slug ?: 'my-website-name';
foreach ($dom->find('img') as $img) {
$src = $img->getAttribute('src');
$isExistingImage = file_exists(storage_path('app/public/blog-projects/' . basename($src)));
$isNewImage = $img->getAttribute('width') && $img->getAttribute('height');
if ($isExistingImage && !$isNewImage) {
// Update src attribute with full app URL
$img->setAttribute('src', $appUrl . basename($src));
} else {
// Convert relative path to absolute path
$src = storage_path('app/public/blog-projects/') . basename($src);
$image = Image::make($src);
// Correct image orientation and resize
$image->orientate()->resize(800, 800, function ($constraint) {
$constraint->aspectRatio();
});
// Encode image
$image->encode('jpg', 75);
// New file name using slug
$timestamp = date('mdYs');
$randomNumber = rand(100, 999);
$newFileName = $safeSlug . '-' . $timestamp . $randomNumber . '.jpg';
// Save the new image
Storage::disk('blog-images')->put($newFileName, $image);
// Update src attribute with full URL
$fullUrl = $appUrl . $newFileName;
$img->setAttribute('src', $fullUrl);
// Delete old image file
if ($newFileName !== basename($src)) {
Storage::disk('blog-images')->delete(basename($src));
}
}
// Remove width and height attributes
$img->removeAttribute('width');
$img->removeAttribute('height');
}
return $dom->outerHtml;
}
private function fillAltTags($htmlContent): string
{
$dom = new Dom;
$dom->loadStr($htmlContent);
$altPhrases = [
'Random Alt Tag 1',
'Random Alt Tag 2',
// Add as many as you need
];
foreach ($dom->find('img') as $img) {
if (empty($img->getAttribute('alt'))) {
$randomAlt = $altPhrases[array_rand($altPhrases)];
$img->setAttribute('alt', $randomAlt);
}
}
return $dom->outerHtml;
}
private function imageRemoveWidthHeight($htmlContent): string
{
$dom = new Dom;
$dom->loadStr($htmlContent);
foreach ($dom->find('img') as $img) {
$img->removeAttribute('width');
$img->removeAttribute('height');
}
return $dom->outerHtml;
}
private function removeParagraphTagsAroundImg($htmlContent): string
{
$pattern = '/<p>\s*(<img[^>]+>)\s*<\/p>/s';
$replacement = '$1'; // Keep only the img tag
return preg_replace($pattern, $replacement, $htmlContent);
}
}
The edit file is pretty much the same:
EditBlog.php
class EditBlog extends EditRecord
{
protected static string $resource = BlogResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
$slug = $data['slug']; // Extract slug from $data
$contentImageOptimized = $this->imageOptimize($data['description'], $slug);
$contentImageFilledAltTags = $this->fillAltTags($contentImageOptimized);
$contentImageRemoveWidthHeight = $this->imageRemoveWidthHeight($contentImageFilledAltTags);
$data['description'] = $this->removeParagraphTagsAroundImg($contentImageRemoveWidthHeight);
return $data;
}
/**
* @throws Exception
*/
public function edit()
{
$this->validate();
Log::info('EditBlog::edit() called');
DB::beginTransaction();
try {
// Update the record with the processed data
$this->record->update($this->data);
// Handling tags synchronization
if ($this->record->tags && is_array($this->record->tags)) {
$tagIds = collect($this->record->tags)->map(function ($name) {
$tag = Tag::findOrCreate($name);
return $tag->id;
});
$this->record->tags()->sync($tagIds);
}
DB::commit();
} catch (Exception $e) {
DB::rollback();
throw $e;
}
return redirect()->route('blog-projects.index');
}
//Copy over the functions from create.
The end result will look like this:
<p>This is test 2.</p> <img
src="https://mywebsite.test/storage/blog-projects/investor-group-agent-1130202340702.jpg"
alt="Random Alt Tag 2" />
<p>Let's add one more image.</p> <img
src="https://mywebsite.test/storage/blog-projects/investor-group-agent-1130202340314.jpg"
alt="Random Alt Tag 1" />
<p>This is the end of test 2.</p>
Again, this is very opinionated and not for everyone. Also needs some refactoring, but at least this is a work around to solve the "Full Path Image" issue plus more. Hopefully this will help someone.
🤙🏻
use;
->convertUrls(false)