π§ Context
Contributors will upload covers of every shape β square, portrait, ultrawide, screenshots, logos. The card tile is a fixed 16:9 area (aspect-video), chosen because the typical cover is a desktop screenshot. The scaffold ships a minimal src/components/CardCover.astro that uses a plain <img> with object-cover, which crops (a tall poster loses its top/bottom, a logo gets chopped). This ticket makes any cover look good with no crop loss and no letterbox bars, plus build-time image optimization.
The chosen approach is a blurred-backdrop fill (YouTube/Spotify style): show the whole image centered, with a blurred, zoomed copy of the same image filling the space behind it so there are no bars. Important: because the tile is 16:9, a properly-sized desktop screenshot fills it completely and the blur is never visible β the blur is purely a fallback that fills the gaps behind off-aspect images (portraits, logos, tall full-page screenshots). It is not an always-on effect.
If you think a different approach might look better, feel free to play around with options and share them for feedback! The overall goal is to ensure any cover image works properly with the project cards without cropping too much or leaving space on the sides.
This ticket also makes the cover optional. Some projects (a CLI, a library) have nothing to screenshot, so a contributor may legitimately omit it. When there's no cover, the card must still look intentional β render a generated title-block fallback (e.g. a block with the project title matching the theme of the rest of the site) in the same 16:9 tile, rather than a broken/empty image.
Files you'll touch:
src/components/CardCover.astro (the main job β blurred fill + the no-cover fallback)
src/content.config.ts (make the cover field optional)
src/pages/projects/[slug].astro (a minimal null-guard only β see step 4)
pnpm-workspace.yaml (flip one build flag)
README.md (a one-line cover-size recommendation)
Don't touch:
src/components/Card.astro β the card body/layout is owned by a separate ticket. (It already passes cover={data.cover} straight through, which becomes ImageMetadata | undefined once the field is optional β no change needed there.)
- The detail page's layout/styling and its own cover fallback β that's owned by a separate detail-page ticket. Your only change to
[slug].astro is the minimal build-green guard in step 4; do not style it.
- The 16:9 tile aspect (
aspect-video) and the bg-canvas fallback β keep them.
π οΈ Implementation Plan
-
Make the cover optional in the schema. In src/content.config.ts, change the projects cover field from image() to image().optional(). This is what lets a project ship without a cover; it also makes data.cover typed as ImageMetadata | undefined everywhere it's read, which drives steps 3 and 4.
-
Blurred-backdrop fill in CardCover.astro (when a cover exists). Inside the 16:9 box, stack two layers: a background copy of the image (object-cover, scaled up, filter: blur(...)) and a foreground copy (object-contain, centered) that's never cropped. Work out the exact layering/values yourself β Tailwind utilities plus a small scoped <style> if needed (tokens are available as var(--color-*)). Optimize via astro:assets: switch the plain <img> to the <Image> component (the cover prop is ImageMetadata). Then set sharp: true in pnpm-workspace.yaml under allowBuilds (it's currently false because the scaffold used a plain <img>):
allowBuilds:
esbuild: true
sharp: true
If pnpm install/build surfaces a supply-chain policy error here, flag it to Jacc rather than working around it.
If you tried a different approach, share with Jacc for feedback.
-
No-cover fallback in CardCover.astro. The prop becomes optional (cover?: ImageMetadata). When it's undefined, render a generated title-block fallback instead of <Image>: the same 16:9 tile filled with a Carleton-themed background (use the tokens β e.g. bg-accent or a canvas/surface block) and the project title centered in it, so the card still reads as intentional. The component already receives title, so use it for the fallback label. Branch on whether cover exists; the blurred-fill path only runs when it does.
-
Keep the detail page building (minimal guard only). Making cover optional means src/pages/projects/[slug].astro β which currently does data.cover.src β will fail pnpm check under strict TS. Add only a minimal null-guard so the build stays green: wrap its existing cover <img> so it renders only when data.cover is defined (e.g. {data.cover && (<img β¦ />)}). Do not style it or build a fancy fallback there β the detail page's own missing-cover treatment is owned by a separate detail-page ticket. This is purely "don't crash the build."
-
Test with real variety. Drop a square, a portrait, and an ultrawide image into a couple of src/content/projects/*/ entries and confirm all three render with no crop loss and no bars. Then remove the cover line from one entry entirely and confirm the card shows the title-block fallback and the build still passes.
-
Document the cover spec. Add a one-line "recommended cover for best results" note to the Adding a project section of README.md (suggested: a 16:9 desktop screenshot, around 1600Γ900, reasonable file size β and that it's optional). The blurred fill means off-spec covers still look fine β this is just guidance.
Leave the coming soon text as well since the full section isn't covered yet. Just add the image size note.
β
Acceptance Criteria
π§ Context
Contributors will upload covers of every shape β square, portrait, ultrawide, screenshots, logos. The card tile is a fixed 16:9 area (
aspect-video), chosen because the typical cover is a desktop screenshot. The scaffold ships a minimalsrc/components/CardCover.astrothat uses a plain<img>withobject-cover, which crops (a tall poster loses its top/bottom, a logo gets chopped). This ticket makes any cover look good with no crop loss and no letterbox bars, plus build-time image optimization.The chosen approach is a blurred-backdrop fill (YouTube/Spotify style): show the whole image centered, with a blurred, zoomed copy of the same image filling the space behind it so there are no bars. Important: because the tile is 16:9, a properly-sized desktop screenshot fills it completely and the blur is never visible β the blur is purely a fallback that fills the gaps behind off-aspect images (portraits, logos, tall full-page screenshots). It is not an always-on effect.
If you think a different approach might look better, feel free to play around with options and share them for feedback! The overall goal is to ensure any cover image works properly with the project cards without cropping too much or leaving space on the sides.
This ticket also makes the cover optional. Some projects (a CLI, a library) have nothing to screenshot, so a contributor may legitimately omit it. When there's no cover, the card must still look intentional β render a generated title-block fallback (e.g. a block with the project title matching the theme of the rest of the site) in the same 16:9 tile, rather than a broken/empty image.
Files you'll touch:
src/components/CardCover.astro(the main job β blurred fill + the no-cover fallback)src/content.config.ts(make thecoverfield optional)src/pages/projects/[slug].astro(a minimal null-guard only β see step 4)pnpm-workspace.yaml(flip one build flag)README.md(a one-line cover-size recommendation)Don't touch:
src/components/Card.astroβ the card body/layout is owned by a separate ticket. (It already passescover={data.cover}straight through, which becomesImageMetadata | undefinedonce the field is optional β no change needed there.)[slug].astrois the minimal build-green guard in step 4; do not style it.aspect-video) and thebg-canvasfallback β keep them.π οΈ Implementation Plan
Make the cover optional in the schema. In
src/content.config.ts, change the projectscoverfield fromimage()toimage().optional(). This is what lets a project ship without a cover; it also makesdata.covertyped asImageMetadata | undefinedeverywhere it's read, which drives steps 3 and 4.Blurred-backdrop fill in
CardCover.astro(when a cover exists). Inside the 16:9 box, stack two layers: a background copy of the image (object-cover, scaled up,filter: blur(...)) and a foreground copy (object-contain, centered) that's never cropped. Work out the exact layering/values yourself β Tailwind utilities plus a small scoped<style>if needed (tokens are available asvar(--color-*)). Optimize viaastro:assets: switch the plain<img>to the<Image>component (the cover prop isImageMetadata). Then setsharp: trueinpnpm-workspace.yamlunderallowBuilds(it's currentlyfalsebecause the scaffold used a plain<img>):If
pnpm install/build surfaces a supply-chain policy error here, flag it to Jacc rather than working around it.If you tried a different approach, share with Jacc for feedback.
No-cover fallback in
CardCover.astro. The prop becomes optional (cover?: ImageMetadata). When it's undefined, render a generated title-block fallback instead of<Image>: the same 16:9 tile filled with a Carleton-themed background (use the tokens β e.g.bg-accentor a canvas/surface block) and the project title centered in it, so the card still reads as intentional. The component already receivestitle, so use it for the fallback label. Branch on whethercoverexists; the blurred-fill path only runs when it does.Keep the detail page building (minimal guard only). Making
coveroptional meanssrc/pages/projects/[slug].astroβ which currently doesdata.cover.srcβ will failpnpm checkunder strict TS. Add only a minimal null-guard so the build stays green: wrap its existing cover<img>so it renders only whendata.coveris defined (e.g.{data.cover && (<img β¦ />)}). Do not style it or build a fancy fallback there β the detail page's own missing-cover treatment is owned by a separate detail-page ticket. This is purely "don't crash the build."Test with real variety. Drop a square, a portrait, and an ultrawide image into a couple of
src/content/projects/*/entries and confirm all three render with no crop loss and no bars. Then remove thecoverline from one entry entirely and confirm the card shows the title-block fallback and the build still passes.Document the cover spec. Add a one-line "recommended cover for best results" note to the
Adding a projectsection ofREADME.md(suggested: a 16:9 desktop screenshot, around 1600Γ900, reasonable file size β and that it's optional). The blurred fill means off-spec covers still look fine β this is just guidance.Leave the coming soon text as well since the full section isn't covered yet. Just add the image size note.
β Acceptance Criteria
coveris optional insrc/content.config.ts; a project entry with nocoverline builds successfully.astro:assets<Image>;sharp: trueis set inpnpm-workspace.yaml.aspect-video) is preserved;Card.astrois untouched;[slug].astrohas only the minimal null-guard (no styling).pnpm format:check,pnpm check, andpnpm buildall pass.