vkashti / app / layout.tsx
layout.tsx
Raw
import { Metadata } from 'next';
import Footer from '@/components/ui/Footer';
import { Toaster } from '@/components/ui/Toasts/toaster';
import { PropsWithChildren, Suspense } from 'react';
import { getURL } from '@/utils/helpers';
import 'styles/main.css';
import Script from 'next/script';
import { Inter, Roboto_Condensed } from 'next/font/google';
import NavbarClient from '@/components/ui/NavbarClient';
import GoogleAnalytics from './components/GoogleAnalytics';

// Font optimization - use display:swap to prevent font blocking
const inter = Inter({
  subsets: ['latin', 'cyrillic'],
  display: 'swap',
  variable: '--font-inter',
  preload: true, // Ensure font preloading
});

// Properly load Roboto Condensed with next/font/google instead of CSS import
const robotoCondensed = Roboto_Condensed({
  subsets: ['latin', 'cyrillic'],
  display: 'swap',
  variable: '--font-roboto-condensed',
  preload: true,
  weight: ['400', '500', '700']
});

const title = 'Вкъщи Бар';
const description =
  'Организираме събития на живо, настолни игри и куизове в приятелска, социална обстановка с готини коктейли. Настроението е това, което ни отличава.';

// Google Analytics ID
const GA_MEASUREMENT_ID = 'G-1E9WF6B13X';

export const metadata: Metadata = {
  metadataBase: new URL(getURL()),
  title: {
    default: title,
    template: `%s | ${title}`
  },
  description: description,
  keywords: ['бар', 'коктейли', 'събития', 'настолни игри', 'куизове', 'София', 'парти', 'резервации'],
  authors: [{ name: 'Вкъщи Бар' }],
  creator: 'Вкъщи Бар',
  publisher: 'Вкъщи Бар',
  formatDetection: {
    email: false,
    address: false,
    telephone: false
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-image-preview': 'large',
      'max-snippet': -1
    }
  },
  openGraph: {
    type: 'website',
    locale: 'bg_BG',
    url: getURL(),
    siteName: title,
    images: [
      {
        url: '/opengraph-image.png',
        width: 1686,
        height: 882,
        alt: 'Вкъщи Бар'
      }
    ],
    title: title,
    description: description
  },
  twitter: {
    card: 'summary_large_image',
    title: title,
    description: description,
    images: ['/opengraph-image.png']
  },
  alternates: {
    canonical: getURL()
  }
};

// JSON-LD structured data for LocalBusiness
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BarOrPub',
  name: 'Вкъщи Бар',
  image: `${getURL()}/opengraph-image.png`,
  '@id': getURL(),
  url: getURL(),
  telephone: '+359 87 961 1154',
  address: {
    '@type': 'PostalAddress',
    streetAddress: 'ул. Жеравна 6',
    addressLocality: 'София',
    postalCode: '1164',
    addressCountry: 'BG'
  },
  geo: {
    '@type': 'GeoCoordinates',
    latitude: 42.674431,
    longitude: 23.342463
  },
  openingHoursSpecification: [
    {
      '@type': 'OpeningHoursSpecification',
      dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday'],
      opens: '14:00',
      closes: '00:00'
    },
    {
      '@type': 'OpeningHoursSpecification',
      dayOfWeek: ['Friday', 'Saturday'],
      opens: '14:00',
      closes: '02:00'
    },
    {
      '@type': 'OpeningHoursSpecification',
      dayOfWeek: 'Sunday',
      opens: '14:00',
      closes: '00:00'
    }
  ],
  menu: `${getURL()}/menu`,
  servesCuisine: 'Bar, Cocktails, Snacks',
  priceRange: '$$',
  paymentAccepted: 'Cash, Credit Card',
  amenityFeature: [
    { name: 'Board Games' },
    { name: 'Quiz Nights' },
    { name: 'Cocktails' },
    { name: 'Coworking' }
  ]
};

export default function RootLayout({ children }: PropsWithChildren) {
  return (
    <html lang="bg" className={`${inter.variable} ${robotoCondensed.variable}`}>
      <head>
        <link rel="canonical" href={getURL()} />
        
        {/* Performance optimization meta tags */}
        <meta httpEquiv="x-dns-prefetch-control" content="on" />
        <link rel="preconnect" href={getURL()} crossOrigin="anonymous" />
        
        {/* Preload critical LCP images */}
        <link 
          rel="preload" 
          href="/logo.webp" 
          as="image" 
          type="image/webp"
          fetchPriority="high"
        />
        
        {/* DNS prefetch for commonly used third-party domains */}
        <link rel="dns-prefetch" href="https://www.googletagmanager.com" />
        <link rel="dns-prefetch" href="https://fonts.googleapis.com" />
        <link rel="dns-prefetch" href="https://fonts.gstatic.com" />
        
        {/* JSON-LD structured data */}
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
        />
      </head>
      <body className="min-h-screen flex flex-col">
        {/* Skip link for accessibility */}
        <a
          href="#main-content"
          className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:text-orange-600 focus:px-6 focus:py-3 focus:rounded-lg focus:shadow-lg focus:ring-2 focus:ring-orange-500 focus:font-bold focus:text-lg focus:border-2 focus:border-orange-500"
          aria-label="Пропусни до основното съдържание"
        >
          Пропусни до основното съдържание
        </a>

        {/* Google Analytics implementation */}
        <GoogleAnalytics />
        
        <NavbarClient />
        
        <main id="main-content" className="flex-grow">
          {children}
        </main>
        
        <Footer />
        
        <Suspense fallback={null}>
          <Toaster />
        </Suspense>
        
        {/* Fix BroadcastChannel caching issue by ensuring it's only created once needed */}
        <Script
          id="fix-bfcache"
          strategy="beforeInteractive"
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                // Save original BroadcastChannel constructor
                const OriginalBroadcastChannel = window.BroadcastChannel;
                
                // Replace with a version that only creates instances when actually needed
                window.BroadcastChannel = function(channel) {
                  let instance = null;
                  let listeners = [];
                  
                  // Return proxy object that defers instance creation until it's actually used
                  return new Proxy({}, {
                    get: function(target, prop) {
                      if (prop === 'postMessage') {
                        // Only create instance when actually posting messages
                        if (!instance) instance = new OriginalBroadcastChannel(channel);
                        return instance.postMessage.bind(instance);
                      }
                      
                      if (prop === 'addEventListener') {
                        return function(type, listener) {
                          if (!instance) instance = new OriginalBroadcastChannel(channel);
                          listeners.push({ type, listener });
                          instance.addEventListener(type, listener);
                        };
                      }
                      
                      if (prop === 'removeEventListener') {
                        return function(type, listener) {
                          if (instance) {
                            instance.removeEventListener(type, listener);
                            listeners = listeners.filter(l => l.type !== type || l.listener !== listener);
                            
                            // If no more listeners, close channel to help with bfcache
                            if (listeners.length === 0) {
                              instance.close();
                              instance = null;
                            }
                          }
                        };
                      }
                      
                      if (prop === 'close') {
                        return function() {
                          if (instance) {
                            instance.close();
                            instance = null;
                            listeners = [];
                          }
                        };
                      }
                      
                      // For any other property, create instance if needed
                      if (!instance) instance = new OriginalBroadcastChannel(channel);
                      return instance[prop];
                    }
                  });
                };
              })();
            `
          }}
        />
        
        {/* Preconnect script - executed after page is interactive */}
        <Script
          id="preconnect-resources"
          strategy="lazyOnload"
          dangerouslySetInnerHTML={{
            __html: `
              // Only load connections we actually need
              const activeConnections = new Set();
              
              // Connect to a domain only when needed
              function connectToDomain(url) {
                if (activeConnections.has(url)) return;
                
                const link = document.createElement('link');
                link.rel = 'preconnect';
                link.href = url;
                link.crossOrigin = 'anonymous';
                document.head.appendChild(link);
                activeConnections.add(url);
              }
              
              // Only connect to domains as they're needed
              if (document.querySelector('script[src*="google"]')) {
                connectToDomain('https://www.google-analytics.com');
              }
              
              // Connect to other domains only when they're about to be used
              document.addEventListener('mouseover', e => {
                const link = e.target.closest('a');
                if (link && link.href && link.href.includes('maps.google')) {
                  connectToDomain('https://maps.googleapis.com');
                }
              }, { passive: true });
            `
          }}
        />
      </body>
    </html>
  );
}