Implement creator studio and upload updates

This commit is contained in:
2026-04-04 10:12:02 +02:00
parent 1da7d3bf88
commit 0b216b7ecd
15107 changed files with 31206 additions and 626514 deletions

View File

@@ -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')

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 ────────────────────────────────────────────────────

View 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&hellip;
</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

View File

@@ -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">

View File

@@ -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

View File

@@ -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 --}}

View File

@@ -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 --}}

View File

@@ -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">

View File

@@ -1,3 +1,4 @@
@php($useUnifiedSeo = true)
@extends('layouts.nova')
@php

View File

@@ -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"

View 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>

View 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

View File

@@ -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 {

View File

@@ -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 ?? '' }}">

View 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>

View 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>

View 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>

View File

@@ -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')

View File

@@ -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">

View File

@@ -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', '')

View File

@@ -3,6 +3,7 @@
@php
use Illuminate\Support\Str;
use App\Banner;
$useUnifiedSeo = true;
@endphp
@section('content')

View File

@@ -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',

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')

View File

@@ -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'] }}">

View File

@@ -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')

View File

@@ -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')

View File

@@ -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>

View File

@@ -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 ?? ''));

View File

@@ -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')