-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[PM-26551] MJML build script #6417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
0085ff9
feat: powershell build script
ike-kottlowski 0805cce
feat: update package.json
ike-kottlowski 1be934d
fix: prettier version
ike-kottlowski a016e6f
feat: add watch functionality to have parity with `build.sh`
ike-kottlowski 57a007d
feat: add watch and hbs output to build script
ike-kottlowski 57b2fe3
docs: update readme for MJML
ike-kottlowski b9a57ab
docs: styling and clarity
ike-kottlowski a6d8984
docs: adding docs for email templates
ike-kottlowski f44e71a
docs: update based on feedback
ike-kottlowski 7828a08
feat: change powershell to node build script for cross platform; alsoโฆ
ike-kottlowski 115ca4b
Merge branch 'main' into auth/pm-26551/mjml-build-script
ike-kottlowski 33251e0
docs: updated based on feedback
ike-kottlowski d50eb5d
Update README with build script details
ike-kottlowski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,112 @@ | ||
# Email templates | ||
# MJML email templating | ||
|
||
This directory contains MJML templates for emails sent by the application. MJML is a markup language designed to reduce the pain of coding responsive email templates. | ||
This directory contains MJML templates for emails. MJML is a markup language designed to reduce the pain of coding responsive email templates. Component based development features in MJML improve code quality and reusability. | ||
|
||
## Usage | ||
MJML stands for MailJet Markup Language. | ||
|
||
```bash | ||
## Implementation considerations | ||
|
||
These `MJML` templates are compiled into HTML which will then be further consumed by our Handlebars mail service. We can continue to use this service to assign values from our View Models. This leverages the existing infrastructure. It also means we can continue to use the double brace (`{{}}`) syntax within MJML since Handlebars can be used to assign values to those `{{variables}}`. | ||
|
||
There is no change on how we interact with our view models. | ||
|
||
There is an added step where we compile `*.mjml` to `*.html.hbs`. `*.html.hbs` is the format we use so the handlebars service can apply the variables. This build pipeline process is in progress and may need to be manually done at times. | ||
|
||
### `*.txt.hbs` | ||
|
||
There is no change to how we create the `txt.hbs`. MJML does not impact how we create these artifacts. | ||
|
||
## Building `MJML` files | ||
|
||
```shell | ||
npm ci | ||
|
||
# Build once | ||
# Build *.html to ./out directory | ||
npm run build | ||
|
||
# To build on changes | ||
npm run watch | ||
# To build on changes to *.mjml and *.js files, new files will not be tracked, you will need to run again | ||
npm run build:watch | ||
|
||
# Build *.html.hbs to ./out directory | ||
npm run build:hbs | ||
|
||
# Build minified *.html.hbs to ./out directory | ||
npm run build:minify | ||
|
||
# apply prettier formatting | ||
npm run prettier | ||
``` | ||
|
||
## Development | ||
|
||
MJML supports components and you can create your own components by adding them to `.mjmlconfig`. | ||
MJML supports components and you can create your own components by adding them to `.mjmlconfig`. Components are simple JavaScript that return MJML markup based on the attributes assigned, see components/mj-bw-hero.js. The markup is not a proper object, but contained in a string. | ||
|
||
When using MJML templating you can use the above [commands](#building-mjml-files) to compile the template and view it in a web browser. | ||
|
||
Not all MJML tags have the same attributes, it is highly recommended to review the documentation on the official MJML website to understand the usages of each of the tags. | ||
|
||
### Recommended development | ||
|
||
#### Mjml email template development | ||
|
||
1. create `cool-email.mjml` in appropriate team directory | ||
2. run `npm run build:watch` | ||
3. view compiled `HTML` output in a web browser | ||
4. iterate -> while `build:watch`'ing you should be able to refresh the browser page after the mjml/js re-compile to see the changes | ||
|
||
#### Testing with `IMailService` | ||
|
||
After the email is developed from the [initial step](#mjml-email-template-development) make sure the email `{{variables}}` are populated properly by running it through an `IMailService` implementation. | ||
|
||
1. run `npm run build:minify` | ||
2. copy built `*.html.hbs` files from the build directory to a location the mail service can consume them | ||
3. run code that will send the email | ||
|
||
The minified `html.hbs` artifacts are deliverables and must be placed into the correct `src/Core/MailTemplates/Handlebars/` directories in order to be used by `IMailService` implementations. | ||
|
||
### Custom tags | ||
|
||
There is currently a `mj-bw-hero` tag you can use within your `*.mjml` templates. This is a good example of how to create a component that takes in attribute values allowing us to be more DRY in our development of emails. Since the attribute's input is a string we are able to define whatever we need into the component, in this case `mj-bw-hero`. | ||
|
||
In order to view the custom component you have written you will need to include it in the `.mjmlconfig` and reference it in an `mjml` template file. | ||
|
||
```html | ||
<!-- Custom component implementation--> | ||
<mj-bw-hero | ||
img-src="https://assets.bitwarden.com/email/v1/business.png" | ||
title="Verify your email to access this Bitwarden Send" | ||
/> | ||
``` | ||
|
||
Attributes in Custom Components are defined by the developer. They can be required or optional depending on implementation. See the official MJML documentation for more information. | ||
|
||
```js | ||
static allowedAttributes = { | ||
"img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area | ||
title: "string", // REQUIRED: large text stating primary purpose of the email | ||
"button-text": "string", // OPTIONAL: text to display in the button | ||
"button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked | ||
"sub-title": "string", // OPTIONAL: smaller text providing additional context for the title | ||
}; | ||
|
||
static defaultAttributes = {}; | ||
``` | ||
|
||
Custom components, such as `mj-bw-hero`, must be defined in the `.mjmlconfig` in order for them to be compiled and rendered properly in the templates. | ||
|
||
```json | ||
{ | ||
"packages": ["components/mj-bw-hero"] | ||
} | ||
``` | ||
|
||
### `mj-include` | ||
|
||
You are also able to reference other more static MJML templates in your MJML file simply by referencing the file within the MJML template. | ||
|
||
```html | ||
<!-- Example of reference to mjml template --> | ||
<mj-wrapper padding="5px 20px 10px 20px"> | ||
<mj-include path="../../components/learn-more-footer.mjml" /> | ||
</mj-wrapper> | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
const mjml2html = require("mjml"); | ||
const { registerComponent } = require("mjml-core"); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const glob = require("glob"); | ||
|
||
// Parse command line arguments | ||
const args = process.argv.slice(2); // Remove 'node' and script path | ||
|
||
// Parse flags | ||
const flags = { | ||
minify: args.includes("--minify") || args.includes("-m"), | ||
watch: args.includes("--watch") || args.includes("-w"), | ||
hbs: args.includes("--hbs") || args.includes("-h"), | ||
trace: args.includes("--trace") || args.includes("-t"), | ||
clean: args.includes("--clean") || args.includes("-c"), | ||
help: args.includes("--help"), | ||
}; | ||
|
||
// Use __dirname to get absolute paths relative to the script location | ||
const config = { | ||
inputDir: path.join(__dirname, "emails"), | ||
outputDir: path.join(__dirname, "out"), | ||
minify: flags.minify, | ||
validationLevel: "strict", | ||
hbsOutput: flags.hbs, | ||
}; | ||
|
||
// Debug output | ||
if (flags.trace) { | ||
console.log("[DEBUG] Script location:", __dirname); | ||
console.log("[DEBUG] Input directory:", config.inputDir); | ||
console.log("[DEBUG] Output directory:", config.outputDir); | ||
} | ||
|
||
// Ensure output directory exists | ||
if (!fs.existsSync(config.outputDir)) { | ||
fs.mkdirSync(config.outputDir, { recursive: true }); | ||
if (flags.trace) { | ||
console.log("[INFO] Created output directory:", config.outputDir); | ||
} | ||
} | ||
|
||
// Find all MJML files with absolute path | ||
const mjmlFiles = glob.sync(`${config.inputDir}/**/*.mjml`); | ||
|
||
console.log(`\n[INFO] Found ${mjmlFiles.length} MJML file(s) to compile...`); | ||
|
||
if (mjmlFiles.length === 0) { | ||
console.error("[ERROR] No MJML files found!"); | ||
console.error("[ERROR] Looked in:", config.inputDir); | ||
console.error( | ||
"[ERROR] Does this directory exist?", | ||
fs.existsSync(config.inputDir), | ||
); | ||
process.exit(1); | ||
} | ||
|
||
// Compile each MJML file | ||
let successCount = 0; | ||
let errorCount = 0; | ||
|
||
mjmlFiles.forEach((filePath) => { | ||
try { | ||
const mjmlContent = fs.readFileSync(filePath, "utf8"); | ||
const fileName = path.basename(filePath, ".mjml"); | ||
const relativePath = path.relative(config.inputDir, filePath); | ||
|
||
console.log(`\n[BUILD] Compiling: ${relativePath}`); | ||
|
||
// Compile MJML to HTML | ||
const result = mjml2html(mjmlContent, { | ||
minify: config.minify, | ||
validationLevel: config.validationLevel, | ||
filePath: filePath, // Important: tells MJML where the file is for resolving includes | ||
mjmlConfigPath: __dirname, // Point to the directory with .mjmlconfig | ||
}); | ||
|
||
// Check for errors | ||
if (result.errors.length > 0) { | ||
console.error(`[ERROR] Failed to compile ${fileName}.mjml:`); | ||
result.errors.forEach((err) => | ||
console.error(` ${err.formattedMessage}`), | ||
); | ||
errorCount++; | ||
return; | ||
} | ||
|
||
// Calculate output path preserving directory structure | ||
const relativeDir = path.dirname(relativePath); | ||
const outputDir = path.join(config.outputDir, relativeDir); | ||
|
||
// Ensure subdirectory exists | ||
if (!fs.existsSync(outputDir)) { | ||
fs.mkdirSync(outputDir, { recursive: true }); | ||
} | ||
|
||
const outputExtension = config.hbsOutput ? ".html.hbs" : ".html"; | ||
const outputPath = path.join(outputDir, `${fileName}${outputExtension}`); | ||
fs.writeFileSync(outputPath, result.html); | ||
|
||
console.log( | ||
`[OK] Built: ${fileName}.mjml โ ${path.relative(__dirname, outputPath)}`, | ||
); | ||
successCount++; | ||
|
||
// Log warnings if any | ||
if (result.warnings && result.warnings.length > 0) { | ||
console.warn(`[WARN] Warnings for ${fileName}.mjml:`); | ||
result.warnings.forEach((warn) => | ||
console.warn(` ${warn.formattedMessage}`), | ||
); | ||
} | ||
} catch (error) { | ||
console.error(`[ERROR] Exception processing ${path.basename(filePath)}:`); | ||
console.error(` ${error.message}`); | ||
errorCount++; | ||
} | ||
}); | ||
|
||
console.log(`\n[SUMMARY] Compilation complete!`); | ||
console.log(` Success: ${successCount}`); | ||
console.log(` Failed: ${errorCount}`); | ||
console.log(` Output: ${config.outputDir}`); | ||
|
||
if (errorCount > 0) { | ||
process.exit(1); | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
Email templating | ||
================ | ||
|
||
We use MJML to generate the HTML that our mail services use to send emails to users. To accomplish this, we use different file types depending on which part of the email generation process we're working with. | ||
|
||
# File Types | ||
|
||
## `*.html.hbs` | ||
These are the compiled HTML email templates that serve as the foundation for all HTML emails sent by the Bitwarden platform. They are generated from MJML source files and enhanced with Handlebars templating capabilities. | ||
|
||
### Generation Process | ||
- **Source**: Built from `*.mjml` files in the `./mjml` directory. | ||
- The MJML source acts as a toolkit for developers to generate HTML. It is the developers responsibility to generate the HTML and then ensure it is accessible to `IMailService` implementations. | ||
- **Build Tool**: Generated via node build scripts: `npm run build`. | ||
- The build script definitions can be viewed in the `Mjml/package.json` as well as in `Mjml/build.js`. | ||
- **Output**: Cross-client compatible HTML with embedded CSS for maximum email client support | ||
- **Template Engine**: Enhanced with Handlebars syntax for dynamic content injection | ||
|
||
### Handlebars Integration | ||
The templates use Handlebars templating syntax for dynamic content replacement: | ||
|
||
```html | ||
<!-- Example Handlebars usage --> | ||
<h1>Welcome {{userName}}!</h1> | ||
<p>Your organization {{organizationName}} has invited you to join.</p> | ||
<a href="{{actionUrl}}">Accept Invitation</a> | ||
``` | ||
|
||
**Variable Types:** | ||
- **Simple Variables**: `{{userName}}`, `{{email}}`, `{{organizationName}}` | ||
|
||
### Email Service Integration | ||
The `IMailService` consumes these templates through the following process: | ||
|
||
1. **Template Selection**: Service selects appropriate `.html.hbs` template based on email type | ||
2. **Model Binding**: View model properties are mapped to Handlebars variables | ||
3. **Compilation**: Handlebars engine processes variables and generates final HTML | ||
|
||
### Development Guidelines | ||
|
||
**Variable Naming:** | ||
- Use camelCase for consistency: `{{userName}}`, `{{organizationName}}` | ||
- Prefix URLs with descriptive names: `{{actionUrl}}`, `{{logoUrl}}` | ||
|
||
**Testing Considerations:** | ||
- Verify Handlebars variable replacement with actual view model data | ||
- Ensure graceful degradation when variables are missing or null, if necessary | ||
- Validate HTML structure and accessibility compliance | ||
|
||
## `*.txt.hbs` | ||
These files provide plain text versions of emails and are essential for email accessibility and deliverability. They serve several important purposes: | ||
|
||
### Purpose and Usage | ||
- **Accessibility**: Screen readers and assistive technologies often work better with plain text versions | ||
- **Email Client Compatibility**: Some email clients prefer or only display plain text versions | ||
- **Fallback Content**: When HTML rendering fails, the plain text version ensures the message is still readable | ||
|
||
### Structure | ||
Plain text email templates use the same Handlebars syntax (`{{variable}}`) as HTML templates for dynamic content replacement. They should: | ||
|
||
- Contain the core message content without HTML formatting | ||
- Use line breaks and spacing for readability | ||
- Include all important links as full URLs | ||
- Maintain logical content hierarchy using spacing and simple text formatting | ||
|
||
### Email Service Integration | ||
The `IMailService` automatically uses both versions when sending emails: | ||
- The HTML version (from `*.html.hbs`) provides rich formatting and styling | ||
- The plain text version (from `*.txt.hbs`) serves as the text alternative | ||
- Email clients can choose which version to display based on user preferences and capabilities | ||
|
||
### Development Guidelines | ||
- Always create a corresponding `*.txt.hbs` file for each `*.html.hbs` template | ||
- Keep the content concise but complete - include all essential information from the HTML version | ||
- Test plain text templates to ensure they're readable and convey the same message | ||
|
||
## `*.mjml` | ||
This is a templating language we use to increase efficiency when creating email content. See the readme within the `./mjml` directory for more comprehensive information. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.