Skip to content

Commit 1048e96

Browse files
committed
OG images
1 parent 722050c commit 1048e96

File tree

9 files changed

+433
-1
lines changed

9 files changed

+433
-1
lines changed

app/Filament/Resources/ArticleResource/Pages/CreateArticle.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@
33
namespace App\Filament\Resources\ArticleResource\Pages;
44

55
use App\Filament\Resources\ArticleResource;
6+
use App\Services\OgImageService;
67
use Filament\Resources\Pages\CreateRecord;
78

89
class CreateArticle extends CreateRecord
910
{
1011
protected static string $resource = ArticleResource::class;
1112

13+
protected function afterCreate(): void
14+
{
15+
$service = app(OgImageService::class);
16+
17+
$ogImageUrl = $service->generate($this->record);
18+
19+
$this->record->update([
20+
'og_image' => $ogImageUrl,
21+
]);
22+
}
23+
1224
protected function getRedirectUrl(): string
1325
{
1426
return static::getResource()::getUrl('edit', ['record' => $this->record->id]);

app/Filament/Resources/ArticleResource/Pages/EditArticle.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@
33
namespace App\Filament\Resources\ArticleResource\Pages;
44

55
use App\Filament\Resources\ArticleResource;
6+
use App\Services\OgImageService;
67
use Filament\Actions;
78
use Filament\Resources\Pages\EditRecord;
89

910
class EditArticle extends EditRecord
1011
{
1112
protected static string $resource = ArticleResource::class;
1213

14+
protected function afterSave(): void
15+
{
16+
$service = app(OgImageService::class);
17+
18+
// Always regenerate the OG image on update to ensure it's current
19+
$ogImageUrl = $service->generate($this->record);
20+
21+
$this->record->update([
22+
'og_image' => $ogImageUrl,
23+
]);
24+
}
25+
1326
protected function getHeaderActions(): array
1427
{
1528
return [

app/Http/Controllers/ShowBlogController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,18 @@ public function show(Article $article)
3131
SEOTools::opengraph()->setDescription($article->excerpt ?: 'Read this article on the NativePHP blog.');
3232
SEOTools::opengraph()->setType('article');
3333

34+
if ($article->og_image) {
35+
SEOTools::opengraph()->addImage($article->og_image);
36+
}
37+
3438
// Set Twitter Card metadata
3539
SEOTools::twitter()->setTitle($article->title);
3640
SEOTools::twitter()->setDescription($article->excerpt ?: 'Read this article on the NativePHP blog.');
3741

42+
if ($article->og_image) {
43+
SEOTools::twitter()->setImage($article->og_image);
44+
}
45+
3846
return view('article', [
3947
'article' => $article,
4048
]);

app/Models/Article.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Article extends Model
1616
'slug',
1717
'title',
1818
'excerpt',
19+
'og_image',
1920
'content',
2021
'published_at',
2122
];

app/Services/NativePhpLayout.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Intervention\Image\Colors\Rgb\Color;
6+
use SimonHamp\TheOg\BorderPosition;
7+
use SimonHamp\TheOg\Layout\Layouts\Standard;
8+
use SimonHamp\TheOg\Layout\PictureBox;
9+
use SimonHamp\TheOg\Layout\Position;
10+
use SimonHamp\TheOg\Layout\TextBox;
11+
12+
class NativePhpLayout extends Standard
13+
{
14+
protected BorderPosition $borderPosition = BorderPosition::All;
15+
16+
protected int $borderWidth = 2;
17+
18+
protected int $height = 630;
19+
20+
protected int $padding = 40;
21+
22+
protected int $width = 1200;
23+
24+
public function features(): void
25+
{
26+
$this->addFeature((new TextBox)
27+
->name('title')
28+
->text($this->title())
29+
->color($this->config->theme->getTitleColor())
30+
->font($this->config->theme->getTitleFont())
31+
->size(60)
32+
->box($this->mountArea()->box->width(), 400)
33+
->position(
34+
x: 0,
35+
y: 0,
36+
relativeTo: function () {
37+
if ($url = $this->getFeature('url')) {
38+
return $url->anchor(Position::BottomLeft)->moveY(25);
39+
}
40+
41+
return $this->mountArea()->anchor()->moveY(20);
42+
}
43+
)
44+
);
45+
46+
if ($description = $this->description()) {
47+
$this->addFeature((new TextBox)
48+
->name('description')
49+
->text($description)
50+
->color($this->config->theme->getDescriptionColor())
51+
->font($this->config->theme->getDescriptionFont())
52+
->size(40)
53+
->box($this->mountArea()->box->width(), 240)
54+
->position(
55+
x: 0,
56+
y: 50,
57+
relativeTo: fn () => $this->getFeature('title')->anchor(Position::BottomLeft),
58+
)
59+
);
60+
}
61+
62+
if ($url = $this->url()) {
63+
$this->addFeature((new TextBox)
64+
->name('url')
65+
->text($url)
66+
->color(new Color(0, 170, 166))
67+
->font($this->config->theme->getUrlFont())
68+
->size(28)
69+
->box($this->mountArea()->box->width(), 45)
70+
->position(
71+
x: 0,
72+
y: 20,
73+
relativeTo: fn () => $this->mountArea()->anchor(),
74+
)
75+
);
76+
}
77+
78+
if ($watermark = $this->watermark()) {
79+
$this->addFeature((new PictureBox)
80+
->path($watermark->path())
81+
->box(200, 200) // Doubled from 100x100 to 200x200
82+
->position(
83+
x: 0,
84+
y: 0,
85+
relativeTo: fn () => $this->mountArea()->anchor(Position::BottomRight),
86+
anchor: Position::BottomRight
87+
)
88+
);
89+
}
90+
}
91+
92+
public function url(): string
93+
{
94+
if ($url = parent::url()) {
95+
return strtoupper($url);
96+
}
97+
98+
return '';
99+
}
100+
}

app/Services/OgImageService.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use App\Models\Article;
6+
use Illuminate\Support\Facades\Storage;
7+
use Intervention\Image\Colors\Rgb\Color;
8+
use SimonHamp\TheOg\BorderPosition;
9+
use SimonHamp\TheOg\Image;
10+
11+
class OgImageService
12+
{
13+
/**
14+
* Generate an OG image for the given article.
15+
*/
16+
public function generate(Article $article): string
17+
{
18+
$image = new Image;
19+
20+
// Ensure the directory exists
21+
$directory = Storage::disk('public')->path('og-images');
22+
if (! is_dir($directory)) {
23+
mkdir($directory, 0755, true);
24+
}
25+
26+
$image
27+
->layout(new NativePhpLayout)
28+
->title($article->title)
29+
->description($article->excerpt ?? '')
30+
->backgroundColor('#ffffff')
31+
->titleColor('#141624')
32+
->descriptionColor('#141624')
33+
->border(BorderPosition::All, new Color(80, 91, 147), 5)
34+
->watermark(public_path('logo.svg'))
35+
->url(url('/blog/'.$article->slug))
36+
->save($this->getImagePath($article));
37+
38+
return $this->getImageUrl($article);
39+
}
40+
41+
/**
42+
* Delete the OG image for the given article.
43+
*/
44+
public function delete(Article $article): bool
45+
{
46+
if ($article->og_image) {
47+
$path = str_replace('/storage/', '', parse_url($article->og_image, PHP_URL_PATH));
48+
49+
return Storage::disk('public')->delete($path);
50+
}
51+
52+
return false;
53+
}
54+
55+
/**
56+
* Get the file path for storing the OG image.
57+
*/
58+
protected function getImagePath(Article $article): string
59+
{
60+
return Storage::disk('public')->path("og-images/{$article->slug}.png");
61+
}
62+
63+
/**
64+
* Get the public URL for the OG image.
65+
*/
66+
protected function getImageUrl(Article $article): string
67+
{
68+
return Storage::disk('public')->url("og-images/{$article->slug}.png");
69+
}
70+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"league/commonmark": "^2.4",
2323
"livewire/livewire": "^3.6.4",
2424
"sentry/sentry-laravel": "^4.13",
25+
"simonhamp/the-og": "^0.7.0",
2526
"spatie/laravel-menu": "^4.1",
2627
"spatie/yaml-front-matter": "^2.0",
2728
"symfony/http-client": "^7.2",

0 commit comments

Comments
 (0)