*/ use HasFactory; protected function casts(): array { return [ 'status' => PostStatus::class, 'published_at' => 'datetime', ]; } protected static function booted(): void { static::creating(function (Post $post) { if (empty($post->slug)) { $post->slug = Str::slug($post->title); } }); } /** * @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * @return BelongsTo */ public function category(): BelongsTo { return $this->belongsTo(Category::class); } /** * @return BelongsToMany */ public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } /** * @param Builder $query */ #[Scope] protected function published(Builder $query): void { $query->where('status', PostStatus::Published) ->whereNotNull('published_at') ->where('published_at', '<=', now()); } protected function bodyHtml(): Attribute { return Attribute::get(function () { $html = Str::markdown($this->body, [ 'html_input' => 'strip', 'allow_unsafe_links' => false, ]); // The page template already renders the post title as an

. If the // Markdown body also opens with a top-level heading repeating that // title, drop it so the page doesn't ship two identical

s. $html = preg_replace_callback( '/^\s*

(.*?)<\/h1>/is', fn ($matches) => trim(strip_tags($matches[1])) === trim($this->title) ? '' : $matches[0], $html, limit: 1 ); return (new ToolLinker)->link($html); }); } protected function excerptOrSummary(): Attribute { return Attribute::get( fn () => $this->excerpt ?: Str::limit(strip_tags(Str::markdown($this->body)), 160) ); } }