Add tests for featured thumbnail generation; apply Pint formatting and related edits

This commit is contained in:
2026-05-06 18:55:40 +02:00
parent 7a8bc8e22a
commit 82f2b1f660
65 changed files with 11325 additions and 49545 deletions

View File

@@ -2,7 +2,7 @@
<a href="{{ route('news.show', $article->slug) }}" class="block">
<div class="relative aspect-[16/9] overflow-hidden bg-black/20">
@if($article->cover_url)
<img src="{{ $article->cover_url }}" alt="{{ $article->title }}" class="h-full w-full object-cover transition duration-300 group-hover:scale-[1.04]">
<img src="{{ $article->cover_url }}" @if($article->cover_srcset) srcset="{{ $article->cover_srcset }}" sizes="(max-width: 767px) 100vw, (max-width: 1279px) 50vw, 390px" @endif alt="{{ $article->title }}" class="h-full w-full object-cover transition duration-300 group-hover:scale-[1.04]">
@else
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_45%),linear-gradient(180deg,rgba(15,23,42,0.92),rgba(2,6,23,0.98))]"></div>
@endif

View File

@@ -37,9 +37,14 @@
@if(!empty($tags) && $tags->isNotEmpty())
<section class="rounded-[24px] border border-white/[0.06] bg-white/[0.025] p-5">
<div class="mb-4 flex items-center gap-2 text-sm font-semibold uppercase tracking-[0.18em] text-white/45">
<i class="fa-solid fa-tags text-[11px] text-sky-300"></i>
Topics
<div class="mb-4 flex items-center justify-between gap-3">
<div class="flex items-center gap-2 text-sm font-semibold uppercase tracking-[0.18em] text-white/45">
<i class="fa-solid fa-tags text-[11px] text-sky-300"></i>
Popular Topics
</div>
<a href="{{ route('tags.index') }}" class="text-[11px] font-semibold uppercase tracking-[0.14em] text-sky-200/75 transition hover:text-sky-100">
All Tags
</a>
</div>
<div class="flex flex-wrap gap-2">
@foreach($tags as $tag)

View File

@@ -11,6 +11,22 @@
(object) ['name' => 'Announcements', 'url' => route('news.index')],
(object) ['name' => $archiveDate->format('F Y'), 'url' => route('news.archive', ['year' => $archiveDate->year, 'month' => $archiveDate->month])],
]);
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
'title' => $archiveDate->format('F Y') . ' — News Archive',
'description' => 'News archive for ' . $archiveDate->format('F Y') . ' on Skinbase.',
'canonical' => route('news.archive', ['year' => $archiveDate->year, 'month' => $archiveDate->month]),
'breadcrumbs' => $headerBreadcrumbs,
'structured_data' => [
[
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $archiveDate->format('F Y') . ' — News Archive',
'description' => 'Published News stories from ' . $archiveDate->format('F Y') . '.',
'url' => route('news.archive', ['year' => $archiveDate->year, 'month' => $archiveDate->month]),
],
],
])->build();
@endphp
<x-nova-page-header

View File

@@ -12,6 +12,22 @@
(object) ['name' => 'Announcements', 'url' => route('news.index')],
(object) ['name' => $authorLabel, 'url' => route('news.author', ['username' => $author->username])],
]);
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
'title' => $authorLabel . ' — News Author',
'description' => 'News stories and announcements by ' . $authorLabel . '.',
'canonical' => route('news.author', ['username' => $author->username]),
'breadcrumbs' => $headerBreadcrumbs,
'structured_data' => [
[
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $authorLabel . ' — News Author',
'description' => 'Editorial stories and updates by ' . $authorLabel . '.',
'url' => route('news.author', ['username' => $author->username]),
],
],
])->build();
@endphp
<x-nova-page-header

View File

@@ -6,15 +6,50 @@
@section('news_content')
@php
$articleItems = collect($articles->items());
$headerBreadcrumbs = collect([
(object) ['name' => 'Community', 'url' => route('community.activity')],
(object) ['name' => 'Announcements', 'url' => route('news.index')],
(object) ['name' => 'News', 'url' => route('news.index')],
(object) ['name' => $category->name, 'url' => route('news.category', $category->slug)],
]);
$structuredData = [
[
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $category->name . ' — News',
'description' => $category->description ?: ('Announcements filed under ' . $category->name . '.'),
'url' => route('news.category', $category->slug),
],
];
if ($articleItems->isNotEmpty()) {
$structuredData[] = [
'@context' => 'https://schema.org',
'@type' => 'ItemList',
'name' => $category->name . ' — News Articles',
'description' => 'Published News stories in the ' . $category->name . ' category.',
'url' => route('news.category', $category->slug),
'numberOfItems' => $articleItems->count(),
'itemListElement' => $articleItems->values()->map(fn ($article, int $index): array => [
'@type' => 'ListItem',
'position' => $index + 1,
'name' => $article->title,
'url' => route('news.show', ['slug' => $article->slug]),
])->all(),
];
}
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
'title' => $category->name . ' — News',
'description' => $category->description ?: ('Announcements in the ' . $category->name . ' category.'),
'canonical' => route('news.category', $category->slug),
'breadcrumbs' => $headerBreadcrumbs,
'structured_data' => $structuredData,
])->build();
@endphp
<x-nova-page-header
section="Community"
section="News"
:title="$category->name"
icon="fa-folder-open"
:breadcrumbs="$headerBreadcrumbs"

View File

@@ -6,14 +6,55 @@
@section('news_content')
@php
$articleItems = collect([$featured])
->merge($highlights)
->merge($articles->items())
->filter(fn ($article) => $article !== null)
->unique('id')
->values();
$headerBreadcrumbs = collect([
(object) ['name' => 'Community', 'url' => route('community.activity')],
(object) ['name' => 'Announcements', 'url' => route('news.index')],
(object) ['name' => 'News', 'url' => route('news.index')],
]);
$structuredData = [
[
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => config('news.rss_title', 'News'),
'description' => config('news.rss_description', 'Latest news, feature rollouts, and team updates from Skinbase.'),
'url' => route('news.index'),
],
];
if ($articleItems->isNotEmpty()) {
$structuredData[] = [
'@context' => 'https://schema.org',
'@type' => 'ItemList',
'name' => config('news.rss_title', 'News') . ' Articles',
'description' => config('news.rss_description', 'Latest news, feature rollouts, and team updates from Skinbase.'),
'url' => route('news.index'),
'numberOfItems' => $articleItems->count(),
'itemListElement' => $articleItems->map(fn ($article, int $index): array => [
'@type' => 'ListItem',
'position' => $index + 1,
'name' => $article->title,
'url' => route('news.show', $article->slug),
])->all(),
];
}
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
'title' => config('news.rss_title', 'News'),
'description' => config('news.rss_description', 'Latest news, feature rollouts, and team updates from Skinbase.'),
'canonical' => route('news.index'),
'breadcrumbs' => $headerBreadcrumbs,
'structured_data' => $structuredData,
])->build();
@endphp
<x-nova-page-header
section="Community"
section="News"
title="News"
icon="fa-newspaper"
:breadcrumbs="$headerBreadcrumbs"
@@ -44,7 +85,7 @@
<div class="grid lg:grid-cols-[1.25fr_0.95fr]">
<div class="relative min-h-[280px] overflow-hidden bg-black/20">
@if($featured->cover_url)
<img src="{{ $featured->cover_url }}" alt="{{ $featured->title }}" class="h-full w-full object-cover transition duration-500 group-hover:scale-[1.03]">
<img src="{{ $featured->cover_url }}" @if($featured->cover_srcset) srcset="{{ $featured->cover_srcset }}" sizes="(max-width: 1023px) 100vw, 768px" @endif alt="{{ $featured->title }}" class="h-full w-full object-cover transition duration-500 group-hover:scale-[1.03]">
@else
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_45%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.98))]"></div>
@endif

View File

@@ -1,5 +1,9 @@
@php
$useUnifiedSeo = true;
$novaCssEntries = [
'resources/css/app.css',
'resources/scss/nova.scss',
];
@endphp
@extends('layouts.nova')

View File

@@ -1,6 +1,11 @@
@php
$isPreview = (bool) ($previewMode ?? false);
$articleUrl = $isPreview ? ($previewCanonical ?? url()->current()) : route('news.show', $article->slug);
$articleSchemaImage = $article->effective_og_image
? url($article->effective_og_image)
: url((string) config('seo.fallback_image_path', '/gfx/skinbase_back_001.webp'));
$articleCoverSizes = '(max-width: 767px) calc(100vw - 3rem), (max-width: 1279px) calc(100vw - 5rem), 768px';
$articleCoverPreloadHref = $article->cover_desktop_url ?: $article->cover_url;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
'title' => $article->meta_title ?: $article->title,
'description' => $article->meta_description ?: Str::limit(strip_tags((string) $article->excerpt), 160),
@@ -12,31 +17,55 @@
'og_description' => $article->effective_og_description,
'og_image' => $article->effective_og_image,
'breadcrumbs' => collect([
(object) ['name' => 'Community', 'url' => route('community.activity')],
(object) ['name' => 'Announcements', 'url' => route('news.index')],
(object) ['name' => 'Home', 'url' => url('/')],
(object) ['name' => 'News', 'url' => route('news.index')],
$article->category
? (object) ['name' => $article->category->name, 'url' => route('news.category', $article->category->slug)]
: null,
(object) ['name' => $article->title, 'url' => route('news.show', $article->slug)],
])->filter()->values(),
])
->addJsonLd(array_filter([
'@context' => 'https://schema.org',
'@type' => 'Article',
'@type' => 'NewsArticle',
'headline' => $article->title,
'description' => $article->meta_description ?: Str::limit(strip_tags((string) $article->excerpt), 160),
'image' => $article->effective_og_image,
'image' => $articleSchemaImage
? array_filter([
'@type' => 'ImageObject',
'url' => $articleSchemaImage,
'contentUrl' => $articleSchemaImage,
'thumbnailUrl' => $article->cover_mobile_url,
'caption' => $article->title,
], fn (mixed $value): bool => $value !== null && $value !== '')
: null,
'datePublished' => $article->published_at?->toIso8601String(),
'dateModified' => $article->updated_at?->toIso8601String(),
'articleSection' => $article->category?->name,
'author' => array_filter([
'@type' => 'Person',
'name' => $article->author?->name,
]),
'publisher' => [
'@type' => 'Organization',
'name' => config('seo.site_name', 'Skinbase'),
],
'mainEntityOfPage' => $articleUrl,
], fn (mixed $value): bool => $value !== null && $value !== ''))
->build();
@endphp
@push('head')
@if($articleCoverPreloadHref)
<link
rel="preload"
as="image"
href="{{ $articleCoverPreloadHref }}"
@if($article->cover_srcset) imagesrcset="{{ $article->cover_srcset }}" imagesizes="{{ $articleCoverSizes }}" @endif
fetchpriority="high"
>
@endif
@endpush
@extends('news.layout', [
'metaTitle' => $article->meta_title ?: $article->title,
'metaDescription' => $article->meta_description ?: Str::limit(strip_tags((string)$article->excerpt), 160),
@@ -48,17 +77,16 @@
@php
$headerBreadcrumbs = collect([
(object) ['name' => 'Community', 'url' => route('community.activity')],
(object) ['name' => 'Announcements', 'url' => route('news.index')],
(object) ['name' => 'Home', 'url' => url('/')],
(object) ['name' => 'News', 'url' => route('news.index')],
$article->category
? (object) ['name' => $article->category->name, 'url' => route('news.category', $article->category->slug)]
: null,
(object) ['name' => $article->title, 'url' => $articleUrl],
])->filter()->values();
@endphp
<x-nova-page-header
section="Community"
section="News"
:title="$article->title"
icon="fa-newspaper"
:breadcrumbs="$headerBreadcrumbs"
@@ -105,7 +133,18 @@
<article class="min-w-0">
@if($article->cover_url)
<div class="overflow-hidden rounded-[32px] border border-white/[0.06] bg-black/20 shadow-[0_24px_60px_rgba(0,0,0,0.24)]">
<img src="{{ $article->cover_url }}" alt="{{ $article->title }}" class="h-auto max-h-[520px] w-full object-cover">
<a href="{{ $articleCoverPreloadHref }}" class="group block focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950" aria-label="Open full cover image">
<div class="relative">
<img src="{{ $article->cover_url }}" @if($article->cover_srcset) srcset="{{ $article->cover_srcset }}" sizes="{{ $articleCoverSizes }}" @endif alt="{{ $article->title }}" fetchpriority="high" loading="eager" decoding="async" class="h-auto max-h-[520px] w-full object-cover transition duration-300 group-hover:scale-[1.01]">
<div class="pointer-events-none absolute inset-x-4 bottom-4 flex items-center justify-between gap-3 rounded-full border border-white/10 bg-slate-950/72 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-white/82 backdrop-blur-sm">
<span>Open Image</span>
<span class="inline-flex items-center gap-2 text-sky-200/90">
<i class="fa-solid fa-magnifying-glass-plus text-[11px]"></i>
Full Image
</span>
</div>
</div>
</a>
</div>
@endif

View File

@@ -11,6 +11,22 @@
(object) ['name' => 'Announcements', 'url' => route('news.index')],
(object) ['name' => '#' . $tag->name, 'url' => route('news.tag', $tag->slug)],
]);
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
'title' => '#' . $tag->name . ' — News',
'description' => 'Announcements tagged with ' . $tag->name . '.',
'canonical' => route('news.tag', $tag->slug),
'breadcrumbs' => $headerBreadcrumbs,
'structured_data' => [
[
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => '#' . $tag->name . ' — News',
'description' => 'Stories and announcements tagged with #' . $tag->name . '.',
'url' => route('news.tag', $tag->slug),
],
],
])->build();
@endphp
<x-nova-page-header