Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions app/(blog)/blog.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,64 @@
object-fit: cover;
}

/* ───────────────────────────────────────────
Overlay Hero
─────────────────────────────────────────── */

.overlayHero {
position: relative;
width: 100vw;
margin-left: calc(50% - 50vw);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full-bleed hero can trigger horizontal scrolling

Low Severity

The new full-bleed hero uses width: 100vw with margin-left: calc(50% - 50vw), which can exceed the layout width when a vertical scrollbar is present. Overlay posts may show an unwanted horizontal scrollbar and slight sideways page shift on some browsers.

Fix in Cursor Fix in Web

margin-top: -80px;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mobile hero offset overshoots viewport top

Medium Severity

margin-top: -80px in blog.module.css cancels desktop postWrapper padding, but mobile uses smaller top padding. Overlay posts can render shifted above the viewport on small screens, so the hero no longer cleanly starts at the top and content alignment becomes inconsistent.

Fix in Cursor Fix in Web

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)
─────────────────────────────────────────── */
Expand Down
33 changes: 33 additions & 0 deletions app/(blog)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,39 @@ export default async function BlogPostPage({ params }: PageProps) {

if (!post) notFound()

if (post.meta.overlay && post.meta.image) {
return (
<div className={styles.postWrapper}>
<div className={styles.overlayHero}>
<img
src={post.meta.image}
alt={post.meta.title}
className={styles.overlayImage}
/>
<div className={styles.overlayGradient} />
<header className={styles.overlayHeader}>
<div className={styles.postMeta}>
<time>{formatDate(post.meta.date)}</time>
{post.meta.readingTime && (
<span className={styles.readingTime}>{post.meta.readingTime} read</span>
)}
</div>
<h1 className={styles.postTitle}>{post.meta.title}</h1>
{post.meta.subtitle && (
<p className={styles.postSubtitle}>{post.meta.subtitle}</p>
)}
</header>
</div>

<BlogContent content={post.content} />

<footer className={styles.postFooter}>
<div className={styles.footerNote}>Blog / 2026</div>
</footer>
</div>
)
}

return (
<div className={styles.postWrapper}>
<header className={styles.postHeader}>
Expand Down
File renamed without changes.
10 changes: 8 additions & 2 deletions lib/blog/loadBlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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$/, '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlay and regular files can collide

Low Severity

getAllPosts() now normalizes both post.md and post.overlay.md to the same slug, but still loads every .md file. If both files exist, the index and static params can contain duplicate slugs, causing duplicate cards and conflicting route generation.

Additional Locations (1)

Fix in Cursor Fix in Web

const filePath = path.join(blogDirectory, fileName)
const fileContent = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(fileContent)
Expand All @@ -35,6 +36,7 @@ export function getAllPosts(): BlogMeta[] {
date: data.date ? String(data.date) : '',
image: findImage(slug),
readingTime: data.readingTime,
overlay,
slug,
}
})
Expand All @@ -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')
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/blog/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface BlogMeta {
date: string
image?: string
readingTime?: string
overlay?: boolean
slug: string
}

Expand Down