- Add sitemap.ts and robots.ts - pageMetadata() helper: consistent title, canonical URL, Open Graph, Twitter card and feed alternate on every page - Generated OG images: a site card, and per post either the cover image or a branded title card (pre-rendered at build) - BlogPosting JSON-LD on post pages - Fail fast with a clear error when SITE_URL is missing Content loading: - Validate post frontmatter at build time and normalize dates to ISO (fixes invalid <time datetime> values) - Add draft: true support (drafts only render in development) - Cache markdown rendering so metadata and page share one render - Replace the three copy-pasted about/projects/setup pages with a shared MarkdownPage component Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
import { Metadata } from 'next'
|
|
|
|
if (!process.env.SITE_URL) {
|
|
throw new Error('SITE_URL environment variable is not set (e.g. SITE_URL=https://example.com)')
|
|
}
|
|
|
|
export const SITE_URL = process.env.SITE_URL.replace(/\/$/, '')
|
|
export const SITE_NAME = 'Ephemeris'
|
|
export const SITE_DESCRIPTION = 'A blog about me and my interests'
|
|
export const AUTHOR = { name: 'Frank Borisjuk', handle: 'hangerthem', url: SITE_URL + '/about' }
|
|
|
|
// Child segments replace `alternates` wholesale, so every page re-declares the feed link
|
|
const feedAlternate = { 'application/atom+xml': [{ url: '/feed.xml', title: SITE_NAME }] }
|
|
|
|
export const metadata: Metadata = {
|
|
metadataBase: new URL(SITE_URL),
|
|
title: SITE_NAME,
|
|
description: SITE_DESCRIPTION,
|
|
creator: AUTHOR.handle,
|
|
authors: [{ name: AUTHOR.name, url: AUTHOR.url }],
|
|
alternates: { types: feedAlternate },
|
|
openGraph: {
|
|
type: 'website',
|
|
title: SITE_NAME,
|
|
description: SITE_DESCRIPTION,
|
|
url: SITE_URL,
|
|
siteName: SITE_NAME,
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: SITE_NAME,
|
|
description: SITE_DESCRIPTION,
|
|
},
|
|
appleWebApp: {
|
|
title: SITE_NAME,
|
|
statusBarStyle: 'default',
|
|
capable: true,
|
|
},
|
|
}
|
|
|
|
type PageMetadataOptions = {
|
|
title: string
|
|
description: string
|
|
/** Path relative to the site root, starting with a slash. */
|
|
path: string
|
|
/** Open Graph / Twitter image URL. Defaults to the generated site card. */
|
|
image?: string
|
|
openGraph?: Metadata['openGraph']
|
|
}
|
|
|
|
// Generated by app/opengraph-image.tsx. Child segments that set `openGraph` don't inherit
|
|
// file-based images, and explicit images win over them, so every page names its image here.
|
|
const defaultOgImage = '/opengraph-image'
|
|
|
|
/** Builds consistent page metadata (title, canonical URL, Open Graph and Twitter card). */
|
|
export function pageMetadata({
|
|
title,
|
|
description,
|
|
path,
|
|
image = defaultOgImage,
|
|
openGraph,
|
|
}: PageMetadataOptions): Metadata {
|
|
const fullTitle = `${title} | ${SITE_NAME}`
|
|
return {
|
|
title: fullTitle,
|
|
description,
|
|
alternates: { canonical: path, types: feedAlternate },
|
|
openGraph: {
|
|
type: 'website',
|
|
title: fullTitle,
|
|
description,
|
|
url: SITE_URL + path,
|
|
siteName: SITE_NAME,
|
|
images: image,
|
|
...openGraph,
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: fullTitle,
|
|
description,
|
|
images: image,
|
|
},
|
|
}
|
|
}
|