create(array_merge([ 'name' => 'Platform Updates', 'slug' => 'platform-updates-' . Str::lower(Str::random(6)), 'description' => 'Skinbase platform updates.', 'position' => 0, 'is_active' => true, ], $attributes)); } function newsTag(array $attributes = []): NewsTag { return NewsTag::query()->create(array_merge([ 'name' => 'Launch', 'slug' => 'launch-' . Str::lower(Str::random(6)), ], $attributes)); } function publishedNewsArticle(User $author, NewsCategory $category, array $attributes = []): NewsArticle { $article = NewsArticle::query()->create(array_merge([ 'title' => 'Nova newsroom launch', 'slug' => 'nova-newsroom-launch-' . Str::lower(Str::random(6)), 'excerpt' => 'A first-party newsroom for launches, tutorials, and editorials.', 'content' => "# Nova newsroom\n\nThis is the first newsroom article.", 'author_id' => $author->id, 'category_id' => $category->id, 'type' => NewsArticle::TYPE_PLATFORM_UPDATE, 'status' => 'published', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_PUBLISHED, 'published_at' => now()->subHour(), 'is_featured' => true, 'is_pinned' => true, ], $attributes)); return $article; } it('renders published news across public discovery routes', function (): void { $author = User::factory()->create([ 'username' => 'newsauthor', 'name' => 'News Author', ]); $category = newsCategory([ 'name' => 'Announcements', 'slug' => 'announcements', ]); $tag = newsTag([ 'name' => 'Release', 'slug' => 'release', ]); $article = publishedNewsArticle($author, $category, [ 'title' => 'Skinbase Newsroom', 'slug' => 'skinbase-newsroom', ]); $article->tags()->sync([$tag->id]); publishedNewsArticle($author, $category, [ 'title' => 'Hidden Draft', 'slug' => 'hidden-draft', 'status' => 'draft', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, 'published_at' => null, 'is_featured' => false, 'is_pinned' => false, ]); $this->get(route('news.index')) ->assertOk() ->assertSee('Skinbase Newsroom') ->assertDontSee('Hidden Draft'); $this->get(route('news.show', ['slug' => $article->slug])) ->assertOk() ->assertSee('Skinbase Newsroom') ->assertSee('News Author'); $this->get(route('news.category', ['slug' => $category->slug])) ->assertOk() ->assertSee('Skinbase Newsroom'); $this->get(route('news.tag', ['slug' => $tag->slug])) ->assertOk() ->assertSee('Skinbase Newsroom'); $this->get(route('news.archive', ['year' => $article->published_at->year, 'month' => $article->published_at->month])) ->assertOk() ->assertSee('Skinbase Newsroom'); $this->get(route('news.author', ['username' => $author->username])) ->assertOk() ->assertSee('Skinbase Newsroom'); }); it('renders news index breadcrumbs and item list schema', function (): void { $author = User::factory()->create([ 'username' => 'indexschemaauthor', 'name' => 'Index Schema Author', ]); $category = newsCategory([ 'name' => 'Announcements', 'slug' => 'announcements-index-schema', ]); $article = publishedNewsArticle($author, $category, [ 'title' => 'Index Schema Article', 'slug' => 'index-schema-article', ]); $this->get(route('news.index')) ->assertOk() ->assertSeeInOrder(['Home', 'News']) ->assertSee('CollectionPage', false) ->assertSee('ItemList', false) ->assertSee(route('news.show', ['slug' => $article->slug]), false); }); it('shows only popular news topics in the sidebar and links to the full tags page', function (): void { config()->set('news.sidebar_tags_limit', 2); $author = User::factory()->create([ 'username' => 'sidebarauthor', 'name' => 'Sidebar Author', ]); $category = newsCategory([ 'name' => 'Sidebar Category', 'slug' => 'sidebar-category', ]); $tagAlpha = newsTag([ 'name' => 'Alpha Topic', 'slug' => 'alpha-topic', ]); $tagBeta = newsTag([ 'name' => 'Beta Topic', 'slug' => 'beta-topic', ]); $tagGamma = newsTag([ 'name' => 'Gamma Topic', 'slug' => 'gamma-topic', ]); $articleOne = publishedNewsArticle($author, $category, [ 'title' => 'Sidebar article one', 'slug' => 'sidebar-article-one', 'is_featured' => false, 'is_pinned' => false, ]); $articleOne->tags()->sync([$tagAlpha->id, $tagBeta->id]); $articleTwo = publishedNewsArticle($author, $category, [ 'title' => 'Sidebar article two', 'slug' => 'sidebar-article-two', 'is_featured' => false, 'is_pinned' => false, 'published_at' => now()->subMinutes(30), ]); $articleTwo->tags()->sync([$tagAlpha->id]); $articleThree = publishedNewsArticle($author, $category, [ 'title' => 'Sidebar article three', 'slug' => 'sidebar-article-three', 'is_featured' => false, 'is_pinned' => false, 'published_at' => now()->subMinutes(15), ]); $articleThree->tags()->sync([$tagGamma->id]); $this->get(route('news.index')) ->assertOk() ->assertSee('Popular Topics') ->assertSee('All Tags') ->assertSee(route('tags.index'), false) ->assertSee('#Alpha Topic') ->assertSee('#Beta Topic') ->assertDontSee('#Gamma Topic'); }); it('renders a public news article when anonymous sessions are skipped', function (): void { Config::set('skinbase-sessions.enabled', true); Config::set('skinbase-sessions.debug_header', true); $author = User::factory()->create([ 'username' => 'guestnewsauthor', 'name' => 'Guest News Author', ]); $category = newsCategory([ 'name' => 'Guest Announcements', 'slug' => 'guest-announcements', ]); $article = publishedNewsArticle($author, $category, [ 'title' => 'Guest Sessionless News Page', 'slug' => 'guest-sessionless-news-page', ]); $this->get(route('news.show', ['slug' => $article->slug])) ->assertOk() ->assertHeader('X-Skinbase-Session', 'skipped') ->assertSee('Guest Sessionless News Page'); }); it('renders article breadcrumbs with home news category hierarchy', function (): void { $author = User::factory()->create([ 'username' => 'breadcrumbauthor', 'name' => 'Breadcrumb Author', ]); $category = newsCategory([ 'name' => 'Technology', 'slug' => 'technology', ]); $article = publishedNewsArticle($author, $category, [ 'title' => 'Breadcrumb Check Article', 'slug' => 'breadcrumb-check-article', ]); $this->get(route('news.show', ['slug' => $article->slug])) ->assertOk() ->assertSeeInOrder(['Home', 'News', 'Technology']); }); it('renders category breadcrumbs and item list schema', function (): void { $author = User::factory()->create([ 'username' => 'categoryschemaauthor', 'name' => 'Category Schema Author', ]); $category = newsCategory([ 'name' => 'Technology', 'slug' => 'technology-schema', ]); $article = publishedNewsArticle($author, $category, [ 'title' => 'Category Schema Article', 'slug' => 'category-schema-article', ]); $this->get(route('news.category', ['slug' => $category->slug])) ->assertOk() ->assertSeeInOrder(['Home', 'News', 'Technology']) ->assertSee('CollectionPage', false) ->assertSee('ItemList', false) ->assertSee(route('news.show', ['slug' => $article->slug]), false); }); it('renders structured data for public news pages', function (): void { $author = User::factory()->create([ 'username' => 'schemaauthor', 'name' => 'Schema Author', ]); $category = newsCategory([ 'name' => 'Technology', 'slug' => 'technology-structured-data', ]); $tag = newsTag([ 'name' => 'Game Art', 'slug' => 'game-art-structured-data', ]); $article = publishedNewsArticle($author, $category, [ 'title' => 'Structured Data Article', 'slug' => 'structured-data-article', ]); $article->tags()->sync([$tag->id]); $this->get(route('news.index')) ->assertOk() ->assertSee('CollectionPage', false) ->assertSee('BreadcrumbList', false) ->assertSee('ItemList', false); $this->get(route('news.category', ['slug' => $category->slug])) ->assertOk() ->assertSee('CollectionPage', false) ->assertSee('BreadcrumbList', false) ->assertSee('ItemList', false); $this->get(route('news.tag', ['slug' => $tag->slug])) ->assertOk() ->assertSee('CollectionPage', false) ->assertSee('BreadcrumbList', false); $this->get(route('news.archive', ['year' => $article->published_at->year, 'month' => $article->published_at->month])) ->assertOk() ->assertSee('CollectionPage', false) ->assertSee('BreadcrumbList', false); $this->get(route('news.author', ['username' => $author->username])) ->assertOk() ->assertSee('CollectionPage', false) ->assertSee('BreadcrumbList', false); $this->get(route('news.show', ['slug' => $article->slug])) ->assertOk() ->assertSee('NewsArticle', false) ->assertSee('ImageObject', false) ->assertSee('BreadcrumbList', false); }); it('prioritizes the news hero image and skips the grid stylesheet on article pages', function (): void { $author = User::factory()->create([ 'username' => 'lcpauthor', 'name' => 'LCP Author', ]); $category = newsCategory([ 'name' => 'Performance', 'slug' => 'performance-news', ]); $article = publishedNewsArticle($author, $category, [ 'title' => 'Performance News Article', 'slug' => 'performance-news-article', 'cover_image' => 'news/covers/ab/cd/abcdef1234567890.webp', ]); $response = $this->get(route('news.show', ['slug' => $article->slug])); $response ->assertOk() ->assertSee('fetchpriority="high"', false) ->assertSee('loading="eager"', false) ->assertSee('imagesizes="(max-width: 767px) calc(100vw - 3rem), (max-width: 1279px) calc(100vw - 5rem), 768px"', false) ->assertSee(' 768w', false) ->assertSee('href="' . e($article->cover_desktop_url ?? $article->cover_url) . '"', false) ->assertDontSee('id="news-cover-preview"', false) ->assertDontSee('resources/css/nova-grid.css', false); });