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:
Ümit Tunç
2026-05-20 23:17:30 +03:00
parent 33c7b6a51e
commit 15896248c4
8 changed files with 367 additions and 24 deletions
@@ -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;
}
}
+1
View File
@@ -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.',
+1
View File
@@ -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,23 +26,26 @@
<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"
>
<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"
placeholder="{{ __('music_productions.search_placeholder') }}"
aria-label="{{ __('music_productions.search_placeholder') }}"
autocomplete="off"
>
</form>
<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="music-production-search__input"
placeholder="{{ __('music_productions.search_placeholder') }}"
aria-label="{{ __('music_productions.search_placeholder') }}"
autocomplete="off"
>
</form>
</div>
</div>
<!-- /column -->
</div>
@@ -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">
+19 -3
View File
@@ -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') }}">