Static Files and Assets
Generate asset URLs with the same helper in Dart and Khnum templates:
asset('css/app.css'); // /css/app.css<link rel="stylesheet" href="{{ asset('css/app.css') }}">A Laravel application never serves its own CSS — nginx does, from public/.
Maat applications usually run without a web server in front, so the
framework does it, through one piece of global middleware.
Serving public/
.withMiddleware( (middleware) => middleware.use([ PublicFiles(publicPath()), TrimStrings(), Cors(), ]),)Put it first. It runs before routing, so a request naming a real file answers immediately and everything else falls through to the router untouched — including a directory, a missing file, and any path that tried to climb out of the directory.
Only GET and HEAD are served; POST /css/app.css belongs to the
router. Responses carry content-type (from the file’s extension),
content-length, last-modified and cache-control, and answer 304
to a matching If-Modified-Since.
PublicFiles(publicPath()) // cache for an hourPublicFiles(publicPath(), maxAge: Duration.zero) // no cache-controlFile bodies stream from disk, so serving a large file does not first copy the whole body into application memory. A web server or CDN is still the better production boundary for range requests and edge caching.
Linking to Assets
<link rel="stylesheet" href="{{ asset('css/app.css') }}">asset() returns a root-relative URL by default. Set app.asset_url and
the same call points at a CDN instead, with no template change:
Map<String, dynamic> get app => { 'asset_url': env('ASSET_URL', ''),};publicPath() is the filesystem counterpart: publicPath() is the
directory, publicPath('css/app.css') a file inside it.
Outside debug mode, asset() appends a ?v=<token> cache buster derived from
the file’s modification time and size. It omits the token in debug mode and
when the file does not exist, keeping a missing development build visible as a
normal 404 rather than turning page rendering into a 500.
Path Traversal
Requests are not trusted to stay inside public/. The candidate path is
joined, normalised and canonicalised, then checked to be inside the
resolved root; the file’s own symlink target is resolved and checked
again. Nothing is decided by scanning the request for .., because that
check does not hold:
GET /..%2f..%2fetc/passwdUri.pathSegments percent-decodes each segment, so this arrives as the
single segment ../../etc — a scan for a .. segment never sees it.
Normalising and then testing containment does. A %00 in the path is
refused outright: it decodes to a real NUL, which truncates the name for
the operating system but not for the Dart string holding it.
Compiling Assets
A new application ships with resources/css/app.css (Tailwind’s entrypoint)
and the maat tailwind command already wired up — no bundler
configuration to write:
maat tailwind --minifymaat tailwind --watchmaat tailwind downloads Tailwind’s standalone CLI on first use, so
the whole pipeline needs neither Node nor node_modules. See
Frontend for the CSS entrypoint’s @import/@source
rules, the binary cache, and how this fits alongside maat serve.
Keep the compiled output out of version control and build it on deploy, or commit it and skip the build step — either works, since the framework only ever reads the finished file.