feat: enhance MusicProductionController with structured data support, improve SEO metadata handling, and add JSON-LD integration in views for better search engine visibility
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Models\MusicProduction;
|
||||
use App\Models\Setting;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Support\MusicProductionStructuredData;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MusicProductionController extends Controller
|
||||
@@ -60,12 +61,22 @@ class MusicProductionController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$canonicalUrl = route('music-productions.index');
|
||||
$pageUrl = $search !== '' ? $canonicalUrl : url()->current();
|
||||
|
||||
$meta = [
|
||||
'title' => __('music_productions.nav-music-productions'),
|
||||
'title' => __('music_productions.meta-index-title'),
|
||||
'description' => __('music_productions.meta-index-description'),
|
||||
'image' => asset('assets/music-production.png'),
|
||||
'image_alt' => __('music_productions.meta-index-title'),
|
||||
'canonical' => $canonicalUrl,
|
||||
'robots' => $search !== '' ? 'noindex, follow' : 'index, follow',
|
||||
'og_type' => 'website',
|
||||
'locale' => app()->getLocale(),
|
||||
];
|
||||
|
||||
$structuredData = MusicProductionStructuredData::forIndex($productions, $pageUrl);
|
||||
|
||||
return view('front.music-productions.index', [
|
||||
'productions' => $productions,
|
||||
'search' => $search,
|
||||
@@ -73,6 +84,7 @@ class MusicProductionController extends Controller
|
||||
'header' => 'partials.header-center-nav',
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => $meta,
|
||||
'structuredData' => $structuredData,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -132,12 +144,24 @@ class MusicProductionController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$pageUrl = route('music-productions.show', $production->slug);
|
||||
$title = $production->translate('title');
|
||||
$description = Str::limit(strip_tags((string) $production->translate('content')), 160)
|
||||
?: __('music_productions.meta-index-description');
|
||||
|
||||
$meta = [
|
||||
'title' => $production->translate('title'),
|
||||
'description' => Str::limit(strip_tags($production->translate('content')), 160),
|
||||
'image' => $production->cover_image_url,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'image' => $production->cover_image_url ?: asset('assets/music-production.png'),
|
||||
'image_alt' => $title,
|
||||
'canonical' => $pageUrl,
|
||||
'robots' => 'index, follow',
|
||||
'og_type' => 'article',
|
||||
'locale' => app()->getLocale(),
|
||||
];
|
||||
|
||||
$structuredData = MusicProductionStructuredData::forShow($production, $pageUrl);
|
||||
|
||||
return view('front.music-productions.show', [
|
||||
'production' => $production,
|
||||
'prev' => $prev,
|
||||
@@ -146,6 +170,7 @@ class MusicProductionController extends Controller
|
||||
'header' => 'partials.header-center-nav',
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => $meta,
|
||||
'structuredData' => $structuredData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\MusicProduction;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MusicProductionStructuredData
|
||||
{
|
||||
public static function forIndex(LengthAwarePaginator $productions, string $pageUrl): array
|
||||
{
|
||||
$siteName = setting('site_name', config('app.name'));
|
||||
$siteUrl = url('/');
|
||||
$pageName = __('music_productions.meta-index-title');
|
||||
$pageDescription = __('music_productions.meta-index-description');
|
||||
|
||||
$itemListElements = [];
|
||||
$offset = ($productions->currentPage() - 1) * $productions->perPage();
|
||||
|
||||
foreach ($productions as $index => $production) {
|
||||
$itemListElements[] = [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $offset + $index + 1,
|
||||
'item' => self::productionSchema($production),
|
||||
];
|
||||
}
|
||||
|
||||
$graph = [
|
||||
self::websiteNode($siteUrl, $siteName),
|
||||
self::organizationNode($siteUrl, $siteName),
|
||||
[
|
||||
'@type' => 'CollectionPage',
|
||||
'@id' => $pageUrl . '#webpage',
|
||||
'url' => $pageUrl,
|
||||
'name' => $pageName,
|
||||
'description' => $pageDescription,
|
||||
'inLanguage' => app()->getLocale(),
|
||||
'isPartOf' => ['@id' => $siteUrl . '#website'],
|
||||
'breadcrumb' => ['@id' => $pageUrl . '#breadcrumb'],
|
||||
'mainEntity' => ['@id' => $pageUrl . '#itemlist'],
|
||||
],
|
||||
[
|
||||
'@type' => 'BreadcrumbList',
|
||||
'@id' => $pageUrl . '#breadcrumb',
|
||||
'itemListElement' => [
|
||||
[
|
||||
'@type' => 'ListItem',
|
||||
'position' => 1,
|
||||
'name' => __('music_productions.breadcrumb_home'),
|
||||
'item' => $siteUrl,
|
||||
],
|
||||
[
|
||||
'@type' => 'ListItem',
|
||||
'position' => 2,
|
||||
'name' => __('music_productions.nav-music-productions'),
|
||||
'item' => route('music-productions.index'),
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'@type' => 'ItemList',
|
||||
'@id' => $pageUrl . '#itemlist',
|
||||
'name' => $pageName,
|
||||
'numberOfItems' => $productions->total(),
|
||||
'itemListElement' => $itemListElements,
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
'@context' => 'https://schema.org',
|
||||
'@graph' => $graph,
|
||||
];
|
||||
}
|
||||
|
||||
public static function forShow(MusicProduction $production, string $pageUrl): array
|
||||
{
|
||||
$siteName = setting('site_name', config('app.name'));
|
||||
$siteUrl = url('/');
|
||||
$title = $production->translate('title');
|
||||
$description = Str::limit(strip_tags((string) $production->translate('content')), 160);
|
||||
|
||||
$graph = [
|
||||
self::websiteNode($siteUrl, $siteName),
|
||||
self::organizationNode($siteUrl, $siteName),
|
||||
[
|
||||
'@type' => 'BreadcrumbList',
|
||||
'@id' => $pageUrl . '#breadcrumb',
|
||||
'itemListElement' => [
|
||||
[
|
||||
'@type' => 'ListItem',
|
||||
'position' => 1,
|
||||
'name' => __('music_productions.breadcrumb_home'),
|
||||
'item' => $siteUrl,
|
||||
],
|
||||
[
|
||||
'@type' => 'ListItem',
|
||||
'position' => 2,
|
||||
'name' => __('music_productions.nav-music-productions'),
|
||||
'item' => route('music-productions.index'),
|
||||
],
|
||||
[
|
||||
'@type' => 'ListItem',
|
||||
'position' => 3,
|
||||
'name' => $title,
|
||||
'item' => $pageUrl,
|
||||
],
|
||||
],
|
||||
],
|
||||
array_merge(
|
||||
[
|
||||
'@type' => 'WebPage',
|
||||
'@id' => $pageUrl . '#webpage',
|
||||
'url' => $pageUrl,
|
||||
'name' => $title,
|
||||
'description' => $description,
|
||||
'inLanguage' => app()->getLocale(),
|
||||
'isPartOf' => ['@id' => $siteUrl . '#website'],
|
||||
'breadcrumb' => ['@id' => $pageUrl . '#breadcrumb'],
|
||||
'mainEntity' => ['@id' => $pageUrl . '#production'],
|
||||
],
|
||||
$production->updated_at ? ['dateModified' => $production->updated_at->toIso8601String()] : [],
|
||||
),
|
||||
array_merge(
|
||||
['@id' => $pageUrl . '#production'],
|
||||
self::productionSchema($production),
|
||||
),
|
||||
];
|
||||
|
||||
return [
|
||||
'@context' => 'https://schema.org',
|
||||
'@graph' => $graph,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function productionSchema(MusicProduction $production): array
|
||||
{
|
||||
$schema = [
|
||||
'@type' => filled($production->spotify_album_id) ? 'MusicAlbum' : 'CreativeWork',
|
||||
'name' => $production->translate('title'),
|
||||
'url' => route('music-productions.show', $production->slug),
|
||||
];
|
||||
|
||||
if ($production->cover_image_url) {
|
||||
$schema['image'] = $production->cover_image_url;
|
||||
}
|
||||
|
||||
$content = strip_tags((string) $production->translate('content'));
|
||||
if ($content !== '') {
|
||||
$schema['description'] = Str::limit($content, 300);
|
||||
}
|
||||
|
||||
if ($production->production_date) {
|
||||
$schema['datePublished'] = $production->production_date->toIso8601String();
|
||||
}
|
||||
|
||||
$clientName = $production->translate('client_name');
|
||||
if ($clientName) {
|
||||
$schema['creator'] = [
|
||||
'@type' => 'Organization',
|
||||
'name' => $clientName,
|
||||
];
|
||||
}
|
||||
|
||||
$sameAs = array_values(array_filter([
|
||||
$production->spotify_url,
|
||||
$production->youtube_url,
|
||||
]));
|
||||
|
||||
if ($sameAs !== []) {
|
||||
$schema['sameAs'] = $sameAs;
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function websiteNode(string $siteUrl, string $siteName): array
|
||||
{
|
||||
return [
|
||||
'@type' => 'WebSite',
|
||||
'@id' => $siteUrl . '#website',
|
||||
'url' => $siteUrl,
|
||||
'name' => $siteName,
|
||||
'publisher' => ['@id' => $siteUrl . '#organization'],
|
||||
'potentialAction' => [
|
||||
'@type' => 'SearchAction',
|
||||
'target' => [
|
||||
'@type' => 'EntryPoint',
|
||||
'urlTemplate' => route('music-productions.index') . '?q={search_term_string}',
|
||||
],
|
||||
'query-input' => 'required name=search_term_string',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function organizationNode(string $siteUrl, string $siteName): array
|
||||
{
|
||||
$node = [
|
||||
'@type' => 'Organization',
|
||||
'@id' => $siteUrl . '#organization',
|
||||
'name' => $siteName,
|
||||
'url' => $siteUrl,
|
||||
];
|
||||
|
||||
$logo = setting('site_logo');
|
||||
if ($logo) {
|
||||
$logoUrl = str_starts_with($logo, 'http') ? $logo : asset('storage/' . ltrim($logo, '/'));
|
||||
$node['logo'] = [
|
||||
'@type' => 'ImageObject',
|
||||
'url' => $logoUrl,
|
||||
];
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user