'boolean', 'active' => 'boolean', 'noindex' => 'boolean', 'published_at' => 'datetime', 'starts_at' => 'datetime', 'ends_at' => 'datetime', ]; protected static function booted(): void { $flushCache = static function (self $story): void { Cache::forget('web_story_index'); Cache::forget('web_story:' . $story->slug); if ($story->world?->slug) { Cache::forget('world:' . $story->world->slug . ':web_story'); } elseif ($story->world_id) { $worldSlug = World::query()->whereKey($story->world_id)->value('slug'); if (is_string($worldSlug) && $worldSlug !== '') { Cache::forget('world:' . $worldSlug . ':web_story'); } } }; static::saved($flushCache); static::deleted($flushCache); static::restored($flushCache); } public function world(): BelongsTo { return $this->belongsTo(World::class); } public function pages(): HasMany { return $this->hasMany(WorldWebStoryPage::class, 'story_id')->orderedPages(); } public function orderedPages(): HasMany { return $this->hasMany(WorldWebStoryPage::class, 'story_id')->orderedPages(); } public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } public function updater(): BelongsTo { return $this->belongsTo(User::class, 'updated_by'); } public function scopePublished(Builder $query): Builder { return $query->where('status', self::STATUS_PUBLISHED); } public function scopeActive(Builder $query): Builder { return $query->where('active', true); } public function scopeFeatured(Builder $query): Builder { return $query->where('featured', true); } public function scopeVisible(Builder $query): Builder { return $query ->active() ->published() ->where('noindex', false) ->where(function (Builder $builder): void { $builder->whereNull('published_at') ->orWhere('published_at', '<=', now()); }) ->where(function (Builder $builder): void { $builder->whereNull('starts_at') ->orWhere('starts_at', '<=', now()); }) ->where(function (Builder $builder): void { $builder->whereNull('ends_at') ->orWhere('ends_at', '>=', now()); }); } public function publicUrl(): string { return route('web-stories.show', ['slug' => $this->slug]); } public function posterPortraitUrl(): ?string { return $this->assetUrl($this->poster_portrait_path); } public function posterSquareUrl(): ?string { return $this->assetUrl($this->poster_square_path); } public function publisherLogoUrl(): ?string { return $this->assetUrl($this->publisher_logo_path); } public function seoTitle(): string { return trim((string) ($this->seo_title ?: $this->title)); } public function seoDescription(): string { return trim((string) ($this->seo_description ?: $this->excerpt ?: $this->description ?: '')); } private function assetUrl(?string $path): ?string { $resolved = trim((string) $path); if ($resolved === '') { return null; } if (str_starts_with($resolved, 'http://') || str_starts_with($resolved, 'https://')) { return $resolved; } return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($resolved, '/'); } }