From e99a519bdce91b1dcb0eb3adf26604e884260e69 Mon Sep 17 00:00:00 2001 From: Ahnaf An Nafee Date: Sun, 26 Apr 2026 21:21:02 -0400 Subject: [PATCH 1/3] fix(content): mobile layout regressions on detail + listing pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five tightly-coupled fixes the user surfaced from a mobile read-through: 1. Blog post detail page no longer renders the OG thumbnail as a body hero. On narrow viewports it dominated the fold and pushed the headline below the scroll. The thumbnail still feeds OG/Twitter metadata + the BlogItem listing card; the post page keeps its canonical claim via JSON-LD primaryImage. 2. ResearchTeaser rebuilt with an `aspect-video` box + `object-contain` so the entire scientific figure is visible at every viewport width. The previous `fill + h-72/h-96/h-[28rem] + object-cover` pattern reframed the image at every breakpoint and clipped figure labels on mobile. 3. ResearchItem listing card stacks vertically on mobile (image full width above content) and lays out side-by-side from md+ — matching the BlogItem / PortfolioItem pattern. The previous always-row layout left a 128px-tall thumbnail squished against a column of wrapping text on phones. `sizes` updated so next/image requests the right resolution at each breakpoint. 4. ResearchItem venue line: dropped the `[…]` square brackets, switched to italic + softer color. Reads as a venue caption (academic convention) instead of bracketed metadata. 5. UnstyledLink no longer passes `scroll={false}` to NextLink. Default Next.js behavior (scroll-to-top on a new path, hash-aware for in-page anchors) is restored — the previous flag stranded readers mid-page when navigating between posts. Verified: yarn type-check, yarn lint, yarn test (48/48), yarn build (50 routes) all clean. --- CLAUDE.md | 2 +- next-env.d.ts | 2 +- .../content/blog/HeadingContent.tsx | 27 ++++------------ .../content/research/ResearchItem.tsx | 8 ++--- .../content/research/ResearchTeaser.tsx | 31 +++++++++++++------ src/components/site/links/UnstyledLink.tsx | 5 ++- 6 files changed, 37 insertions(+), 38 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0f0309d..3defbaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,7 +134,7 @@ ISR endpoint at `GET /api/revalidate?secret=&slug=/blog/`. Secret - `‡` (principal investigator) — `principalInvestigator: true` on the author. Shows when **2+ authors** AND at least one is PI. - `¹ ² ³` (affiliation indices) — derived from `affiliations: [...]` on each author. Shows when **2+ affiliations** exist on the entry. - Marker order on each author follows academic convention: `*†‡` then numeric indices. The affiliation legend below the author line drops its leading `` under the same `showAffSup` rule. Single-author / single-affiliation entries collapse to clean text — no orphan markers. Each `` has `cursor-help` + a native `title` tooltip (affiliation index sups resolve to the full affiliation name; symbol sups resolve to their caption text). A combined caption line appears below the affiliation row when any symbol marker is shown — e.g. `*Corresponding author · †Equal contribution · ‡Principal investigator` — joined by ` · `. + Marker order on each author follows academic convention: `*†‡` then numeric indices. The affiliation legend below the author line drops its leading `` under the same `showAffSup` rule. Single-author / single-affiliation entries collapse to clean text — no orphan markers. Each `` has `cursor-help` + a native `title` tooltip (affiliation index sups resolve to the full affiliation name; symbol sups resolve to their caption text). A combined caption line appears below the affiliation row when any symbol marker is shown — e.g. `*Corresponding author · †Equal contribution · ‡Principal investigator` — joined by `·`. ## Indexing Helper diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4e7c0e 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import './.next/types/routes.d.ts' // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/components/content/blog/HeadingContent.tsx b/src/components/content/blog/HeadingContent.tsx index 95eb3bc..f052864 100644 --- a/src/components/content/blog/HeadingContent.tsx +++ b/src/components/content/blog/HeadingContent.tsx @@ -41,29 +41,14 @@ export const HeadingContent: React.FunctionComponent = (pro return (
- {/* Hero thumbnail. Rendering the same image that BlogItem uses on /blog - gives the post detail page a strong claim as the canonical landing - for that image — Google Images links should resolve to the post - rather than the list page. */} - {props.thumbnail && ( -
- -
- )} - - {/* Title */} + {/* Title — the thumbnail in frontmatter feeds Open Graph + Twitter cards + (social previews) and BlogItem listing thumbnails. Intentionally NOT + rendered as an in-page hero: on narrow viewports it dominated the + fold and pushed the headline below the scroll, and the post detail + page already has a strong canonical claim via JSON-LD primaryImage. */}

{props.title} diff --git a/src/components/content/research/ResearchItem.tsx b/src/components/content/research/ResearchItem.tsx index 3ea3fbd..3f28c6c 100644 --- a/src/components/content/research/ResearchItem.tsx +++ b/src/components/content/research/ResearchItem.tsx @@ -59,10 +59,10 @@ export const ResearchItem: React.FunctionComponent = (props) return (
-
+
{props.comingSoon ? ( @@ -71,7 +71,7 @@ export const ResearchItem: React.FunctionComponent = (props) src={imageSrc} alt={props.title} fill - sizes='(max-width: 640px) 128px, (max-width: 768px) 144px, 176px' + sizes='(max-width: 768px) 100vw, 176px' quality={90} className='object-cover transition-transform duration-300 group-hover:scale-[1.03]' priority={props.priority ?? false} @@ -107,7 +107,7 @@ export const ResearchItem: React.FunctionComponent = (props) )} {venueLine && ( -
[{venueLine}]
+
{venueLine}
)} {actions.length > 0 && ( diff --git a/src/components/content/research/ResearchTeaser.tsx b/src/components/content/research/ResearchTeaser.tsx index 71c2e94..f2671c6 100644 --- a/src/components/content/research/ResearchTeaser.tsx +++ b/src/components/content/research/ResearchTeaser.tsx @@ -1,7 +1,7 @@ -import { WrappedImage } from '@/components/site/images' - import { twclsx } from '@/libs/twclsx' +import NextImage from 'next/image' + type ResearchTeaserProps = { src: string alt: string @@ -9,17 +9,28 @@ type ResearchTeaserProps = { priority?: boolean } +/** + * Hero figure on the research detail page. Uses a 16:9 aspect-ratio box with + * `object-contain` so the entire scientific figure is visible at every viewport + * width — research teasers usually have important content (axes, labels, + * sub-panels) edge-to-edge that can't be cropped by `object-cover`. The bg + * tile shows behind the image only when the source aspect ratio mismatches. + */ export const ResearchTeaser: React.FunctionComponent = ({ src, alt, caption, priority }) => { return (
- +
+ +
{caption && (
(({ ) } + // Default Next.js scroll behavior: scroll-to-top on a new path, hash-aware + // for in-page anchors. The previous `scroll={false}` blocked both, leaving + // readers stranded mid-page when navigating between posts. return ( - + {children} ) From 4360fda1045e1970079d1b4e633e90f784fb181d Mon Sep 17 00:00:00 2001 From: Ahnaf An Nafee Date: Sun, 26 Apr 2026 21:25:33 -0400 Subject: [PATCH 2/3] fix(research): show full thumbnail in listing (object-contain) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The research listing thumbnail used `object-cover` with `aspect-[5/4]`, which cropped the sides of the image when the source had a wider aspect ratio than 5:4 — losing visible content (e.g. the "Vertex Clustering" column on the mesh-decimation thumbnail got clipped on mobile). Research figures often have edge-to-edge content (axes, labels, panel titles) that can't be safely cropped by a center-focused fit. Switch to `object-contain` so the full image always fits within the box; the existing bg-gray tile fills any letterbox gaps when the source aspect ratio doesn't match 5:4. BlogItem / PortfolioItem stay on object-cover since those thumbnails are typically photographic with a center subject. --- next-env.d.ts | 2 +- src/components/content/research/ResearchItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index c4e7c0e..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import './.next/types/routes.d.ts' +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/components/content/research/ResearchItem.tsx b/src/components/content/research/ResearchItem.tsx index 3f28c6c..e8112e8 100644 --- a/src/components/content/research/ResearchItem.tsx +++ b/src/components/content/research/ResearchItem.tsx @@ -73,7 +73,7 @@ export const ResearchItem: React.FunctionComponent = (props) fill sizes='(max-width: 768px) 100vw, 176px' quality={90} - className='object-cover transition-transform duration-300 group-hover:scale-[1.03]' + className='object-contain transition-transform duration-300 group-hover:scale-[1.03]' priority={props.priority ?? false} /> )} From 3e09128a96a53d36e38e0b13ac509f7003c2c1de Mon Sep 17 00:00:00 2001 From: Ahnaf An Nafee Date: Sun, 26 Apr 2026 21:28:08 -0400 Subject: [PATCH 3/3] fix(research): card adopts image's natural aspect ratio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the fixed `aspect-[5/4]` box + `object-contain` (which letterboxed the image with a bg-gray tile) with a native `` rendered at `w-full h-auto`. The listing card now sizes itself to the image's intrinsic aspect ratio — no cropping, no letterboxing. Trade-off: gives up next/image's automatic AVIF/WebP and srcset for the listing thumbnails. ImageKit URLs already serve modern formats based on Accept headers, and we still apply the right width transform via resolveListingImage (`?tr=w-600`), so the practical loss is small. The ComingSoon placeholder keeps the explicit `aspect-[5/4]` since it's a styled div with no intrinsic dimensions to fall back on. --- .../content/research/ResearchItem.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/content/research/ResearchItem.tsx b/src/components/content/research/ResearchItem.tsx index e8112e8..09cc0a7 100644 --- a/src/components/content/research/ResearchItem.tsx +++ b/src/components/content/research/ResearchItem.tsx @@ -7,7 +7,6 @@ import { twclsx } from '@/libs/twclsx' import { ComingSoonImage } from './ComingSoonImage' import type { Research } from 'me' -import NextImage from 'next/image' import { Fragment } from 'react' type ResearchItemProps = Research & { priority?: boolean } @@ -62,19 +61,29 @@ export const ResearchItem: React.FunctionComponent = (props)
{props.comingSoon ? ( ) : ( - instead of next/image: next/image with `fill` + // requires a fixed-aspect parent and would either crop + // (object-cover) or letterbox (object-contain) — neither matches + // "card adopts the image's natural aspect ratio". ImageKit URLs + // already include the right width transform via resolveListingImage. + // eslint-disable-next-line @next/next/no-img-element + {props.title} )}