{{-- Public landing wrapper. Resolves the active skin for THIS request (visitor's panel, or a staff ?preview_landing=X) and the panel's content, seeds the CSS variables the shared stylesheet reads, then renders one shared section partial for every skin. Self-contained: inline styles + inline SVG icons + tiny inline JS (scroll reveal). No @vite, no CDN, no icon library — SEO / no-JS safe. Controller passes: $tpl (key), $meta (skin config), $content, $appName, $studioPreview (true only for signed-in staff on "/?preview=1" — the Landing Studio canvas). --}} @php $flags = (array) ($meta['flags'] ?? []); $vars = (array) ($meta['vars'] ?? []); $brand = panel_brand(); $onBrand = panel_on_primary(); // Font names are written UNQUOTED. CSS accepts a multi-word family name as a // sequence of identifiers, so dropping the quotes removes the only reason // this value ever needed an escaper it could not have (see App\Support\Css). $displayFont = ($flags['display_font'] ?? 'sans') === 'serif' ? 'Georgia, Times New Roman, ui-serif, serif' : 'var(--lp-font-body)'; $c = $content; $studioPreview = $studioPreview ?? false; // SEO: the Landing Studio's own fields override Settings → SEO, but ONLY // when they are filled in. Blank inherits the global default — it must never // fall back to a literal written here, which is what made a saved // Settings → SEO title look like it did nothing on the home page. $seoTitle = \App\Support\Seo::landingTitle(data_get($c, 'seo.title')); $seoDesc = \App\Support\Seo::description(data_get($c, 'seo.description')); $seoKeywords = trim((string) data_get($c, 'seo.keywords', '')); $seoImage = \App\Support\Seo::image(); $seoCanonical = \App\Support\Seo::canonical(); // Sanitise the skin variables ONCE so the style block below is a dumb // printer. Any pair that is not a valid custom property and a valid // declaration value is dropped rather than emitted half-escaped. $cssVars = []; foreach ($vars as $k => $v) { $cssName = \App\Support\Css::name($k); $cssVal = \App\Support\Css::value($v); if ($cssName !== '' && $cssVal !== '') { $cssVars[$cssName] = $cssVal; } } @endphp {{-- Flag JS-available before paint so scroll-reveal only hides what it can also un-hide (no-JS / crawlers / print see everything). --}} {{ $seoTitle }} @if ($seoDesc !== '')@endif @if ($seoKeywords !== '')@endif {{-- Indexing switch, canonical host and share image come from Settings → SEO exactly as they do on every other public page. The landing shipped without them, so the one page a crawler always reaches was the one page the operator's SEO settings could not reach. --}} @if ($seoDesc !== '')@endif @if ($seoImage !== '')@endif {{-- The buyer's own favicon wins, exactly as it does in every panel layout. The generated square is only the fallback for an install that has not uploaded one yet; leaving the landing page hard-wired to it was the one surface where our tab icon survived a rebrand. --}} @php $rfFavicon = panel_favicon_url(); @endphp @if ($rfFavicon) @else @endif {{-- Skin seed: brand + skin vars + display font. This is the ONLY per-request style; everything else is static in partials.styles. Printed with {!! !!} through App\Support\Css, NOT with {{ }}. Inside a style block Blade's HTML escaping is the wrong escaper twice over: it mangles legitimate CSS (a quoted font name arrived as '…' and the browser dropped the declaration) while leaving ; { } — the characters that actually break out of a rule — untouched. Css::color() and Css::value() sanitise server-side against an allowlist, which is what makes printing raw here safe. --}} @include('frontend.partials.styles') {{-- Analytics (GA4 / GTM / Meta pixel / custom) from Settings → Analytics. Emits nothing unless the owner enabled it — no CDN loads while off. --}} @include('partials.analytics') @if (request()->query('preview_landing') && staff_may_preview_templates())
{{ __('Preview — :template skin (not saved).', ['template' => $meta['label'] ?? $tpl]) }}
@endif @include('frontend.partials.sections') {{-- Watch-demo lightbox. The player is only built on open (so it never loads or autoplays in the background) and torn down on close (stops playback). Supports a self-hosted .mp4/.webm