@Bean ArmeriaServerConfigurator uiServerConfigurator(
    HttpService indexService,
    Optional<MeterRegistry> meterRegistry
  ) throws IOException {
    ServerCacheControl maxAgeYear =
      ServerCacheControl.builder().maxAgeSeconds(TimeUnit.DAYS.toSeconds(365)).build();

    HttpService uiFileService = FileService.builder(getClass().getClassLoader(), "zipkin-lens")
      .cacheControl(maxAgeYear)
      .build();

    String config = writeConfig(ui);
    return sb -> {
      sb.service("/zipkin/config.json", HttpFile.builder(HttpData.ofUtf8(config))
        .cacheControl(ServerCacheControl.builder().maxAgeSeconds(600).build())
        .contentType(MediaType.JSON_UTF_8)
        .build()
        .asService());

      sb.serviceUnder("/zipkin/", uiFileService);

      // TODO This approach requires maintenance when new UI routes are added. Change to the following:
      // If the path is a a file w/an extension, treat normally.
      // Otherwise instead of returning 404, forward to the index.
      // See https://github.com/twitter/finatra/blob/458c6b639c3afb4e29873d123125eeeb2b02e2cd/http/src/main/scala/com/twitter/finatra/http/response/ResponseBuilder.scala#L321
      sb.service("/zipkin/", indexService)
        .service("/zipkin/index.html", indexService)
        .service("/zipkin/traces/{id}", indexService)
        .service("/zipkin/dependency", indexService)
        .service("/zipkin/traceViewer", indexService);

      sb.service("/favicon.ico", new RedirectService(HttpStatus.FOUND, "/zipkin/favicon.ico"))
        .service("/", new RedirectService(HttpStatus.FOUND, "/zipkin/"))
        .service("/zipkin", new RedirectService(HttpStatus.FOUND, "/zipkin/"));

      // don't add metrics for favicon
      meterRegistry.ifPresent(m -> m.config().meterFilter(MeterFilter.deny(id -> {
        String uri = id.getTag("uri");
        return uri != null && uri.startsWith("/favicon.ico");
      })));
    };
  }
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