From 338818c33ad91d9685fd84e5475137db123dff7f Mon Sep 17 00:00:00 2001 From: Josh Coolman Date: Thu, 12 Feb 2026 10:32:16 -0800 Subject: [PATCH] Add overlay hero layout for blog posts Posts with .overlay.md filename get a full-bleed hero image with title/meta overlaid on a gradient. The slug and image lookup are unchanged -- only the markdown filename triggers the variant. - Parse .overlay.md convention in loadBlog.ts - Add overlay boolean to BlogMeta type - Conditional overlay layout in post page - Full-bleed hero with gradient (transparent to opaque) - Applied to your-basic-blog as first overlay post Co-Authored-By: Claude Opus 4.6 --- app/(blog)/blog.module.css | 58 +++++++++++++++++++ app/(blog)/blog/[slug]/page.tsx | 33 +++++++++++ ...sic-blog.md => your-basic-blog.overlay.md} | 0 lib/blog/loadBlog.ts | 10 +++- lib/blog/types.ts | 1 + 5 files changed, 100 insertions(+), 2 deletions(-) rename blog/{your-basic-blog.md => your-basic-blog.overlay.md} (100%) diff --git a/app/(blog)/blog.module.css b/app/(blog)/blog.module.css index 82aadad..9964051 100644 --- a/app/(blog)/blog.module.css +++ b/app/(blog)/blog.module.css @@ -194,6 +194,64 @@ object-fit: cover; } +/* ─────────────────────────────────────────── + Overlay Hero + ─────────────────────────────────────────── */ + +.overlayHero { + position: relative; + width: 100vw; + margin-left: calc(50% - 50vw); + margin-top: -80px; + aspect-ratio: 16 / 9; + max-height: 480px; + overflow: hidden; + margin-bottom: 64px; +} + +.overlayImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.overlayGradient { + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + transparent 20%, + rgba(11, 11, 11, 0.5) 50%, + rgba(11, 11, 11, 0.85) 75%, + rgb(11, 11, 11) 100% + ); +} + +.overlayHeader { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: var(--col-max); + padding: 0 24px 32px; + z-index: 1; +} + +.overlayHeader .postMeta { + margin-bottom: 16px; + color: rgba(240, 237, 232, 0.7); +} + +.overlayHeader .postTitle { + color: #fff; + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5); +} + +.overlayHeader .postSubtitle { + color: rgba(240, 237, 232, 0.8); +} + /* ─────────────────────────────────────────── Prose (MDX content) ─────────────────────────────────────────── */ diff --git a/app/(blog)/blog/[slug]/page.tsx b/app/(blog)/blog/[slug]/page.tsx index a365f40..f477883 100644 --- a/app/(blog)/blog/[slug]/page.tsx +++ b/app/(blog)/blog/[slug]/page.tsx @@ -38,6 +38,39 @@ export default async function BlogPostPage({ params }: PageProps) { if (!post) notFound() + if (post.meta.overlay && post.meta.image) { + return ( +
+
+ {post.meta.title} +
+
+
+ + {post.meta.readingTime && ( + {post.meta.readingTime} read + )} +
+

{post.meta.title}

+ {post.meta.subtitle && ( +

{post.meta.subtitle}

+ )} +
+
+ + + +
+
Blog / 2026
+
+
+ ) + } + return (
diff --git a/blog/your-basic-blog.md b/blog/your-basic-blog.overlay.md similarity index 100% rename from blog/your-basic-blog.md rename to blog/your-basic-blog.overlay.md diff --git a/lib/blog/loadBlog.ts b/lib/blog/loadBlog.ts index 2958232..ab78b3a 100644 --- a/lib/blog/loadBlog.ts +++ b/lib/blog/loadBlog.ts @@ -24,7 +24,8 @@ export function getAllPosts(): BlogMeta[] { const files = fs.readdirSync(blogDirectory).filter((f) => f.endsWith('.md')) const posts: BlogMeta[] = files.map((fileName) => { - const slug = fileName.replace(/\.md$/, '') + const overlay = fileName.endsWith('.overlay.md') + const slug = fileName.replace(/\.overlay\.md$/, '').replace(/\.md$/, '') const filePath = path.join(blogDirectory, fileName) const fileContent = fs.readFileSync(filePath, 'utf-8') const { data } = matter(fileContent) @@ -35,6 +36,7 @@ export function getAllPosts(): BlogMeta[] { date: data.date ? String(data.date) : '', image: findImage(slug), readingTime: data.readingTime, + overlay, slug, } }) @@ -43,7 +45,10 @@ export function getAllPosts(): BlogMeta[] { } export function getPostBySlug(slug: string): BlogPost | null { - const filePath = path.join(blogDirectory, `${slug}.md`) + const overlayPath = path.join(blogDirectory, `${slug}.overlay.md`) + const regularPath = path.join(blogDirectory, `${slug}.md`) + const overlay = fs.existsSync(overlayPath) + const filePath = overlay ? overlayPath : regularPath if (!fs.existsSync(filePath)) return null const fileContent = fs.readFileSync(filePath, 'utf-8') @@ -56,6 +61,7 @@ export function getPostBySlug(slug: string): BlogPost | null { date: data.date ? String(data.date) : '', image: findImage(slug), readingTime: data.readingTime, + overlay, slug, }, content, diff --git a/lib/blog/types.ts b/lib/blog/types.ts index d658b79..02bfad3 100644 --- a/lib/blog/types.ts +++ b/lib/blog/types.ts @@ -4,6 +4,7 @@ export interface BlogMeta { date: string image?: string readingTime?: string + overlay?: boolean slug: string }