static String maybeResource(String basePath, Resource resource) throws IOException {
    if (!resource.isReadable()) return null;

    try (InputStream stream = resource.getInputStream()) {
      String content = StreamUtils.copyToString(stream, UTF_8);
      if (DEFAULT_BASEPATH.equals(basePath)) return content;

      if (basePath.equals("/")) basePath = "";

      // relativize any href or src in index.html
      // TODO: see if vite config can make these relative by default!
      return content.replace("=\"" + DEFAULT_BASEPATH, "=\".")
        // set the base href, used in js, to absolute
        .replace("<base href=\".", "<base href=\"" + basePath);
    }
  }
Zipkin-UI is a single-page application mounted at /zipkin. For simplicity, assume paths mentioned
below are relative to that. For example, the UI reads config.json, from the absolute path
/zipkin/config.json

<p>When looking at a trace, the browser is sent to the path "/traces/{id}". For the single-page
app to serve that route, the server needs to forward the request to "/index.html". The same
forwarding applies to "/dependencies" and any other routes the UI controls.

<p>Under the scenes the JavaScript code looks at {@code window.location} to figure out what the
UI should do. This is handled by a route api defined in the crossroads library.

<h3>Caching</h3>
<p>This includes a hard-coded cache policy, consistent with zipkin-scala.
<ul>
  <li>1 minute for index.html</li>
  <li>10 minute for /config.json</li>
  <li>365 days for hashed resources (ex /app-e12b3bbb7e5a572f270d.min.js)</li>
</ul>
Since index.html links to hashed resource names, any change to it will orphan old resources.
That's why hashed resource age can be 365 days.
zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiConfiguration.java