An explorable explanation · artifact.land
Three small commits, one evening,
all about covers.
On a Thursday evening in April, the cover image flow got three upgrades in a row. Each one pulled the next. Here's what they did — poke the knobs to feel how the pieces fit.
Commit 01 · 13b42fa
Paste a screenshot, straight into the upload field.
macOS can copy a screenshot right to the clipboard with ⌘⇧⌃4. If the cover input listens for paste, you skip the trip through Desktop. The Stimulus controller handles it — but only when the clipboard actually has an image. Pick a clipboard, click paste, and watch the handler walk through each step.
Your clipboard contains…
Handler trace
connect() { this.boundPaste = this.handlePaste.bind(this) window.addEventListener("paste", this.boundPaste)}handlePaste(event) { if (!this.hasInputTarget) return const items = event.clipboardData?.items if (!items) return for (const item of items) { if (item.type.startsWith("image/")) { const file = item.getAsFile() if (file) { event.preventDefault() this.assignFile(file) return } } }}assignFile(file) { const dt = new DataTransfer() dt.items.add(file) this.inputTarget.files = dt.files this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))}
Commit 02 · 9ad5d2e
First real paste tripped the 2MB ceiling.
Retina screenshots are often 4–5MB. The paste path worked; the validation path didn't. The fix is one line: raise the cap to 10MB. Why not worry about storage? Server-side resize to 1200×630 means what's stored is small regardless of input.
Before · 2MB limit
After · 10MB limit
# The cap is about bounding request size, not storage. # Output is always OUTPUT_WIDTH × OUTPUT_HEIGHT after resize. MAX_FILE_SIZE = 10.megabytes # was 2.megabytes OUTPUT_WIDTH = 1200 OUTPUT_HEIGHT = 630 ALLOWED_CONTENT_TYPES = %w[image/jpeg image/png image/webp].freeze def validate! raise ProcessingError, "File is required" unless @uploaded_file.present? raise ProcessingError, "Cover image must be under 10MB" if @uploaded_file.size > MAX_FILE_SIZE unless ALLOWED_CONTENT_TYPES.include?(@uploaded_file.content_type) raise ProcessingError, "Cover image must be JPEG, PNG, or WebP" end end
Commit 03 · 8730244
Now that every artifact has a cover, show it.
Every artifact has a cover — custom or generated — so the Discord webhook embed might as well carry it. The URL points at the OG image endpoint, which picks the right source. The ?v=cover_version cache-buster makes Discord re-fetch when a cover changes.
host = ENV.fetch("APP_HOST", "artifact.land") url = "https://#{host}/@#{post.user.username}/#{post.slug}" # Served by OgImagesController#artifact — custom cover if uploaded, # generated stylized cover otherwise. `v` cache-busts on change. image_url = "https://#{host}/og/a/#{post.id}?v=#{post.cover_version}" notify(ENV["DISCORD_ARTIFACT_WEBHOOK_URL"], { title: post.title, url: url, description: post.description.presence, color: 0x58B4A0, # teal image: { url: image_url }, # ← new author: { name: "@#{post.user.username}" } })
The three as one chain.
Each commit solved a problem the previous commit created (or revealed).
① Paste
Let users drop a screenshot in without saving it first.
② Size
Paste gave us retina PNGs. The 2MB cap was now the bottleneck. Raise it.
③ Discord
Everyone's got a cover now — so the notification embed carries it too.