diff --git a/.gitignore b/.gitignore index 37560a94..e040b505 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ composer.phar /public/hot /public/storage /public/media -/public/sitemap.xml +/public/**/*.xml /storage/*.key /storage/framework/cache .env diff --git a/app/Actions/Article/ApprovedArticleAction.php b/app/Actions/Article/ApprovedArticleAction.php new file mode 100644 index 00000000..d2bef220 --- /dev/null +++ b/app/Actions/Article/ApprovedArticleAction.php @@ -0,0 +1,21 @@ +approved_at = now(); + $article->save(); + + givePoint(new ArticlePublished($article)); + + return $article; + } +} diff --git a/app/Console/Commands/GenerateSitemap.php b/app/Console/Commands/GenerateSitemap.php index 79bff9c9..cb22f3ca 100644 --- a/app/Console/Commands/GenerateSitemap.php +++ b/app/Console/Commands/GenerateSitemap.php @@ -5,47 +5,44 @@ namespace App\Console\Commands; use Illuminate\Console\Command; -use Illuminate\Support\Str; -use Psr\Http\Message\UriInterface; -use Spatie\Sitemap\SitemapGenerator; +use Spatie\Sitemap\Sitemap; +use Spatie\Sitemap\SitemapIndex; use Spatie\Sitemap\Tags\Url; final class GenerateSitemap extends Command { protected $signature = 'sitemap:generate'; - protected $description = 'Crawl the site to generate a sitemap.xml file'; - - /** - * @var array|string[] - */ - private array $noIndexPaths = [ - '', - '/forum/*', - '/user/*', - ]; + protected $description = 'Generate the sitemap'; public function handle(): void { - SitemapGenerator::create(config('app.url')) - ->shouldCrawl(fn (UriInterface $url) => $this->shouldIndex($url->getPath())) - ->hasCrawled(function (Url $url) { - if ($this->shouldNotIndex($url->path())) { - return; - } - - return $url; - }) - ->writeToFile(public_path('sitemap.xml')); - } - - private function shouldNotIndex(string $path): bool - { - return Str::is($this->noIndexPaths, $path); - } - - private function shouldIndex(string $path): bool - { - return ! $this->shouldNotIndex($path); + Sitemap::create() + ->add( + Url::create(route('home')) + ->setLastModificationDate(now()->subMinutes(10)) + ->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY) + ->setPriority(0.5) + ) + ->add( + Url::create(route('sponsors')) + ->setLastModificationDate(now()->subMinutes(10)) + ->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY) + ->setPriority(0.5) + ) + ->add( + Url::create(route('about')) + ->setLastModificationDate(now()->subMinutes(10)) + ->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY) + ->setPriority(0.5) + ) + ->writeToFile(public_path('sitemaps/base_sitemap.xml')); + + $sitemap = SitemapIndex::create() + ->add('/sitemaps/base_sitemap.xml') + ->add('/sitemaps/discussion_sitemap.xml') + ->add('/sitemaps/blog_sitemap.xml'); + + $sitemap->writeToFile(public_path('sitemap.xml')); } } diff --git a/app/Console/Commands/Sitemap/GenerateArticlesSitemapCommand.php b/app/Console/Commands/Sitemap/GenerateArticlesSitemapCommand.php new file mode 100644 index 00000000..7a5c62f5 --- /dev/null +++ b/app/Console/Commands/Sitemap/GenerateArticlesSitemapCommand.php @@ -0,0 +1,27 @@ +whereNotNull('approved_at')->each(function ($article) use ($sitemap): void { + $sitemap->add($article); // @phpstan-ignore-line + }); + + $sitemap->writeToFile(public_path('sitemaps/blog_sitemap.xml')); + } +} diff --git a/app/Console/Commands/Sitemap/GenerateDiscussionsSitemapCommand.php b/app/Console/Commands/Sitemap/GenerateDiscussionsSitemapCommand.php new file mode 100644 index 00000000..bc1a49b7 --- /dev/null +++ b/app/Console/Commands/Sitemap/GenerateDiscussionsSitemapCommand.php @@ -0,0 +1,27 @@ +whereHas('replies')->each(function ($discussion) use ($sitemap): void { + $sitemap->add($discussion); // @phpstan-ignore-line + }); + + $sitemap->writeToFile(public_path('sitemaps/discussion_sitemap.xml')); + } +} diff --git a/app/Filament/Resources/ArticleResource.php b/app/Filament/Resources/ArticleResource.php index e66a284c..6558d56f 100644 --- a/app/Filament/Resources/ArticleResource.php +++ b/app/Filament/Resources/ArticleResource.php @@ -4,8 +4,8 @@ namespace App\Filament\Resources; +use App\Actions\Article\ApprovedArticleAction; use App\Filament\Resources\ArticleResource\Pages; -use App\Gamify\Points\ArticlePublished; use App\Models\Article; use Filament\Resources\Resource; use Filament\Support\Enums\MaxWidth; @@ -89,10 +89,7 @@ public static function table(Table $table): Table ->action(function ($record): void { Gate::authorize('approve', $record); - $record->approved_at = now(); - $record->save(); - - givePoint(new ArticlePublished($record)); + app(ApprovedArticleAction::class)->execute($record); }), Action::make('declined') ->visible(fn (Article $record) => $record->isAwaitingApproval()) diff --git a/app/Livewire/Components/Discussion/Comments.php b/app/Livewire/Components/Discussion/Comments.php index 4c61074b..58b524cf 100644 --- a/app/Livewire/Components/Discussion/Comments.php +++ b/app/Livewire/Components/Discussion/Comments.php @@ -74,6 +74,7 @@ public function comments(): Collection { $replies = collect(); + // @phpstan-ignore-next-line foreach ($this->discussion->replies->load(['allChildReplies', 'user']) as $reply) { /** @var Reply $reply */ if ($reply->allChildReplies->isNotEmpty()) { diff --git a/app/Models/Article.php b/app/Models/Article.php index 1fb33d69..32c697b6 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -12,6 +12,7 @@ use App\Traits\HasTags; use App\Traits\Reactable; use App\Traits\RecordsActivity; +use Carbon\Carbon; use CyrildeWit\EloquentViewable\Contracts\Viewable; use CyrildeWit\EloquentViewable\InteractsWithViews; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -19,6 +20,8 @@ use Illuminate\Support\Str; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; +use Spatie\Sitemap\Contracts\Sitemapable; +use Spatie\Sitemap\Tags\Url; /** * @property-read int $id @@ -33,17 +36,17 @@ * @property int $user_id * @property string | null $locale * @property-read User $user - * @property \Carbon\Carbon | null $published_at - * @property \Carbon\Carbon | null $submitted_at - * @property \Carbon\Carbon | null $approved_at - * @property \Carbon\Carbon | null $shared_at - * @property \Carbon\Carbon | null $declined_at - * @property \Carbon\Carbon | null $sponsored_at - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at + * @property Carbon | null $published_at + * @property Carbon | null $submitted_at + * @property Carbon | null $approved_at + * @property Carbon | null $shared_at + * @property Carbon | null $declined_at + * @property Carbon | null $sponsored_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property \Illuminate\Database\Eloquent\Collection | Tag[] $tags */ -final class Article extends Model implements HasMedia, ReactableInterface, Viewable +final class Article extends Model implements HasMedia, ReactableInterface, Sitemapable, Viewable { use HasAuthor; use HasFactory; @@ -96,6 +99,14 @@ public function newEloquentBuilder($query): ArticleQueryBuilder return new ArticleQueryBuilder($query); } + public function toSitemapTag(): Url + { + return Url::create(route('articles.show', $this)) + ->setLastModificationDate(Carbon::create($this->updated_at)) // @phpstan-ignore-line + ->setChangeFrequency(Url::CHANGE_FREQUENCY_YEARLY) + ->setPriority(0.5); + } + public function excerpt(int $limit = 110): string { return Str::limit(strip_tags((string) md_to_html($this->body)), $limit); diff --git a/app/Models/Discussion.php b/app/Models/Discussion.php index 350b10f1..cd96c408 100644 --- a/app/Models/Discussion.php +++ b/app/Models/Discussion.php @@ -26,6 +26,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Spatie\Sitemap\Contracts\Sitemapable; +use Spatie\Sitemap\Tags\Url; /** * @property-read int $id @@ -41,8 +43,9 @@ * @property Carbon $updated_at * @property User $user * @property Collection | SpamReport[] $spamReports + * @property Collection | Reply[] $replies */ -final class Discussion extends Model implements ReactableInterface, ReplyInterface, SpamReportableContract, SubscribeInterface, Viewable +final class Discussion extends Model implements ReactableInterface, ReplyInterface, Sitemapable, SpamReportableContract, SubscribeInterface, Viewable { use HasAuthor; use HasFactory; @@ -107,6 +110,14 @@ public function excerpt(int $limit = 110): string return Str::limit(strip_tags((string) md_to_html($this->body)), $limit); } + public function toSitemapTag(): Url + { + return Url::create(route('discussions.show', $this)) + ->setLastModificationDate(Carbon::create($this->updated_at)) // @phpstan-ignore-line + ->setChangeFrequency(Url::CHANGE_FREQUENCY_YEARLY) + ->setPriority(0.5); + } + public function isPinned(): bool { return $this->is_pinned; diff --git a/composer.json b/composer.json index f8a3150f..3d551980 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "spatie/laravel-feed": "^4.2.1", "spatie/laravel-google-fonts": "^1.2.3", "spatie/laravel-permission": "^6.10.0", - "spatie/laravel-sitemap": "^7.2.1", + "spatie/laravel-sitemap": "^7.3", "stevebauman/location": "^7.4.0", "symfony/http-client": "^7.1.8", "symfony/mailgun-mailer": "^7.1", diff --git a/composer.lock b/composer.lock index 2bd88a3a..f79cb730 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8115d0fbb68dbb37f8726499e23e4221", + "content-hash": "231723be1478153245cd48c8cc81d53a", "packages": [ { "name": "abraham/twitteroauth", diff --git a/public/sitemaps/.gitkeep b/public/sitemaps/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/routes/console.php b/routes/console.php index c476778d..9af55b33 100644 --- a/routes/console.php +++ b/routes/console.php @@ -4,7 +4,12 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; Artisan::command('inspire', function (): void { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote')->hourly(); + +Schedule::command('sitemap:blog-generate')->dailyAt('01:00'); +Schedule::command('sitemap:discussion-generate')->dailyAt('01:10'); +Schedule::command('sitemap:generate')->dailyAt('02:00');