Replies: 3 comments
-
Local development in Maizzle works with files you have on disk. So if they offer an API, you may be able to connect to the Postmark API in your Then, maybe in This is just a high level overview, you'd need to connect to the API and write all the code to fetch from there yourself. Things might get even more complicated if you need to populate certain variables with data coming from Postmark... |
Beta Was this translation helpful? Give feedback.
-
Hi! I've spent the last week integrating Maizzle with our systems to generate Postmark email templates and thought I'd give some pointers on how we approached it as well as some pitfalls we went through. The general idea is to use Maizzle to generate Postmark/Mustachio templates that then our CI/CD system builds and pushes to Postmark, while keeping a working local development environment that can properly render the template with test data. The first thing we did was to change Maizzle delimiters to
build: {
posthtml: {
expressions: {
delimiters: ['[[', ']]'],
unescapeDelimiters: ['[[[', ']]]'],
},
}, Now - for some reason not all delimiters are changed correctly (saw an issue posted about this also: #419 ). Due to limited time I patched
#!/usr/bin/env sh
sed -i "s/'{{{'/'[[['/" node_modules/posthtml-expressions/lib/index.js
sed -i "s/'}}}'/']]]'/" node_modules/posthtml-expressions/lib/index.js
sed -i "s/'{{'/'[['/" node_modules/posthtml-expressions/lib/index.js
sed -i "s/'}}'/']]'/" node_modules/posthtml-expressions/lib/index.js In order to get a usable local development environment running that can render Postmark template data, each template can define a datafile with local JSON test data (same structure as you would use when previewing templates in Postmark):
---
slug: "my-template"
subject: "Subject here {{mustachio_variable}}"
preheader: "Preheader here"
testDataFile: "src/data/my-template.json"
---
<extends src="src/layouts/base.html">
<block name="content">
Start content
{{#some_mustachio_condition}}
Text
{{#each ../some_mustachio_iterable}}
{{obj_property}}
One level up in the object hierarchy:
{{ ../mustachio_variable}}
{{/each}}
{{/some_mustachio_condition}}
</block>
</extends>
{
"mustachio_variable": "some value",
"some_mustachio_condition": true,
"some_mustachio_iterable": [
{
"obj_property": "obj value"
}
]
} that then the events: {
async afterRender(html, config) {
return renderTestData(html, config);
}
}, In order to inject the test data there are a few things to keep in mind here - Postmark uses Mustachio as templating engine and our Maizzle templates will output Mustachio tags. Mustachio is written in C# and I haven't seen any NodeJS version available. To keep things simple I will just "convert" some Mustachio specifics to Mustache syntax instead and use Mustache for rendering on the local environment. This is the script I'm currently using - it's very simple/specific for our current needs at the moment, such as only non-nested loops and accessing variables one level up when inside a block/loop so you'd need to adapt it depending on how advanced your templates are: const {get} = require('lodash')
const fs = require("fs");
const Mustache = require('mustache');
module.exports = (html, config) => {
const testDataFile = get(config, 'testDataFile', null);
if (testDataFile) {
let rawData = fs.readFileSync(testDataFile);
let testData = JSON.parse(rawData);
html = html.replace(/({{#)(each \.\.\/)(.+?)(}}.+?{{\/)(each)(}})/msg, '$1$3$4$3$6');
html = html.replace(/({{)( \.\.\/)(.+?)(}})/msg, '$1$3$4');
html = Mustache.render(html, testData);
}
return html;
} To push to Postmark (your Maizzle production config): async beforeRender(html, config) {
extractSubject(config);
return html;
},
const fs = require("fs");
const {get} = require("lodash");
module.exports = function (config) {
const buildPath = get(config, 'build.templates.destination.path');
const slug = get(config, 'slug', null);
const subject = get(config, 'subject', null);
if (slug && subject) {
fs.writeFile(`${buildPath}/${slug}.subject.txt`, subject, function (err) {
if (err) {
return console.log(err);
}
});
}
}; I also have a Then blank out the async afterRender(html, config) {
// Empty block needed to override dev transformations.
return html;
}, Then in our CI/CD system depending on the build step that's running I use various NodeJS scripts to validate/push to Postmark, e.g.:
#!/usr/bin/env node
const axios = require('axios');
const fs = require('fs');
const templateData = require('./postmark-template-data');
const {POSTMARK_DEV_SERVER_TOKEN} = require("./postmark-env");
const url = "https://api.postmarkapp.com/templates/validate";
axios.defaults.headers.common['X-Postmark-Server-Token'] = POSTMARK_DEV_SERVER_TOKEN;
axios.defaults.headers.post['Accept'] = 'application/json';
async function validateTemplate({
slug: slug,
htmlFilePath: htmlFilePath,
subjectFilePath: subjectFilePath,
textFilePath: textFilePath,
dataFilePath: dataFilePath
}) {
let htmlContent;
let subjectContent;
let textContent;
let dataContent;
try {
htmlContent = fs.readFileSync(htmlFilePath, 'utf8');
subjectContent = fs.readFileSync(subjectFilePath, 'utf8');
textContent = fs.readFileSync(textFilePath, 'utf8');
dataContent = fs.readFileSync(dataFilePath, 'utf8');
} catch (e) {
console.log('File read error:', e.stack);
}
const data = {
"Subject": subjectContent,
"HtmlBody": htmlContent,
"TextBody": textContent,
"TestRenderModel": JSON.parse(dataContent),
"TemplateType": "Standard"
}
let success = await axios
.post(url, data)
.then(res => {
const isValid = res.status === 200 && res.data["AllContentIsValid"] === true;
if (!isValid) {
console.log("Status code:", res.status);
console.log("HtmlBody", res.data["HtmlBody"]["ValidationErrors"]);
console.log("Subject", res.data["Subject"]["ValidationErrors"]);
console.log("TextBody", res.data["TextBody"]["ValidationErrors"]);
}
return isValid
})
.catch(error => {
console.error("Axios error:", error)
return false;
})
return success;
}
let promises = [];
for (let data of templateData) {
promises.push(validateTemplate({...data}));
}
Promise.all(promises).then((values) => {
process.exit(values.includes(false) ? 1 : 0);
});
const fs = require('fs');
const buildBasePath = 'build/prod';
const dataBasePath = 'src/data';
function getTemplatesData() {
let templateFilesData = [];
if (!fs.existsSync(buildBasePath)) {
console.log("no dir ", buildBasePath);
return;
}
const files = fs.readdirSync(buildBasePath);
for (let file of files) {
if (file.indexOf('.html') >= 0) {
let slug = file.replace('.html', '');
templateFilesData.push({
'slug': slug,
'htmlFilePath': `${buildBasePath}/${slug}.html`,
'subjectFilePath': `${buildBasePath}/${slug}.subject.txt`,
'textFilePath': `${buildBasePath}/${slug}.txt`,
'dataFilePath': `${dataBasePath}/${slug}.json`,
})
}
}
return templateFilesData;
}
module.exports = getTemplatesData(); Now - this is all very tailored for our specific needs and CI/CD setup and will probably need a lot of adaptation from you (and there are a lot of improvements that can be done to the snippets above), but I hope this can be useful as inspiration/starting point on how a Maizzle <> Postmark integration could look like. |
Beta Was this translation helpful? Give feedback.
-
Thanks for the info, it was really helpful. I'm not sure if some of what you've put together here is for your own custom purposes, I was able to do it a little more simpler: Production config file const fs = require("fs");
const get = require("lodash/get");
function buildMetaFile(config) {
const alias = get(config, 'alias', null);
const subject = get(config, 'subject', null);
const name = get(config, 'name', subject);
const buildPath = get(config, 'build.templates.destination.path');
const destPath = `${buildPath}/${alias}`;
if (alias && subject) {
const meta = {
"Name": name,
"Alias": alias,
"Subject": subject,
"TemplateType": "Standard"
}
if (!fs.existsSync(destPath)) {
fs.mkdirSync(destPath);
}
fs.writeFile(`${destPath}/meta.json`, JSON.stringify(meta), function (err) {
if (err) {
return console.log(err);
}
});
}
}
module.exports = {
baseURL: <base image path>,
build: {
templates: {
filetypes: 'html',
source: 'src/templates',
destination: {
path: 'dist',
extension: 'html',
},
plaintext: {
destination: {
path: 'dist',
extension: 'txt'
},
},
assets: {
source: 'src/images',
destination: 'images',
},
},
},
inlineCSS: {
applySizeAttribute: {
width: ['IMG'],
height: ['IMG'],
},
keepOnlyAttributeSizes: {
width: ['IMG', 'VIDEO'],
height: ['IMG', 'VIDEO'],
},
mergeLonghand: true,
preferBgColorAttribute: true,
},
prettify: {
indent_inner_html: false,
indent_body_inner_html: false,
ocd: true,
},
events: {
beforeRender: async (html, config) => {
buildMetaFile(config)
return html;
},
afterBuild(files) {
files.forEach(function (path, index) {
if (path.endsWith('.html') || path.endsWith('.txt')) {
const pathParts = path.split('/')
const filename = pathParts[1]
const fileParts = filename.split('.')
const dest = `./dist/${fileParts[0]}`
if (fs.existsSync(dest)) {
fs.renameSync(path, `${dest}/content.${fileParts[1]}`)
}
}
});
}
},
removeUnusedCSS: true
} Once I had this I was able to automate the build and deploy to Postmark: name: Push and Deploy Postmark Templates
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Cancel Previous Run
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}
- name: Setup node
uses: actions/setup-node@v2
- name: Setup Maizzle CLI
run: npm install -g @maizzle/cli
- name: Setup postmark-cli
run: npm install postmark-cli -g
- name: Install node modules
run: npm install
- name: Build templates
run: maizzle build production
- name: Push static assets to S3
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read --follow-symlinks
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.AWS_BUCKET }}
SOURCE_DIR: 'src/assets/images'
DEST_DIR: 'email'
- name: Push templates to Postmark
run: postmark templates push dist -f
env:
POSTMARK_SERVER_TOKEN: ${{secrets.POSTMARK_SERVER_TOKEN}} Hope it helps someone else. |
Beta Was this translation helpful? Give feedback.
-
I would really like to use this framework to develop postmark emails, however, it does not fit with existing tools.
For example, there is postmark cli (official) - https://github.com/wildbit/postmark-cli/ that allows for live preview and development of postmark templates. So it's easy to pull the templates from postmark, make changes and push them again (because the structure also setups the metadata).
It also renders loops using the https://github.com/wildbit/mustachio template library.
Is there a workflow I can adopt to making the workflow for transactional templates with dynamic variables a bit easier?
Beta Was this translation helpful? Give feedback.
All reactions