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:
@@ -40,6 +40,54 @@ class BlogRepository extends ServiceEntityRepository
|
||||
->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* A representative's own posts, newest first, optionally filtered by status.
|
||||
* @return Blog[]
|
||||
*/
|
||||
public function findByRepresentation(\App\Representation\Entity\Representation $rep, int $page = 1, int $limit = 20, ?string $status = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('b')
|
||||
->leftJoin('b.city', 'c')->addSelect('c')
|
||||
->where('b.representation = :rep')
|
||||
->setParameter('rep', $rep)
|
||||
->orderBy('b.createdAt', 'DESC')
|
||||
->setFirstResult(($page - 1) * $limit)
|
||||
->setMaxResults($limit);
|
||||
if ($status !== null && $status !== '') {
|
||||
$qb->andWhere('b.status = :status')->setParameter('status', $status);
|
||||
}
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function countByRepresentation(\App\Representation\Entity\Representation $rep, ?string $status = null): int
|
||||
{
|
||||
$qb = $this->createQueryBuilder('b')
|
||||
->select('COUNT(b.id)')
|
||||
->where('b.representation = :rep')
|
||||
->setParameter('rep', $rep);
|
||||
if ($status !== null && $status !== '') {
|
||||
$qb->andWhere('b.status = :status')->setParameter('status', $status);
|
||||
}
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draft posts due for automatic publish: scheduled_at reached, and either not
|
||||
* in the review workflow or already approved (the review gate wins).
|
||||
* @return Blog[]
|
||||
*/
|
||||
public function findDueForPublish(int $now): array
|
||||
{
|
||||
return $this->createQueryBuilder('b')
|
||||
->where('b.status = :draft')
|
||||
->andWhere('b.scheduledAt IS NOT NULL AND b.scheduledAt <= :now')
|
||||
->andWhere('b.reviewStatus IS NULL OR b.reviewStatus = :approved')
|
||||
->setParameter('draft', Blog::STATUS_DRAFT)
|
||||
->setParameter('now', $now)
|
||||
->setParameter('approved', Blog::REVIEW_APPROVED)
|
||||
->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/** @return Blog[] published, newest first */
|
||||
public function findPublished(int $page = 1, int $limit = 20, ?string $tag = null, ?int $cityId = null): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user