Skip to content
Ori Pekelman edited this page May 11, 2026 · 1 revision

Tep::Assets

Compile-time static-asset bundling. Anything you drop under <app>/assets/ is baked into the binary at tep build time and served straight from memory — no separate static-file server, no disk reads at runtime, no path-traversal class of bug.

Layout

my-app/
├── app.rb
└── assets/
    ├── styles.css
    ├── logo.svg
    ├── htmx.min.js
    └── img/
        └── header.png

Build:

tep build app.rb

At build, bin/tep walks <app>/assets/**/*, computes each path relative to the assets root, and emits a Tep::Assets._add(path, body, mime) call per file. The resulting binary contains the asset bytes in its read-only data segment.

Serving

The framework's request dispatcher checks Tep::Assets.serve(path, response) before route table lookup. Hits short-circuit:

GET /styles.css       → asset → 200 with bytes from memory
GET /img/header.png   → asset → 200 with bytes from memory
GET /users/42         → not an asset → falls through to your routes

You don't write any handler code. The path-to-asset mapping is <request.path> matched against /<filename> (the leading / matters).

MIME types

Inferred from the extension at build time:

Extension Content-Type
.html .htm text/html
.css text/css
.js application/javascript
.json application/json
.svg image/svg+xml
.png image/png
.jpg .jpeg image/jpeg
.gif image/gif
.txt text/plain
.ico image/x-icon
.woff / .woff2 font/woff / font/woff2
anything else application/octet-stream

Override per-file at the source level by editing the assets table post-build is not supported — if the default's wrong, post-process the file to a known extension, or wrap the asset in a real route.

Reading an asset from your code

Rare but possible — e.g. inlining an SVG into an HTML template:

get '/' do
  if Tep::Assets.has?("/logo.svg")
    logo = Tep::APP.asset_bodies["/logo.svg"]
    "<div>" + logo + "</div>"
  else
    "logo missing"
  end
end

Tep::Assets.has?(path) returns whether the asset was bundled; the byte content lives in Tep::APP.asset_bodies[path] (and the mime in Tep::APP.asset_mimes[path]).

Cookbook

A single-page app with a JS bundle

my-app/
├── app.rb
└── assets/
    ├── index.html
    ├── bundle.js
    └── styles.css
require 'sinatra'

get '/' do
  redirect '/index.html'
end

# Everything else is served straight from the asset bundle.

Cache headers

The framework sets Cache-Control: public, max-age=3600 on assets by default (1 hour). Re-deploying the binary produces a new asset table; bump the URL or override the header if you want a longer lifetime. If you want a different policy, write your own route that overrides:

get '/styles.css' do
  response.headers["Cache-Control"] = "no-cache"
  Tep::APP.asset_bodies["/styles.css"]
end

(Caveat: a hand-written route shadows the asset table for that path.)

Pitfalls

  • Binary size grows linearly with asset bytes. A 5 MB image bundled in produces a 5 MB+ binary. For large static content, bundle a manifest and pull at runtime from object storage instead.
  • No build-time minification. If you want minified CSS / JS, run the minifier as a pre-step (e.g. npm run build writes into assets/).
  • Paths are case-sensitive everywhere. Linux is case-sensitive by default; URLs match exactly. Don't ship /Logo.png and reference /logo.png from your HTML.
  • No directory listing. Requesting /img/ 404s. There's no index.html magic; if you want one, route it explicitly.

Clone this wiki locally