diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUIController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUIController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed29297569a60db42f9c20de679747697ef95e24
--- /dev/null
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUIController.java
@@ -0,0 +1,120 @@
+package gov.usgs.earthquake.nshmp.www;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import org.apache.commons.io.IOUtils;
+
+import io.micronaut.core.io.scan.ClassPathResourceLoader;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.annotation.Produces;
+import io.swagger.v3.oas.annotations.Hidden;
+import jakarta.annotation.Nullable;
+import jakarta.inject.Inject;
+
+/**
+ * Handle Swagger static resources.
+ */
+@Controller(
+    value = "${nshmp.context-path}/",
+    produces = {
+        MediaType.APPLICATION_YAML,
+        MediaType.TEXT_HTML,
+        MediaType.IMAGE_PNG,
+        MediaType.TEXT_PLAIN,
+        "font/woff",
+        "font/woff2",
+        "font/ttf",
+        "image/svg+xml",
+        "text/css",
+        "text/javascript",
+    })
+public class SwaggerUIController {
+  private static final String SWAGGER_UI_RESOURCE_LOCATION = "classpath:swagger/";
+  private final ClassPathResourceLoader loader;
+
+  public SwaggerUIController(ClassPathResourceLoader loader) {
+    this.loader = loader;
+  }
+
+  @Inject
+  private NshmpMicronautServlet servlet;
+
+  @Get("{/path:.*}{.ext:png}")
+  @Produces("image/png; charset=utf-8")
+  @Hidden
+  public byte[] getSwaggerPngImages(
+      @PathVariable @Nullable String path,
+      @PathVariable @Nullable String ext) throws IOException {
+    Optional<URL> resource = loader.getResource(SWAGGER_UI_RESOURCE_LOCATION + path + "." + ext);
+    return IOUtils.toByteArray(resource.orElseThrow().openStream());
+  }
+
+  @Get("{/path:.*}{.ext:b64}")
+  @Produces(MediaType.TEXT_PLAIN)
+  @Hidden
+  public HttpResponse<String> getSwaggerBase64Images(
+      @PathVariable @Nullable String path,
+      @PathVariable @Nullable String ext) throws IOException {
+    return HttpResponse.ok(getResourceString(SWAGGER_UI_RESOURCE_LOCATION + path + "." + ext))
+        .contentType(MediaType.TEXT_PLAIN);
+  }
+
+  @Get("{/path:.*}{.ext:css|js}")
+  @Produces({ "text/css", "text/javascript" })
+  @Hidden
+  public String getSwaggerLibrary(
+      @PathVariable @Nullable String path,
+      @PathVariable @Nullable String ext) throws IOException {
+    return getResourceString(SWAGGER_UI_RESOURCE_LOCATION + path + "." + ext);
+  }
+
+  @Get("{/path:.*}{.ext:svg}")
+  @Produces("image/svg+xml")
+  @Hidden
+  public String getSwaggerSvgImages(
+      @PathVariable @Nullable String path,
+      @PathVariable @Nullable String ext) throws IOException {
+    return getResourceString(SWAGGER_UI_RESOURCE_LOCATION + path + "." + ext);
+  }
+
+  @Get("{/path:.*}{.ext:eot}")
+  @Produces("application/vnd.ms-fontobject")
+  @Hidden
+  public String getSwaggerEotFont(
+      @PathVariable @Nullable String path,
+      @PathVariable @Nullable String ext) throws IOException {
+    return getResourceString(SWAGGER_UI_RESOURCE_LOCATION + path + "." + ext);
+  }
+
+  @Get("{/path:.*}{.ext:woff|woff2|ttf|otf}")
+  @Produces("font/*")
+  @Hidden
+  public String getSwaggerFont(
+      @PathVariable @Nullable String path,
+      @PathVariable @Nullable String ext) throws IOException {
+    return getResourceString(SWAGGER_UI_RESOURCE_LOCATION + path + "." + ext);
+  }
+
+  @Get("/")
+  @Produces(MediaType.TEXT_HTML)
+  @Hidden
+  public String getSwaggerIndexPage() throws IOException {
+    return getResourceString("classpath:swagger/index.html");
+  }
+
+  private String getResourceString(String path) throws IOException {
+    Optional<URL> resource = loader.getResource(path);
+
+    return IOUtils.toString(
+        (BufferedInputStream) resource.orElseThrow().getContent(),
+        StandardCharsets.UTF_8);
+  }
+}