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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ ISR endpoint at `GET /api/revalidate?secret=<SECRET>&slug=/blog/<slug>`. 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 `<sup>` under the same `showAffSup` rule. Single-author / single-affiliation entries collapse to clean text — no orphan markers. Each `<sup>` 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 `<sup>` under the same `showAffSup` rule. Single-author / single-affiliation entries collapse to clean text — no orphan markers. Each `<sup>` 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

Expand Down
27 changes: 6 additions & 21 deletions src/components/content/blog/HeadingContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,14 @@ export const HeadingContent: React.FunctionComponent<HeadingContentProps> = (pro

return (
<section className='mx-auto flex w-full max-w-2xl flex-col items-center'>
{/* 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 && (
<figure className='mt-10 mb-6 w-full overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-800/40'>
<WrappedImage
src={props.thumbnail}
alt={props.title}
width={1200}
height={630}
sizes='(max-width: 768px) 100vw, 768px'
priority
className='h-auto w-full object-cover'
/>
</figure>
)}

{/* 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. */}
<h1
className={twclsx(
'mb-4 text-center text-4xl leading-tight font-extrabold tracking-tight text-gray-900 md:text-5xl dark:text-white',
!props.thumbnail && 'mt-10'
'mt-10 mb-4 text-center text-4xl leading-tight font-extrabold tracking-tight text-gray-900 md:text-5xl dark:text-white'
)}
>
{props.title}
Expand Down
29 changes: 19 additions & 10 deletions src/components/content/research/ResearchItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -59,22 +58,32 @@ export const ResearchItem: React.FunctionComponent<ResearchItemProps> = (props)

return (
<article className='group w-full py-8'>
<div className='flex flex-row items-start gap-5 md:gap-7'>
<div className='flex flex-col gap-5 md:flex-row md:items-start md:gap-7'>
<UnstyledLink
href={urlPost}
className='relative block aspect-[5/4] w-32 flex-shrink-0 overflow-hidden rounded-md bg-gray-100 shadow-sm ring-1 ring-gray-200 sm:w-36 md:w-44 dark:bg-gray-800 dark:ring-gray-800'
className={twclsx(
'relative block w-full flex-shrink-0 overflow-hidden rounded-md bg-gray-100 shadow-sm ring-1 ring-gray-200 md:w-44 dark:bg-gray-800 dark:ring-gray-800',
// ComingSoon needs an explicit aspect-ratio (the placeholder is a
// styled div with no intrinsic dimensions). Real images render at
// their natural ratio so the card height tracks the image.
props.comingSoon && 'aspect-[5/4]'
)}
>
{props.comingSoon ? (
<ComingSoonImage className='transition-transform duration-300 group-hover:scale-[1.03]' />
) : (
<NextImage
// Native <img> 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
<img
src={imageSrc}
alt={props.title}
fill
sizes='(max-width: 640px) 128px, (max-width: 768px) 144px, 176px'
quality={90}
className='object-cover transition-transform duration-300 group-hover:scale-[1.03]'
priority={props.priority ?? false}
loading={props.priority ? 'eager' : 'lazy'}
decoding='async'
className='block h-auto w-full transition-transform duration-300 group-hover:scale-[1.03]'
/>
)}
</UnstyledLink>
Expand Down Expand Up @@ -107,7 +116,7 @@ export const ResearchItem: React.FunctionComponent<ResearchItemProps> = (props)
)}

{venueLine && (
<div className='mb-2.5 text-sm leading-snug font-bold text-gray-900 dark:text-gray-200'>[{venueLine}]</div>
<div className='mb-2.5 text-sm leading-snug text-gray-700 italic dark:text-gray-400'>{venueLine}</div>
)}

{actions.length > 0 && (
Expand Down
31 changes: 21 additions & 10 deletions src/components/content/research/ResearchTeaser.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { WrappedImage } from '@/components/site/images'

import { twclsx } from '@/libs/twclsx'

import NextImage from 'next/image'

type ResearchTeaserProps = {
src: string
alt: string
caption?: string
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<ResearchTeaserProps> = ({ src, alt, caption, priority }) => {
return (
<figure className={twclsx('not-prose my-4')}>
<WrappedImage
src={src}
alt={alt}
fill
priority={priority ?? true}
parentStyle='w-full h-72 sm:h-96 md:h-[28rem]'
className={twclsx('rounded-lg object-cover')}
/>
<div
className={twclsx('relative aspect-video w-full overflow-hidden rounded-lg', 'bg-gray-100 dark:bg-gray-800/40')}
>
<NextImage
src={src}
alt={alt}
fill
priority={priority ?? true}
sizes='(max-width: 768px) 100vw, 768px'
className='object-contain'
/>
</div>
{caption && (
<figcaption
className={twclsx('text-center text-sm text-gray-500 dark:text-gray-400', 'mx-auto mt-3 max-w-2xl italic')}
Expand Down
5 changes: 4 additions & 1 deletion src/components/site/links/UnstyledLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ export const UnstyledLink = forwardRef<HTMLAnchorElement, UnstyledLinkProps>(({
)
}

// 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 (
<NextLink href={href} scroll={false} {...props} ref={ref}>
<NextLink href={href} {...props} ref={ref}>
{children}
</NextLink>
)
Expand Down
Loading