fix: apply seo agent improvements to src/app/layout.tsx

This commit is contained in:
cupadev-admin 2026-03-09 20:55:08 +00:00
parent 5bc59452f6
commit dfa4d9db48
1 changed files with 151 additions and 29 deletions

View File

@ -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 (
<html lang="en" suppressHydrationWarning>
<head>
{/* Preconnect to common CDNs — adjust to your actual origins */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
</head>
<body>
{/* Skip-to-content for keyboard / screen-reader users */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[9999] focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white focus:rounded-lg focus:text-sm focus:font-medium"
className="skip-link"
aria-label="Skip to main content"
>
Skip to main content
</a>
{children}
<Toaster
position="top-right"
toastOptions={{
duration: 3500,
style: {
background: '#0f172a',
color: '#f8fafc',
fontSize: '14px',
fontWeight: '500',
borderRadius: '10px',
padding: '12px 16px',
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.2)',
},
success: {
iconTheme: { primary: '#34d399', secondary: '#0f172a' },
},
error: {
iconTheme: { primary: '#f87171', secondary: '#0f172a' },
},
}}
{/* Root structured data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
/>
{children}
</body>
</html>
)