Skip to main content

Include System

mjml-java supports mj-include for splitting MJML templates across multiple files. The include system resolves external content during rendering, letting you reuse headers, footers, styles, and other template fragments.

Basic Usage

Use the mj-include tag with a path attribute to include external content:

<mjml>
<mj-head>
<mj-include path="partials/styles.mjml" />
</mj-head>
<mj-body>
<mj-include path="partials/header.mjml" />
<mj-section>
<mj-column>
<mj-text>Main content</mj-text>
</mj-column>
</mj-section>
<mj-include path="partials/footer.mjml" />
</mj-body>
</mjml>

Include Types

The type attribute controls how the included content is interpreted. There are four include types (mjml, html, css, and css-inline):

MJML Includes (default)

When type is omitted or set to "mjml", the included file is parsed as MJML. The children of the included content replace the mj-include element.

<mj-include path="partials/header.mjml" />

The included file can be a full MJML document or a fragment:

<!-- partials/header.mjml (fragment) -->
<mj-section>
<mj-column>
<mj-image src="logo.png" />
</mj-column>
</mj-section>

If the included file is a full <mjml> document, mjml-java extracts the appropriate section: head children are used when the include is inside mj-head, and body children are used otherwise.

HTML Includes

Set type="html" to include raw HTML. The content is wrapped in an mj-raw element:

<mj-include path="partials/tracking-pixel.html" type="html" />

CSS Includes

Set type="css" to include a CSS stylesheet. The content becomes an mj-style element:

<mj-include path="styles/theme.css" type="css" />

For inline CSS (applied directly to matching elements via the CSS inliner), add the css-inline attribute:

<mj-include path="styles/inline.css" type="css" css-inline="inline" />

Configuring the Include Resolver

Includes require an IncludeResolver to be configured. Without one, mj-include tags are left unresolved and a warning is logged during rendering.

FileSystemIncludeResolver

The built-in FileSystemIncludeResolver reads files from a base directory:

import dev.jcputney.mjml.FileSystemIncludeResolver;

MjmlConfiguration config = MjmlConfiguration.builder()
.includeResolver(new FileSystemIncludeResolver(Path.of("/templates")))
.build();

String mjml = Files.readString(Path.of("/templates/email.mjml"));
String html = MjmlRenderer.render(mjml, config).html();

Paths in mj-include are resolved relative to the base directory. For example, if the base directory is /templates and the include path is partials/header.mjml, the resolver reads /templates/partials/header.mjml.

Path Traversal Protection

FileSystemIncludeResolver prevents path traversal attacks. Any resolved path that escapes the base directory is rejected:

<!-- These are rejected with MjmlIncludeException -->
<mj-include path="../../../etc/passwd" />
<mj-include path="partials/../../secret.txt" />

The resolver normalizes paths before checking, so encoded traversal sequences are also caught.

Custom Resolvers

Implement the IncludeResolver interface to load templates from any source:

import dev.jcputney.mjml.IncludeResolver;
import dev.jcputney.mjml.ResolverContext;

@FunctionalInterface
public interface IncludeResolver {
String resolve(String path, ResolverContext context);
}

The ResolverContext provides metadata about the include chain: the including file path, include type ("mjml", "html", "css", "css-inline"), and nesting depth.

Classpath Resolver

Load templates from the Java classpath (useful for bundled application templates):

IncludeResolver classpathResolver = (path, context) -> {
try (InputStream is = getClass().getResourceAsStream("/templates/" + path)) {
if (is == null) {
throw new MjmlException("Template not found: " + path);
}
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new MjmlException("Failed to read template: " + path, e);
}
};

MjmlConfiguration config = MjmlConfiguration.builder()
.includeResolver(classpathResolver)
.build();

Database Resolver

Load templates stored in a database:

IncludeResolver dbResolver = (path, context) -> {
String content = templateRepository.findByPath(path);
if (content == null) {
throw new MjmlException("Template not found: " + path);
}
return content;
};

HTTP Resolver (with Security Caveats)

SSRF Risk

An HTTP-based resolver is vulnerable to Server-Side Request Forgery (SSRF). If an attacker controls the MJML input, they can use mj-include paths to probe internal network resources. Only use HTTP resolvers with trusted input, and validate/restrict allowed hosts. Consider using the built-in UrlIncludeResolver from the mjml-java-resolvers module, which includes SSRF protection out of the box.

For hostname URLs (for example https://cdn.example.com/template.mjml), configure an explicit host allowlist with UrlIncludeResolver.builder().allowedHosts(...).

IncludeResolver httpResolver = (path, context) -> {
// Validate the URL against an allowlist
URI uri = URI.create(path);
if (!ALLOWED_HOSTS.contains(uri.getHost())) {
throw new MjmlException("Host not allowed: " + uri.getHost());
}

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
};

Nested Includes

Included files can themselves contain mj-include tags. The processor resolves includes recursively up to a maximum depth of 50 levels (configurable via MjmlConfiguration.builder().maxIncludeDepth()).

<!-- layout.mjml -->
<mj-section>
<mj-include path="partials/nav.mjml" />
</mj-section>

<!-- partials/nav.mjml -->
<mj-column>
<mj-include path="partials/nav-links.mjml" />
</mj-column>

Circular Include Detection

The processor tracks visited paths and throws MjmlIncludeException if a circular include is detected:

<!-- a.mjml includes b.mjml, which includes a.mjml -->
<!-- Throws: "Circular include detected for path: a.mjml" -->

Processing Order

Include resolution is phase 3 of the 7-phase rendering pipeline:

  1. Preprocess (CDATA wrapping)
  2. Parse (XML to node tree)
  3. Resolve includes -- mj-include elements are replaced with resolved content
  4. Process head (fonts, styles, attributes)
  5. Resolve attributes (cascade applied during rendering)
  6. Render body
  7. Assemble skeleton + CSS inlining

This means included content is fully integrated into the document tree before head processing and attribute resolution occur. Included mj-attributes, mj-font, and mj-style elements work exactly as if they were written inline.