Auto-Generate Branded Social Share Images in Laravel

Auto-Generate Branded Social Share Images in Laravel

April 25, 2025
By Mike Wall
4 min read

I'm always exploring new ways to enhance user experience and improve content visibility on social media. Social media platforms like X (formerly Twitter), LinkedIn, and Facebook prominently display shared content images, which can directly affect click-through rates. My goal was to automatically create attractive, branded, consistent, professional-looking social share images—without the tedious manual effort.

Tooling and Setup

I was already using Laravel and Spatie Media Library to manage my blog content and media uploads, which made integration seamless. The star of this solution, however, is the Intervention Image library. Its simplicity and expressive API made it incredibly easy to create, manipulate, and layer images dynamically—perfect for generating polished, on-brand social share images.

Here's the stack I used:

  • Laravel for backend structure

  • Spatie Media Library for media handling

  • Intervention Image for all the heavy lifting in image generation

Step-by-Step Implementation

Creating the Service Class

The core logic lives in a service class: SocialShareImageService. It’s responsible for composing and saving the image. Here's a breakdown of how it works:

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateForPost</span><span class="hljs-params">(Post $post)</span>: <span class="hljs-title">string</span>
</span>{
    $image = Image::create(<span class="hljs-number">1200</span>, <span class="hljs-number">630</span>);
    $image->fill(<span class="hljs-string">'#0F172A'</span>);

    $featuredImage = $post->getFirstMediaPath(<span class="hljs-string">'default'</span>, <span class="hljs-string">'featured'</span>);
    <span class="hljs-keyword">if</span> ($featuredImage) {
        $featured = Image::read($featuredImage);
        $featured->cover(<span class="hljs-number">1200</span>, <span class="hljs-number">630</span>)
        ->brightness(<span class="hljs-number">5</span>)
        ->contrast(<span class="hljs-number">10</span>);
        
        $image->place($featured);
    }

This sets up a 1200x630 canvas (ideal for social sharing), optionally adds a featured image with enhancements, and prepares the base design.

Styling with Overlays and Text

To improve legibility and reinforce branding, overlays and text elements are added:

<span class="hljs-comment">// Add a dark gradient overlay for better contrast</span>
<span class="hljs-keyword">$this</span>->addProfessionalOverlay($image);

<span class="hljs-comment">// Add the post title</span>
$image->text($post->post_title, <span class="hljs-number">100</span>, <span class="hljs-number">250</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(FontFactory $font)</span> </span>{
    $font->file(public_path(<span class="hljs-string">'fonts/Inter-Bold.ttf'</span>));
    $font->size(<span class="hljs-number">64</span>);
    $font->color(<span class="hljs-string">'#FFFFFF'</span>);
    $font->align(<span class="hljs-string">'left'</span>);
    $font->valign(<span class="hljs-string">'top'</span>);
    $font->wrap(<span class="hljs-number">1000</span>);
});

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addProfessionalOverlay</span><span class="hljs-params">($image)</span>: <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// Create dark overlays with varying opacity for a gradient-like effect</span>

        <span class="hljs-comment">// Left side overlay (darker on left for text area)</span>
        <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">0</span>; $i < <span class="hljs-number">8</span>; $i++) {
            $width = <span class="hljs-number">100</span>;
            $leftOverlay = Image::create($width, <span class="hljs-number">630</span>);
            $leftOverlay->fill(<span class="hljs-string">'#000000'</span>);
            $image->place($leftOverlay, <span class="hljs-string">'top-left'</span>, offset_x: $i * $width, opacity: <span class="hljs-number">80</span> - ($i * <span class="hljs-number">10</span>));
        }

        <span class="hljs-comment">// Bottom overlay (darker at bottom for brand information)</span>
        <span class="hljs-keyword">for</span> ($i = <span class="hljs-number">0</span>; $i < <span class="hljs-number">5</span>; $i++) {
            $height = <span class="hljs-number">40</span>;
            $bottomOverlay = Image::create(<span class="hljs-number">1200</span>, $height);
            $bottomOverlay->fill(<span class="hljs-string">'#000000'</span>);
            $image->place($bottomOverlay, <span class="hljs-string">'bottom-left'</span>, offset_y: <span class="hljs-number">630</span> - ($i + <span class="hljs-number">1</span>) * $height, opacity: <span class="hljs-number">70</span> - ($i * <span class="hljs-number">15</span>));
        }

        <span class="hljs-comment">// Add a subtle vignette effect</span>
        <span class="hljs-keyword">$this</span>->addVignetteEffect($image);
    }

These gradients improve contrast, while the title is styled for readability using the Inter font. You can also optionally include a publication date and website URL for additional context.

Branding the Image

We include a decorative small corner accent for subtle but clear branding:

<span class="hljs-comment">// Corner badge with initials</span>
<span class="hljs-keyword">$this</span>->addCornerBranding($image);

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addCornerBranding</span><span class="hljs-params">($image)</span>: <span class="hljs-title">void</span>  
</span>{  
    $size = <span class="hljs-number">80</span>;  
    $cornerX = <span class="hljs-number">1120</span>;  
    $cornerY = <span class="hljs-number">550</span>;  
  
    $square = Image::create($size, $size);  
    $square->fill(<span class="hljs-string">'#6B4B96'</span>);  
    $image->place($square, <span class="hljs-string">'top-left'</span>, offset_x: $cornerX, offset_y: $cornerY, opacity: <span class="hljs-number">80</span>);  
  
    <span class="hljs-comment">// Add text or logo inside  </span>
    $image->text(  
        <span class="hljs-string">'MW'</span>,  
        $cornerX + ($size / <span class="hljs-number">2</span>),  
        $cornerY + ($size / <span class="hljs-number">2</span>) + <span class="hljs-number">5</span>,  
        <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(FontFactory $font)</span>: <span class="hljs-title">void</span> </span>{  
            $font->file(public_path(<span class="hljs-string">'fonts/Inter-Bold.ttf'</span>));  
            $font->size(<span class="hljs-number">32</span>);  
            $font->color(<span class="hljs-string">'#FFFFFF'</span>);  
            $font->align(<span class="hljs-string">'center'</span>);  
            $font->valign(<span class="hljs-string">'middle'</span>);  
        }  
    );  
}

Inside addCornerBranding(), we create a square and place text in the center for a custom logo effect, giving the image a distinctive signature.

Saving the Image

Finally, the image is saved with a unique name. This filename is generated by hashing the post's ID and title:

$filename = <span class="hljs-string">'social-share-'</span> . md5($post->id . <span class="hljs-string">'-'</span> . $post->post_title) . <span class="hljs-string">'.jpg'</span>;
Storage::disk(<span class="hljs-string">'public'</span>)->put(<span class="hljs-string">"social-share/$filename"</span>, $image->toJpeg(<span class="hljs-number">95</span>)->toString());

Hashing ensures consistent filenames for unchanged posts. If a title changes, the hash changes too—automatically triggering a new image to be generated.

Using a Trait for Easy Access

The HasSocialShareImage trait allows any model (like Post) to use the image generator easily:

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSocialShareImageUrl</span><span class="hljs-params">()</span>: ?<span class="hljs-title">string</span>
</span>{
    <span class="hljs-keyword">if</span> (Storage::disk(<span class="hljs-string">'public'</span>)->exists($path)) {
        <span class="hljs-keyword">return</span> Storage::disk(<span class="hljs-string">'public'</span>)->url($path);
    }
    <span class="hljs-keyword">return</span> app(SocialShareImageService::class)->generateForPost(<span class="hljs-keyword">$this</span>);
}

This approach keeps the logic centralized, making it easy to reuse across your Laravel application.

Example & Source Code

Want to see it in action? Here's a sample social share image generated by the service:

Example results

You can view the full source code for this implementation on GitHub Gist:

👉 View Gist: SocialShareImageService + Trait

Final Thoughts

This automated solution significantly reduces manual workload while improving brand consistency across social media channels. It's a great example of how Laravel’s flexibility and Intervention Image’s power can be combined to enhance digital content effectively.

Feel free to adapt or expand this solution for your own Laravel projects!

Table of Contents