feat: implement structured data support for blog and page components, enhancing SEO and user experience through JSON-LD integration
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
---
|
||||
description: Google yapılandırılmış veri (JSON-LD) standartları
|
||||
globs: app/Support/*StructuredData*.php, app/Http/Controllers/**/*.php, resources/views/**/*.blade.php
|
||||
---
|
||||
# Yapılandırılmış Veri (Structured Data) Kuralları
|
||||
|
||||
Google [Yapılandırılmış Veri Genel Yönergeleri](https://developers.google.com/search/docs/appearance/structured-data/sd-policies?hl=tr) ve [Giriş](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data?hl=tr) dokümanlarına uy.
|
||||
|
||||
## Temel İlkeler
|
||||
|
||||
1. **JSON-LD kullan** — Google'ın önerdiği format; `<head>` içinde `<script type="application/ld+json">` ile sunulur.
|
||||
2. **Sayfa içeriğiyle eşleş** — İşaretlenen veri kullanıcının gördüğü içerikle birebir uyumlu olmalı; görünmeyen veya yanlış bilgi ekleme.
|
||||
3. **Eksiksiz ve doğru özellikler** — Zorunlu alanları doldur; eksik/hatalı önerilen alanları doldurmaya çalışmak yerine az ama doğru veri tercih et.
|
||||
4. **schema.org sözlüğü** — `@context: https://schema.org` kullan; Google Arama özellikleri için [Search Central özellik rehberlerini](https://developers.google.com/search/docs/appearance/structured-data/search-gallery?hl=tr) referans al.
|
||||
|
||||
## Proje Mimarisi
|
||||
|
||||
```
|
||||
app/Support/
|
||||
├── StructuredData.php # Ortak yardımcılar (WebSite, Organization, WebPage, BreadcrumbList)
|
||||
├── PageStructuredData.php # CMS sayfaları
|
||||
├── BlogStructuredData.php # Blog listesi ve detay
|
||||
└── MusicProductionStructuredData.php
|
||||
```
|
||||
|
||||
### Controller → View akışı
|
||||
|
||||
```php
|
||||
// Controller
|
||||
'structuredData' => BlogStructuredData::forShow($post, url()->current()),
|
||||
|
||||
// Layout (layouts/site.blade.php) otomatik render eder:
|
||||
@if(!empty($structuredData))
|
||||
<x-seo.json-ld :data="$structuredData" />
|
||||
@endif
|
||||
```
|
||||
|
||||
**Asla** Blade içinde ham JSON string interpolasyonu kullanma — `json_encode` ile `<x-seo.json-ld>` bileşenini kullan.
|
||||
|
||||
## Sayfa Türü → Schema Eşlemesi
|
||||
|
||||
| Sayfa türü | Schema türleri | Sınıf |
|
||||
|---|---|---|
|
||||
| Ana sayfa / CMS sayfaları | WebSite, Organization, WebPage, BreadcrumbList | `PageStructuredData` |
|
||||
| Blog listesi | CollectionPage, ItemList, BlogPosting (özet) | `BlogStructuredData::forIndex` |
|
||||
| Blog detay | BlogPosting, WebPage, BreadcrumbList | `BlogStructuredData::forShow` |
|
||||
| Müzik prodüksiyonları | CollectionPage / MusicAlbum, ItemList | `MusicProductionStructuredData` |
|
||||
| Ürün/hizmet sayfaları | WebPage, Product veya Service | Henüz eklenmedi |
|
||||
| Kariyer / staj | WebPage | Henüz eklenmedi |
|
||||
| İletişim | WebPage, Organization (contactPoint) | Henüz eklenmedi |
|
||||
|
||||
## @graph Deseni
|
||||
|
||||
Birden fazla entity için `@graph` kullan; `@id` ile cross-reference yap:
|
||||
|
||||
```php
|
||||
return self::wrap([
|
||||
self::websiteNode(),
|
||||
self::organizationNode(),
|
||||
self::webPageNode($pageUrl, $name, $description),
|
||||
self::breadcrumbNode($pageUrl, $items),
|
||||
// sayfa-özel entity (BlogPosting, MusicAlbum, vb.)
|
||||
]);
|
||||
```
|
||||
|
||||
## Yeni Sayfa Eklerken
|
||||
|
||||
1. `app/Support/` altında `*StructuredData.php` sınıfı oluştur veya mevcut sınıfı genişlet.
|
||||
2. `StructuredData` base sınıfındaki ortak node'ları kullan (DRY).
|
||||
3. Controller'da `structuredData` key'ini view'a geçir.
|
||||
4. Görünür içerikle eşleştiğini doğrula (başlık, açıklama, görsel, tarih).
|
||||
5. [Zengin Sonuçlar Testi](https://search.google.com/test/rich-results) ile doğrula.
|
||||
|
||||
## Yasaklar
|
||||
|
||||
- ❌ Boş veya içeriksiz sayfalara yalnızca schema eklemek
|
||||
- ❌ Blade'de `"headline": "{{ $title }}"` gibi kaçışsız JSON
|
||||
- ❌ `@push('scripts')` ile body sonuna JSON-LD koymak (head'de olmalı)
|
||||
- ❌ data-vocabulary.org işaretlemesi
|
||||
- ❌ Kullanıcıya görünmeyen rating/review verisi uydurmak
|
||||
|
||||
## Doğrulama
|
||||
|
||||
- Geliştirme: [Zengin Sonuçlar Testi](https://search.google.com/test/rich-results)
|
||||
- Prod: Search Console → Zengin sonuçlar raporları
|
||||
@@ -9,6 +9,7 @@ use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Models\Page;
|
||||
use App\Support\BlogStructuredData;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
@@ -115,6 +116,8 @@ class BlogController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
$pageUrl = url()->current();
|
||||
|
||||
return view('blog.index', [
|
||||
'settings' => $settings,
|
||||
'posts' => $posts,
|
||||
@@ -125,6 +128,9 @@ class BlogController extends Controller
|
||||
'title' => __('blog.meta-index-title'),
|
||||
'description' => __('blog.meta-index-description'),
|
||||
],
|
||||
'structuredData' => $posts instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
? BlogStructuredData::forIndex($posts, $pageUrl)
|
||||
: null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -215,6 +221,8 @@ class BlogController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$pageUrl = route('blog.show', $post->slug);
|
||||
|
||||
return view('blog.show', [
|
||||
'settings' => $settings,
|
||||
'post' => $post,
|
||||
@@ -226,7 +234,9 @@ class BlogController extends Controller
|
||||
'title' => method_exists($post, 'translate') ? ($post->translate('meta_title') ?: $post->translate('title')) : ($post->meta_title ?? $post->title),
|
||||
'description' => method_exists($post, 'translate') ? ($post->translate('meta_description') ?: $post->translate('excerpt')) : ($post->meta_description ?? $post->excerpt),
|
||||
'image' => $post->featured_image ? asset('storage/' . $post->featured_image) : null,
|
||||
'og_type' => 'article',
|
||||
],
|
||||
'structuredData' => BlogStructuredData::forShow($post, $pageUrl),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\Setting;
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Support\PageStructuredData;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -32,10 +33,13 @@ class PageController extends Controller
|
||||
|
||||
// Hiç sayfa yoksa fallback
|
||||
if (!$page) {
|
||||
$meta = $this->getMeta(null, $settings);
|
||||
|
||||
return view('templates.home', [
|
||||
'page' => null,
|
||||
'settings' => $settings,
|
||||
'meta' => $this->getMeta(null, $settings),
|
||||
'meta' => $meta,
|
||||
'structuredData' => PageStructuredData::forPage(null, url('/'), $meta['title'] ?? null, $meta['description'] ?? null),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -136,6 +140,9 @@ class PageController extends Controller
|
||||
$renderedFooter = $this->renderFooter($page, $settings);
|
||||
}
|
||||
|
||||
$meta = $this->getMeta($page, $settings);
|
||||
$pageUrl = url()->current();
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'settings' => $settings,
|
||||
@@ -143,7 +150,13 @@ class PageController extends Controller
|
||||
'templatedSections' => $page ? ($page->templated_sections ?? collect([])) : collect([]),
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => $this->getMeta($page, $settings),
|
||||
'meta' => $meta,
|
||||
'structuredData' => PageStructuredData::forPage(
|
||||
$page,
|
||||
$pageUrl,
|
||||
$meta['title'] ?? null,
|
||||
$meta['description'] ?? null,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Blog;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BlogStructuredData extends StructuredData
|
||||
{
|
||||
public static function forIndex(LengthAwarePaginator $posts, string $pageUrl): array
|
||||
{
|
||||
$pageName = __('blog.meta-index-title');
|
||||
$pageDescription = __('blog.meta-index-description');
|
||||
|
||||
$itemListElements = [];
|
||||
$offset = ($posts->currentPage() - 1) * $posts->perPage();
|
||||
|
||||
foreach ($posts as $index => $post) {
|
||||
$itemListElements[] = self::listItem(
|
||||
$offset + $index + 1,
|
||||
self::postSummarySchema($post),
|
||||
);
|
||||
}
|
||||
|
||||
return self::wrap([
|
||||
self::websiteNode(),
|
||||
self::organizationNode(),
|
||||
self::webPageNode($pageUrl, $pageName, $pageDescription, [
|
||||
'@type' => 'CollectionPage',
|
||||
'mainEntity' => ['@id' => $pageUrl . '#itemlist'],
|
||||
]),
|
||||
self::breadcrumbNode($pageUrl, [
|
||||
['name' => __('blog.breadcrumb_home'), 'item' => self::siteUrl()],
|
||||
['name' => $pageName, 'item' => $pageUrl],
|
||||
]),
|
||||
[
|
||||
'@type' => 'ItemList',
|
||||
'@id' => $pageUrl . '#itemlist',
|
||||
'name' => $pageName,
|
||||
'numberOfItems' => $posts->total(),
|
||||
'itemListElement' => $itemListElements,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function forShow(Blog $post, string $pageUrl): array
|
||||
{
|
||||
$title = $post->translate('title');
|
||||
$description = Str::limit(strip_tags((string) $post->translate('excerpt')), 160);
|
||||
$imageUrl = $post->featured_image_url ?: self::defaultImageUrl();
|
||||
$publishedAt = $post->published_at ?? $post->created_at;
|
||||
|
||||
$blogPosting = [
|
||||
'@type' => 'BlogPosting',
|
||||
'@id' => $pageUrl . '#article',
|
||||
'mainEntityOfPage' => ['@id' => $pageUrl . '#webpage'],
|
||||
'headline' => $title,
|
||||
'description' => $description,
|
||||
'image' => [$imageUrl],
|
||||
'inLanguage' => app()->getLocale(),
|
||||
'author' => [
|
||||
'@type' => 'Person',
|
||||
'name' => $post->author->name ?? __('blog.default_author'),
|
||||
],
|
||||
'publisher' => self::publisherNode(),
|
||||
'datePublished' => $publishedAt->toIso8601String(),
|
||||
'dateModified' => $post->updated_at->toIso8601String(),
|
||||
'url' => $pageUrl,
|
||||
];
|
||||
|
||||
$content = strip_tags((string) $post->translate('content'));
|
||||
if ($content !== '') {
|
||||
$blogPosting['articleBody'] = Str::limit($content, 5000);
|
||||
}
|
||||
|
||||
return self::wrap([
|
||||
self::websiteNode(),
|
||||
self::organizationNode(),
|
||||
self::webPageNode($pageUrl, $title, $description, [
|
||||
'mainEntity' => ['@id' => $pageUrl . '#article'],
|
||||
'dateModified' => $post->updated_at->toIso8601String(),
|
||||
]),
|
||||
self::breadcrumbNode($pageUrl, [
|
||||
['name' => __('blog.breadcrumb_home'), 'item' => self::siteUrl()],
|
||||
['name' => __('blog.meta-index-title'), 'item' => route('blog.index')],
|
||||
['name' => $title, 'item' => $pageUrl],
|
||||
]),
|
||||
$blogPosting,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function postSummarySchema(Blog $post): array
|
||||
{
|
||||
$schema = [
|
||||
'@type' => 'BlogPosting',
|
||||
'headline' => $post->translate('title'),
|
||||
'url' => route('blog.show', $post->slug),
|
||||
];
|
||||
|
||||
if ($post->featured_image_url) {
|
||||
$schema['image'] = [$post->featured_image_url];
|
||||
}
|
||||
|
||||
$publishedAt = $post->published_at ?? $post->created_at;
|
||||
if ($publishedAt) {
|
||||
$schema['datePublished'] = $publishedAt->toIso8601String();
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function publisherNode(): array
|
||||
{
|
||||
$publisher = [
|
||||
'@type' => 'Organization',
|
||||
'@id' => self::siteUrl() . '#organization',
|
||||
'name' => self::siteName(),
|
||||
];
|
||||
|
||||
$logo = setting('site_logo');
|
||||
if ($logo) {
|
||||
$logoUrl = str_starts_with($logo, 'http') ? $logo : asset('storage/' . ltrim($logo, '/'));
|
||||
$publisher['logo'] = [
|
||||
'@type' => 'ImageObject',
|
||||
'url' => $logoUrl,
|
||||
];
|
||||
}
|
||||
|
||||
return $publisher;
|
||||
}
|
||||
|
||||
protected static function defaultImageUrl(): string
|
||||
{
|
||||
$defaultImage = setting('default_meta_image');
|
||||
|
||||
if ($defaultImage) {
|
||||
return str_starts_with($defaultImage, 'http') ? $defaultImage : asset($defaultImage);
|
||||
}
|
||||
|
||||
return asset('assets/img/logo.png');
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,10 @@ use App\Models\MusicProduction;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MusicProductionStructuredData
|
||||
class MusicProductionStructuredData extends StructuredData
|
||||
{
|
||||
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');
|
||||
|
||||
@@ -19,45 +17,23 @@ class MusicProductionStructuredData
|
||||
$offset = ($productions->currentPage() - 1) * $productions->perPage();
|
||||
|
||||
foreach ($productions as $index => $production) {
|
||||
$itemListElements[] = [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $offset + $index + 1,
|
||||
'item' => self::productionSchema($production),
|
||||
];
|
||||
$itemListElements[] = self::listItem(
|
||||
$offset + $index + 1,
|
||||
self::productionSchema($production),
|
||||
);
|
||||
}
|
||||
|
||||
$graph = [
|
||||
self::websiteNode($siteUrl, $siteName),
|
||||
self::organizationNode($siteUrl, $siteName),
|
||||
[
|
||||
return self::wrap([
|
||||
self::websiteNode(route('music-productions.index') . '?q={search_term_string}'),
|
||||
self::organizationNode(),
|
||||
self::webPageNode($pageUrl, $pageName, $pageDescription, [
|
||||
'@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'),
|
||||
],
|
||||
],
|
||||
],
|
||||
]),
|
||||
self::breadcrumbNode($pageUrl, [
|
||||
['name' => __('music_productions.breadcrumb_home'), 'item' => self::siteUrl()],
|
||||
['name' => __('music_productions.nav-music-productions'), 'item' => route('music-productions.index')],
|
||||
]),
|
||||
[
|
||||
'@type' => 'ItemList',
|
||||
'@id' => $pageUrl . '#itemlist',
|
||||
@@ -65,72 +41,36 @@ class MusicProductionStructuredData
|
||||
'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'],
|
||||
$webPageExtra = [
|
||||
'mainEntity' => ['@id' => $pageUrl . '#production'],
|
||||
],
|
||||
$production->updated_at ? ['dateModified' => $production->updated_at->toIso8601String()] : [],
|
||||
),
|
||||
];
|
||||
|
||||
if ($production->updated_at) {
|
||||
$webPageExtra['dateModified'] = $production->updated_at->toIso8601String();
|
||||
}
|
||||
|
||||
return self::wrap([
|
||||
self::websiteNode(route('music-productions.index') . '?q={search_term_string}'),
|
||||
self::organizationNode(),
|
||||
self::webPageNode($pageUrl, $title, $description, $webPageExtra),
|
||||
self::breadcrumbNode($pageUrl, [
|
||||
['name' => __('music_productions.breadcrumb_home'), 'item' => self::siteUrl()],
|
||||
['name' => __('music_productions.nav-music-productions'), 'item' => route('music-productions.index')],
|
||||
['name' => $title, 'item' => $pageUrl],
|
||||
]),
|
||||
array_merge(
|
||||
['@id' => $pageUrl . '#production'],
|
||||
self::productionSchema($production),
|
||||
),
|
||||
];
|
||||
|
||||
return [
|
||||
'@context' => 'https://schema.org',
|
||||
'@graph' => $graph,
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +85,7 @@ class MusicProductionStructuredData
|
||||
];
|
||||
|
||||
if ($production->cover_image_url) {
|
||||
$schema['image'] = $production->cover_image_url;
|
||||
$schema['image'] = [$production->cover_image_url];
|
||||
}
|
||||
|
||||
$content = strip_tags((string) $production->translate('content'));
|
||||
@@ -176,50 +116,4 @@ class MusicProductionStructuredData
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Page;
|
||||
|
||||
class PageStructuredData extends StructuredData
|
||||
{
|
||||
public static function forPage(?Page $page, string $pageUrl, ?string $name = null, ?string $description = null): array
|
||||
{
|
||||
$pageName = $name ?: ($page
|
||||
? (method_exists($page, 'translate')
|
||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||
: ($page->meta_title ?? $page->title ?? self::siteName()))
|
||||
: self::siteName());
|
||||
|
||||
$pageDescription = $description ?: ($page
|
||||
? (method_exists($page, 'translate')
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null))
|
||||
: setting('seo_meta_description'));
|
||||
|
||||
$webPageExtra = [];
|
||||
|
||||
if ($page?->updated_at) {
|
||||
$webPageExtra['dateModified'] = $page->updated_at->toIso8601String();
|
||||
}
|
||||
|
||||
if ($page?->is_homepage) {
|
||||
$webPageExtra['@type'] = 'WebPage';
|
||||
}
|
||||
|
||||
$graph = [
|
||||
self::websiteNode(),
|
||||
self::organizationNode(),
|
||||
self::webPageNode($pageUrl, $pageName, $pageDescription, $webPageExtra),
|
||||
self::breadcrumbNode($pageUrl, self::breadcrumbItems($page, $pageUrl, $pageName)),
|
||||
];
|
||||
|
||||
return self::wrap($graph);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{name: string, item: string}>
|
||||
*/
|
||||
protected static function breadcrumbItems(?Page $page, string $pageUrl, string $pageName): array
|
||||
{
|
||||
$items = [
|
||||
['name' => __('pages.breadcrumb_home'), 'item' => self::siteUrl()],
|
||||
];
|
||||
|
||||
if ($page && !$page->is_homepage) {
|
||||
$items[] = ['name' => $pageName, 'item' => $pageUrl];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
abstract class StructuredData
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $graph
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function wrap(array $graph): array
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://schema.org',
|
||||
'@graph' => array_values($graph),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function siteUrl(): string
|
||||
{
|
||||
return url('/');
|
||||
}
|
||||
|
||||
protected static function siteName(): string
|
||||
{
|
||||
return (string) setting('site_name', config('app.name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function websiteNode(?string $searchUrlTemplate = null): array
|
||||
{
|
||||
$siteUrl = static::siteUrl();
|
||||
$siteName = static::siteName();
|
||||
|
||||
$node = [
|
||||
'@type' => 'WebSite',
|
||||
'@id' => $siteUrl . '#website',
|
||||
'url' => $siteUrl,
|
||||
'name' => $siteName,
|
||||
'inLanguage' => app()->getLocale(),
|
||||
'publisher' => ['@id' => $siteUrl . '#organization'],
|
||||
];
|
||||
|
||||
if ($searchUrlTemplate) {
|
||||
$node['potentialAction'] = [
|
||||
'@type' => 'SearchAction',
|
||||
'target' => [
|
||||
'@type' => 'EntryPoint',
|
||||
'urlTemplate' => $searchUrlTemplate,
|
||||
],
|
||||
'query-input' => 'required name=search_term_string',
|
||||
];
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function organizationNode(): array
|
||||
{
|
||||
$siteUrl = static::siteUrl();
|
||||
$siteName = static::siteName();
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{name: string, item: string}> $items
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function breadcrumbNode(string $pageUrl, array $items): array
|
||||
{
|
||||
$elements = [];
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$elements[] = [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'name' => $item['name'],
|
||||
'item' => $item['item'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'@type' => 'BreadcrumbList',
|
||||
'@id' => $pageUrl . '#breadcrumb',
|
||||
'itemListElement' => $elements,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $extra
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function webPageNode(
|
||||
string $pageUrl,
|
||||
string $name,
|
||||
?string $description = null,
|
||||
array $extra = [],
|
||||
): array {
|
||||
$node = array_merge([
|
||||
'@type' => 'WebPage',
|
||||
'@id' => $pageUrl . '#webpage',
|
||||
'url' => $pageUrl,
|
||||
'name' => $name,
|
||||
'inLanguage' => app()->getLocale(),
|
||||
'isPartOf' => ['@id' => static::siteUrl() . '#website'],
|
||||
'breadcrumb' => ['@id' => $pageUrl . '#breadcrumb'],
|
||||
], $extra);
|
||||
|
||||
if ($description) {
|
||||
$node['description'] = $description;
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function listItem(int $position, array $item): array
|
||||
{
|
||||
return [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $position,
|
||||
'item' => $item,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ return [
|
||||
'nav-blog' => 'Blog',
|
||||
'meta-index-title' => 'Blog',
|
||||
'meta-index-description' => 'Latest posts',
|
||||
'breadcrumb_home' => 'Home',
|
||||
'default_author' => 'Trunçgil Technology Editor',
|
||||
'empty' => 'No posts yet.',
|
||||
|
||||
// Module labels
|
||||
|
||||
@@ -6,6 +6,7 @@ return [
|
||||
'nav-about' => 'About',
|
||||
'nav-services' => 'Services',
|
||||
'nav-contact' => 'Contact',
|
||||
'breadcrumb_home' => 'Home',
|
||||
|
||||
// Module labels
|
||||
'title' => 'Pages',
|
||||
|
||||
@@ -5,6 +5,8 @@ return [
|
||||
'nav-blog' => 'Blog',
|
||||
'meta-index-title' => 'Blog',
|
||||
'meta-index-description' => 'Güncel paylaşımlarımız',
|
||||
'breadcrumb_home' => 'Ana Sayfa',
|
||||
'default_author' => 'Trunçgil Teknoloji Editörü',
|
||||
'empty' => 'Şu anda blog yazısı bulunmuyor.',
|
||||
|
||||
// Module labels
|
||||
|
||||
@@ -6,6 +6,7 @@ return [
|
||||
'nav-about' => 'Hakkımızda',
|
||||
'nav-services' => 'Hizmetler',
|
||||
'nav-contact' => 'İletişim',
|
||||
'breadcrumb_home' => 'Ana Sayfa',
|
||||
|
||||
// Module labels
|
||||
'title' => 'Sayfalar',
|
||||
|
||||
@@ -405,34 +405,4 @@
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
</section>
|
||||
|
||||
@push('scripts')
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": "{{ route('blog.show', $post->slug) }}"
|
||||
},
|
||||
"headline": "{{ method_exists($post, 'translate') ? $post->translate('title') : $post->title }}",
|
||||
"description": "{{ method_exists($post, 'translate') ? $post->translate('excerpt') : ($post->excerpt ?? '') }}",
|
||||
"image": "{{ $post->featured_image_url ? $post->featured_image_url : asset('assets/img/logo.png') }}",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "{{ $post->author->name ?? 'Trunçgil Teknoloji Editörü' }}"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "{{ setting('site_name', 'Trunçgil Teknoloji') }}",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "{{ asset('storage/' . setting('site_logo')) }}"
|
||||
}
|
||||
},
|
||||
"datePublished": "{{ $post->published_at ? $post->published_at->toIso8601String() : $post->created_at->toIso8601String() }}",
|
||||
"dateModified": "{{ $post->updated_at->toIso8601String() }}"
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@extends('layouts.site')
|
||||
|
||||
<x-seo.json-ld :data="$structuredData ?? null" />
|
||||
|
||||
@section('content')
|
||||
{{-- demo28.html: hero + itemgrid --}}
|
||||
<section class="wrapper bg-gradient-blend">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@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">
|
||||
|
||||
@@ -51,6 +51,9 @@ $favicon_path = setting('site_favicon');
|
||||
<meta name="twitter:image:alt" content="{{ $meta['image_alt'] ?? $seo_title }}">
|
||||
@endif
|
||||
|
||||
@if(!empty($structuredData))
|
||||
<x-seo.json-ld :data="$structuredData" />
|
||||
@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') }}">
|
||||
|
||||
Reference in New Issue
Block a user