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.

13b42fa paste an image into the upload field 9ad5d2e bump upload limit 2MB → 10MB 8730244 Discord notifications show the cover

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…

Or for real: copy any image (screenshot, web image) and press ⌘V / Ctrl+V anywhere on this page.

Handler trace

Nothing yet — click Paste (or press ⌘V with an image in your clipboard) to see what runs.
frameTarget is hidden until a file lands
app/javascript/controllers/cover_preview_controller.jsnew in 13b42fa
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.

4.5 MB

Before · 2MB limit

After · 10MB limit

User uploads
4.5 MB
whatever they pasted
validate!
CoverUploader resize
1200 × 630
fixed output dims
store
R2 storage
~130 KB
always small
app/services/cover_uploader.rbchanged in 9ad5d2e
# 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.

User uploaded a custom cover off → OG endpoint serves the generated fallback
1 v
https://artifact.land/og/a/42?v=1
Discord webhook preview#artifacts
artifact.land APP Today at 20:23
A new artifact just landed —
@ada
A small clock
Hands that slip when you hover.
user's uploaded cover
v=1
app/services/discord_notifier.rbadded in 8730244
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.