Skip to content

Asset upload pipeline: auto-generate image variants (thumbnail, WebP, blurhash) on put #390

@MajorTal

Description

@MajorTal

Context

Kychon portals are image-heavy: hero backgrounds, event covers, member avatars, image accordions, slideshows, media library thumbnails. The Run402 image pipeline already handles AVIF/WebP derivatives for some formats (see existing internal pipeline noted in Kychon's docs/wild-apricot-port-friction-2026-04-30.md), but apps still have to:

  1. Display the original full-size image as a thumbnail in admin UIs (no thumbnail derivative is returned).
  2. Hand-write Astro <Picture> components and pick which derivative to load at which breakpoint.
  3. Live without blurhash / LQIP placeholders, so cold image loads cause CLS or empty space.

Kychon's upcoming media library (admin-content-management change) needs to render a 4-column grid of image thumbnails. With current behavior, scrolling 8 visible items pulls 8 × full-resolution images — easily 10–20 MB per picker open for a club with high-res photos.

Current friction

  • assets.put() returns a single cdnUrl for the original upload only.
  • There is no thumbnail derivative URL returned, so admin UIs must load the original.
  • No blurhash / dominant color / LQIP placeholder is generated, so apps either ship custom <canvas> blurhash decode or accept layout shift on image load.
  • HEIF decode failure (also tracked in Kychon's friction doc, F4) is a related gap in the same pipeline.

Desired outcome

On assets.put() of an image, automatically generate and return derivative URLs:

const ref = await assets.put('images/hero.jpg', bytes, { contentType: 'image/jpeg' });

// ref = {
//   path: 'images/hero.jpg',
//   cdnUrl: '...',
//   variants: {
//     thumb:   { url: '...', width: 200,  height: 200,  format: 'webp' },
//     medium:  { url: '...', width: 800,  format: 'webp' },
//     large:   { url: '...', width: 1920, format: 'webp' },
//     avif:    { url: '...', format: 'avif' }
//   },
//   blurhash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'  // ~30 bytes
// }

Variant sizes could be configurable via a project-level config, with sensible defaults (200 / 800 / 1920 covers most web use cases).

Ideally also:

  • Add HEIF/HEIC decode support (closes the existing F4 friction).
  • Serve variants with long cache-control headers (they're content-addressed).
  • Support on-demand variant generation via URL params (?w=400&fmt=webp) for use cases not covered by the default variant set.

Why it matters

  • Media picker UIs become 50–100× lighter (4 KB thumbnails vs 2 MB originals).
  • Blurhash placeholders eliminate CLS on image-heavy pages without app-side LQIP machinery.
  • Performance is the most-cited Wild Apricot complaint Kychon tries to fix; this is foundational.
  • Any Run402 app using images benefits — not just Kychon.

Workaround until shipped

Kychon's media library will load original images as thumbnails, accepting the bandwidth cost. We will not implement custom blurhash generation in the admin-content-management change; cold image loads will continue to cause minor CLS until this lands.

Related Kychon change

/Users/talweiss/Developer/kychon/openspec/changes/admin-content-management/ — specifically the MediaPicker grid in specs/media-library/spec.md.

Related friction items

  • Kychon friction doc F4: HEIF decode support gap (docs/wild-apricot-port-friction-2026-04-30.md)

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-requestFlagged by /bugs as a feature request, not an auto-fixable bug

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions