Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Static files and assets

Put files in public/. There is no asset pipeline, no fingerprinting, and no file uploads.

public/
  app.css        -> /app.css
  favicon.svg    -> /favicon.svg
  robots.txt     -> /robots.txt
  img/logo.png   -> /img/logo.png

Anything under public/ is served straight from disk by melee-server, before any route is considered — for GET and HEAD. A POST to a path under public/ still goes to your app, so a route can shadow a file for one method and not the other. No route declaration, no helper, no manifest. The same files are served by melee dev, which prints them differently so you can tell:

GET /app.css -> 200 (public/)

Reference them with plain paths:

<link rel="stylesheet" href="/app.css">
<img src="/img/logo.png" alt="">

What the server sends

HTTP/1.1 200 OK
content-type: text/css; charset=utf-8
cache-control: public, max-age=300
server: melee
content-length: 182

Content type from the extension, and a flat five-minute cache. That is deliberately conservative: there is no fingerprinting, so a longer cache would mean visitors stuck on an old stylesheet after a deploy. Better cache headers for content-addressed assets is a known open task.

If you want a long cache today, do the fingerprinting yourself — name the file app-7f3a.css, reference it by that name, and change both when it changes. You still only get max-age=300, so this buys correctness rather than speed.

There is no build step for assets

No Sprockets, no esbuild, no Tailwind CLI, no import. If you want compiled CSS or bundled JavaScript, run the tool on your own machine and commit the output into public/melee push sends whatever is there.

Keep node_modules/ and source files out of the tarball with .meleeignore:

node_modules/
src/
*.scss

For most apps of this size, one hand-written stylesheet is genuinely the right answer.

There are no file uploads

This is the significant gap on this page. A melee app cannot usefully accept a file today:

  • request.form handles urlencoded bodies only. Multipart is not parsed. A file input posts multipart, and the fields come back empty.
  • The whole request body is capped at 8 MB by the server.
  • An app can only write to its own data directory, which is on the machine’s local disk and is not backed up for you.
  • There is no object-storage connector and no request signing, so you cannot hand the bytes to S3 either.

If you need uploads, the honest options are to put them somewhere else (a form that posts directly to a service that accepts browser uploads) or to use a different platform for that app. An object storage connector with presigned URLs is designed but not built.

Serving a file from a route

You can always return bytes yourself, for something generated rather than stored:

# at the top of app.rb
require "csv"

get "/notes.csv" do
  out = +""
  out << CSV.generate_line(%w[id text done])
  db.query("SELECT id, text, done FROM notes ORDER BY id").each do |row|
    out << CSV.generate_line([row["id"], row["text"], row["done"]])
  end
  header "Content-Disposition", "attachment; filename=\"notes.csv\""
  text(out, type: "text/csv; charset=utf-8")
end

Note +"" and <<: string literals are frozen in this dialect, so building a String starts with an unfrozen one.

The response is collected in memory before it is sent — streaming is passed through as a whole today — so this is for kilobytes and megabytes, not gigabytes, and it runs under the 30-second request timeout.

See also