diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ab61159..8165052 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,48 +1,170 @@ -import type { Metadata } from 'next' -import { Toaster } from 'react-hot-toast' +import type { Metadata, Viewport } from 'next' import './globals.css' -export const metadata: Metadata = { - title: { default: 'CMS', template: '%s | CMS' }, - description: 'Personal Content Management System', +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com' +const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME || 'My Personal Site' +const SITE_DESCRIPTION = + process.env.NEXT_PUBLIC_SITE_DESCRIPTION || + 'A personal website and blog built with Next.js.' +const TWITTER_HANDLE = process.env.NEXT_PUBLIC_TWITTER_HANDLE || '@handle' + +// ─── Viewport (separate export, Next.js 14+) ──────────────────────────────── +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#ffffff' }, + { media: '(prefers-color-scheme: dark)', color: '#0f172a' }, + ], } -export default function RootLayout({ children }: { children: React.ReactNode }) { +// ─── Root Metadata ─────────────────────────────────────────────────────────── +export const metadata: Metadata = { + // Absolute template: child pages override `title.default` + title: { + default: SITE_NAME, + template: `%s | ${SITE_NAME}`, + }, + description: SITE_DESCRIPTION, + metadataBase: new URL(SITE_URL), + + // Canonical + RSS + alternates: { + canonical: '/', + types: { + 'application/rss+xml': `${SITE_URL}/feed.xml`, + }, + }, + + // Open Graph defaults (individual pages override these) + openGraph: { + type: 'website', + locale: 'en_US', + url: SITE_URL, + siteName: SITE_NAME, + title: SITE_NAME, + description: SITE_DESCRIPTION, + images: [ + { + url: `/og-default.png`, + width: 1200, + height: 630, + alt: SITE_NAME, + type: 'image/png', + }, + ], + }, + + // Twitter / X card defaults + twitter: { + card: 'summary_large_image', + site: TWITTER_HANDLE, + creator: TWITTER_HANDLE, + title: SITE_NAME, + description: SITE_DESCRIPTION, + images: [`/og-default.png`], + }, + + // Robots — allow indexing everywhere except /admin + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + + // Favicons / PWA icons + icons: { + icon: [ + { url: '/favicon.ico', sizes: 'any' }, + { url: '/icon.svg', type: 'image/svg+xml' }, + ], + apple: '/apple-touch-icon.png', + shortcut: '/favicon-32x32.png', + }, + + // Web app manifest + manifest: '/site.webmanifest', + + // Verification codes (fill in from Search Console / Bing etc.) + verification: { + google: process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION || '', + // bing: process.env.NEXT_PUBLIC_BING_SITE_VERIFICATION || '', + }, +} + +// ─── Root JSON-LD (WebSite + SearchAction) ────────────────────────────────── +const websiteJsonLd = { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: SITE_NAME, + url: SITE_URL, + description: SITE_DESCRIPTION, + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${SITE_URL}/search?q={search_term_string}`, + }, + 'query-input': 'required name=search_term_string', + }, +} + +const personJsonLd = { + '@context': 'https://schema.org', + '@type': 'Person', + name: SITE_NAME, + url: SITE_URL, + sameAs: [ + // Add your social profile URLs here + // 'https://twitter.com/handle', + // 'https://github.com/handle', + ], +} + +// ─── Layout Component ──────────────────────────────────────────────────────── +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { return (
+ {/* Preconnect to common CDNs — adjust to your actual origins */} - + + {/* Skip-to-content for keyboard / screen-reader users */} Skip to main content - {children} -