$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|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 $data * @return array */ 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 $data * @return array|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, ], ]; } }