diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java
new file mode 100644
index 000000000..86c635ed7
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.mvc.freemarker;
+
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.TemplateLoader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import javax.servlet.ServletContext;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+/**
+ * A copy of the {@link TemplateLoader} used by Krazo.
+ *
+ * The {@code TemplateLoader} used by Krazo is defined as inner class. This
+ * class provides the same behaviour as "real" class so that we can use it with
+ * Freemarker {@link MultiTemplateLoader}.
+ *
+ * As extension this implementation of the {@code TemplateLoader} interface will
+ * not process template paths which start with {@code @themes/} or
+ * {@code /@themes/}. These path are processed by the templates loaders for the
+ * theming system.
+ *
+ * @author Jens Pelzetter
+ */
+public class KrazoTemplateLoader implements TemplateLoader {
+
+ private final ServletContext servletContext;
+
+ public KrazoTemplateLoader(final ServletContext servletContext) {
+ super();
+ this.servletContext = servletContext;
+ }
+
+ @Override
+ public Object findTemplateSource(final String name) throws IOException {
+ if (name.startsWith("@themes") || name.startsWith("/@themes")) {
+ return null;
+ } else {
+ // Freemarker drops "/"
+ return servletContext.getResourceAsStream(
+ String.format("/%s", name)
+ );
+ }
+ }
+
+ @Override
+ public long getLastModified(final Object templateSource) {
+ return -1;
+ }
+
+ @Override
+ public Reader getReader(
+ final Object templateSource, final String encoding
+ ) throws IOException {
+ return new InputStreamReader((InputStream) templateSource, encoding);
+ }
+
+ @Override
+ public void closeTemplateSource(
+ final Object templateSource
+ ) throws IOException {
+ final InputStream inputStream = (InputStream) templateSource;
+ inputStream.close();
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java
new file mode 100644
index 000000000..a209a462a
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.mvc.freemarker;
+
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.cache.WebappTemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.TemplateExceptionHandler;
+import org.eclipse.krazo.engine.ViewEngineConfig;
+import org.eclipse.krazo.ext.freemarker.DefaultConfigurationProducer;
+import org.libreccm.theming.Themes;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.inject.Specializes;
+import javax.inject.Inject;
+import javax.servlet.ServletContext;
+import javax.ws.rs.Produces;
+
+/**
+ * Extends the default configuration for Freemarker of Eclipse Krazo to
+ * support Freemarker templates in CCM themes.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+public class MvcFreemarkerConfigurationProducer
+ extends DefaultConfigurationProducer {
+
+ @Inject
+ private ServletContext servletContext;
+
+ @Inject
+ private Themes themes;
+
+ @Produces
+ @ViewEngineConfig
+ @Specializes
+ @Override
+ public Configuration getConfiguration() {
+ final Configuration configuration = new Configuration(
+ Configuration.VERSION_2_3_30
+ );
+
+ configuration.setDefaultEncoding("UTF-8");
+ configuration.setTemplateExceptionHandler(
+ TemplateExceptionHandler.RETHROW_HANDLER
+ );
+ configuration.setLogTemplateExceptions(false);
+ configuration.setWrapUncheckedExceptions(false);
+ configuration.setLocalizedLookup(false);
+ configuration.setTemplateLoader(
+ new MultiTemplateLoader(
+ new TemplateLoader[]{
+ new KrazoTemplateLoader(servletContext),
+ new ThemesTemplateLoader(themes),
+ // For loading Freemarker macro libraries from WEB-INF
+ // resources
+ new WebappTemplateLoader(
+ servletContext, "/themes/freemarker"
+ ),
+ // For loading Freemarker macro libraries from classpath
+ // resources
+ new ClassTemplateLoader(getClass(), "/themes/freemarker")
+ }
+ )
+ );
+
+ return configuration;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java
new file mode 100644
index 000000000..677046738
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.mvc.freemarker;
+
+import freemarker.cache.TemplateLoader;
+import org.libreccm.theming.ThemeInfo;
+import org.libreccm.theming.ThemeVersion;
+import org.libreccm.theming.Themes;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Loads Freemarker templates from a theme.
+ *
+ * @author Jens Pelzetter
+ */
+public class ThemesTemplateLoader implements TemplateLoader {
+
+ private final Themes themes;
+
+ public ThemesTemplateLoader(final Themes themes) {
+ this.themes = themes;
+ }
+
+ /**
+ * Loads the template from a theme. The path of the theme file must follow
+ * the following format:
+ *
+ * {@code @themes/$themeName/$version/$path/$to/$file}
+ *
+ * The {@code @themes} prefix is mandantory. {@code $themeName} is the name
+ * of the theme from which the template is loaded. {@code $version} is the
+ * version of the theme to use. This token is converted to
+ * {@link ThemeVersion}. Valid values are therefore {@code DRAFT} and
+ * {@code LIVE}. The remainder of the path is the path to the file inside the
+ * theme.
+ *
+ * @param path The path of the file. The path must include the theme and its
+ * version.
+ *
+ * @return An {@link InputStream} for the template if the template was found
+ * in the theme. Otherwise {@code null} is returned.
+ *
+ * @throws IOException
+ */
+ @Override
+ public Object findTemplateSource(final String path) throws IOException {
+ if (path.startsWith("@themes") || path.startsWith("/@themes")) {
+ final String[] tokens;
+ if (path.startsWith("/")) {
+ tokens = path.substring(1).split("/");
+ } else {
+ tokens = path.split("/");
+ }
+ return findTemplateSource(tokens);
+ } else {
+ return null;
+ }
+ }
+
+ private InputStream findTemplateSource(final String[] tokens) {
+ if (tokens.length >= 4) {
+ final String themeName = tokens[1];
+ final ThemeVersion themeVersion = ThemeVersion
+ .valueOf(tokens[2]);
+ final String filePath = String.join(
+ "/",
+ Arrays.copyOfRange(
+ tokens, 3, tokens.length, String[].class
+ )
+ );
+
+ return findTemplateSource(themeName, themeVersion, filePath);
+ } else {
+ return null;
+ }
+ }
+
+ private InputStream findTemplateSource(
+ final String themeName,
+ final ThemeVersion themeVersion,
+ final String filePath
+ ) {
+ final Optional themeInfo = themes.getTheme(
+ themeName, themeVersion
+ );
+ if (themeInfo.isPresent()) {
+ return findTemplateSource(themeInfo.get(), filePath);
+ } else {
+ return null;
+ }
+ }
+
+ private InputStream findTemplateSource(
+ final ThemeInfo themeInfo, final String filePath
+ ) {
+ final Optional source = themes.getFileFromTheme(
+ themeInfo, filePath
+ );
+ if (source.isPresent()) {
+ return source.get();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public long getLastModified(Object templateSource) {
+ return -1;
+ }
+
+ @Override
+ public Reader getReader(
+ final Object templateSource, final String encoding
+ ) throws IOException {
+ final InputStream inputStream = (InputStream) templateSource;
+ return new InputStreamReader(inputStream, encoding);
+ }
+
+ @Override
+ public void closeTemplateSource(
+ final Object templateSource
+ ) throws IOException {
+ final InputStream inputStream = (InputStream) templateSource;
+ inputStream.close();
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/package-info.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/package-info.java
new file mode 100644
index 000000000..d3d2157ad
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+/**
+ * Integration of the Freemarker View Engine of Eclipse Krazo with LibreCCM.
+ */
+package org.libreccm.mvc.freemarker;
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/package-info.java b/ccm-core/src/main/java/org/libreccm/mvc/package-info.java
new file mode 100644
index 000000000..5d3b11aa2
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+/**
+ * The classes in this package integrate LibreCCM with Jakarta EE MVC and its
+ * reference implementation Eclipse Krazo.
+ *
+ * @see https://www.mvc-spec.org/
+ */
+package org.libreccm.mvc;