Skip to content

Commit

Permalink
Initial command line tool (#1)
Browse files Browse the repository at this point in the history
* Adding code + ESLint + Prettier

* Make cli executable

* Add examples

* Add yargs for CLI tool

* Refactor with async/await

* Don't apply Prettier to markdown files

* Use keeptags option to preserve MailChimp tags

* Remove comment

* Fix arguments + display edit url
  • Loading branch information
MarcL authored Nov 5, 2019
1 parent 08452a1 commit 6724476
Show file tree
Hide file tree
Showing 13 changed files with 3,870 additions and 1 deletion.
16 changes: 16 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": ["airbnb-base", "prettier"],
"globals": {},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": ["error", 4],
"quotes": ["error"]
}
}
6 changes: 6 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Markdown To Mailchimp

> Test project to create Mailchimp newsletters using Markdown
> Create Mailchimp newsletters using Markdown
## Install

## Usage

### CLI

### Library
79 changes: 79 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env node
const yargs = require('yargs')
const chalk = require('chalk')
const markdownToHtmlEmail = require('../src/markdownToHtmlEmail')
const createMailchimpCampaign = require('../src/createMailchimpCampaign')

const logError = error => console.error(chalk.bold.red(`❌ ${error}`))
const logSuccess = message => console.log(chalk.green(`✅ ${message}`))

const { argv } = yargs
.usage('Usage: $0 [options]')
.example('$0 --md ./test.md --t ./template.mjml --o ./test.html')
.option('m', {
alias: 'markdown',
demandOption: true,
describe: 'File containing email content in markdown format',
type: 'string',
})
.option('t', {
alias: 'template',
demandOption: true,
describe: 'File containing email template in MJML format',
type: 'string',
})
.option('o', {
alias: 'output',
describe:
'Optional directory to write HTML email to. Filename matches markdown name.',
type: 'string',
})
.option('a', {
alias: 'apikey',
default: process.env.MAILCHIMP_API_KEY,
describe: 'Mailchimp API key',
type: 'string',
})
.option('l', {
alias: 'listid',
default: process.env.MAILCHIMP_LIST_ID,
describe: 'Mailchimp list identifier',
type: 'string',
})
.option('k', {
alias: 'keeptags',
default: true,
describe: 'Keep Mailchimp merge tags',
type: 'boolean',
})
.help('h')
.alias('h', 'help')

const convertAndCreateCampaign = async args => {
try {
const emailData = await markdownToHtmlEmail(args)

const { apikey: apiKey, listid: listId } = args
const options = {
apiKey,
listId,
...emailData,
}

logSuccess('Created email data')

const campaignData = await createMailchimpCampaign(options)
if (!campaignData) {
logError('No Mailchimp campaign created')
} else {
const { web_id: id } = campaignData
const editUrl = 'https://admin.mailchimp.com/campaigns/edit'

logSuccess(`Mailchimp campaign created - ${editUrl}?id=${id}`)
}
} catch (error) {
console.error(error.toString())
}
}

convertAndCreateCampaign(argv)
16 changes: 16 additions & 0 deletions examples/markdown/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: 'Title of the email campaign in Mailchimp'
subject: 'Email subject line'
preview: 'Preview text for the email'
fromName: 'Test User'
replyTo: '[email protected]'
---

Hi *|FNAME|*,

Thanks for subscribing to my mailing list.

This is an email which has been converted from markdown to HTML.

Thanks,
Marc
33 changes: 33 additions & 0 deletions examples/templates/testTemplate.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="Helvetica" font-size="16px" color="#222" />
</mj-attributes>
<mj-preview>{{frontmatter.preview}}</mj-preview>
</mj-head>

<!-- Main body -->
<mj-body background-color="#fff">

<!-- Main content -->
<mj-section>
<mj-column>
<mj-text>{{content}}</mj-text>
<mj-divider border-width="1px" border-style="dotted" border-color="#111" />
</mj-column>
</mj-section>

<!-- Footer with Mailchimp specifics -->
<mj-section>
<mj-column>
<mj-text font-style="italic" font-size="12px">This email was sent to *|EMAIL|*.</mj-text>
<mj-text font-style="italic" font-size="12px">If you would like to stop getting these emails, click <a href="*|UNSUB|*" target="_blank">this link to unsubscribe</a> from this mailing list.</mj-text>

<mj-text font-style="italic" font-size="12px">Copyright © *|CURRENT_YEAR|* *|LIST:COMPANY|*, All rights reserved.</mj-text>
<mj-text font-size="12px">*|IFNOT:ARCHIVE_PAGE|* *|LIST:DESCRIPTION|*</mj-text>
<mj-text font-weight="bold" font-size="12px">My mailing address is:</mj-text>
<mj-text font-size="12px">*|HTML:LIST_ADDRESS_HTML|* *|END:IF|*</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
47 changes: 47 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "markdown-to-mailchimp",
"version": "0.1.0",
"main": "index.js",
"bin": {
"md2mc": "./bin/cli.js"
},
"repository": "[email protected]:MarcL/markdown-to-mailchimp.git",
"author": "Marc Littlemore <[email protected]>",
"license": "MIT",
"dependencies": {
"chalk": "^2.4.2",
"frontmatter": "^0.0.3",
"handlebars": "^4.5.1",
"mailchimp-api-v3": "^1.13.1",
"marked": "^0.7.0",
"mjml": "^4.5.1",
"yargs": "^14.2.0"
},
"devDependencies": {
"eslint": "^6.6.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.5.0",
"eslint-plugin-import": "^2.18.2",
"husky": ">=1",
"lint-staged": ">=8",
"prettier": "1.18.2"
},
"scripts": {
"lint": "eslint ."
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,css,json}": [
"prettier --write",
"git add"
],
"*.js": [
"eslint --fix",
"git add"
]
}
}
31 changes: 31 additions & 0 deletions src/createMailchimpCampaign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const mailchimp = require('./mailchimp')

const createMailchimpCampaign = async options => {
const { apiKey, listId, frontmatter, html } = options

if (!apiKey || !listId) {
return false
}

const campaignOptions = {
apiKey,
listId,
...frontmatter,
html,
}

let campaign = await mailchimp.findCampaignIdByMetaData(campaignOptions)

if (!campaign) {
campaign = await mailchimp.createCampaign(campaignOptions)
} else {
await mailchimp.updateCampaignHtml({
...campaignOptions,
id: campaign.id,
})
}

return campaign
}

module.exports = createMailchimpCampaign
54 changes: 54 additions & 0 deletions src/mailchimp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const Mailchimp = require('mailchimp-api-v3')

const createCampaign = ({
apiKey,
listId,
subject,
preview,
title,
fromName,
replyTo,
type = 'regular',
}) => {
const mailchimp = new Mailchimp(apiKey)
return mailchimp.post('/campaigns', {
type,
recipients: {
list_id: listId,
},
settings: {
subject_line: subject,
preview_text: preview,
title,
from_name: fromName,
reply_to: replyTo,
to_name: '*|FNAME|*',
},
})
}

const updateCampaignHtml = ({ apiKey, id, html }) => {
const mailchimp = new Mailchimp(apiKey)
return mailchimp.put(`/campaigns/${id}/content`, {
html,
})
}

const findCampaignIdByMetaData = ({ apiKey, listId, title, subject }) => {
const mailchimp = new Mailchimp(apiKey)
return mailchimp.get('/campaigns', { count: 1000 }).then(response => {
return response.campaigns.find(campaign => {
return (
campaign.recipients.list_id === listId &&
campaign.settings.subject_line === subject &&
campaign.settings.title === title
)
})
})
}

module.exports = {
createCampaign,
findCampaignIdByMetaData,
updateCampaignHtml,
}
36 changes: 36 additions & 0 deletions src/markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const fs = require('fs').promises
const frontmatter = require('frontmatter')
const marked = require('marked')

// https://mailchimp.com/help/all-the-merge-tags-cheat-sheet/
const isMailChimpTag = text => /\|(.+?)\|/.test(text)

const createHtmlFromMarkdown = (content, keepMailChimpTags = true) => {
const newRenderer = new marked.Renderer()
newRenderer.em = text =>
keepMailChimpTags && isMailChimpTag(text)
? `*${text}*`
: `<em>${text}</em>`

return marked(content, {
renderer: newRenderer,
})
}

const parseMarkdownFile = async (
markdownFilename,
keepMailChimpTags = true
) => {
const fileContent = await fs.readFile(markdownFilename, 'utf8')

const { content, data } = frontmatter(fileContent)
const html = createHtmlFromMarkdown(content, keepMailChimpTags)

return {
html,
markdown: content,
frontmatter: data,
}
}

module.exports = parseMarkdownFile
55 changes: 55 additions & 0 deletions src/markdownToHtmlEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const fs = require('fs').promises
const mjml = require('mjml')

const parseMarkdownFile = require('./markdown')
const renderHandlebars = require('./renderHandlebars')

const createHtmlEmailFromTemplate = mjmlContent => {
return mjml(mjmlContent, {
minify: true,
})
}

const getFilenameWithoutExtension = filename => {
const fileParts = filename.split('/')
return fileParts[fileParts.length - 1].split('.md')[0]
}

const markdownToHtmlEmail = async options => {
const {
markdown: markdownFilename,
template: mjmlTemplateFilename,
output: outputDirectory,
keeptags: keepMailChimpTags,
} = options

const fileData = await parseMarkdownFile(
markdownFilename,
keepMailChimpTags
)

const mjmlRenderedTemplate = await renderHandlebars({
filename: mjmlTemplateFilename,
context: {
frontmatter: fileData.frontmatter,
content: fileData.html,
},
})

const htmlEmail = createHtmlEmailFromTemplate(mjmlRenderedTemplate)

if (outputDirectory) {
const filename = getFilenameWithoutExtension(markdownFilename)
const outputFilename = `${outputDirectory}/${filename}.html`

await fs.writeFile(outputFilename, htmlEmail.html, 'utf8')
}

return {
...fileData,
...htmlEmail,
mjml: mjmlRenderedTemplate,
}
}

module.exports = markdownToHtmlEmail
Loading

0 comments on commit 6724476

Please sign in to comment.