diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 8eac8fa80..71c402aa8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -33,14 +33,35 @@ public class WebMvcConfig implements WebMvcConfigurer { public void addResourceHandlers(ResourceHandlerRegistry registry) { // Cache hashed assets (JS/CSS with content hashes) for 1 year // These files have names like index-ChAS4tCC.js that change when content changes + // Check customFiles/static first, then fall back to classpath registry.addResourceHandler("/assets/**") - .addResourceLocations("classpath:/static/assets/") + .addResourceLocations( + "file:" + + stirling.software.common.configuration.InstallationPathConfig + .getStaticPath() + + "assets/", + "classpath:/static/assets/") .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic()); // Don't cache index.html - it needs to be fresh to reference latest hashed assets + // Note: index.html is handled by ReactRoutingController for dynamic processing registry.addResourceHandler("/index.html") - .addResourceLocations("classpath:/static/") + .addResourceLocations( + "file:" + + stirling.software.common.configuration.InstallationPathConfig + .getStaticPath(), + "classpath:/static/") .setCacheControl(CacheControl.noCache().mustRevalidate()); + + // Handle all other static resources (js, css, images, fonts, etc.) + // Check customFiles/static first for user overrides + registry.addResourceHandler("/**") + .addResourceLocations( + "file:" + + stirling.software.common.configuration.InstallationPathConfig + .getStaticPath(), + "classpath:/static/") + .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)); } @Override diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index ab8f3b75b..eddff0e1e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -3,9 +3,14 @@ package stirling.software.SPDF.controller.web; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -14,6 +19,11 @@ import org.springframework.web.bind.annotation.GetMapping; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.configuration.InstallationPathConfig; + +@Slf4j @Controller public class ReactRoutingController { @@ -22,24 +32,44 @@ public class ReactRoutingController { private String cachedIndexHtml; private boolean indexHtmlExists = false; + private boolean useExternalIndexHtml = false; @PostConstruct public void init() { - // Only cache if index.html exists (production builds) + log.info("Static files custom path: {}", InstallationPathConfig.getStaticPath()); + + // Check for external index.html first (customFiles/static/) + Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html"); + log.debug("Checking for custom index.html at: {}", externalIndexPath); + if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) { + log.info("Using custom index.html from: {}", externalIndexPath); + try { + this.cachedIndexHtml = processIndexHtml(); + this.indexHtmlExists = true; + this.useExternalIndexHtml = true; + return; + } catch (IOException e) { + log.warn("Failed to load custom index.html, falling back to classpath", e); + } + } + + // Fall back to classpath index.html ClassPathResource resource = new ClassPathResource("static/index.html"); if (resource.exists()) { try { this.cachedIndexHtml = processIndexHtml(); this.indexHtmlExists = true; + this.useExternalIndexHtml = false; } catch (IOException e) { // Failed to cache, will process on each request + log.warn("Failed to cache index.html", e); this.indexHtmlExists = false; } } } private String processIndexHtml() throws IOException { - ClassPathResource resource = new ClassPathResource("static/index.html"); + Resource resource = getIndexHtmlResource(); try (InputStream inputStream = resource.getInputStream()) { String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); @@ -62,6 +92,17 @@ public class ReactRoutingController { } } + private Resource getIndexHtmlResource() throws IOException { + // Check external location first + Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html"); + if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) { + return new FileSystemResource(externalIndexPath.toFile()); + } + + // Fall back to classpath + return new ClassPathResource("static/index.html"); + } + @GetMapping( value = {"/", "/index.html"}, produces = MediaType.TEXT_HTML_VALUE)