Implement creator studio and upload updates
This commit is contained in:
@@ -19,74 +19,11 @@
|
||||
$artworkData = $artworkData ?? [];
|
||||
$relatedItems = $relatedItems ?? [];
|
||||
$comments = $comments ?? [];
|
||||
$useUnifiedSeo = true;
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] }}</title>
|
||||
<meta name="description" content="{{ $meta['description'] }}">
|
||||
<link rel="canonical" href="{{ $meta['canonical'] }}">
|
||||
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:site_name" content="Skinbase">
|
||||
<meta property="og:title" content="{{ $meta['title'] }}">
|
||||
<meta property="og:description" content="{{ $meta['description'] }}">
|
||||
<meta property="og:url" content="{{ $meta['canonical'] }}">
|
||||
@if(!empty($meta['og_image']))
|
||||
<meta property="og:image" content="{{ $meta['og_image'] }}">
|
||||
<meta property="og:image:type" content="image/webp">
|
||||
@if(!empty($meta['og_width']))
|
||||
<meta property="og:image:width" content="{{ $meta['og_width'] }}">
|
||||
@endif
|
||||
@if(!empty($meta['og_height']))
|
||||
<meta property="og:image:height" content="{{ $meta['og_height'] }}">
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ $meta['title'] }}">
|
||||
<meta name="twitter:description" content="{{ $meta['description'] }}">
|
||||
@if(!empty($meta['og_image']))
|
||||
<meta name="twitter:image" content="{{ $meta['og_image'] }}">
|
||||
@endif
|
||||
|
||||
@php
|
||||
$authorName = $artwork->user?->name ?: $artwork->user?->username ?: null;
|
||||
$keywords = $artwork->tags->pluck('name')->filter()->unique()->values()->all();
|
||||
$license = $artwork->license_url ?? null;
|
||||
|
||||
$imageObject = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ImageObject',
|
||||
'name' => (string) $artwork->title,
|
||||
'description' => (string) ($artwork->description ?? ''),
|
||||
'url' => $meta['canonical'],
|
||||
'contentUrl' => $meta['og_image'] ?? null,
|
||||
'thumbnailUrl' => $presentMd['url'] ?? ($meta['og_image'] ?? null),
|
||||
'encodingFormat' => 'image/webp',
|
||||
'width' => !empty($meta['og_width']) ? (int) $meta['og_width'] : null,
|
||||
'height' => !empty($meta['og_height']) ? (int) $meta['og_height'] : null,
|
||||
'author' => $authorName ? ['@type' => 'Person', 'name' => $authorName] : null,
|
||||
'datePublished' => optional($artwork->published_at)->toAtomString(),
|
||||
'license' => $license,
|
||||
'keywords' => !empty($keywords) ? $keywords : null,
|
||||
];
|
||||
|
||||
$creativeWork = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CreativeWork',
|
||||
'name' => (string) $artwork->title,
|
||||
'description' => (string) ($artwork->description ?? ''),
|
||||
'url' => $meta['canonical'],
|
||||
'author' => $authorName ? ['@type' => 'Person', 'name' => $authorName] : null,
|
||||
'datePublished' => optional($artwork->published_at)->toAtomString(),
|
||||
'license' => $license,
|
||||
'keywords' => !empty($keywords) ? $keywords : null,
|
||||
'image' => $meta['og_image'] ?? null,
|
||||
];
|
||||
|
||||
$imageObject = array_filter($imageObject, static fn ($value) => $value !== null && $value !== '');
|
||||
$creativeWork = array_filter($creativeWork, static fn ($value) => $value !== null && $value !== '');
|
||||
|
||||
$preloadSrcset = ($presentMd['url'] ?? '') . ' 640w, ' . ($presentLg['url'] ?? '') . ' 1280w, ' . ($presentXl['url'] ?? '') . ' 1920w';
|
||||
@endphp
|
||||
|
||||
@@ -97,8 +34,6 @@
|
||||
imagesizes="(min-width: 1280px) 1200px, (min-width: 768px) 90vw, 100vw">
|
||||
@endif
|
||||
|
||||
<script type="application/ld+json">{!! json_encode($imageObject, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) !!}</script>
|
||||
<script type="application/ld+json">{!! json_encode($creativeWork, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) !!}</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] ?? 'Card Challenges - Skinbase Nova' }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.challenges') }}" />
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<section class="px-6 pt-8 md:px-10">
|
||||
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] ?? ($collection['name'] . ' - Nova Cards Collection - Skinbase Nova') }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] ?? $collection['public_url'] }}" />
|
||||
@if(!empty($meta['robots']))
|
||||
<meta name="robots" content="{{ $meta['robots'] }}" />
|
||||
@endif
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)
|
||||
->addJsonLd([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $collection['name'],
|
||||
@@ -24,9 +18,12 @@
|
||||
'name' => data_get($item, 'card.title'),
|
||||
'url' => data_get($item, 'card.public_url'),
|
||||
])->values()->all(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
@endpush
|
||||
])
|
||||
->build();
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@section('content')
|
||||
<section class="px-6 pt-8 md:px-10">
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] ?? 'Nova Cards - Skinbase Nova' }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.index') }}" />
|
||||
@if(!empty($meta['robots']))
|
||||
<meta name="robots" content="{{ $meta['robots'] }}" />
|
||||
@endif
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)
|
||||
->addJsonLd([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $meta['title'] ?? 'Nova Cards - Skinbase Nova',
|
||||
@@ -31,9 +25,12 @@
|
||||
],
|
||||
];
|
||||
})->values()->all(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
@endpush
|
||||
])
|
||||
->build();
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@section('content')
|
||||
<section class="px-6 pt-8 md:px-10">
|
||||
@@ -631,7 +628,7 @@
|
||||
@if(empty($cards))
|
||||
<div class="rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-14 text-center">
|
||||
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-[24px] border border-white/12 bg-white/[0.05] text-slate-400">
|
||||
<i class="fa-solid fa-rectangle-history-circle-user text-3xl"></i>
|
||||
<i class="fa-solid fa-id-card text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="mt-5 text-2xl font-semibold text-white">No public cards yet</h3>
|
||||
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-slate-300">As creators publish their Nova Cards, they will appear here with crawlable quote text and preview imagery.</p>
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)
|
||||
->addJsonLd([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $meta['title'] ?? 'Nova Card Lineage - Skinbase Nova',
|
||||
'description' => $meta['description'] ?? 'Trace the remix lineage for this Nova Card.',
|
||||
'url' => $meta['canonical'] ?? route('cards.lineage', ['slug' => $card['slug'], 'id' => $card['id']]),
|
||||
'mainEntity' => collect($familyCards ?? [])->map(fn ($familyCard) => [
|
||||
'@type' => 'CreativeWork',
|
||||
'name' => data_get($familyCard, 'title'),
|
||||
'url' => data_get($familyCard, 'public_url'),
|
||||
])->values()->all(),
|
||||
])
|
||||
->build();
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] ?? 'Nova Card Lineage - Skinbase Nova' }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.lineage', ['slug' => $card['slug'], 'id' => $card['id']]) }}" />
|
||||
@if(!empty($meta['robots']))
|
||||
<meta name="robots" content="{{ $meta['robots'] }}" />
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<section class="px-6 pt-8 md:px-10">
|
||||
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] ?? 'Nova Cards Resources - Skinbase Nova' }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.index') }}" />
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<section class="px-6 pt-8 md:px-10">
|
||||
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] ?? ($card['title'] . ' - Nova Cards - Skinbase Nova') }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] ?? $card['public_url'] }}" />
|
||||
@if(!empty($meta['robots']))
|
||||
<meta name="robots" content="{{ $meta['robots'] }}" />
|
||||
@endif
|
||||
@if(!empty($card['og_preview_url']))
|
||||
<meta property="og:image" content="{{ $card['og_preview_url'] }}" />
|
||||
@endif
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)
|
||||
->og(
|
||||
type: 'article',
|
||||
title: $meta['title'] ?? ($card['title'] . ' - Nova Cards - Skinbase Nova'),
|
||||
description: $meta['description'] ?? $card['quote_text'],
|
||||
url: $meta['canonical'] ?? $card['public_url'],
|
||||
image: $card['og_preview_url'] ?? $card['preview_url'] ?? null,
|
||||
)
|
||||
->addJsonLd([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CreativeWork',
|
||||
'name' => $card['title'],
|
||||
@@ -34,9 +32,12 @@
|
||||
'name' => config('app.name'),
|
||||
'url' => url('/'),
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
@endpush
|
||||
])
|
||||
->build();
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'] ?? '')
|
||||
|
||||
@section('content')
|
||||
<section class="px-6 pt-8 md:px-10">
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
@param \Illuminate\Support\Collection $breadcrumbs
|
||||
Collection of objects with ->name and ->url properties.
|
||||
--}}
|
||||
@if(isset($breadcrumbs) && $breadcrumbs->isNotEmpty())
|
||||
@php
|
||||
$breadcrumbTrail = \App\Support\Seo\BreadcrumbTrail::normalize($breadcrumbs ?? collect());
|
||||
@endphp
|
||||
@if($breadcrumbTrail !== [])
|
||||
<nav class="flex items-center gap-1.5 flex-wrap text-sm text-neutral-400" aria-label="Breadcrumb">
|
||||
<a class="hover:text-white transition-colors" href="/">Home</a>
|
||||
@foreach($breadcrumbs as $crumb)
|
||||
<span class="opacity-40" aria-hidden="true">›</span>
|
||||
@foreach($breadcrumbTrail as $crumb)
|
||||
@if(!$loop->first)
|
||||
<span class="opacity-40" aria-hidden="true">›</span>
|
||||
@endif
|
||||
@if(!$loop->last)
|
||||
<a class="hover:text-white transition-colors" href="{{ $crumb->url }}">{{ $crumb->name }}</a>
|
||||
<a class="hover:text-white transition-colors" href="{{ $crumb['url'] }}">{{ $crumb['name'] }}</a>
|
||||
@else
|
||||
<span class="text-white/70">{{ $crumb->name }}</span>
|
||||
<span class="text-white/70" aria-current="page">{{ $crumb['name'] }}</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
@php
|
||||
use App\Banner;
|
||||
$useUnifiedSeo = true;
|
||||
@endphp
|
||||
|
||||
@php
|
||||
@@ -16,66 +17,12 @@
|
||||
? $artworks->nextPageUrl() : null;
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
<link rel="canonical" href="{{ $seoUrl($seoPage) }}">
|
||||
@if($seoPrev)<link rel="prev" href="{{ $seoPrev }}">@endif
|
||||
@if($seoNext)<link rel="next" href="{{ $seoNext }}">@endif
|
||||
<meta name="robots" content="index,follow">
|
||||
{{-- OpenGraph --}}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ $page_canonical ?? $seoUrl(1) }}" />
|
||||
<meta property="og:title" content="{{ $page_title ?? ($hero_title ?? 'Skinbase') }}" />
|
||||
<meta property="og:description" content="{{ $page_meta_description ?? '' }}" />
|
||||
<meta property="og:site_name" content="Skinbase" />
|
||||
{{-- Twitter card --}}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="{{ $page_title ?? ($hero_title ?? 'Skinbase') }}" />
|
||||
<meta name="twitter:description" content="{{ $page_meta_description ?? '' }}" />
|
||||
|
||||
{{-- Breadcrumb structured data --}}
|
||||
@if(isset($breadcrumbs) && $breadcrumbs->isNotEmpty())
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'BreadcrumbList',
|
||||
'itemListElement' => $breadcrumbs->values()->map(fn ($crumb, $i) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $i + 1,
|
||||
'name' => $crumb->name,
|
||||
'item' => url($crumb->url),
|
||||
])->all(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
||||
</script>
|
||||
@endif
|
||||
|
||||
@if(($gallery_type ?? null) === 'tag' && !empty($tag_context))
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $page_title ?? ($hero_title ?? ('#' . ($tag_context['name'] ?? 'Tag'))),
|
||||
'description' => $page_meta_description ?? ($hero_description ?? null),
|
||||
'url' => $page_canonical ?? $seoUrl($seoPage),
|
||||
'mainEntity' => [
|
||||
'@type' => 'ItemList',
|
||||
'numberOfItems' => method_exists($artworks, 'total') ? $artworks->total() : count($artworks ?? []),
|
||||
'itemListElement' => collect(method_exists($artworks, 'getCollection') ? $artworks->getCollection() : ($artworks ?? []))
|
||||
->take(12)
|
||||
->values()
|
||||
->map(fn ($artwork, $index) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'url' => !empty($artwork->slug) ? url('/' . $artwork->slug) : null,
|
||||
'name' => $artwork->name ?? null,
|
||||
])
|
||||
->filter(fn (array $item) => filled($item['url']) || filled($item['name']))
|
||||
->values()
|
||||
->all(),
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
||||
</script>
|
||||
@endif
|
||||
@endpush
|
||||
@php
|
||||
$page_canonical = $page_canonical ?? $seoUrl($seoPage);
|
||||
$page_rel_prev = $page_rel_prev ?? $seoPrev;
|
||||
$page_rel_next = $page_rel_next ?? $seoNext;
|
||||
$page_robots = $page_robots ?? 'index,follow';
|
||||
@endphp
|
||||
|
||||
@php
|
||||
// ── Rank API endpoint ────────────────────────────────────────────────────
|
||||
|
||||
352
resources/views/gallery/similar.blade.php
Normal file
352
resources/views/gallery/similar.blade.php
Normal file
@@ -0,0 +1,352 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
use App\Banner;
|
||||
$src = $sourceArtwork;
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
<title>{{ $page_title ?? 'Similar Artworks — Skinbase' }}</title>
|
||||
<meta name="description" content="{{ $page_meta_description ?? '' }}">
|
||||
<link rel="canonical" href="{{ $page_canonical ?? url()->current() }}">
|
||||
<meta name="robots" content="{{ $page_robots ?? 'noindex,follow' }}">
|
||||
|
||||
{{-- OpenGraph --}}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ $page_canonical ?? url()->current() }}" />
|
||||
<meta property="og:title" content="{{ $page_title ?? 'Similar Artworks' }}" />
|
||||
<meta property="og:description" content="{{ $page_meta_description ?? '' }}" />
|
||||
@if(!empty($src->thumb_lg))
|
||||
<meta property="og:image" content="{{ $src->thumb_lg }}" />
|
||||
@endif
|
||||
<meta property="og:site_name" content="Skinbase" />
|
||||
|
||||
{{-- Twitter card --}}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="{{ $page_title ?? 'Similar Artworks' }}" />
|
||||
<meta name="twitter:description" content="{{ $page_meta_description ?? '' }}" />
|
||||
@if(!empty($src->thumb_lg))
|
||||
<meta name="twitter:image" content="{{ $src->thumb_lg }}" />
|
||||
@endif
|
||||
|
||||
{{-- Breadcrumb structured data --}}
|
||||
@if(isset($breadcrumbs) && $breadcrumbs->isNotEmpty())
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'BreadcrumbList',
|
||||
'itemListElement' => $breadcrumbs->values()->map(fn ($crumb, $i) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $i + 1,
|
||||
'name' => $crumb->name,
|
||||
'item' => url($crumb->url),
|
||||
])->all(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
||||
</script>
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid legacy-page">
|
||||
@php Banner::ShowResponsiveAd(); @endphp
|
||||
|
||||
$useUnifiedSeo = true;
|
||||
<div class="pt-0">
|
||||
|
||||
{{-- Source info --}}
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<h1 class="text-2xl font-bold leading-tight text-white md:text-3xl">
|
||||
Artworks similar to
|
||||
<a href="{{ $src->url }}" class="underline decoration-white/20 underline-offset-4 transition hover:decoration-sky-400 focus-visible:outline-none">{{ $src->title }}</a>
|
||||
</h1>
|
||||
|
||||
{{-- Author & category --}}
|
||||
<div class="flex flex-wrap items-center gap-3 text-sm text-white/50">
|
||||
@if(!empty($src->author_name))
|
||||
<span class="flex items-center gap-2">
|
||||
@if(!empty($src->author_avatar))
|
||||
<img src="{{ $src->author_avatar }}"
|
||||
alt="{{ $src->author_name }}"
|
||||
class="h-5 w-5 rounded-full object-cover ring-1 ring-white/20"
|
||||
onerror="this.style.display='none'">
|
||||
@endif
|
||||
<a href="/{{ $src->author_username }}" class="font-medium text-white/70 hover:text-white transition">{{ $src->author_name }}</a>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if(!empty($src->category_name))
|
||||
<span class="inline-flex items-center gap-1 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-xs font-medium text-white/60">
|
||||
{{ $src->category_name }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if(!empty($src->content_type_name))
|
||||
<span class="inline-flex items-center gap-1 rounded-full border border-sky-400/20 bg-sky-400/[0.08] px-2.5 py-0.5 text-xs font-medium text-sky-300">
|
||||
{{ $src->content_type_name }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Tags --}}
|
||||
@if(!empty($src->tag_slugs))
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach($src->tag_slugs as $tagSlug)
|
||||
<a href="{{ route('tags.show', $tagSlug) }}"
|
||||
class="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-0.5 text-xs text-white/50 transition hover:border-sky-400/30 hover:bg-sky-400/[0.07] hover:text-white/80">
|
||||
#{{ $tagSlug }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="flex items-center gap-3 pt-1">
|
||||
<a href="{{ $src->url }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/60">
|
||||
<svg class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Back to artwork
|
||||
</a>
|
||||
|
||||
@if(!empty($src->content_type_slug))
|
||||
<a href="/{{ $src->content_type_slug }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.08] hover:text-white/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/60">
|
||||
Browse {{ $src->content_type_name ?: 'artworks' }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════════ --}}
|
||||
{{-- RESULTS SECTION (loaded asynchronously) --}}
|
||||
{{-- ══════════════════════════════════════════════════════════════ --}}
|
||||
<section class="px-6 pb-10 pt-8 md:px-10" id="similar-results-section" data-artwork-id="{{ $src->id }}">
|
||||
|
||||
{{-- Section heading --}}
|
||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold text-white">
|
||||
<span class="mr-1">✨</span> Similar Artworks
|
||||
<span id="similar-results-count" class="ml-2 text-sm font-normal text-white/40" style="display:none;"></span>
|
||||
</h2>
|
||||
<span id="similar-source-badge"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Loading skeleton --}}
|
||||
<div id="similar-loading-skeleton">
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5">
|
||||
@for($i = 0; $i < 12; $i++)
|
||||
<div class="animate-pulse">
|
||||
<div class="aspect-[3/4] rounded-2xl bg-white/[0.06]"></div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<div class="h-5 w-5 rounded-full bg-white/[0.06]"></div>
|
||||
<div class="h-3 w-24 rounded bg-white/[0.06]"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
<p class="mt-6 text-center text-sm text-white/30">
|
||||
<svg class="mr-2 inline-block h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
||||
</svg>
|
||||
Finding similar artworks…
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Error state (hidden by default) --}}
|
||||
<div id="similar-error-state" style="display:none;">
|
||||
<div class="flex flex-col items-center justify-center rounded-2xl border border-rose-400/20 bg-rose-400/[0.06] px-8 py-16 text-center">
|
||||
<div class="mb-4 rounded-full border border-rose-300/20 bg-rose-500/10 p-5">
|
||||
<svg class="h-8 w-8 text-rose-300/60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-white">Could not load similar artworks</h3>
|
||||
<p class="mt-2 max-w-sm text-sm text-white/50">Something went wrong. Please try again.</p>
|
||||
<button type="button" id="similar-retry-btn"
|
||||
class="mt-5 inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Empty state (hidden by default) --}}
|
||||
<div id="similar-empty-state" style="display:none;">
|
||||
<div class="flex flex-col items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.03] px-8 py-16 text-center">
|
||||
<div class="mb-4 rounded-full border border-white/10 bg-white/[0.05] p-5">
|
||||
<svg class="h-8 w-8 text-white/30" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-white">No similar artworks found yet</h3>
|
||||
<p class="mt-2 max-w-sm text-sm text-white/50">
|
||||
We could not find strong matches for this artwork right now. Try browsing the full gallery or check back later.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap justify-center gap-3">
|
||||
<a href="{{ $sourceArtwork->url }}"
|
||||
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">
|
||||
← Back to artwork
|
||||
</a>
|
||||
<a href="/explore"
|
||||
class="inline-flex items-center gap-2 rounded-xl border border-sky-400/20 bg-sky-400/[0.08] px-4 py-2.5 text-sm font-medium text-sky-300 transition hover:bg-sky-400/[0.14] hover:text-sky-200">
|
||||
Browse Explore
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Masonry grid mount point (hidden until loaded) --}}
|
||||
<div id="similar-masonry-mount" style="display:none;"
|
||||
data-gallery-type="similar"
|
||||
data-limit="24"
|
||||
class="min-h-32"
|
||||
></div>
|
||||
|
||||
{{-- Pagination (hidden until loaded) --}}
|
||||
<div id="similar-pagination" style="display:none;" class="mt-10 flex items-center justify-center gap-3"></div>
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@vite('resources/js/entry-masonry-gallery.jsx')
|
||||
<script>
|
||||
(function () {
|
||||
const section = document.getElementById('similar-results-section');
|
||||
if (!section) return;
|
||||
|
||||
const artworkId = section.dataset.artworkId;
|
||||
const skeleton = document.getElementById('similar-loading-skeleton');
|
||||
const errorState = document.getElementById('similar-error-state');
|
||||
const emptyState = document.getElementById('similar-empty-state');
|
||||
const masonryMount = document.getElementById('similar-masonry-mount');
|
||||
const pagination = document.getElementById('similar-pagination');
|
||||
const countEl = document.getElementById('similar-results-count');
|
||||
const badgeEl = document.getElementById('similar-source-badge');
|
||||
const retryBtn = document.getElementById('similar-retry-btn');
|
||||
|
||||
function showOnly(el) {
|
||||
[skeleton, errorState, emptyState, masonryMount, pagination].forEach(function (node) {
|
||||
if (node) node.style.display = 'none';
|
||||
});
|
||||
if (el) el.style.display = '';
|
||||
}
|
||||
|
||||
function renderSourceBadge(source) {
|
||||
if (!badgeEl) return;
|
||||
var badges = {
|
||||
visual: { classes: 'border-violet-400/25 bg-violet-400/[0.08] text-violet-300', dot: 'bg-violet-400', text: 'Visual similarity' },
|
||||
hybrid: { classes: 'border-emerald-400/20 bg-emerald-400/[0.08] text-emerald-300', dot: 'bg-emerald-400', text: 'AI similarity' },
|
||||
tags: { classes: 'border-white/[0.08] bg-white/[0.04] text-white/40', dot: '', text: 'Tag match' },
|
||||
};
|
||||
var b = badges[source] || badges.tags;
|
||||
badgeEl.innerHTML = '<span class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] ' + b.classes + '">'
|
||||
+ (b.dot ? '<span class="h-1.5 w-1.5 rounded-full ' + b.dot + '"></span>' : '')
|
||||
+ b.text + '</span>';
|
||||
}
|
||||
|
||||
function renderPagination(data) {
|
||||
if (!pagination) return;
|
||||
if (data.last_page <= 1) return;
|
||||
|
||||
var html = '';
|
||||
|
||||
if (data.current_page <= 1) {
|
||||
html += '<span class="inline-flex items-center gap-2 rounded-xl border border-white/[0.06] bg-white/[0.03] px-4 py-2.5 text-sm text-white/30 cursor-default select-none">'
|
||||
+ '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg>'
|
||||
+ 'Previous</span>';
|
||||
} else {
|
||||
html += '<a href="' + data.prev_page_url + '" class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">'
|
||||
+ '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg>'
|
||||
+ 'Previous</a>';
|
||||
}
|
||||
|
||||
html += '<span class="text-sm text-white/40">Page ' + data.current_page + ' of ' + data.last_page + '</span>';
|
||||
|
||||
if (data.current_page >= data.last_page) {
|
||||
html += '<span class="inline-flex items-center gap-2 rounded-xl border border-white/[0.06] bg-white/[0.03] px-4 py-2.5 text-sm text-white/30 cursor-default select-none">'
|
||||
+ 'Next<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg></span>';
|
||||
} else {
|
||||
html += '<a href="' + data.next_page_url + '" class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">'
|
||||
+ 'Next<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg></a>';
|
||||
}
|
||||
|
||||
pagination.innerHTML = html;
|
||||
pagination.style.display = '';
|
||||
}
|
||||
|
||||
function loadResults(page) {
|
||||
showOnly(skeleton);
|
||||
if (pagination) pagination.style.display = 'none';
|
||||
|
||||
var url = '/art/' + encodeURIComponent(artworkId) + '/similar-results';
|
||||
if (page && page > 1) url += '?page=' + page;
|
||||
|
||||
fetch(url, { headers: { Accept: 'application/json' }, credentials: 'same-origin' })
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
if (!json.data || json.data.length === 0) {
|
||||
showOnly(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
renderSourceBadge(json.similarity_source || 'tags');
|
||||
|
||||
if (countEl) {
|
||||
countEl.textContent = '(' + Number(json.total).toLocaleString() + ')';
|
||||
countEl.style.display = '';
|
||||
}
|
||||
|
||||
// Hydrate the masonry gallery mount
|
||||
masonryMount.setAttribute('data-react-masonry-gallery', '');
|
||||
masonryMount.setAttribute('data-artworks', JSON.stringify(json.data));
|
||||
if (json.next_page_url) {
|
||||
masonryMount.setAttribute('data-next-page-url', json.next_page_url);
|
||||
} else {
|
||||
masonryMount.removeAttribute('data-next-page-url');
|
||||
}
|
||||
showOnly(masonryMount);
|
||||
|
||||
// Re-trigger React masonry hydration
|
||||
if (window.__hydrateMasonryGalleries) {
|
||||
window.__hydrateMasonryGalleries();
|
||||
} else {
|
||||
// Dispatch a custom event as fallback
|
||||
window.dispatchEvent(new CustomEvent('masonry-gallery:hydrate'));
|
||||
}
|
||||
|
||||
renderPagination(json);
|
||||
})
|
||||
.catch(function () {
|
||||
showOnly(errorState);
|
||||
});
|
||||
}
|
||||
|
||||
if (retryBtn) {
|
||||
retryBtn.addEventListener('click', function () {
|
||||
loadResults(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Parse page from URL if present
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var initialPage = parseInt(urlParams.get('page'), 10) || 1;
|
||||
loadResults(initialPage);
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@@ -1,15 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ app()->getLocale() }}">
|
||||
<head>
|
||||
<title>{{ $page_title ?? 'Skinbase' }}</title>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="{{ $page_meta_description ?? '' }}">
|
||||
<meta name="keywords" content="{{ $page_meta_keywords ?? '' }}">
|
||||
@isset($page_canonical)
|
||||
<link rel="canonical" href="{{ $page_canonical }}" />
|
||||
@endisset
|
||||
@if($useUnifiedSeo ?? false)
|
||||
@include('partials.seo.head', ['seo' => $seo ?? null])
|
||||
@else
|
||||
<title>{{ $page_title ?? 'Skinbase' }}</title>
|
||||
<meta name="description" content="{{ $page_meta_description ?? '' }}">
|
||||
<meta name="keywords" content="{{ $page_meta_keywords ?? '' }}">
|
||||
@isset($page_canonical)
|
||||
<link rel="canonical" href="{{ $page_canonical }}" />
|
||||
@endisset
|
||||
@endif
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@php
|
||||
$gridVersion = request()->query('grid') === 'v2' ? 'v2' : 'v1';
|
||||
$deferToolbarSearch = request()->routeIs('index');
|
||||
$metaDescription = trim($__env->yieldContent('meta-description', $page_meta_description ?? ''));
|
||||
$metaKeywords = trim($__env->yieldContent('meta-keywords', $page_meta_keywords ?? ''));
|
||||
$isInertiaPage = isset($page) && is_array($page);
|
||||
$shouldRenderBladeSeo = ($useUnifiedSeo ?? false) && (($renderBladeSeo ?? false) || ! $isInertiaPage);
|
||||
$novaViteEntries = [
|
||||
'resources/css/app.css',
|
||||
'resources/css/nova-grid.css',
|
||||
@@ -17,25 +17,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ app()->getLocale() }}" data-grid-version="{{ $gridVersion }}">
|
||||
<head>
|
||||
<title>{{ $page_title ?? 'Skinbase' }}</title>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="description" content="{{ $metaDescription }}">
|
||||
<meta name="keywords" content="{{ $metaKeywords }}">
|
||||
@isset($page_robots)
|
||||
<meta name="robots" content="{{ $page_robots }}" />
|
||||
@endisset
|
||||
@isset($page_canonical)
|
||||
<link rel="canonical" href="{{ $page_canonical }}" />
|
||||
@endisset
|
||||
@isset($page_rel_prev)
|
||||
<link rel="prev" href="{{ $page_rel_prev }}" />
|
||||
@endisset
|
||||
@isset($page_rel_next)
|
||||
<link rel="next" href="{{ $page_rel_next }}" />
|
||||
@endisset
|
||||
@if($shouldRenderBladeSeo)
|
||||
@include('partials.seo.head', ['seo' => $seo ?? null])
|
||||
@endif
|
||||
|
||||
{{-- Global RSS feed discovery --}}
|
||||
<link rel="alternate" type="application/rss+xml" title="Skinbase Latest Artworks" href="{{ url('/rss') }}">
|
||||
@@ -105,7 +92,7 @@
|
||||
</script>
|
||||
@endif
|
||||
|
||||
@if(isset($page) && is_array($page))
|
||||
@if($isInertiaPage)
|
||||
@inertiaHead
|
||||
@endif
|
||||
|
||||
|
||||
@@ -7,36 +7,9 @@
|
||||
$breadcrumbs (collection, optional)
|
||||
Content via @yield('page-content')
|
||||
--}}
|
||||
@php($useUnifiedSeo = true)
|
||||
@extends('layouts.nova')
|
||||
|
||||
@push('head')
|
||||
@isset($page_canonical)
|
||||
<link rel="canonical" href="{{ $page_canonical }}" />
|
||||
@endisset
|
||||
<meta name="robots" content="{{ $page_robots ?? 'index,follow' }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ $page_canonical ?? url()->current() }}" />
|
||||
<meta property="og:title" content="{{ $page_title ?? 'Skinbase' }}" />
|
||||
<meta property="og:description" content="{{ $page_meta_description ?? '' }}" />
|
||||
<meta property="og:site_name" content="Skinbase" />
|
||||
|
||||
{{-- Breadcrumb structured data --}}
|
||||
@if(isset($breadcrumbs) && $breadcrumbs->isNotEmpty())
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'BreadcrumbList',
|
||||
'itemListElement' => $breadcrumbs->values()->map(fn ($crumb, $i) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $i + 1,
|
||||
'name' => $crumb->name,
|
||||
'item' => url($crumb->url),
|
||||
])->all(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
||||
</script>
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Minimal hero --}}
|
||||
|
||||
@@ -6,20 +6,9 @@
|
||||
$page_title, $description, $icon, $section
|
||||
Content via @yield('discover-content')
|
||||
--}}
|
||||
@php($useUnifiedSeo = true)
|
||||
@extends('layouts.nova')
|
||||
|
||||
@push('head')
|
||||
@isset($page_canonical)
|
||||
<link rel="canonical" href="{{ $page_canonical }}" />
|
||||
@endisset
|
||||
<meta name="robots" content="{{ $page_robots ?? 'index,follow' }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ $page_canonical ?? url()->current() }}" />
|
||||
<meta property="og:title" content="{{ $page_title ?? 'Discover — Skinbase' }}" />
|
||||
<meta property="og:description" content="{{ $description ?? '' }}" />
|
||||
<meta property="og:site_name" content="Skinbase" />
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Compact header --}}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
$contentTypes (collection, optional), $activeType (string, optional)
|
||||
$page_title, $page_meta_description, $page_canonical, $page_robots
|
||||
--}}
|
||||
@php($useUnifiedSeo = true)
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
@@ -24,33 +25,12 @@
|
||||
? $artworks->nextPageUrl() : null;
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
<link rel="canonical" href="{{ $page_canonical ?? $seoUrl($seoPage) }}">
|
||||
@if($seoPrev)<link rel="prev" href="{{ $seoPrev }}">@endif
|
||||
@if($seoNext)<link rel="next" href="{{ $seoNext }}">@endif
|
||||
<meta name="robots" content="{{ $page_robots ?? 'index,follow' }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ $page_canonical ?? $seoUrl(1) }}" />
|
||||
<meta property="og:title" content="{{ $page_title ?? ($hero_title ?? 'Skinbase') }}" />
|
||||
<meta property="og:description" content="{{ $page_meta_description ?? '' }}" />
|
||||
<meta property="og:site_name" content="Skinbase" />
|
||||
|
||||
{{-- Breadcrumb structured data --}}
|
||||
@if(isset($breadcrumbs) && $breadcrumbs->isNotEmpty())
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'BreadcrumbList',
|
||||
'itemListElement' => $breadcrumbs->values()->map(fn ($crumb, $i) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $i + 1,
|
||||
'name' => $crumb->name,
|
||||
'item' => url($crumb->url),
|
||||
])->all(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
||||
</script>
|
||||
@endif
|
||||
@endpush
|
||||
@php
|
||||
$page_canonical = $page_canonical ?? $seoUrl($seoPage);
|
||||
$page_rel_prev = $page_rel_prev ?? $seoPrev;
|
||||
$page_rel_next = $page_rel_next ?? $seoNext;
|
||||
$page_robots = $page_robots ?? 'index,follow';
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid legacy-page">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@php($useUnifiedSeo = true)
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
@php
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
|
||||
'title' => $article->meta_title ?: $article->title,
|
||||
'description' => $article->meta_description ?: Str::limit(strip_tags((string) $article->excerpt), 160),
|
||||
'keywords' => $article->meta_keywords,
|
||||
'canonical' => route('news.show', $article->slug),
|
||||
'robots' => 'index,follow',
|
||||
'og_type' => 'article',
|
||||
'og_title' => $article->effective_og_title,
|
||||
'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')],
|
||||
$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',
|
||||
'headline' => $article->title,
|
||||
'description' => $article->meta_description ?: Str::limit(strip_tags((string) $article->excerpt), 160),
|
||||
'image' => $article->effective_og_image,
|
||||
'datePublished' => $article->published_at?->toIso8601String(),
|
||||
'dateModified' => $article->updated_at?->toIso8601String(),
|
||||
'author' => array_filter([
|
||||
'@type' => 'Person',
|
||||
'name' => $article->author?->name,
|
||||
]),
|
||||
'mainEntityOfPage' => route('news.show', $article->slug),
|
||||
], fn (mixed $value): bool => $value !== null && $value !== ''))
|
||||
->build();
|
||||
@endphp
|
||||
|
||||
@extends('news.layout', [
|
||||
'metaTitle' => $article->meta_title ?: $article->title,
|
||||
'metaDescription' => $article->meta_description ?: Str::limit(strip_tags((string)$article->excerpt), 160),
|
||||
@@ -17,21 +54,6 @@
|
||||
])->filter()->values();
|
||||
@endphp
|
||||
|
||||
{{-- OpenGraph meta --}}
|
||||
@push('head')
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content="{{ $article->effective_og_title }}">
|
||||
<meta property="og:description" content="{{ $article->effective_og_description }}">
|
||||
@if($article->effective_og_image)
|
||||
<meta property="og:image" content="{{ $article->effective_og_image }}">
|
||||
@endif
|
||||
<meta property="article:published_time" content="{{ $article->published_at?->toIso8601String() }}">
|
||||
<meta property="article:author" content="{{ $article->author?->name }}">
|
||||
@if($article->meta_keywords)
|
||||
<meta name="keywords" content="{{ $article->meta_keywords }}">
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
<x-nova-page-header
|
||||
section="Community"
|
||||
:title="$article->title"
|
||||
|
||||
34
resources/views/nova-cards/render-frame.blade.php
Normal file
34
resources/views/nova-cards/render-frame.blade.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Nova Card Render Frame</title>
|
||||
<link rel="preload" href="/fonts/nova-cards/inter-400-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/nova-cards/playfair-display-600-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/nova-cards/anton-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/nova-cards/caveat-400-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/nova-cards/libre-franklin-400-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/nova-cards/cormorant-garamond-500-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<script>
|
||||
window.__NOVA_CARD__ = {!! json_encode($cardData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) !!};
|
||||
window.__NOVA_CARD_FONTS__ = {!! json_encode($fonts, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) !!};
|
||||
</script>
|
||||
@vite(['resources/css/app.css', 'resources/js/render-frame.jsx'])
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
width: {{ $width }}px;
|
||||
height: {{ $height }}px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
#card-render-mount {
|
||||
width: {{ $width }}px;
|
||||
height: {{ $height }}px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="card-render-mount"></div>
|
||||
</body>
|
||||
</html>
|
||||
61
resources/views/partials/seo/head.blade.php
Normal file
61
resources/views/partials/seo/head.blade.php
Normal file
@@ -0,0 +1,61 @@
|
||||
@php
|
||||
$resolvedSeo = ($seo ?? null) instanceof \App\Support\Seo\SeoData
|
||||
? $seo->toArray()
|
||||
: (is_array($seo ?? null)
|
||||
? $seo
|
||||
: app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars()));
|
||||
|
||||
$jsonLdEntries = collect($resolvedSeo['json_ld'] ?? [])->filter(fn ($schema) => is_array($schema) && $schema !== [])->values();
|
||||
@endphp
|
||||
<title>{{ $resolvedSeo['title'] ?? config('seo.default_title', 'Skinbase') }}</title>
|
||||
@if(!empty($resolvedSeo['description']))
|
||||
<meta name="description" content="{{ $resolvedSeo['description'] }}" />
|
||||
@endif
|
||||
@if(config('seo.keywords_enabled', true) && !empty($resolvedSeo['keywords']))
|
||||
<meta name="keywords" content="{{ $resolvedSeo['keywords'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['robots']))
|
||||
<meta name="robots" content="{{ $resolvedSeo['robots'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['canonical']))
|
||||
<link rel="canonical" href="{{ $resolvedSeo['canonical'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['prev']))
|
||||
<link rel="prev" href="{{ $resolvedSeo['prev'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['next']))
|
||||
<link rel="next" href="{{ $resolvedSeo['next'] }}" />
|
||||
@endif
|
||||
|
||||
<meta property="og:site_name" content="{{ $resolvedSeo['og_site_name'] ?? config('seo.site_name', 'Skinbase') }}" />
|
||||
<meta property="og:type" content="{{ $resolvedSeo['og_type'] ?? 'website' }}" />
|
||||
@if(!empty($resolvedSeo['og_title']))
|
||||
<meta property="og:title" content="{{ $resolvedSeo['og_title'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['og_description']))
|
||||
<meta property="og:description" content="{{ $resolvedSeo['og_description'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['og_url']))
|
||||
<meta property="og:url" content="{{ $resolvedSeo['og_url'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['og_image']))
|
||||
<meta property="og:image" content="{{ $resolvedSeo['og_image'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['og_image_alt']))
|
||||
<meta property="og:image:alt" content="{{ $resolvedSeo['og_image_alt'] }}" />
|
||||
@endif
|
||||
|
||||
<meta name="twitter:card" content="{{ $resolvedSeo['twitter_card'] ?? config('seo.twitter_card', 'summary_large_image') }}" />
|
||||
@if(!empty($resolvedSeo['twitter_title']))
|
||||
<meta name="twitter:title" content="{{ $resolvedSeo['twitter_title'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['twitter_description']))
|
||||
<meta name="twitter:description" content="{{ $resolvedSeo['twitter_description'] }}" />
|
||||
@endif
|
||||
@if(!empty($resolvedSeo['twitter_image']))
|
||||
<meta name="twitter:image" content="{{ $resolvedSeo['twitter_image'] }}" />
|
||||
@endif
|
||||
|
||||
@foreach($jsonLdEntries as $schema)
|
||||
<script type="application/ld+json">{!! json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) !!}</script>
|
||||
@endforeach
|
||||
@@ -1,13 +1,13 @@
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$renderBladeSeo = true;
|
||||
@endphp
|
||||
|
||||
@extends('layouts.nova')
|
||||
|
||||
@push('head')
|
||||
@vite(['resources/js/profile.jsx'])
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||
{{-- OG image (not in nova base layout) --}}
|
||||
@if(!empty($og_image))
|
||||
<meta property="og:image" content="{{ $og_image }}">
|
||||
@endif
|
||||
<meta property="og:url" content="{{ $page_canonical ?? url()->current() }}">
|
||||
<style>
|
||||
/* Ensure profile tab bar does not hide behind the main navbar */
|
||||
.profile-tabs-sticky {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@push('head')
|
||||
<meta name="robots" content="noindex,follow">
|
||||
<meta name="description" content="Search Skinbase artworks, photography, wallpapers and skins.">
|
||||
@endpush
|
||||
@php($useUnifiedSeo = true)
|
||||
|
||||
@section('content')
|
||||
<div class="px-6 pt-10 pb-8 md:px-10" id="search-page" data-q="{{ $q ?? '' }}">
|
||||
|
||||
11
resources/views/sitemaps/index.blade.php
Normal file
11
resources/views/sitemaps/index.blade.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
@foreach ($items as $item)
|
||||
<sitemap>
|
||||
<loc>{{ e($item->loc) }}</loc>
|
||||
@if ($item->lastModified)
|
||||
<lastmod>{{ $item->lastModified->format(DATE_ATOM) }}</lastmod>
|
||||
@endif
|
||||
</sitemap>
|
||||
@endforeach
|
||||
</sitemapindex>
|
||||
16
resources/views/sitemaps/news-urlset.blade.php
Normal file
16
resources/views/sitemaps/news-urlset.blade.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
|
||||
@foreach ($items as $item)
|
||||
<url>
|
||||
<loc>{{ e($item->loc) }}</loc>
|
||||
<news:news>
|
||||
<news:publication>
|
||||
<news:name>{{ e($item->publicationName) }}</news:name>
|
||||
<news:language>{{ e($item->publicationLanguage) }}</news:language>
|
||||
</news:publication>
|
||||
<news:publication_date>{{ $item->publicationDate->format(DATE_ATOM) }}</news:publication_date>
|
||||
<news:title>{{ e($item->title) }}</news:title>
|
||||
</news:news>
|
||||
</url>
|
||||
@endforeach
|
||||
</urlset>
|
||||
19
resources/views/sitemaps/urlset.blade.php
Normal file
19
resources/views/sitemaps/urlset.blade.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"@if($hasImages) xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"@endif>
|
||||
@foreach ($items as $item)
|
||||
<url>
|
||||
<loc>{{ e($item->loc) }}</loc>
|
||||
@if ($item->lastModified)
|
||||
<lastmod>{{ $item->lastModified->format(DATE_ATOM) }}</lastmod>
|
||||
@endif
|
||||
@foreach ($item->images as $image)
|
||||
<image:image>
|
||||
<image:loc>{{ e($image->loc) }}</image:loc>
|
||||
@if ($image->title)
|
||||
<image:title>{{ e($image->title) }}</image:title>
|
||||
@endif
|
||||
</image:image>
|
||||
@endforeach
|
||||
</url>
|
||||
@endforeach
|
||||
</urlset>
|
||||
@@ -1,37 +1,9 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php($useUnifiedSeo = true)
|
||||
|
||||
@push('head')
|
||||
<link rel="canonical" href="{{ $page_canonical }}">
|
||||
<meta name="robots" content="{{ $page_robots ?? 'index,follow' }}">
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ $tag->name }} Artworks — Skinbase" href="{{ url('/rss/tag/' . $tag->slug) }}">
|
||||
@if(!empty($ogImage))
|
||||
<meta property="og:image" content="{{ $ogImage }}">
|
||||
<meta property="og:image:alt" content="{{ $tag->name }} artworks on Skinbase">
|
||||
@endif
|
||||
<meta property="og:title" content="{{ $page_title }}">
|
||||
<meta property="og:description" content="{{ $page_meta_description }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ $page_canonical }}">
|
||||
<script type="application/ld+json">{!! json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => 'Artworks tagged "' . $tag->name . '"',
|
||||
'description' => $page_meta_description,
|
||||
'url' => $page_canonical,
|
||||
'image' => $ogImage,
|
||||
'hasPart' => $artworks->getCollection()->take(6)->map(fn($a) => [
|
||||
'@type' => 'ImageObject',
|
||||
'name' => $a->title,
|
||||
'url' => url('/' . ($a->slug ?? $a->id)),
|
||||
'thumbnail' => $a->thumbUrl('sm'),
|
||||
])->values()->all(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}</script>
|
||||
@if($artworks->previousPageUrl())
|
||||
<link rel="prev" href="{{ $artworks->previousPageUrl() }}">
|
||||
@endif
|
||||
@if($artworks->nextPageUrl())
|
||||
<link rel="next" href="{{ $artworks->nextPageUrl() }}">
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -5,31 +5,37 @@
|
||||
|
||||
@php
|
||||
$hero_title = $post->title;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)
|
||||
->og(
|
||||
type: 'article',
|
||||
title: $post->meta_title ?: $post->title,
|
||||
description: $post->meta_description ?: $post->excerpt ?: '',
|
||||
url: $post->url,
|
||||
image: $post->featured_image,
|
||||
)
|
||||
->addJsonLd([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Article',
|
||||
'headline' => $post->title,
|
||||
'datePublished' => $post->published_at?->toIso8601String(),
|
||||
'dateModified' => $post->updated_at?->toIso8601String(),
|
||||
'author' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => 'Skinbase',
|
||||
],
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => 'Skinbase',
|
||||
],
|
||||
'description' => $post->meta_description ?: $post->excerpt ?: '',
|
||||
'mainEntityOfPage' => $post->url,
|
||||
'image' => $post->featured_image,
|
||||
])
|
||||
->build();
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
{{-- Article structured data --}}
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Article',
|
||||
'headline' => $post->title,
|
||||
'datePublished' => $post->published_at?->toIso8601String(),
|
||||
'dateModified' => $post->updated_at?->toIso8601String(),
|
||||
'author' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => 'Skinbase',
|
||||
],
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => 'Skinbase',
|
||||
],
|
||||
'description' => $post->meta_description ?: $post->excerpt ?: '',
|
||||
'mainEntityOfPage' => $post->url,
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('page-content')
|
||||
|
||||
<article class="max-w-3xl">
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@push('head')
|
||||
<link rel="canonical" href="{{ $page_canonical ?? url('/categories') }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Skinbase">
|
||||
<meta property="og:title" content="{{ $page_title ?? 'Categories' }}">
|
||||
<meta property="og:description" content="{{ $page_meta_description ?? '' }}">
|
||||
<meta property="og:url" content="{{ $page_canonical ?? url('/categories') }}">
|
||||
@if(!empty($structured_data ?? null))
|
||||
<script type="application/ld+json">{!! json_encode($structured_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) !!}</script>
|
||||
@endif
|
||||
@endpush
|
||||
@php($useUnifiedSeo = true)
|
||||
|
||||
@section('main-class', '')
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@php
|
||||
use Illuminate\Support\Str;
|
||||
use App\Banner;
|
||||
$useUnifiedSeo = true;
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
@php
|
||||
$page_title = $page_title ?? 'Community Activity';
|
||||
$page_meta_description = 'Track comments, replies, reactions, and mentions from across the Skinbase community in one live feed.';
|
||||
$page_canonical = route('community.activity', array_filter([
|
||||
'filter' => ($initialFilter ?? null) && ($initialFilter ?? 'all') !== 'all' ? $initialFilter : null,
|
||||
'user_id' => $initialUserId ?? null,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== ''));
|
||||
$useUnifiedSeo = true;
|
||||
$headerBreadcrumbs = collect([
|
||||
(object) ['name' => $page_title ?? 'Community Activity', 'url' => route('community.activity')],
|
||||
(object) ['name' => $page_title, 'url' => $page_canonical],
|
||||
]);
|
||||
$breadcrumbs = $headerBreadcrumbs;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
|
||||
$initialFilterLabel = match (($initialFilter ?? 'all')) {
|
||||
'comments' => 'Comments',
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$page_title = $page_title ?? 'Monthly Top Commentators';
|
||||
$page_meta_description = 'Members who posted the most comments in the last 30 days.';
|
||||
$page_canonical = route('comments.monthly', request()->query());
|
||||
$useUnifiedSeo = true;
|
||||
$headerBreadcrumbs = collect([
|
||||
(object) ['name' => $page_title ?? 'Monthly Top Commentators', 'url' => route('comments.monthly')],
|
||||
(object) ['name' => $page_title, 'url' => $page_canonical],
|
||||
]);
|
||||
$breadcrumbs = $headerBreadcrumbs;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$page_title = $page_title ?? 'Daily Uploads';
|
||||
$page_meta_description = 'Browse public artworks grouped by upload date across the last two weeks on Skinbase.';
|
||||
$page_canonical = route('uploads.daily', request()->query());
|
||||
$useUnifiedSeo = true;
|
||||
$headerBreadcrumbs = collect([
|
||||
(object) ['name' => $page_title ?? 'Daily Uploads', 'url' => route('uploads.daily')],
|
||||
(object) ['name' => $page_title, 'url' => $page_canonical],
|
||||
]);
|
||||
$breadcrumbs = $headerBreadcrumbs;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$page_title = $page_title ?? 'Most Downloaded Today';
|
||||
$page_meta_description = 'Browse the artworks downloaded the most today on Skinbase.';
|
||||
$page_canonical = route('downloads.today', request()->query());
|
||||
$gallery_type = 'today-downloads';
|
||||
$useUnifiedSeo = true;
|
||||
$headerBreadcrumbs = collect([
|
||||
(object) ['name' => $page_title ?? 'Most Downloaded Today', 'url' => route('downloads.today')],
|
||||
(object) ['name' => $page_title, 'url' => $page_canonical],
|
||||
]);
|
||||
$breadcrumbs = $headerBreadcrumbs;
|
||||
$galleryArtworks = collect($artworks->items())->map(fn ($art) => [
|
||||
'id' => $art->id,
|
||||
'name' => $art->name ?? null,
|
||||
@@ -26,6 +32,9 @@
|
||||
] : null,
|
||||
])->values();
|
||||
$galleryNextPageUrl = $artworks->nextPageUrl();
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$page_title = $page_title ?? 'Featured Artworks';
|
||||
$page_meta_description = 'Browse staff-picked and community-highlighted artwork in the shared gallery feed.';
|
||||
$page_canonical = request()->fullUrl();
|
||||
$gallery_type = 'featured';
|
||||
$useUnifiedSeo = true;
|
||||
$featuredBreadcrumbs = collect([
|
||||
(object) ['name' => 'Featured', 'url' => request()->path()],
|
||||
(object) ['name' => $page_title ?? 'Featured Artworks', 'url' => request()->fullUrl()],
|
||||
(object) ['name' => $page_title, 'url' => $page_canonical],
|
||||
]);
|
||||
$breadcrumbs = $featuredBreadcrumbs;
|
||||
|
||||
$galleryArtworks = collect($artworks->items())->map(fn ($art) => [
|
||||
'id' => $art->id,
|
||||
@@ -24,6 +30,9 @@
|
||||
])->values();
|
||||
|
||||
$galleryNextPageUrl = $artworks->nextPageUrl();
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -1,48 +1,8 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('meta-description', $meta['description'])
|
||||
@section('meta-keywords', $meta['keywords'])
|
||||
@php($useUnifiedSeo = true)
|
||||
|
||||
@push('head')
|
||||
<title>{{ $meta['title'] }}</title>
|
||||
<link rel="canonical" href="{{ $meta['canonical'] }}">
|
||||
|
||||
{{-- Open Graph --}}
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Skinbase">
|
||||
<meta property="og:title" content="{{ $meta['title'] }}">
|
||||
<meta property="og:description" content="{{ $meta['description'] }}">
|
||||
<meta property="og:url" content="{{ $meta['canonical'] }}">
|
||||
@if(!empty($meta['og_image']))
|
||||
<meta property="og:image" content="{{ $meta['og_image'] }}">
|
||||
<meta property="og:image:type" content="image/webp">
|
||||
@endif
|
||||
|
||||
{{-- Twitter --}}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ $meta['title'] }}">
|
||||
<meta name="twitter:description" content="{{ $meta['description'] }}">
|
||||
@if(!empty($meta['og_image']))
|
||||
<meta name="twitter:image" content="{{ $meta['og_image'] }}">
|
||||
@endif
|
||||
|
||||
{{-- JSON-LD WebSite schema --}}
|
||||
@php
|
||||
$websiteSchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'WebSite',
|
||||
'name' => 'Skinbase',
|
||||
'url' => url('/'),
|
||||
'description' => $meta['description'],
|
||||
'potentialAction' => [
|
||||
'@type' => 'SearchAction',
|
||||
'target' => url('/search') . '?q={search_term_string}',
|
||||
'query-input' => 'required name=search_term_string',
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
<script type="application/ld+json">{!! json_encode($websiteSchema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) !!}</script>
|
||||
|
||||
{{-- Preload hero image for faster LCP --}}
|
||||
@if(!empty($props['hero']['thumb_lg']))
|
||||
<link rel="preload" as="image" href="{{ $props['hero']['thumb_lg'] }}">
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$page_title = $page_title ?? 'Member Photos';
|
||||
$page_meta_description = 'Artwork submitted by the Skinbase community, presented in the shared Nova gallery feed.';
|
||||
$page_canonical = route('members.photos', request()->query());
|
||||
$gallery_type = 'member-photos';
|
||||
$useUnifiedSeo = true;
|
||||
$memberPhotoBreadcrumbs = collect([
|
||||
(object) ['name' => 'Members', 'url' => route('creators.top')],
|
||||
(object) ['name' => $page_title ?? 'Member Photos', 'url' => route('members.photos')],
|
||||
(object) ['name' => $page_title, 'url' => $page_canonical],
|
||||
]);
|
||||
$breadcrumbs = $memberPhotoBreadcrumbs;
|
||||
$memberPhotoItems = collect(method_exists($artworks ?? null, 'items') ? $artworks->items() : ($artworks ?? []));
|
||||
$memberPhotoGallery = $memberPhotoItems->map(fn ($art) => [
|
||||
'id' => $art->id ?? null,
|
||||
@@ -28,6 +34,9 @@
|
||||
: null,
|
||||
]);
|
||||
$memberPhotosNextPageUrl = method_exists($artworks ?? null, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)
|
||||
->addJsonLd([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $page_title,
|
||||
'description' => $page_meta_description,
|
||||
'url' => $page_canonical,
|
||||
])
|
||||
->build();
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -2,36 +2,27 @@
|
||||
|
||||
@php
|
||||
$storySummary = $story->excerpt ?: \Illuminate\Support\Str::limit(trim(strip_tags($safeContent)), 160);
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
@php
|
||||
$storyUrl = $story->canonical_url ?: route('stories.show', ['slug' => $story->slug]);
|
||||
$creatorName = $story->creator?->display_name ?: $story->creator?->username ?: 'Unknown creator';
|
||||
$metaDescription = $story->meta_description ?: $storySummary;
|
||||
$metaTitle = $story->meta_title ?: $story->title;
|
||||
$ogImage = $story->og_image ?: $story->cover_url;
|
||||
$creatorFollowProps = $story->creator ? [
|
||||
'username' => $story->creator->username,
|
||||
'following' => (bool) ($storySocialProps['state']['is_following_creator'] ?? false),
|
||||
'followers_count' => (int) ($storySocialProps['creator']['followers_count'] ?? 0),
|
||||
] : null;
|
||||
@endphp
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content="{{ $metaTitle }}" />
|
||||
<meta property="og:description" content="{{ $metaDescription }}" />
|
||||
<meta property="og:url" content="{{ $storyUrl }}" />
|
||||
@if($ogImage)
|
||||
<meta property="og:image" content="{{ $ogImage }}" />
|
||||
@endif
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="{{ $metaTitle }}" />
|
||||
<meta name="twitter:description" content="{{ $metaDescription }}" />
|
||||
@if($ogImage)
|
||||
<meta name="twitter:image" content="{{ $ogImage }}" />
|
||||
@endif
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode([
|
||||
$storyUrl = $story->canonical_url ?: route('stories.show', ['slug' => $story->slug]);
|
||||
$creatorName = $story->creator?->display_name ?: $story->creator?->username ?: 'Unknown creator';
|
||||
$metaDescription = $story->meta_description ?: $storySummary;
|
||||
$metaTitle = $story->meta_title ?: $story->title;
|
||||
$ogImage = $story->og_image ?: $story->cover_url;
|
||||
$creatorFollowProps = $story->creator ? [
|
||||
'username' => $story->creator->username,
|
||||
'following' => (bool) ($storySocialProps['state']['is_following_creator'] ?? false),
|
||||
'followers_count' => (int) ($storySocialProps['creator']['followers_count'] ?? 0),
|
||||
] : null;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)
|
||||
->og(
|
||||
type: 'article',
|
||||
title: $metaTitle,
|
||||
description: $metaDescription,
|
||||
url: $storyUrl,
|
||||
image: $ogImage,
|
||||
)
|
||||
->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Article',
|
||||
'headline' => $story->title,
|
||||
@@ -44,9 +35,9 @@
|
||||
'datePublished' => optional($story->published_at)->toIso8601String(),
|
||||
'dateModified' => optional($story->updated_at)->toIso8601String(),
|
||||
'mainEntityOfPage' => $storyUrl,
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
||||
</script>
|
||||
@endpush
|
||||
], fn (mixed $value): bool => $value !== null && $value !== ''))
|
||||
->build();
|
||||
@endphp
|
||||
|
||||
@section('page-hero')
|
||||
<div class="hidden" aria-hidden="true"></div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$hero_title = 'Tags';
|
||||
$hero_description = 'Browse all artwork tags on Skinbase.';
|
||||
$breadcrumbs = $breadcrumbs ?? collect([
|
||||
@@ -9,18 +10,6 @@
|
||||
]);
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
@isset($page_canonical)
|
||||
<link rel="canonical" href="{{ $page_canonical }}" />
|
||||
@endisset
|
||||
<meta name="robots" content="{{ $page_robots ?? 'index,follow' }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ $page_canonical ?? url()->current() }}" />
|
||||
<meta property="og:title" content="{{ $page_title ?? 'Skinbase' }}" />
|
||||
<meta property="og:description" content="{{ $page_meta_description ?? $hero_description }}" />
|
||||
<meta property="og:site_name" content="Skinbase" />
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$query = trim((string) ($query ?? ''));
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@php
|
||||
$page_title = $page_title ?? 'Latest Artworks';
|
||||
$page_meta_description = 'Fresh public uploads across skins, photography, wallpapers, and the rest of the Skinbase catalog.';
|
||||
$page_canonical = route('uploads.latest', request()->query());
|
||||
$gallery_type = 'latest-uploads';
|
||||
$useUnifiedSeo = true;
|
||||
$latestBreadcrumbs = collect([
|
||||
(object) ['name' => 'Uploads', 'url' => route('uploads.latest')],
|
||||
(object) ['name' => $page_title ?? 'Latest Artworks', 'url' => route('uploads.latest')],
|
||||
(object) ['name' => $page_title, 'url' => $page_canonical],
|
||||
]);
|
||||
$breadcrumbs = $latestBreadcrumbs;
|
||||
$cursorStateLabel = request()->filled('cursor') ? 'Browsing archive' : 'Latest slice';
|
||||
$cursorStateCopy = request()->filled('cursor')
|
||||
? 'You are viewing an older slice of the cursor-based feed.'
|
||||
@@ -29,6 +35,9 @@
|
||||
'url' => isset($art->id) ? '/art/' . $art->id . '/' . ($art->slug ?: \Illuminate\Support\Str::slug($art->name ?? 'artwork')) : '#',
|
||||
])->values();
|
||||
$galleryNextPageUrl = method_exists($artworks, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
|
||||
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
|
||||
)->build();
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
|
||||
Reference in New Issue
Block a user