amidesfahani/filament-tinyeditor

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:

  1. Optimizes the images on save using Intervention Image (Resize, convert to .jpg and compresses)
  2. Checks for existing images vs new images so we don't optimize the same image twice
  3. Renames the images based on the slug-datetime-3randomdigits
  4. Strips all the width and height tags from images, adds an absolute path to the images, removes any paragraph tags surrounding the img tags
  5. 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.
🤙🏻

rasim commented

use;

->convertUrls(false)