generated-media-gallery-share-links

/home/avalon/.hermes/skills/.archive/software-development/generated-media-gallery-share-links/SKILL.md · raw

Generated Media Gallery + Share Links

Use when a web app generates images/videos, saves them to S3 or local disk, and needs a gallery that shows metadata and provides stable share/download links.

Problem Pattern

A generation succeeds, the app saves the file, but the UI still uses the provider's transient preview URL (previewUrl) for display or sharing. Result: users see generic load failures even though the media exists locally.

Core Rules

  1. Prefer persisted asset URLs over provider preview URLs for gallery/display/share.
  2. Store generation metadata at save time, not only in transient API responses.
  3. Return absolute share URLs from the backend for easy copying/sharing.
  4. Support both S3-hosted and local fallback assets.

In the assets table, store at least: - model - prompt - result_kind - storage_key - storage_url - storage_mode - params_json - inference_time - output_duration - created_at

For SQLite migrations in an existing app: - ALTER TABLE assets ADD COLUMN inference_time REAL - ALTER TABLE assets ADD COLUMN output_duration TEXT

Backend Pattern

1. Persist stable metadata when saving an asset

When creating the DB record, write: - inferenceTime: result.inferenceTime ?? null - outputDuration: result.duration != null ? String(result.duration) : null

2. Normalize asset records for API responses

Return a normalized object including: - prompt - model - modelName - imageUrl / videoUrl - storageMode - inferenceTime - duration - shareUrl

3. Absolute share URL helper

Use a helper like:

function absolutizeUrl(url, origin) {
  if (!url) return null
  if (/^https?:\/\//i.test(url)) return url
  if (!origin) return url
  const normalizedOrigin = origin.replace(/\/$/, '')
  const normalizedPath = url.startsWith('/') ? url : `/${url}`
  return `${normalizedOrigin}${normalizedPath}`
}

4. Derive request origin behind nginx

For Express apps behind a proxy, build absolute links from: - x-forwarded-proto - req.get('host')

Pattern:

function getRequestOrigin(req) {
  const forwardedProto = req.headers['x-forwarded-proto']
  const proto = (Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto) || req.protocol || 'https'
  return `${proto}://${req.get('host')}`
}

In /api/gallery, call the normalizer with the computed origin so each asset includes shareUrl.

Frontend Pattern

1. URL preference order

For generated results and gallery cards, prefer persisted URLs first:

result.videoUrl || result.imageUrl || result.storageUrl || result.previewUrl

For gallery items:

item.localUrl || item.imageUrl || item.videoUrl || item.storageUrl || item.previewUrl

Do NOT prefer previewUrl first.

2. Never use a video file as a poster image

If the result is a video, do NOT set poster to the video URL itself. Browsers may surface this as a vague client-side Load failed / preview failure even when the MP4 exists and returns 200.

Use a helper like:

export function getPosterUrl(result) {
  if (!result) return null
  return result.imageUrl || null
}

Then only pass poster={getPosterUrl(result) || undefined}.

Display at least: - Prompt: - Model: - Took: inference time - Output: duration when present

4. Share action

Implement a single share helper: - use navigator.share() if available - else navigator.clipboard.writeText(shareUrl) - else fallback to window.prompt('Copy this share link:', shareUrl)

5. Replace janky lightboxes with a real viewer surface

For gallery media viewing, prefer a modal surface over an old-style full-screen overlay with floating controls.

Recommended structure: - blurred/dimmed backdrop - solid centered surface with rounded corners - header with title/prompt and close button - scrollable body (overflow-y: auto) - dedicated framed media area for image/video - footer actions for download/share

This avoids: - non-scrollable overlays - controls floating on top of media - browser video controls fighting with app chrome

6. Delete actions need explicit confirmation UI

Do not delete immediately from a tiny trash icon. Use a confirmation modal with: - clear prompt (Delete this item?) - model + prompt summary - explicit Cancel and Delete buttons - disabled state while delete is in flight

7. Lightbox and post-generation actions

Add share buttons in both places: - newly generated result view - gallery viewer/modal

Testing Pattern

Use node:test or the project test framework to verify normalization logic.

Good regression tests: 1. Local asset path (/api/local-assets/...) becomes an absolute https://.../api/local-assets/... share URL. 2. Absolute S3 URL is preserved as-is. 3. Inference time and duration are exposed by the normalizer.

Verification Checklist

Key Lesson

If the provider returns a preview URL and the app also persists the file locally or to S3, the gallery/share system must treat the persisted file as the source of truth. The preview URL is only a transient convenience.