diff --git a/app/layout.tsx b/app/layout.tsx index dc4fc2b..fc7dcf1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -14,28 +14,36 @@ import { Footer } from '@/components/Footer'; import { DevToolbar } from '@/components/DevToolbar'; import { getConfig, getGoogleFontsUrl } from '@/lib/config'; -const siteTitle = process.env.SITE_TITLE || 'Gallery'; -const siteDescription = 'A curated photography portfolio'; +export async function generateMetadata(): Promise { + const config = getConfig(); + const siteTitle = config.seo.title; + const siteDescription = config.seo.description; + const robots = { + index: !config.seo.noIndex, + follow: !config.seo.noFollow, + }; -export const metadata: Metadata = { - title: siteTitle, - description: siteDescription, - icons: { - apple: '/apple-touch-icon.png', - }, - openGraph: { + return { title: siteTitle, description: siteDescription, - type: 'website', - images: [`/api/og?title=${encodeURIComponent(siteTitle)}`], - }, - twitter: { - card: 'summary_large_image', - title: siteTitle, - description: siteDescription, - images: [`/api/og?title=${encodeURIComponent(siteTitle)}`], - }, -}; + robots, + icons: { + apple: '/apple-touch-icon.png', + }, + openGraph: { + title: siteTitle, + description: siteDescription, + type: 'website', + images: [`/api/og?title=${encodeURIComponent(siteTitle)}`], + }, + twitter: { + card: 'summary_large_image', + title: siteTitle, + description: siteDescription, + images: [`/api/og?title=${encodeURIComponent(siteTitle)}`], + }, + }; +} export default function RootLayout({ children }: { children: React.ReactNode }) { const config = getConfig(); diff --git a/content/settings.yaml.example b/content/settings.yaml.example index 55e6a48..955f574 100644 --- a/content/settings.yaml.example +++ b/content/settings.yaml.example @@ -6,6 +6,14 @@ title: "My Portfolio" subtitle: "A visual journal" +# ── SEO & Metadata ─────────────────────────────────────────────── +# Optional: Overrides global title/subtitle for search engines and social cards +# seo: +# title: "My Portfolio | Photography" +# description: "A curated selection of my best photography work." +# noIndex: false # Set to true to hide from search engines +# noFollow: false # Set to true to prevent search engines from following links + # Show EXIF metadata (camera, lens, ISO) on photo grid hover exifOnHover: true diff --git a/lib/config.ts b/lib/config.ts index d98c07c..56e3344 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -154,6 +154,12 @@ interface GalleryYaml { interface SettingsYaml { title?: string; subtitle?: string; + seo?: { + title?: string; + description?: string; + noIndex?: boolean; + noFollow?: boolean; + }; exifOnHover?: boolean; map?: boolean; transitions?: boolean; @@ -245,6 +251,12 @@ export interface AppConfig { subpages: SubpageConfig[]; siteTitle: string; siteSubtitle: string; + seo: { + title: string; + description: string; + noIndex: boolean; + noFollow: boolean; + }; /** One or more hero image asset IDs for the homepage carousel. */ heroImages: string[]; /** Show EXIF (camera/lens) on photo grid hover. Default: true. */ @@ -472,6 +484,12 @@ export function getConfig(): AppConfig { subpages, siteTitle: settings.title ?? env.SITE_TITLE, siteSubtitle: settings.subtitle ?? env.SITE_SUBTITLE, + seo: { + title: settings.seo?.title || settings.title || env.SITE_TITLE || 'Gallery', + description: settings.seo?.description || settings.subtitle || env.SITE_SUBTITLE || 'A curated photography portfolio', + noIndex: settings.seo?.noIndex === true, + noFollow: settings.seo?.noFollow === true, + }, heroImages: gallery.hero ? (Array.isArray(gallery.hero) ? gallery.hero : [gallery.hero]).map((id) => validateUuid(id, 'gallery.yaml hero'),