SEO: gallery VisualArtwork contentUrl -> single best URL; update gallery unit test
This commit is contained in:
@@ -42,8 +42,9 @@ final class SeoFactory
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>|null> $thumbs
|
||||
* @param array<int, array{name: string, url: string}>|iterable<mixed> $breadcrumbs
|
||||
*/
|
||||
public function artwork(Artwork $artwork, array $thumbs, string $canonical): SeoData
|
||||
public function artwork(Artwork $artwork, array $thumbs, string $canonical, iterable $breadcrumbs = []): SeoData
|
||||
{
|
||||
$authorName = html_entity_decode((string) ($artwork->user?->name ?: $artwork->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$title = html_entity_decode((string) ($artwork->title ?: 'Artwork'), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
@@ -51,6 +52,9 @@ final class SeoFactory
|
||||
$description = Str::limit($description !== '' ? $description : $title, 160, '…');
|
||||
$image = $thumbs['xl']['url'] ?? $thumbs['lg']['url'] ?? $thumbs['md']['url'] ?? null;
|
||||
$keywords = $artwork->tags->pluck('name')->filter()->unique()->values()->all();
|
||||
$licenseUrl = $this->clean((string) ($artwork->license_url ?? ''));
|
||||
$publisherName = (string) config('seo.site_name', 'Skinbase');
|
||||
$publisherUrl = url('/');
|
||||
|
||||
$imageWidth = $thumbs['xl']['width'] ?? $thumbs['lg']['width'] ?? null;
|
||||
$imageHeight = $thumbs['xl']['height'] ?? $thumbs['lg']['height'] ?? null;
|
||||
@@ -60,6 +64,7 @@ final class SeoFactory
|
||||
->description($description)
|
||||
->keywords($keywords)
|
||||
->canonical($canonical)
|
||||
->breadcrumbs($breadcrumbs)
|
||||
->og(type: 'article', image: $image, imageAlt: sprintf('%s by %s', $title, $authorName), imageWidth: is_int($imageWidth) ? $imageWidth : null, imageHeight: is_int($imageHeight) ? $imageHeight : null)
|
||||
->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
@@ -73,9 +78,13 @@ final class SeoFactory
|
||||
'width' => $thumbs['xl']['width'] ?? $thumbs['lg']['width'] ?? null,
|
||||
'height' => $thumbs['xl']['height'] ?? $thumbs['lg']['height'] ?? null,
|
||||
'author' => ['@type' => 'Person', 'name' => $authorName],
|
||||
'creator' => ['@type' => 'Person', 'name' => $authorName],
|
||||
'publisher' => ['@type' => 'Organization', 'name' => $publisherName, 'url' => $publisherUrl],
|
||||
'creditText' => $authorName,
|
||||
'datePublished' => optional($artwork->published_at)->toAtomString(),
|
||||
'license' => $artwork->license_url,
|
||||
'license' => $licenseUrl,
|
||||
'keywords' => $keywords !== [] ? $keywords : null,
|
||||
'representativeOfPage' => true,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
|
||||
->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
@@ -84,14 +93,257 @@ final class SeoFactory
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'author' => ['@type' => 'Person', 'name' => $authorName],
|
||||
'publisher' => ['@type' => 'Organization', 'name' => $publisherName, 'url' => $publisherUrl],
|
||||
'datePublished' => optional($artwork->published_at)->toAtomString(),
|
||||
'license' => $artwork->license_url,
|
||||
'license' => $licenseUrl,
|
||||
'keywords' => $keywords !== [] ? $keywords : null,
|
||||
'image' => $image,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{name: string, url: string}>|iterable<mixed> $breadcrumbs
|
||||
* @param array<int, string> $keywords
|
||||
*/
|
||||
public function academyLessonPage(
|
||||
string $title,
|
||||
string $description,
|
||||
string $canonical,
|
||||
?string $image = null,
|
||||
iterable $breadcrumbs = [],
|
||||
array $keywords = [],
|
||||
?string $publishedAt = null,
|
||||
?string $modifiedAt = null,
|
||||
?string $articleSection = null,
|
||||
): SeoData {
|
||||
$publisherName = (string) config('seo.site_name', 'Skinbase');
|
||||
$publisherUrl = url('/');
|
||||
$licenseUrl = route('terms-of-service');
|
||||
$imageUrl = $this->normalizeUrl($image);
|
||||
|
||||
$builder = SeoDataBuilder::make()
|
||||
->title($title)
|
||||
->description($description)
|
||||
->keywords($keywords)
|
||||
->canonical($canonical)
|
||||
->breadcrumbs($breadcrumbs)
|
||||
->og(type: 'article', image: $imageUrl, imageAlt: $title);
|
||||
|
||||
if ($imageUrl !== null) {
|
||||
$builder->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ImageObject',
|
||||
'name' => $title,
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'contentUrl' => $imageUrl,
|
||||
'thumbnailUrl' => $imageUrl,
|
||||
'license' => $licenseUrl,
|
||||
'acquireLicensePage' => $licenseUrl,
|
||||
'creditText' => $publisherName,
|
||||
'creator' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'datePublished' => $publishedAt,
|
||||
'keywords' => $keywords !== [] ? $keywords : null,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));
|
||||
}
|
||||
|
||||
$builder->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Article',
|
||||
'headline' => $title,
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'mainEntityOfPage' => $canonical,
|
||||
'datePublished' => $publishedAt,
|
||||
'dateModified' => $modifiedAt ?? $publishedAt,
|
||||
'author' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'articleSection' => $articleSection,
|
||||
'keywords' => $keywords !== [] ? $keywords : null,
|
||||
'image' => $imageUrl,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));
|
||||
|
||||
return $builder->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<int, array<string, mixed>> $courses
|
||||
* @param array<int, array{name: string, url: string}>|iterable<mixed> $breadcrumbs
|
||||
*/
|
||||
public function academyCourseListingPage(
|
||||
string $title,
|
||||
string $description,
|
||||
string $canonical,
|
||||
iterable $courses,
|
||||
iterable $breadcrumbs = [],
|
||||
?string $image = null,
|
||||
): SeoData {
|
||||
$publisherName = (string) config('seo.site_name', 'Skinbase');
|
||||
$publisherUrl = url('/');
|
||||
$imageUrl = $this->normalizeUrl($image);
|
||||
|
||||
return SeoDataBuilder::make()
|
||||
->title($title)
|
||||
->description($description)
|
||||
->canonical($canonical)
|
||||
->breadcrumbs($breadcrumbs)
|
||||
->og(type: 'website', image: $imageUrl)
|
||||
->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $title,
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'mainEntity' => [
|
||||
'@type' => 'ItemList',
|
||||
'itemListElement' => collect($courses)
|
||||
->values()
|
||||
->map(function (array $course, int $index) use ($publisherName, $publisherUrl): array {
|
||||
$courseImage = $this->normalizeUrl((string) ($course['cover_image_url'] ?? $course['teaser_image_url'] ?? $course['cover_image'] ?? $course['teaser_image'] ?? ''));
|
||||
|
||||
return array_filter([
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'item' => array_filter([
|
||||
'@type' => 'Course',
|
||||
'name' => $this->clean((string) ($course['title'] ?? '')),
|
||||
'description' => $this->clean((string) ($course['excerpt'] ?? $course['description'] ?? '')),
|
||||
'url' => $this->clean((string) ($course['public_url'] ?? '')),
|
||||
'image' => $courseImage,
|
||||
'provider' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'isAccessibleForFree' => $this->academyCourseAccessibleForFree($course['access_level'] ?? null),
|
||||
'educationalLevel' => $this->academyCourseEducationalLevel($course['difficulty'] ?? null),
|
||||
'timeRequired' => $this->academyDuration((int) ($course['estimated_minutes'] ?? 0)),
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []),
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []);
|
||||
})
|
||||
->filter(fn (array $item): bool => data_get($item, 'item.name') !== null && data_get($item, 'item.url') !== null)
|
||||
->values()
|
||||
->all(),
|
||||
],
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{name: string, url: string}>|iterable<mixed> $breadcrumbs
|
||||
* @param array<int, string> $keywords
|
||||
* @param iterable<int, array<string, mixed>> $lessons
|
||||
*/
|
||||
public function academyCoursePage(
|
||||
string $title,
|
||||
string $description,
|
||||
string $canonical,
|
||||
?string $image = null,
|
||||
iterable $breadcrumbs = [],
|
||||
array $keywords = [],
|
||||
?string $publishedAt = null,
|
||||
?string $modifiedAt = null,
|
||||
?string $accessLevel = null,
|
||||
?string $difficulty = null,
|
||||
?int $estimatedMinutes = null,
|
||||
iterable $lessons = [],
|
||||
): SeoData {
|
||||
$publisherName = (string) config('seo.site_name', 'Skinbase');
|
||||
$publisherUrl = url('/');
|
||||
$licenseUrl = route('terms-of-service');
|
||||
$imageUrl = $this->normalizeUrl($image);
|
||||
|
||||
$builder = SeoDataBuilder::make()
|
||||
->title($title)
|
||||
->description($description)
|
||||
->keywords($keywords)
|
||||
->canonical($canonical)
|
||||
->breadcrumbs($breadcrumbs)
|
||||
->og(type: 'website', image: $imageUrl, imageAlt: $title);
|
||||
|
||||
if ($imageUrl !== null) {
|
||||
$builder->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ImageObject',
|
||||
'name' => $title,
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'contentUrl' => $imageUrl,
|
||||
'thumbnailUrl' => $imageUrl,
|
||||
'license' => $licenseUrl,
|
||||
'acquireLicensePage' => $licenseUrl,
|
||||
'creditText' => $publisherName,
|
||||
'creator' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'representativeOfPage' => true,
|
||||
'datePublished' => $publishedAt,
|
||||
'keywords' => $keywords !== [] ? $keywords : null,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));
|
||||
}
|
||||
|
||||
$builder->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Course',
|
||||
'name' => $title,
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'image' => $imageUrl,
|
||||
'provider' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => $publisherName,
|
||||
'url' => $publisherUrl,
|
||||
],
|
||||
'isAccessibleForFree' => $this->academyCourseAccessibleForFree($accessLevel),
|
||||
'educationalLevel' => $this->academyCourseEducationalLevel($difficulty),
|
||||
'timeRequired' => $this->academyDuration($estimatedMinutes),
|
||||
'datePublished' => $publishedAt,
|
||||
'dateModified' => $modifiedAt ?? $publishedAt,
|
||||
'keywords' => $keywords !== [] ? $keywords : null,
|
||||
'hasCourseInstance' => collect($lessons)
|
||||
->values()
|
||||
->map(function (array $lesson, int $index): array {
|
||||
return array_filter([
|
||||
'@type' => 'CourseInstance',
|
||||
'name' => $this->clean((string) ($lesson['title'] ?? '')),
|
||||
'url' => $this->clean((string) ($lesson['course_url'] ?? '')),
|
||||
'courseMode' => 'online',
|
||||
'position' => $index + 1,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []);
|
||||
})
|
||||
->filter(fn (array $lesson): bool => ($lesson['name'] ?? null) !== null && ($lesson['url'] ?? null) !== null)
|
||||
->values()
|
||||
->all(),
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));
|
||||
|
||||
return $builder->build();
|
||||
}
|
||||
|
||||
public function profilePage(string $title, string $canonical, string $description, ?string $image = null, iterable $breadcrumbs = []): SeoData
|
||||
{
|
||||
$profileName = trim(str_replace([' Gallery on Skinbase', ' on Skinbase'], '', $title));
|
||||
@@ -129,6 +381,58 @@ final class SeoFactory
|
||||
->build();
|
||||
}
|
||||
|
||||
private function normalizeUrl(?string $url): ?string
|
||||
{
|
||||
$url = $this->clean($url);
|
||||
|
||||
if ($url === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\/\//i', $url) === 1) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return url($url);
|
||||
}
|
||||
|
||||
private function clean(?string $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
private function academyCourseAccessibleForFree(mixed $accessLevel): ?bool
|
||||
{
|
||||
$access = strtolower(trim((string) $accessLevel));
|
||||
|
||||
if ($access === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $access === 'free';
|
||||
}
|
||||
|
||||
private function academyCourseEducationalLevel(mixed $difficulty): ?string
|
||||
{
|
||||
$value = strtolower(trim((string) $difficulty));
|
||||
|
||||
return match ($value) {
|
||||
'beginner' => 'Beginner',
|
||||
'intermediate' => 'Intermediate',
|
||||
'advanced' => 'Advanced',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function academyDuration(?int $minutes): ?string
|
||||
{
|
||||
$minutes = (int) ($minutes ?? 0);
|
||||
|
||||
return $minutes > 0 ? sprintf('PT%dM', $minutes) : null;
|
||||
}
|
||||
|
||||
public function collectionPage(string $title, string $description, string $canonical, ?string $image = null, bool $indexable = true): SeoData
|
||||
{
|
||||
return SeoDataBuilder::make()
|
||||
@@ -229,48 +533,149 @@ final class SeoFactory
|
||||
->values()
|
||||
->map(function (mixed $artwork, int $index): ?array {
|
||||
$name = trim((string) (data_get($artwork, 'name') ?? data_get($artwork, 'title') ?? ''));
|
||||
$url = data_get($artwork, 'url');
|
||||
|
||||
if (! filled($url)) {
|
||||
$slug = data_get($artwork, 'slug');
|
||||
$id = data_get($artwork, 'id');
|
||||
if (filled($slug) && filled($id)) {
|
||||
$url = route('art.show', ['id' => $id, 'slug' => $slug]);
|
||||
}
|
||||
}
|
||||
$url = $this->galleryArtworkUrl($artwork);
|
||||
|
||||
if ($name === '' && ! filled($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$thumbnailUrl = $this->normalizeUrl((string) (data_get($artwork, 'thumb_url') ?? ''));
|
||||
$contentUrl = $this->galleryContentUrl($artwork, $thumbnailUrl);
|
||||
$creatorName = $this->clean((string) (data_get($artwork, 'publisher.name') ?? data_get($artwork, 'uname') ?? data_get($artwork, 'username') ?? ''));
|
||||
$creatorUrl = $this->normalizeUrl((string) (data_get($artwork, 'publisher.profile_url') ?? data_get($artwork, 'profile_url') ?? ''));
|
||||
$creatorType = data_get($artwork, 'published_as_type') === 'group' || data_get($artwork, 'publisher.type') === 'group'
|
||||
? 'Organization'
|
||||
: 'Person';
|
||||
$publishedAt = data_get($artwork, 'published_at');
|
||||
$publishedAt = $publishedAt instanceof \DateTimeInterface ? $publishedAt->format(DATE_ATOM) : $this->clean(is_string($publishedAt) ? $publishedAt : null);
|
||||
$width = data_get($artwork, 'width');
|
||||
$height = data_get($artwork, 'height');
|
||||
$genre = $this->clean((string) (data_get($artwork, 'content_type_name') ?? data_get($artwork, 'category_name') ?? ''));
|
||||
$imageFormat = $this->galleryImageFormat($thumbnailUrl ?? $contentUrl);
|
||||
|
||||
return array_filter([
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'name' => $name !== '' ? $name : null,
|
||||
'url' => filled($url) ? url((string) $url) : null,
|
||||
'url' => filled($url) ? $url : null,
|
||||
'item' => array_filter([
|
||||
'@type' => 'VisualArtwork',
|
||||
'@id' => filled($url) ? $url . '#artwork' : null,
|
||||
'name' => $name !== '' ? $name : null,
|
||||
'url' => filled($url) ? $url : null,
|
||||
'image' => $thumbnailUrl,
|
||||
'thumbnailUrl' => $thumbnailUrl,
|
||||
'contentUrl' => $contentUrl,
|
||||
'creator' => $creatorName !== null ? array_filter([
|
||||
'@type' => $creatorType,
|
||||
'name' => $creatorName,
|
||||
'url' => $creatorUrl,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '') : null,
|
||||
'datePublished' => $publishedAt,
|
||||
'genre' => $genre,
|
||||
'encodingFormat' => $imageFormat,
|
||||
'width' => is_numeric($width) ? (int) $width : null,
|
||||
'height' => is_numeric($height) ? (int) $height : null,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []),
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '');
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($itemListElement === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$count = $artworks instanceof AbstractPaginator ? $artworks->total() : count($itemListElement);
|
||||
$name = (string) ($data['page_title'] ?? $data['hero_title'] ?? config('seo.default_title', 'Skinbase'));
|
||||
$description = (string) ($data['page_meta_description'] ?? $data['hero_description'] ?? config('seo.default_description'));
|
||||
$canonical = (string) ($data['page_canonical'] ?? url()->current());
|
||||
$about = collect(preg_split('/\s*,\s*/', (string) ($data['page_meta_keywords'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [])
|
||||
->map(fn (string $keyword): string => trim($keyword))
|
||||
->filter()
|
||||
->take(5)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => (string) ($data['page_title'] ?? $data['hero_title'] ?? config('seo.default_title', 'Skinbase')),
|
||||
'description' => (string) ($data['page_meta_description'] ?? $data['hero_description'] ?? config('seo.default_description')),
|
||||
'url' => (string) ($data['page_canonical'] ?? url()->current()),
|
||||
'@type' => ['CollectionPage', 'ImageGallery'],
|
||||
'name' => $name,
|
||||
'headline' => (string) ($data['hero_title'] ?? $name),
|
||||
'description' => $description,
|
||||
'url' => $canonical,
|
||||
'about' => $about !== [] ? $about : null,
|
||||
'mainEntity' => [
|
||||
'@type' => 'ItemList',
|
||||
'name' => (string) ($data['hero_title'] ?? $name),
|
||||
'itemListOrder' => $this->galleryItemListOrder((string) ($data['current_sort'] ?? 'trending')),
|
||||
'numberOfItems' => $count,
|
||||
'itemListElement' => $itemListElement,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function galleryArtworkUrl(mixed $artwork): ?string
|
||||
{
|
||||
$url = $this->normalizeUrl((string) (data_get($artwork, 'url') ?? ''));
|
||||
|
||||
if ($url !== null) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$id = data_get($artwork, 'id');
|
||||
|
||||
if (! filled($id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slug = $this->clean((string) (data_get($artwork, 'slug') ?? ''));
|
||||
|
||||
return route('art.show', array_filter([
|
||||
'id' => $id,
|
||||
'slug' => $slug,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== ''));
|
||||
}
|
||||
|
||||
private function galleryImageFormat(?string $url): ?string
|
||||
{
|
||||
$path = parse_url((string) $url, PHP_URL_PATH);
|
||||
$extension = strtolower((string) pathinfo((string) $path, PATHINFO_EXTENSION));
|
||||
|
||||
return $extension !== '' ? 'image/' . $extension : null;
|
||||
}
|
||||
|
||||
private function galleryContentUrl(mixed $artwork, ?string $fallbackUrl): ?string
|
||||
{
|
||||
$raw = trim((string) (data_get($artwork, 'thumb_srcset') ?? ''));
|
||||
|
||||
if ($raw === '') {
|
||||
return $fallbackUrl;
|
||||
}
|
||||
|
||||
$candidates = collect(explode(',', $raw))
|
||||
->map(static function (string $candidate): array {
|
||||
$parts = preg_split('/\s+/', trim($candidate)) ?: [];
|
||||
$url = $parts[0] ?? null;
|
||||
$descriptor = $parts[1] ?? null;
|
||||
$width = is_string($descriptor) && preg_match('/^(\d+)w$/', $descriptor, $matches) === 1
|
||||
? (int) $matches[1]
|
||||
: 0;
|
||||
|
||||
return [
|
||||
'url' => $url,
|
||||
'width' => $width,
|
||||
];
|
||||
})
|
||||
->filter(fn (array $candidate): bool => filled($candidate['url']))
|
||||
->sortByDesc('width')
|
||||
->values();
|
||||
|
||||
$bestUrl = $this->normalizeUrl((string) ($candidates->first()['url'] ?? ''));
|
||||
|
||||
return $bestUrl ?? $fallbackUrl;
|
||||
}
|
||||
|
||||
private function galleryItemListOrder(string $sort): string
|
||||
{
|
||||
return $sort === 'oldest'
|
||||
? 'https://schema.org/ItemListOrderAscending'
|
||||
: 'https://schema.org/ItemListOrderDescending';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user