fix: apply seo agent improvements to src/app/layout.tsx
This commit is contained in:
parent
5bc59452f6
commit
dfa4d9db48
|
|
@ -1,48 +1,170 @@
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata, Viewport } from 'next'
|
||||||
import { Toaster } from 'react-hot-toast'
|
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com'
|
||||||
title: { default: 'CMS', template: '%s | CMS' },
|
const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME || 'My Personal Site'
|
||||||
description: 'Personal Content Management System',
|
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 (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
|
{/* Preconnect to common CDNs — adjust to your actual origins */}
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{/* Skip-to-content for keyboard / screen-reader users */}
|
||||||
<a
|
<a
|
||||||
href="#main-content"
|
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
|
Skip to main content
|
||||||
</a>
|
</a>
|
||||||
{children}
|
|
||||||
<Toaster
|
{/* Root structured data */}
|
||||||
position="top-right"
|
<script
|
||||||
toastOptions={{
|
type="application/ld+json"
|
||||||
duration: 3500,
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
|
||||||
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' },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue