=== Static Files

Static files are served using the javadoc:Router[assets, java.lang.String, java.nio.file.Path] method. The `assets` route supports serving resources from both the classpath and the file system.

.Serving a directory:
[source, java, role="primary"]
----
{
  assets("/static/*", "/source"); // <1>
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  assets("/static/*", "/source")  // <1>
}
----

<1> Maps all incoming requests starting with `/static/` to the `/source` location.

For example:
* `GET /static/index.html` => `/source/index.html`
* `GET /static/js/file.js` => `/source/js/file.js`
* `GET /static/css/styles.css` => `/source/css/styles.css`

When passing a string as the source (`"/source"`), Jooby attempts to resolve it against the file system first (by prepending the `user.dir` path). If the directory exists on the file system, it uses it; otherwise, it falls back to looking for the `/source` directory on the classpath.

To explicitly serve from the file system, pass a `Path` object:

.Explicit File System Resources:
[source, java, role="primary"]
----
{
  assets("/static/*", Paths.get("www")); // <1>
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  assets("/static/*", Paths.get("www"))  // <1>
}
----

<1> Maps all incoming requests starting with `/static/` directly to the `www` directory on the file system.

You can also map individual files rather than entire directories:

.Single File Mapping
[source, java, role="primary"]
----
{
  // Classpath
  assets("/myfile.js", "/static/myfile.js");
  
  // File System
  Path basedir = Paths.get("www");
  assets("/myfile.js", basedir.resolve("myfile.js"));
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  // Classpath
  assets("/myfile.js", "/static/myfile.js")
  
  // File System
  val basedir = Paths.get("www")
  assets("/myfile.js", basedir.resolve("myfile.js"))
}
----

==== Static Sites

The `assets` route can also serve full static websites (like generated documentation). To enable automatic resolution of `index.html` files, use the special `/?*` path mapping:

.Static Site Directory
[source, java, role="primary"]
----
{
  Path docs = Paths.get("docs"); // <1>
  assets("/docs/?*", docs);      // <2>
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  val docs = Paths.get("docs")   // <1>
  assets("/docs/?*", docs)       // <2>
}
----

<1> Serve from the `docs` directory.
<2> Use the `/?*` mapping to enable automatic root resolution.

The `/?*` syntax automatically resolves root paths to their underlying `index.html`:
* `GET /docs` => `/docs/index.html`
* `GET /docs/index.html` => `/docs/index.html`
* `GET /docs/about.html` => `/docs/about.html`
* `GET /docs/note` => `/docs/note/index.html`

==== SPAs

The `assets` route also supports Single Page Applications (SPAs) built with React, Vue, or Angular.

For an SPA to handle client-side routing properly, the server must return the main `index.html` file whenever a requested asset or route is not found. You can configure this using the `AssetHandler`:

.SPA Routing
[source, java, role="primary"]
----
{
  AssetSource buildDir = AssetSource.create(Paths.get("build")); // <1>
  assets("/?*", new AssetHandler("index.html", buildDir));       // <2>
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  val buildDir = AssetSource.create(Paths.get("build"))          // <1>
  assets("/?*", AssetHandler("index.html", buildDir))            // <2>
}
----

<1> Define the source directory containing the built SPA.
<2> Map the root using `/?*` and provide `index.html` as the fallback asset.

In SPA mode, the server never generates a `404 NOT FOUND` response for missing files; it simply falls back to serving the `index.html` so the client-side router can take over.

==== Options

The javadoc:handler.AssetHandler[] automatically generates and handles `ETag` and `Last-Modified` headers. You can control this behavior programmatically:

.Disable ETag and Last-Modified:
[source, java, role="primary"]
----
{
  assets("/static/*", Paths.get("www"))
    .setLastModified(false)
    .setEtag(false);
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  assets("/static/*", Paths.get("www"))
    .setLastModified(false)
    .setEtag(false)
}
----

You can also set a `Cache-Control` header using the `maxAge` option:

.Cache Control:
[source, java, role="primary"]
----
{
  assets("/static/*", Paths.get("www"))
    .setMaxAge(Duration.ofDays(365));
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  assets("/static/*", Paths.get("www"))
    .setMaxAge(Duration.ofDays(365))
}
----

To explicitly forbid web browsers from caching assets, use the javadoc:handler.AssetHandler[setNoCache] method.

If you need different cache configurations for different files, you can provide a function via javadoc:handler.AssetHandler[cacheControl, java.util.Function]:

.Per-Asset Cache Control:
[source, java, role="primary"]
----
{
  assets("/static/*", Paths.get("www"))
      .cacheControl(path -> {
        if (path.endsWith("dont-cache-me.html")) {
          return CacheControl.noCache(); // Disable caching
        } else if (path.equals("foo.js")) {
          return CacheControl.defaults()
              .setETag(false)
              .setMaxAge(Duration.ofDays(365));
        } else {
          return CacheControl.defaults(); // Use default AssetHandler caching
        }
      });
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  assets("/static/*", Paths.get("www"))
      .cacheControl { path ->
        when {
          path.endsWith("dont-cache-me.html") -> CacheControl.noCache() // Disable caching
          path == "foo.js" -> CacheControl.defaults()
              .setETag(false)
              .setMaxAge(Duration.ofDays(365))
          else -> CacheControl.defaults() // Use default AssetHandler caching
        }
      }
}
----

By default, the asset handler generates a `404` response if the requested file is not found (unless you are using the SPA fallback). You can override this behavior by throwing a custom exception or generating alternative content:

.Custom Not Found Behavior:
[source, java, role="primary"]
----
{
  assets("/static/*", Paths.get("www"))
      .notFound(ctx -> {
        throw new MyAssetException();
      });

  error(MyAssetException.class, (ctx, cause, code) -> {
    // Render MyAssetException the way you want
  });
}
----

.Kotlin
[source, kotlin, role="secondary"]
----
{
  assets("/static/*", Paths.get("www"))
    .notFound { _ ->
        throw MyAssetException()
    }
    
  error(MyAssetException::class) { ctx, cause, code ->
    // Render MyAssetException the way you want
  }
}
----
