@php($activeTheme = app()->bound('active_theme') ? app('active_theme') : 'default') @php($activeThemeColor = app()->bound('active_theme_color') ? app('active_theme_color') : 'neutral') @php($siteName = site_name()) @php($smoothNavigation = (bool) setting('smooth_navigation', true)) {{-- Favicon resolution chain (most → least specific): 1. theme_favicon_{slug} — admin-uploaded per-theme favicon 2. site_favicon — admin-uploaded global favicon 3. /favicon.png — bundled fallback that ships with the app The first present value wins; that file is used as the primary , the apple-touch-icon, and the legacy shortcut icon. --}} @php($themeFavicon = (string) (setting('theme_favicon_'.$activeTheme) ?: '')) @php($siteFavicon = (string) (setting('site_favicon') ?: '')) @php($favicon = $themeFavicon ?: ($siteFavicon ?: 'favicon.png')) @php($themeColor = '#0f172a') @php($pageProps = (array) ($page['props'] ?? [])) @php($og = is_array($pageProps['og'] ?? null) ? $pageProps['og'] : null) @php($jsonLd = $pageProps['jsonLd'] ?? null) @php($hreflang = is_array($pageProps['hreflang'] ?? null) ? $pageProps['hreflang'] : []) @php($shouldNoindex = (bool) ($og['noindex'] ?? false)) @php($twitterCard = $og['twitter_card'] ?? (! empty($og['image']) ? 'summary_large_image' : 'summary')) @php($activeDir = isset($pageProps['locale']['isRtl']) ? (($pageProps['locale']['isRtl'] ?? false) ? 'rtl' : 'ltr') : 'ltr') {{-- ─── SEO meta (server-rendered) ───────────────────────────────────── The OG / Twitter Card / canonical / robots / JSON-LD / hreflang payload is built by SocialMetaService + JsonLdService in PHP and shipped via Inertia props. We render it from Blade (rather than React's ) so non-JS social-share crawlers — Twitter card validator, Facebook scraper (Messenger/WhatsApp), Discord embed bot, Slack/LinkedIn/Telegram preview fetchers — see the full preview payload in the initial HTML response, before any JS executes. Googlebot also reads it directly without waiting for hydration. After client-side Inertia navigation the head goes "stale" relative to the new URL, but nothing meaningful reads head meta post-hydration (crawlers always fetch fresh, browsers track which Inertia auto-updates, the Web Share API uses document.title not OG). --}} @if($og) @if(! empty($og['description'])) <meta name="description" content="{{ $og['description'] }}"> @endif @if(! empty($og['url'])) <link rel="canonical" href="{{ $og['url'] }}"> @endif @if($shouldNoindex) {{-- Match the X-Robots-Tag header AddNoindexHeader emits and the meta tag on auth pages — `nofollow` is stronger than `follow` and the three signal sources should agree. --}} <meta name="robots" content="noindex, nofollow"> @endif {{-- Open Graph --}} @if(! empty($og['type'])) <meta property="og:type" content="{{ $og['type'] }}"> @endif @if(! empty($og['site_name'])) <meta property="og:site_name" content="{{ $og['site_name'] }}"> @endif @if(! empty($og['title'])) <meta property="og:title" content="{{ $og['title'] }}"> @endif @if(! empty($og['description'])) <meta property="og:description" content="{{ $og['description'] }}"> @endif @if(! empty($og['url'])) <meta property="og:url" content="{{ $og['url'] }}"> @endif @if(! empty($og['locale'])) <meta property="og:locale" content="{{ $og['locale'] }}"> @endif @if(! empty($og['image'])) <meta property="og:image" content="{{ $og['image'] }}"> @if(! empty($og['image_width'])) <meta property="og:image:width" content="{{ (int) $og['image_width'] }}"> @endif @if(! empty($og['image_height'])) <meta property="og:image:height" content="{{ (int) $og['image_height'] }}"> @endif @if(! empty($og['image_alt'])) <meta property="og:image:alt" content="{{ $og['image_alt'] }}"> @endif @if(! empty($og['image_type'])) <meta property="og:image:type" content="{{ $og['image_type'] }}"> @endif @endif {{-- Twitter Cards --}} <meta name="twitter:card" content="{{ $twitterCard }}"> @if(! empty($og['title'])) <meta name="twitter:title" content="{{ $og['title'] }}"> @endif @if(! empty($og['description'])) <meta name="twitter:description" content="{{ $og['description'] }}"> @endif @if(! empty($og['image'])) <meta name="twitter:image" content="{{ $og['image'] }}"> @if(! empty($og['image_alt'])) <meta name="twitter:image:alt" content="{{ $og['image_alt'] }}"> @endif @endif @endif {{-- hreflang alternates — suppressed on noindexed pages because Google's guidance is that hreflang on a noindexed URL is ignored and emitting an asymmetric alternate set creates a confusing signal. --}} @if(! $shouldNoindex && ! empty($hreflang)) @foreach($hreflang as $alt) @if(is_array($alt) && ! empty($alt['code']) && ! empty($alt['url'])) <link rel="alternate" hreflang="{{ $alt['code'] }}" href="{{ $alt['url'] }}"> @endif @endforeach @php($defaultAlt = collect($hreflang)->firstWhere('is_default', true)) @if($defaultAlt && ! empty($defaultAlt['url'])) <link rel="alternate" hreflang="x-default" href="{{ $defaultAlt['url'] }}"> @endif @endif {{-- JSON-LD structured data. JSON_HEX_TAG escapes < and > to the Unicode hex sequences < and >, so any "</script>" embedded in data becomes "</script>" and cannot terminate the enclosing script element — defense-in-depth against user content (game titles, profile bios) breaking out. JSON_UNESCAPED_SLASHES is safe because the < guard already neutralizes every closing-tag sequence. --}} @if($jsonLd) <script type="application/ld+json">{!! json_encode($jsonLd, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}</script> @endif {{-- Server-rendered title — gives non-JS bots a per-page title rather than the site-name placeholder. Inertia's client manages the `<title inertia>` element on subsequent client-side navigation (the marker attribute is what Inertia tracks; the initial content is overwritten on hydration). --}} @if($og && ! empty($og['title'])) <title inertia>{{ $og['title'] }} @else {{ $siteName }} @endif @routes @viteReactRefresh @vite(['resources/css/app.css', 'resources/js/app.tsx']) @inertiaHead @inertia