feat(blog): add SEO fields, scheduling, and representative ownership to blog posts

- Introduced new SEO fields (meta_title, meta_description, primary_keyword, secondary_keywords, faq, internal_links, external_links, reading_time, canonical_url, og_image) to the Blog entity.
- Added scheduling capability with a scheduled_at field to manage automatic publishing of blog posts.
- Implemented representative ownership through a foreign key representation_id in the Blog entity, allowing representatives to manage their own posts.
- Updated BlogController and RepresentationBlogController to handle new fields and ensure proper data handling for SEO and scheduling.
- Created BlogWriter service to encapsulate the logic for applying SEO and scheduling fields to blog entities.
- Added PublishScheduledBlogsMessage and its handler to manage the publishing of scheduled blogs.
- Implemented ScheduledBlogPublisher service to publish drafts whose scheduled_at has arrived, respecting review status.
- Created migration to update the database schema with new fields and constraints.
- Added tests to ensure the correct functionality of new features, including SEO fields, representative scope, and scheduled publishing.
This commit is contained in:
hamed
2026-07-23 22:05:23 +03:30
parent 14730e43ce
commit 62b2f28c4f
14 changed files with 786 additions and 0 deletions
+3
View File
@@ -23,6 +23,7 @@ class BlogController extends BaseController
private readonly BlogRepository $blogRepo,
private readonly CityRepository $cityRepo,
private readonly FileValidatorService $fileValidator,
private readonly \App\Blog\Service\BlogWriter $blogWriter,
private readonly string $projectDir,
) {}
@@ -243,6 +244,7 @@ class BlogController extends BaseController
if (!empty($data['review_status'])) $blog->setReviewStatus($data['review_status']);
// نبودِ city_id یعنی سراسری — پس همیشه اعمال می‌شود، نه فقط وقتی مقدار دارد.
$blog->setCity($this->resolveCity($data['city_id'] ?? null));
$this->blogWriter->applySeoFields($blog, $data);
// Ensure slug uniqueness
if ($this->blogRepo->findBySlug($blog->getSlug()) !== null) {
@@ -319,6 +321,7 @@ class BlogController extends BaseController
if (array_key_exists('image_url', $data)) $blog->setImageUrl($data['image_url'] ?: null);
// PATCH: فقط وقتی صریحاً فرستاده شد تغییر کند. ارسال null یعنی «سراسری‌اش کن».
if (array_key_exists('city_id', $data)) $blog->setCity($this->resolveCity($data['city_id']));
$this->blogWriter->applySeoFields($blog, $data);
$this->blogRepo->save($blog);