262 lines
11 KiB
PHP
262 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Seo;
|
|
|
|
use App\Models\Artwork;
|
|
use Illuminate\Pagination\AbstractPaginator;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
|
|
final class SeoFactory
|
|
{
|
|
/**
|
|
* @param array<string, mixed> $meta
|
|
*/
|
|
public function homepage(array $meta): SeoData
|
|
{
|
|
$description = trim((string) ($meta['description'] ?? config('seo.default_description')));
|
|
|
|
return SeoDataBuilder::make()
|
|
->title((string) ($meta['title'] ?? config('seo.default_title')))
|
|
->description($description)
|
|
->keywords($meta['keywords'] ?? null)
|
|
->canonical((string) ($meta['canonical'] ?? url('/')))
|
|
->og(type: 'website', image: $meta['og_image'] ?? null)
|
|
->addJsonLd([
|
|
'@context' => 'https://schema.org',
|
|
'@type' => 'WebSite',
|
|
'name' => (string) config('seo.site_name', 'Skinbase'),
|
|
'url' => url('/'),
|
|
'description' => $description,
|
|
'potentialAction' => [
|
|
'@type' => 'SearchAction',
|
|
'target' => url('/search') . '?q={search_term_string}',
|
|
'query-input' => 'required name=search_term_string',
|
|
],
|
|
])
|
|
->build();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array<string, mixed>|null> $thumbs
|
|
*/
|
|
public function artwork(Artwork $artwork, array $thumbs, string $canonical): 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');
|
|
$description = trim(strip_tags(html_entity_decode((string) ($artwork->description ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
|
|
$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();
|
|
|
|
return SeoDataBuilder::make()
|
|
->title(sprintf('%s by %s — %s', $title, $authorName, config('seo.site_name', 'Skinbase')))
|
|
->description($description)
|
|
->keywords($keywords)
|
|
->canonical($canonical)
|
|
->og(type: 'article', image: $image)
|
|
->addJsonLd(array_filter([
|
|
'@context' => 'https://schema.org',
|
|
'@type' => 'ImageObject',
|
|
'name' => $title,
|
|
'description' => $description,
|
|
'url' => $canonical,
|
|
'contentUrl' => $image,
|
|
'thumbnailUrl' => $thumbs['md']['url'] ?? $image,
|
|
'encodingFormat' => 'image/webp',
|
|
'width' => $thumbs['xl']['width'] ?? $thumbs['lg']['width'] ?? null,
|
|
'height' => $thumbs['xl']['height'] ?? $thumbs['lg']['height'] ?? null,
|
|
'author' => ['@type' => 'Person', 'name' => $authorName],
|
|
'datePublished' => optional($artwork->published_at)->toAtomString(),
|
|
'license' => $artwork->license_url,
|
|
'keywords' => $keywords !== [] ? $keywords : null,
|
|
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
|
|
->addJsonLd(array_filter([
|
|
'@context' => 'https://schema.org',
|
|
'@type' => 'CreativeWork',
|
|
'name' => $title,
|
|
'description' => $description,
|
|
'url' => $canonical,
|
|
'author' => ['@type' => 'Person', 'name' => $authorName],
|
|
'datePublished' => optional($artwork->published_at)->toAtomString(),
|
|
'license' => $artwork->license_url,
|
|
'keywords' => $keywords !== [] ? $keywords : null,
|
|
'image' => $image,
|
|
], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
|
|
->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));
|
|
|
|
return SeoDataBuilder::make()
|
|
->title($title)
|
|
->description($description)
|
|
->canonical($canonical)
|
|
->breadcrumbs($breadcrumbs)
|
|
->og(type: 'profile', image: $image)
|
|
->addJsonLd([
|
|
'@context' => 'https://schema.org',
|
|
'@type' => 'ProfilePage',
|
|
'name' => $title,
|
|
'description' => $description,
|
|
'url' => $canonical,
|
|
'mainEntity' => array_filter([
|
|
'@type' => 'Person',
|
|
'name' => $profileName,
|
|
'url' => $canonical,
|
|
'image' => $image,
|
|
], fn (mixed $value): bool => $value !== null && $value !== ''),
|
|
])
|
|
->build();
|
|
}
|
|
|
|
public function collectionListing(string $title, string $description, string $canonical, ?string $image = null, bool $indexable = true): SeoData
|
|
{
|
|
return SeoDataBuilder::make()
|
|
->title($title)
|
|
->description($description)
|
|
->canonical($canonical)
|
|
->indexable($indexable)
|
|
->og(type: 'website', image: $image)
|
|
->build();
|
|
}
|
|
|
|
public function collectionPage(string $title, string $description, string $canonical, ?string $image = null, bool $indexable = true): SeoData
|
|
{
|
|
return SeoDataBuilder::make()
|
|
->title($title)
|
|
->description($description)
|
|
->canonical($canonical)
|
|
->indexable($indexable)
|
|
->og(type: 'website', image: $image)
|
|
->build();
|
|
}
|
|
|
|
public function leaderboardPage(string $title, string $description, string $canonical): SeoData
|
|
{
|
|
return SeoDataBuilder::make()
|
|
->title($title)
|
|
->description($description)
|
|
->canonical($canonical)
|
|
->og(type: 'website')
|
|
->build();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function fromViewData(array $data): array
|
|
{
|
|
if (($data['seo'] ?? null) instanceof SeoData) {
|
|
return $data['seo']->toArray();
|
|
}
|
|
|
|
if (is_array($data['seo'] ?? null) && ($data['seo'] ?? []) !== []) {
|
|
return SeoDataBuilder::fromArray($data['seo'])->build()->toArray();
|
|
}
|
|
|
|
$attributes = [
|
|
'title' => $data['page_title'] ?? data_get($data, 'meta.title') ?? data_get($data, 'metaTitle'),
|
|
'description' => $data['page_meta_description'] ?? data_get($data, 'meta.description') ?? data_get($data, 'metaDescription'),
|
|
'keywords' => $data['page_meta_keywords'] ?? data_get($data, 'meta.keywords'),
|
|
'canonical' => $data['page_canonical'] ?? data_get($data, 'meta.canonical') ?? url()->current(),
|
|
'robots' => $data['page_robots'] ?? data_get($data, 'meta.robots'),
|
|
'prev' => $data['page_rel_prev'] ?? null,
|
|
'next' => $data['page_rel_next'] ?? null,
|
|
'breadcrumbs' => $data['breadcrumbs'] ?? [],
|
|
'structured_data' => Arr::wrap($data['structured_data'] ?? []),
|
|
'faq_schema' => Arr::wrap($data['faq_schema'] ?? []),
|
|
'og_type' => $data['seo_og_type'] ?? data_get($data, 'meta.og_type'),
|
|
'og_title' => $data['og_title'] ?? data_get($data, 'meta.og_title'),
|
|
'og_description' => $data['og_description'] ?? data_get($data, 'meta.og_description'),
|
|
'og_url' => $data['og_url'] ?? data_get($data, 'meta.og_url'),
|
|
'og_image' => $data['og_image']
|
|
?? $data['ogImage']
|
|
?? data_get($data, 'meta.og_image')
|
|
?? data_get($data, 'meta.ogImage')
|
|
?? data_get($data, 'props.hero.thumb_lg')
|
|
?? data_get($data, 'props.hero.thumb')
|
|
?? null,
|
|
'og_image_alt' => $data['og_image_alt'] ?? null,
|
|
];
|
|
|
|
$builder = SeoDataBuilder::fromArray($attributes);
|
|
|
|
if (($data['gallery_type'] ?? null) !== null) {
|
|
$builder->addJsonLd($this->gallerySchema($data));
|
|
}
|
|
|
|
return $builder->build()->toArray();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function gallerySchema(array $data): ?array
|
|
{
|
|
$artworks = $data['artworks'] ?? null;
|
|
if (! $artworks instanceof AbstractPaginator && ! $artworks instanceof Collection && ! is_array($artworks)) {
|
|
return null;
|
|
}
|
|
|
|
$items = $artworks instanceof AbstractPaginator
|
|
? collect($artworks->items())
|
|
: collect($artworks);
|
|
|
|
$itemListElement = $items
|
|
->take(12)
|
|
->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]);
|
|
}
|
|
}
|
|
|
|
if ($name === '' && ! filled($url)) {
|
|
return null;
|
|
}
|
|
|
|
return array_filter([
|
|
'@type' => 'ListItem',
|
|
'position' => $index + 1,
|
|
'name' => $name !== '' ? $name : null,
|
|
'url' => filled($url) ? url((string) $url) : null,
|
|
], fn (mixed $value): bool => $value !== null && $value !== '');
|
|
})
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
if ($itemListElement === []) {
|
|
return null;
|
|
}
|
|
|
|
$count = $artworks instanceof AbstractPaginator ? $artworks->total() : count($itemListElement);
|
|
|
|
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()),
|
|
'mainEntity' => [
|
|
'@type' => 'ItemList',
|
|
'numberOfItems' => $count,
|
|
'itemListElement' => $itemListElement,
|
|
],
|
|
];
|
|
}
|
|
} |