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;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
return [
|
||||
// Navigation & list
|
||||
'nav-music-productions' => 'Music Productions',
|
||||
'breadcrumb_home' => 'Home',
|
||||
'meta-index-subtitle' => 'Truncgil',
|
||||
'meta-index-title' => 'Music production, soundtracks and corporate music projects.',
|
||||
'meta-index-description' => 'Explore our albums, singles and production work published on Spotify, YouTube and digital platforms.',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
return [
|
||||
// Navigation & list
|
||||
'nav-music-productions' => 'Müzik Prodüksiyonları',
|
||||
'breadcrumb_home' => 'Ana Sayfa',
|
||||
'meta-index-subtitle' => 'Truncgil',
|
||||
'meta-index-title' => 'Müzik prodüksiyonu, film müzikleri ve kurumsal müzik çalışmalarımız.',
|
||||
'meta-index-description' => 'Spotify, YouTube ve dijital platformlarda yayımlanan albüm, single ve prodüksiyon projelerimizi keşfedin.',
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@props(['data' => null])
|
||||
|
||||
@if(!empty($data))
|
||||
@push('structured-data')
|
||||
<script type="application/ld+json">{!! json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) !!}</script>
|
||||
@endpush
|
||||
@endif
|
||||
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.site')
|
||||
|
||||
<x-seo.json-ld :data="$structuredData ?? null" />
|
||||
|
||||
@section('content')
|
||||
{{-- demo28.html: hero + itemgrid --}}
|
||||
<section class="wrapper bg-gradient-blend">
|
||||
@@ -24,24 +26,27 @@
|
||||
<div class="md:w-10/12 lg:w-9/12 xl:w-9/12 xxl:w-8/12 w-full flex-[0_0_auto] !px-[15px] max-w-full !mx-auto !text-center">
|
||||
|
||||
<h1 class="xl:!text-[2.4rem] !text-[calc(1.365rem_+_1.38vw)] font-semibold !leading-[1.15] !mb-5 xl:!px-14">{{ __('music_productions.meta-index-title') }}</h1>
|
||||
<p class="lead !text-[1.1rem]">{{ __('music_productions.meta-index-description') }}</p>
|
||||
<form
|
||||
action="{{ route('music-productions.index') }}"
|
||||
method="GET"
|
||||
role="search"
|
||||
class="search-form relative !mt-8 w-full max-w-3xl !mx-auto before:content-['\eca5'] before:block before:absolute before:-translate-y-2/4 before:text-[1.05rem] before:!text-[#747ed1] before:z-[1] before:left-5 before:top-2/4 before:font-Unicons"
|
||||
>
|
||||
<p class="lead !text-[1.1rem] !mb-0">{{ __('music_productions.meta-index-description') }}</p>
|
||||
<div class="music-production-search">
|
||||
<form action="{{ route('music-productions.index') }}" method="GET" role="search" class="music-production-search__form">
|
||||
<span class="music-production-search__icon" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="7"/>
|
||||
<path d="M20 20l-3-3"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
name="q"
|
||||
value="{{ $search ?? request('q') }}"
|
||||
class="form-control relative block w-full text-[0.95rem] font-medium !text-[#60697b] bg-[rgba(255,255,255,0.95)] shadow-[0_0_1.25rem_rgba(30,34,40,0.08)] rounded-[50rem] border border-solid border-[rgba(8,60,130,0.1)] !pl-12 !pr-5 py-[0.85rem] min-h-[3rem] focus:shadow-[0_0_1.25rem_rgba(116,126,209,0.18)] focus-visible:!border-[#747ed1] placeholder:!text-[#959ca9] placeholder:opacity-100"
|
||||
class="music-production-search__input"
|
||||
placeholder="{{ __('music_productions.search_placeholder') }}"
|
||||
aria-label="{{ __('music_productions.search_placeholder') }}"
|
||||
autocomplete="off"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /column -->
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
@@ -170,6 +175,67 @@
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.music-production-search {
|
||||
width: 100%;
|
||||
max-width: 36rem;
|
||||
margin: 2.25rem auto 1.75rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
.music-production-search__form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.music-production-search__icon {
|
||||
position: absolute;
|
||||
left: 1.35rem;
|
||||
top: 50%;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #747ed1;
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.music-production-search__icon svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.music-production-search__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 3.35rem;
|
||||
margin: 0;
|
||||
padding: 0 1.35rem 0 3.35rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: #60697b;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
border: 1px solid rgba(8, 60, 130, 0.12);
|
||||
border-radius: 50rem;
|
||||
box-shadow: 0 0.35rem 1.35rem rgba(30, 34, 40, 0.1);
|
||||
outline: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.music-production-search__input::placeholder {
|
||||
color: #959ca9;
|
||||
opacity: 1;
|
||||
}
|
||||
.music-production-search__input:focus {
|
||||
border-color: #747ed1;
|
||||
box-shadow: 0 0.35rem 1.5rem rgba(116, 126, 209, 0.22);
|
||||
}
|
||||
.music-production-search__input::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* 1:1 kapak karesi — dikdörtgen görseli kırpar, yan boşluk bırakmaz */
|
||||
.projects-masonry .music-production-cover {
|
||||
position: relative;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@extends('layouts.site')
|
||||
|
||||
<x-seo.json-ld :data="$structuredData ?? null" />
|
||||
|
||||
@section('content')
|
||||
<section class="wrapper !bg-[#edf2fc]">
|
||||
<div class="container pt-10 pb-36 xl:pt-[4.5rem] lg:pt-[4.5rem] md:pt-[4.5rem] xl:pb-60 lg:pb-60 md:pb-60 !text-center">
|
||||
|
||||
@@ -16,27 +16,43 @@ $favicon_path = setting('site_favicon');
|
||||
$seo_image = asset($seo_image);
|
||||
}
|
||||
@endphp
|
||||
@php
|
||||
$seo_canonical = $meta['canonical'] ?? url()->current();
|
||||
$seo_robots = $meta['robots'] ?? 'index, follow';
|
||||
$seo_og_type = $meta['og_type'] ?? 'website';
|
||||
$seo_og_locale = str_replace('_', '-', $meta['locale'] ?? app()->getLocale());
|
||||
@endphp
|
||||
<title>{{ $seo_title }}</title>
|
||||
<meta name="description" content="{{ $seo_description }}">
|
||||
<meta name="robots" content="{{ $seo_robots }}">
|
||||
<link rel="canonical" href="{{ $seo_canonical }}">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ url()->current() }}">
|
||||
<meta property="og:type" content="{{ $seo_og_type }}">
|
||||
<meta property="og:url" content="{{ $seo_canonical }}">
|
||||
<meta property="og:title" content="{{ $seo_title }}">
|
||||
<meta property="og:description" content="{{ $seo_description }}">
|
||||
<meta property="og:locale" content="{{ $seo_og_locale }}">
|
||||
@if(setting('site_name'))
|
||||
<meta property="og:site_name" content="{{ setting('site_name') }}">
|
||||
@endif
|
||||
@if($seo_image)
|
||||
<meta property="og:image" content="{{ $seo_image }}">
|
||||
<meta property="og:image:alt" content="{{ $meta['image_alt'] ?? $seo_title }}">
|
||||
@endif
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:url" content="{{ url()->current() }}">
|
||||
<meta name="twitter:url" content="{{ $seo_canonical }}">
|
||||
<meta name="twitter:title" content="{{ $seo_title }}">
|
||||
<meta name="twitter:description" content="{{ $seo_description }}">
|
||||
@if($seo_image)
|
||||
<meta name="twitter:image" content="{{ $seo_image }}">
|
||||
<meta name="twitter:image:alt" content="{{ $meta['image_alt'] ?? $seo_title }}">
|
||||
@endif
|
||||
|
||||
@stack('structured-data')
|
||||
|
||||
<link rel="icon" href="{{ $favicon_path ? (str_starts_with($favicon_path, 'http') ? $favicon_path : asset($favicon_path)) : asset('assets/img/favicon.png') }}">
|
||||
<link rel="stylesheet" href="{{ asset('html/style.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('assets/fonts/unicons/unicons.css') }}">
|
||||
|
||||
Reference in New Issue
Block a user