Files
SkinbaseNova/app/Support/Seo/SeoFactory.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,
],
];
}
}