diff --git a/pom.xml b/pom.xml
index f1cf015..008547e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -88,6 +88,13 @@
slf4j-simple
2.0.17
+
+
+
+ in.wilsonl.minifyhtml
+ minify-html
+ 0.15.0
+
@@ -127,6 +134,7 @@
com.github.luben:zstd-jni
+ in.wilsonl.minifyhtml:minify-html
diff --git a/src/main/java/eu/m724/blog/BlogBuilder.java b/src/main/java/eu/m724/blog/BlogBuilder.java
index b793f3d..31c2144 100644
--- a/src/main/java/eu/m724/blog/BlogBuilder.java
+++ b/src/main/java/eu/m724/blog/BlogBuilder.java
@@ -41,6 +41,7 @@ public class BlogBuilder {
private Site site;
private TemplateRenderer template;
private RenderOptions renderOptions;
+ private Minifier minifier;
private Path templateDirectory;
private Path outputDirectory;
@@ -136,11 +137,19 @@ public class BlogBuilder {
if (renderOptions == null)
this.renderOptions = RenderOptions.fromConfig(workingDirectory.resolve("render.yml"));
+ if (this.site.minify()) {
+ try {
+ this.minifier = new Minifier();
+ } catch (NoClassDefFoundError e) {
+ LOGGER.warn("Minifier not available");
+ }
+ }
+
LOGGER.debug("Copying static assets...");
var fileHashes = copyStaticAssets();
if (template == null)
- this.template = new TemplateRenderer(site, templateDirectory, fileHashes);
+ this.template = new TemplateRenderer(site, minifier, templateDirectory, fileHashes);
LOGGER.debug("Rendering articles...");
var articles = renderArticles();
@@ -210,24 +219,36 @@ public class BlogBuilder {
private Map copyStaticAssets() throws IOException {
var fileHashes = new HashMap();
+ var userAssetsDir = workingDirectory.resolve("assets");
+ var templateStaticDir = templateDirectory.resolve("static");
+
+ var outputUserAssetsDir = outputDirectory.resolve("assets");
+ var outputTemplateStaticDir = outputDirectory.resolve("static");
+
+
if (renderOptions.remapAssets()) {
- var assetHashes = CacheBuster.copyTree(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
+ var assetHashes = CacheBuster.copyTree(userAssetsDir, outputUserAssetsDir);
assetHashes.forEach((k, v) -> {
- fileHashes.put("assets/" + k, v); // TODO this seems like a hack
+ fileHashes.put("assets/" + k, v); // TODO this feels like a hack
});
} else {
- copyTree(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
+ FileUtils.copyTree(userAssetsDir, outputUserAssetsDir);
}
if (renderOptions.remapTemplateStatic()) {
- var templateStaticHashes = CacheBuster.copyTree(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
+ var templateStaticHashes = CacheBuster.copyTree(templateStaticDir, outputTemplateStaticDir);
templateStaticHashes.forEach((k, v) -> {
- fileHashes.put("static/" + k, v); // TODO this seems like a hack
+ fileHashes.put("static/" + k, v); // TODO this feels like a hack
});
} else {
- copyTree(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
+ FileUtils.copyTree(templateStaticDir, outputTemplateStaticDir);
+ }
+
+
+ if (minifier != null) {
+ minifier.minifyTree(outputTemplateStaticDir);
}
return fileHashes;
@@ -261,26 +282,4 @@ public class BlogBuilder {
}
}
}
-
-
- /* Internal functions */
-
- private void copyTree(Path srcDir, Path destDir) throws IOException {
- try (var walk = Files.walk(srcDir)) {
- for (var src : walk.collect(Collectors.toSet())) {
- var rel = srcDir.relativize(src);
- var dest = destDir.resolve(rel);
-
- if (Files.isRegularFile(src)) {
- var parent = dest.getParent();
-
- if (!Files.isDirectory(parent)) {
- Files.createDirectories(parent);
- }
-
- Files.copy(src, dest);
- }
- }
- }
- }
}
diff --git a/src/main/java/eu/m724/blog/CacheBuster.java b/src/main/java/eu/m724/blog/CacheBuster.java
index 917eb2e..2390733 100644
--- a/src/main/java/eu/m724/blog/CacheBuster.java
+++ b/src/main/java/eu/m724/blog/CacheBuster.java
@@ -11,7 +11,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
-import java.util.stream.Collectors;
import java.util.zip.CRC32C;
public class CacheBuster {
@@ -19,27 +18,17 @@ public class CacheBuster {
public static Map copyTree(Path srcDir, Path destDir) throws IOException {
var map = new HashMap();
- try (var walk = Files.walk(srcDir)) {
- for (var src : walk.collect(Collectors.toSet())) {
- var rel = srcDir.relativize(src);
- var dest = destDir.resolve(rel);
+ for (var e : FileUtils.srcDestMap(srcDir, destDir).entrySet()) {
+ var sourceFile = e.getKey();
+ var destinationFile = e.getValue();
- if (Files.isRegularFile(src)) {
- var parent = dest.getParent();
+ var hash = hashFile(sourceFile);
- if (!Files.isDirectory(parent)) {
- Files.createDirectories(parent);
- }
+ var filename = insertHashInPath(destinationFile.getFileName().toString(), hash);
+ destinationFile = destinationFile.resolveSibling(filename);
- var hash = hashFile(src);
-
- var filename = insertHashInPath(dest.getFileName().toString(), hash);
- dest = dest.resolveSibling(filename);
-
- Files.copy(src, dest);
- map.put(rel.toString(), hash);
- }
- }
+ Files.copy(sourceFile, destinationFile);
+ map.put(srcDir.relativize(sourceFile).toString(), hash);
}
return map;
diff --git a/src/main/java/eu/m724/blog/FileUtils.java b/src/main/java/eu/m724/blog/FileUtils.java
new file mode 100644
index 0000000..b83362d
--- /dev/null
+++ b/src/main/java/eu/m724/blog/FileUtils.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2025 blog-software-java developers
+ * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.blog;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class FileUtils {
+ public static void copyTree(Path srcDir, Path destDir) throws IOException {
+ for (var e : srcDestMap(srcDir, destDir).entrySet()) {
+ Files.copy(e.getKey(), e.getValue());
+ }
+ }
+
+ public static Map srcDestMap(Path srcDir, Path destDir) throws IOException {
+ var map = new HashMap();
+
+ for (var sourceFile : walkFilesList(srcDir)) {
+ var rel = srcDir.relativize(sourceFile);
+ var destinationFile = destDir.resolve(rel);
+
+ var parent = destinationFile.getParent();
+
+ if (!Files.isDirectory(parent)) {
+ Files.createDirectories(parent);
+ }
+
+ map.put(sourceFile, destinationFile);
+ }
+
+ return map;
+ }
+
+ /**
+ * Walks over a directory and only returns files.
+ * IMPORTANT Don't forget to close the stream.
+ *
+ * @param start the starting file
+ * @return the {@link Stream} of {@link Path}
+ * @throws IOException if an I/O error is thrown when accessing the starting file.
+ *
+ * @see FileUtils#walkFilesList(Path)
+ */
+ public static Stream walkFilesStream(Path start) throws IOException {
+ return Files.walk(start).filter(Files::isRegularFile);
+ }
+
+ /**
+ * Walks over a directory and only returns files.
+ *
+ * @param start the starting file
+ * @return the {@link List} of {@link Path}
+ * @throws IOException if an I/O error is thrown when accessing the starting file.
+ *
+ * @see FileUtils#walkFilesStream(Path)
+ */
+ public static List walkFilesList(Path start) throws IOException {
+ try (var stream = walkFilesStream(start)) {
+ return stream.toList();
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/blog/Minifier.java b/src/main/java/eu/m724/blog/Minifier.java
new file mode 100644
index 0000000..f2c742b
--- /dev/null
+++ b/src/main/java/eu/m724/blog/Minifier.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2025 blog-software-java developers
+ * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.blog;
+
+import in.wilsonl.minifyhtml.Configuration;
+import in.wilsonl.minifyhtml.MinifyHtml;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+
+public class Minifier {
+ private static final Set CONTENT_TYPES_TO_MINIFY = Set.of(
+ "text/html",
+ "text/css",
+ "text/javascript"
+ );
+
+ private final Configuration configuration = new Configuration.Builder()
+ .setMinifyJs(true)
+ .setMinifyCss(true)
+ .setKeepHtmlAndHeadOpeningTags(true)
+ .build();
+
+ public String minify(String text) {
+ return MinifyHtml.minify(text, configuration);
+ }
+
+ public void minifyTree(Path dir) throws IOException {
+ for (var file : FileUtils.walkFilesList(dir)) {
+ var contentType = Files.probeContentType(file);
+ var doMinify = CONTENT_TYPES_TO_MINIFY.contains(contentType);
+
+ if (doMinify) {
+ var content = Files.readString(file);
+ var minified = minify(content);
+ Files.writeString(file, minified);
+ }
+ }
+ }
+
+ public void copyAndMinifyTree(Path srcDir, Path destDir) throws IOException {
+ for (var e : FileUtils.srcDestMap(srcDir, destDir).entrySet()) {
+ var sourceFile = e.getKey();
+ var destinationFile = e.getValue();
+
+ var contentType = Files.probeContentType(sourceFile);
+ var doMinify = CONTENT_TYPES_TO_MINIFY.contains(contentType);
+
+ if (doMinify) {
+ var content = Files.readString(sourceFile);
+ var minified = minify(content);
+ Files.writeString(destinationFile, minified);
+ } else {
+ Files.copy(sourceFile, destDir);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/eu/m724/blog/object/Site.java b/src/main/java/eu/m724/blog/object/Site.java
index 4bb96ed..2b767f8 100644
--- a/src/main/java/eu/m724/blog/object/Site.java
+++ b/src/main/java/eu/m724/blog/object/Site.java
@@ -21,6 +21,7 @@ import java.util.Map;
* @param baseUrl the base URL of the site
* @param directory The directory that the site is installed into. Like "https://example.com/blog" turns to "/blog"
* @param templateArticles whether to parse articles with Pebble templating
+ * @param minify whether to minify HTML, CSS, JS, etc.
* @param custom a map of additional custom properties
*/
public record Site(
@@ -30,6 +31,7 @@ public record Site(
String directory,
boolean templateArticles,
+ boolean minify,
Map custom
) {
@@ -48,6 +50,7 @@ public record Site(
String name = (String) yaml.get("name");
String baseUrl = (String) yaml.getOrDefault("baseUrl", "/");
var templateArticles = (boolean) yaml.getOrDefault("templateArticles", false);
+ var minify = (boolean) yaml.getOrDefault("minify", true);
String directory = "/";
if (baseUrl != null) {
@@ -59,7 +62,7 @@ public record Site(
}
return new Site(
- name, baseUrl, directory, templateArticles, yaml
+ name, baseUrl, directory, templateArticles, minify, yaml
);
}
}
diff --git a/src/main/java/eu/m724/blog/template/TemplateRenderer.java b/src/main/java/eu/m724/blog/template/TemplateRenderer.java
index 54d473c..a6e2fae 100644
--- a/src/main/java/eu/m724/blog/template/TemplateRenderer.java
+++ b/src/main/java/eu/m724/blog/template/TemplateRenderer.java
@@ -6,6 +6,7 @@
package eu.m724.blog.template;
+import eu.m724.blog.Minifier;
import eu.m724.blog.object.Article;
import eu.m724.blog.object.Site;
import io.pebbletemplates.pebble.PebbleEngine;
@@ -24,6 +25,7 @@ import java.util.Map;
*/
public class TemplateRenderer {
private final Site site;
+ private final Minifier minifier;
private final PebbleEngine pebbleEngine;
private final PebbleTemplate indexTemplate, articleTemplate;
@@ -35,7 +37,11 @@ public class TemplateRenderer {
* @param templateDirectory the root directory containing the template file
* @param fileHashes file hashes. currently only applies to assets
*/
- public TemplateRenderer(Site site, Path templateDirectory, Map fileHashes) {
+ public TemplateRenderer(Site site, Minifier minifier, Path templateDirectory, Map fileHashes) {
+ this.site = site;
+ this.minifier = minifier;
+
+
var loader = new FileLoader();
loader.setPrefix(templateDirectory.toString());
loader.setSuffix(".html");
@@ -45,7 +51,6 @@ public class TemplateRenderer {
.extension(new TemplateExtension(site, fileHashes))
.build();
- this.site = site;
this.indexTemplate = pebbleEngine.getTemplate("index_template");
this.articleTemplate = pebbleEngine.getTemplate("article_template");
}
@@ -94,7 +99,12 @@ public class TemplateRenderer {
private String renderTemplate(PebbleTemplate template, Map context) throws IOException {
var writer = new StringWriter();
template.evaluate(writer, (Map) context);
+ var html = writer.toString();
- return writer.toString();
+ if (minifier != null) { // not checking site.minify because the minifier may not be available
+ html = minifier.minify(html);
+ }
+
+ return html;
}
}