From 2d0f33f369febb6b7b60f0aa977a72b7c541b0fc Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 5 Dec 2022 16:30:24 -0600 Subject: [PATCH] Website: Add script for creating HTML email partials from Markdown articles (#7574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add to-html-email * email-preview * add build-html-email script, update .gitignore * email preview and email-templates pages, update routes, importer and policies * add newsletter email layout, build-html-email script, to-html-email updates * Update .gitignore * add newsletter emails section to growth handbook * update scripts * update fonts in emails * Update README.md * Update build-html-email.js * update comments, add page scripts to layouts * revert change to build-static-content * update script * update email layout and helper * update handbook entry * update template data and helper * update email layout * update fake data and unsubscribe link * move added handbook section to the marketing handbook * rename script * update colors in unused email template * update capitalization in helper * view-email-preview » view-email-template-preview * Hide emails generated from markdown if they don't exist * update preview page * update markdown email directory, create sample article email * lint fixes, add target="_blank" to links in emails * update page layouts & styles * update sample newsletter email, code font, newsletter layout * Update README.md * Update view-email-template-preview.js Co-authored-by: Tim Kern Co-authored-by: Mike Thomas <78363703+mike-j-thomas@users.noreply.github.com> Co-authored-by: Mike McNeil --- handbook/marketing/README.md | 46 ++++ .../admin/view-email-template-preview.js | 133 +++++++++++ .../controllers/admin/view-email-templates.js | 60 +++++ website/api/helpers/strings/to-html-email.js | 164 +++++++++++++ .../js/pages/admin/email-preview.page.js | 27 +++ .../js/pages/admin/email-templates.page.js | 25 ++ website/assets/styles/importer.less | 2 + .../styles/pages/admin/email-preview.less | 76 ++++++ .../styles/pages/admin/email-templates.less | 7 + website/config/routes.js | 15 ++ .../generate-html-email-from-article.js | 103 +++++++++ .../views/emails/email-verify-new-email.ejs | 8 +- .../newsletter/email-article-fleet-4-22-0.ejs | 216 ++++++++++++++++++ website/views/layouts/layout-customer.ejs | 2 + .../views/layouts/layout-email-newsletter.ejs | 8 +- website/views/layouts/layout-landing.ejs | 2 + website/views/layouts/layout-sandbox.ejs | 2 + website/views/layouts/layout.ejs | 2 + website/views/pages/admin/email-preview.ejs | 82 +++++++ website/views/pages/admin/email-templates.ejs | 19 ++ 20 files changed, 991 insertions(+), 8 deletions(-) create mode 100644 website/api/controllers/admin/view-email-template-preview.js create mode 100644 website/api/controllers/admin/view-email-templates.js create mode 100644 website/api/helpers/strings/to-html-email.js create mode 100644 website/assets/js/pages/admin/email-preview.page.js create mode 100644 website/assets/js/pages/admin/email-templates.page.js create mode 100644 website/assets/styles/pages/admin/email-preview.less create mode 100644 website/assets/styles/pages/admin/email-templates.less create mode 100644 website/scripts/generate-html-email-from-article.js create mode 100644 website/views/emails/newsletter/email-article-fleet-4-22-0.ejs create mode 100644 website/views/pages/admin/email-preview.ejs create mode 100644 website/views/pages/admin/email-templates.ejs diff --git a/handbook/marketing/README.md b/handbook/marketing/README.md index 4d50d0680a8b..dbabfedb955d 100644 --- a/handbook/marketing/README.md +++ b/handbook/marketing/README.md @@ -422,6 +422,52 @@ Use "bcc" so recipients don't see each other's email addresses and send an email `sails run deliver-release-announcement --emailAddresses='["foo@example.com","bar@example.com"]'` +### Newsletter emails + +The content for our newsletter emails comes from our articles. Because our HTML emails require that the styles are added inline, we generate HTML emails by using a script and manually QA them before sending them out to subscribers. + +#### Generating emails for the Fleet newsletter + +To convert a Markdown article into an email for the newsletter, you'll need the following: + +- A local copy of the [Fleet repo](https://github.com/fleetdm/fleet). +- [Node.js](https://nodejs.org/en/download/) +- (Optional) [Sails.js](https://sailsjs.com) installed globally on your machine (`npm install sails -g`) + +Once you have the above follow these steps: + +1. Open your terminal program, and navigate to the `website/` folder of your local copy of the Fleet repo. + +>Note: If this is your first time running this script, you will need to run `npm install` inside of the `website/` folder to install the website's dependencies. + +2. Run the `build-html-email` script and pass in the filename of the Markdown article you would like to convert with the `--articleFilename` flag. + + - **With Node**, you will need to use `node ./node_modules/sails/bin/sails run build-html-email` to execute the script. e.g., `node ./node_modules/sails/bin/sails run build-html-email --articleFilename="fleet-4.19.0.md"` + - **With Sails.js installed globally** you can use `sails run build-html-email` to execute the script. e.g., `sails run build-html-email --articleFilename="fleet-4.19.0.md"` + +> Note: Only Markdown (`.md`) files are supported by the build-html-email script. The file extension is optional when providing the articleFilename. + +4. Once the script is complete, a new email partial will be added to the `website/views/emails/newsletter-partials/` folder. + +> Note: If an email partial has already been created from the specified Markdown article, the old version will be overwritten by the new file. + +5. Start the website server locally to preview the generated email. To test the changes locally, open a terminal window in the `website/` folder of the Fleet repo and run the following command: + + - **With Node.js:** start the server by running `node ./node_modules/sails/bin/sails lift`. + - **With Sails.js installed globally:** start the server by running `sails lift`. + +6. With the server lifted, navigate to http://localhost:2024/admin/email-preview and login with the test admin user credentials (email:`admin@example.com` pw: `abc123`). + + Click on the generated email in the list of emails generated from Markdown content and navigate to the preview page. On this page, you can view the see how the email will look on a variety of screen sizes. + + When you've made sure the content of the email looks good at all screen sizes, commit the new email partial to a new branch and open a pull request to add the file. You can request a review from a member of the digital experience team. + +**Things to keep in mind when generating newsletter emails:** + +- The emails will be generated using the Markdown file locally, any changes present in the local Markdown file will be reflected in the generated email. +- HTML elements in the Markdown file can cause rendering issues when previewing the generated email. If you see a "Script error" overlay while trying to preview an email, reach out to [Eric Shaw](https://github.com/eashaw) for help. +- The filename of the generated email will have periods changed to dashes. e.g., The generated email partial for `fleet-4.19.0.md` would be `fleet-4-19-0.ejs` + ### Using Figma We use Figma for most of our design work. This includes the Fleet product, our website, and our marketing collateral. diff --git a/website/api/controllers/admin/view-email-template-preview.js b/website/api/controllers/admin/view-email-template-preview.js new file mode 100644 index 000000000000..33a4481b5263 --- /dev/null +++ b/website/api/controllers/admin/view-email-template-preview.js @@ -0,0 +1,133 @@ +module.exports = { + + + friendlyName: 'View email template preview', + + + description: 'Display "email template preview" page.', + + + urlWildcardSuffix: 'template', + + + inputs: { + + template: { + description: 'The path to an email template, specified in precisely the same way as the equivalent input of the sendTemplateEmail() helper.', + example: 'email-reset-password', + type: 'string', + required: true + }, + + raw: { + description: 'Whether to return the raw HTML for the email with no JS/CSS (rather than a personalized previewer web page.)', + extendedDescription: 'This can be used from an iframe to allow for accurately previewing email templates without worrying about style interference from the rest of the Sails app.', + type: 'boolean', + } + + }, + + exits: { + + success: { + viewTemplatePath: 'pages/admin/email-preview' + }, + + sendRawHtmlInstead: { + statusCode: 200, + outputType: 'string', + outputDescription: 'The raw HTML for the email as a string.', + }, + + }, + + + fn: async function ({template, raw}) { + + var path = require('path'); + var url = require('url'); + var util = require('util'); + // Determine appropriate email layout and fake data to use. + let layout; + let fakeData; + switch (template) { + case 'internal/email-contact-form': + layout = false; + fakeData = { + contactName: 'Sage', + contactEmail: 'sage@example.com', + topic: 'Pricing question', + message: 'What is the difference between the "Free" plan and the "Premium" plan?', + }; + break; + case 'email-reset-password': + layout = 'layout-email'; + fakeData = { + token: '4-32fad81jdaf$329', + }; + break; + case 'email-verify-account': + layout = 'layout-email'; + fakeData = { + firstName: 'Fleet user', + token: '4-32fad81jdaf$329', + }; + break; + case 'email-verify-new-email': + layout = 'layout-email'; + fakeData = { + fullName: 'Fleet user', + token: '4-32fad81jdaf$329', + }; + break; + case 'email-order-confirmation': + layout = 'layout-email'; + fakeData = { + firstName: 'Fleet', + lastName: 'user', + }; + break; + default: + layout = 'layout-email-newsletter'; + fakeData = { + emailAddress: 'sage@example.com', + }; + } + + // Compile HTML template using the appropriate layout. + // > Note that we set the layout, provide access to core `url` package (for + // > building links and image srcs, etc.), and also provide access to core + // > `util` package (for dumping debug data in internal emails). + let emailTemplatePath = path.join('emails/', template); + if (layout) { + layout = path.relative(path.dirname(emailTemplatePath), path.resolve('layouts/', layout)); + } else { + layout = false; + } + + let sampleHtml = await sails.renderView( + emailTemplatePath, + Object.assign({layout, url, util, _ }, fakeData) + ) + .intercept((err)=>{ + err.message = 'Whoops, that email template failed to render. Could there be some fake data missing for this particular template in the `switch` statement api/controllers/admin/view-email-template-preview.js? Any chance you need to re-lift the app after making backend changes?\nMore details: '+err.message; + return err; + }); + + if (raw) { + // Respond with raw, rendered HTML for this email: + throw {sendRawHtmlInstead: sampleHtml}; + } else { + // Respond with the previewer page for this email: + return { + sampleHtml, + template, + fakeData, + }; + } + + + } + + +}; diff --git a/website/api/controllers/admin/view-email-templates.js b/website/api/controllers/admin/view-email-templates.js new file mode 100644 index 000000000000..b018e8cd90a2 --- /dev/null +++ b/website/api/controllers/admin/view-email-templates.js @@ -0,0 +1,60 @@ +module.exports = { + + + friendlyName: 'View email templates', + + + description: 'Display "Email templates" page.', + + + exits: { + + success: { + viewTemplatePath: 'pages/admin/email-templates' + } + + }, + + + fn: async function () { + + + var path = require('path'); + + // Sniff for top level email templates + let templatePaths = await sails.helpers.fs.ls.with({ + dir: path.join(sails.config.paths.views, 'emails/'), + depth: 1, + includeDirs: false, + includeSymlinks: false, + }); + + let markdownEmailPaths = await sails.helpers.fs.ls.with({ + dir: path.join(sails.config.paths.views, 'emails/newsletter'), + depth: 99, + includeDirs: false, + includeSymlinks: false, + }); + + markdownEmailPaths = markdownEmailPaths.map((templatePath)=>{ + let relativePath = path.relative(path.join(sails.config.paths.views, 'emails/'), templatePath); + let extension = path.extname(relativePath); + return _.trimRight(relativePath, extension); + }); + + templatePaths = templatePaths.map((templatePath)=>{ + let relativePath = path.relative(path.join(sails.config.paths.views, 'emails/'), templatePath); + let extension = path.extname(relativePath); + return _.trimRight(relativePath, extension); + }); + + // Respond with view. + return { + templatePaths, + markdownEmailPaths + }; + + } + + +}; diff --git a/website/api/helpers/strings/to-html-email.js b/website/api/helpers/strings/to-html-email.js new file mode 100644 index 000000000000..59aa7e999fb2 --- /dev/null +++ b/website/api/helpers/strings/to-html-email.js @@ -0,0 +1,164 @@ +module.exports = { + + + friendlyName: 'To HTML email', + + + description: 'Compile a Markdown string into an HTML string with styles added inline for the Fleet newsletter.', + + + extendedDescription: + 'Expects GitHub-flavored Markdown syntax. Uses [`marked`](https://github.com/chjj/marked)@v0.3.5. '+ + 'Inspired by https://github.com/mikermcneil/machinepack-markdown/tree/5d8cee127e8ce45c702ec9bbb2b4f9bc4b7fafac', + + + moreInfoUrl: 'https://help.github.com/articles/basic-writing-and-formatting-syntax/', + + + sideEffects: 'cacheable', + + + inputs: { + + mdString: { + description: 'Markdown string to convert', + example: '# hello world\n it\'s me, some markdown string \n\n ```js\n//but maybe i have code snippets too...\n```', + required: true + }, + }, + + + exits: { + + success: { + outputFriendlyName: 'HTML', + outputExample: '

hello world

\n

it's me, some markdown string

\n
//but maybe i have code snippets too...
\n' + }, + + unsafeMarkdown: { + friendlyName: 'Unsafe Markdown detected', + description: 'The provided input contained unsafe content (like HTML tags).' + } + + }, + + + fn: function(inputs, exits) { + const { marked } = require('marked'); + + + var markedOpts = { + gfm: true, + tables: true, + breaks: false, + pedantic: false, + smartLists: true, + smartypants: false, + }; + + // Creating a custom renderer to add inline styles to HTML elements + let customRenderer = new marked.Renderer(); + + // For codeblocks + customRenderer.code = function(codeHTML) { + return '
'+_.escape(codeHTML)+'
'; + }; + // For blockquotes + customRenderer.blockquote = function(quoteHTML) { + return `
\n${quoteHTML}\n
\n`; + }; + + customRenderer.heading = function(textHTML, level) { + let inlineStyles; + if(level === 1) { // For h1s + inlineStyles = 'font-weight: 800; font-size: 24px; line-height: 32px; margin-bottom: 16px;'; + } else if (level === 2) { // For h2s + inlineStyles = 'font-weight: 700; font-size: 20px; line-height: 28px; margin-bottom: 16px; margin-top: 32px;'; + } else if (level === 3) { // for h3s + inlineStyles = 'font-weight: 700; font-size: 20px; line-height: 24px; margin-bottom: 16px;'; + } else {// H4s or higher + inlineStyles = 'font-weight: 700; font-size: 16px; line-height: 20px; margin-bottom: 16px;'; + } + return `\n${textHTML}\n\n`; + }; + + // For
elements + customRenderer.hr = function() { + return `
`; + }; + + // For lists + customRenderer.list = function(bodyHTML, ordered) { + if(ordered){ + return `
    \n${bodyHTML}
\n`; + } else { + return `
    \n${bodyHTML}
\n`; + } + }; + + // For list items + customRenderer.listitem = function(textHTML) { + return `
  • \n${textHTML}\n
  • \n`; + }; + + customRenderer.paragraph = function(text) { + return `

    \n${text}\n

    \n`; + }; + + // For bold text + customRenderer.strong = function(textHTML) { + return `${textHTML}`; + }; + + // For emphasized text + customRenderer.em = function(textHTML) { + return `'+codeHTML+''; + }; + + // For links + customRenderer.link = function(href, title, textHTML) { + (href)=>{ + let isExternal = ! href.match(/^https?:\/\/([^\.|blog]+\.)*fleetdm\.com/g);// « FUTURE: make this smarter with sails.config.baseUrl + _.escapeRegExp() + // Check if this link is to fleetdm.com or www.fleetdm.com. + let isBaseUrl = href.match(/^(https?:\/\/)([^\.]+\.)*fleetdm\.com$/g); + if (isExternal) { + href = href.replace(/(https?:\/\/([^"]+))/g, '$1 target="_blank"'); + } else { + // Otherwise, change the link to be web root relative. + // (e.g. 'href="http://sailsjs.com/documentation/concepts"'' becomes simply 'href="/documentation/concepts"'') + // > Note: See the Git version history of "compile-markdown-content.js" in the sailsjs.com website repo for examples of ways this can work across versioned subdomains. + if (isBaseUrl) { + href = href.replace(/https?:\/\//, ''); + } else { + href = href.replace(/https?:\/\//, ''); + } + } + }; + return `${textHTML}`; + }; + + // For images + customRenderer.image = function(href, title) { + let linkToImageInAssetsFolder = href.replace(/^(\.\.\/website\/assets)/gi, 'https://fleetdm.com'); + return `${title}`; + }; + + markedOpts.renderer = customRenderer; + + // Now actually compile the markdown to HTML. + marked(inputs.mdString, markedOpts, function afterwards (err, htmlString) { + if (err) { + return exits.error(err); + } + + return exits.success(htmlString); + }); + } + + +}; diff --git a/website/assets/js/pages/admin/email-preview.page.js b/website/assets/js/pages/admin/email-preview.page.js new file mode 100644 index 000000000000..a01af24b6d18 --- /dev/null +++ b/website/assets/js/pages/admin/email-preview.page.js @@ -0,0 +1,27 @@ +parasails.registerPage('email-preview', { + // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ + // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ + // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ + data: { + //… + preview: 'Responsive', + }, + + // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ + // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ + // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ + beforeMount: function() { + //… + _.extend(this, SAILS_LOCALS); + }, + mounted: async function() { + //… + }, + + // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ + // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ + // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ + methods: { + //… + } +}); diff --git a/website/assets/js/pages/admin/email-templates.page.js b/website/assets/js/pages/admin/email-templates.page.js new file mode 100644 index 000000000000..008d1f6ef45f --- /dev/null +++ b/website/assets/js/pages/admin/email-templates.page.js @@ -0,0 +1,25 @@ +parasails.registerPage('email-templates', { + // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ + // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ + // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ + data: { + //… + }, + + // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ + // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ + // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ + beforeMount: function() { + //… + }, + mounted: async function() { + //… + }, + + // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ + // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ + // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ + methods: { + //… + } +}); diff --git a/website/assets/styles/importer.less b/website/assets/styles/importer.less index 85b6aee0d542..f5c6e22f570a 100644 --- a/website/assets/styles/importer.less +++ b/website/assets/styles/importer.less @@ -61,6 +61,8 @@ @import 'pages/articles/basic-article.less'; @import 'pages/articles/articles.less'; @import 'pages/reports/state-of-device-management.less'; +@import 'pages/admin/email-templates.less'; +@import 'pages/admin/email-preview.less'; @import 'pages/osquery-table-details.less'; @import 'pages/admin/generate-license.less'; diff --git a/website/assets/styles/pages/admin/email-preview.less b/website/assets/styles/pages/admin/email-preview.less new file mode 100644 index 000000000000..e6c9c1a5d5c8 --- /dev/null +++ b/website/assets/styles/pages/admin/email-preview.less @@ -0,0 +1,76 @@ +#email-preview { + padding-top: 80px; + padding-bottom: 80px; + overflow-x: scroll; + + h2 { + font-size: 24px; + margin-bottom: 0px; + } + h3 { + font-size: 20px; + font-weight: 700; + margin-bottom: 0px; + } + + [purpose='form-factors'] { + margin-top: 40px; + margin-bottom: 40px; + } + + [purpose='preview-title'] { + max-width: 1200px; + margin-right: auto; + margin-left: auto; + } + + [purpose='preview-selector'] { + cursor: pointer; + border: solid 1px @core-vibrant-blue-15; + min-width: 160px; + background-color: #FAFAFA; + height: 40px; + border-radius: 4px; + padding: 9px 16px 9px; + img { + margin-left: auto; + } + } + [purpose='preview-selector-dropwdown'] { + cursor: pointer; + } + + .pretend-inbox-wrapper-for-sample-email { + margin-left: auto; + margin-right: auto; + .preview-iframe { + margin-top: 40px; + border: 1px solid #666; + width: 100%; + height: 900px; + max-width: 100%; + } + &.simulate-320px-width { + width: 320px; + } + &.simulate-480px-width { + width: 480px; + } + &.simulate-768px-width { + width: 768px; + } + &.simulate-1024px-width { + width: 1024px; + } + } + @media (max-width: 768px) { + padding-top: 40px; + h2 { + font-size: 20px; + } + h3 { + font-size: 18px; + } + } + +} diff --git a/website/assets/styles/pages/admin/email-templates.less b/website/assets/styles/pages/admin/email-templates.less new file mode 100644 index 000000000000..b665dc56ab7b --- /dev/null +++ b/website/assets/styles/pages/admin/email-templates.less @@ -0,0 +1,7 @@ +#email-templates { + + a { + color: @core-fleet-black; + } + +} diff --git a/website/config/routes.js b/website/config/routes.js index c8008cba9ba8..c48aed3459ad 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -208,6 +208,21 @@ module.exports.routes = { }, }, + 'GET /admin/email-preview': { + action: 'admin/view-email-templates', + locals: { + layout: 'layouts/layout-customer' + }, + }, + + 'GET /admin/email-preview/*': { + action: 'admin/view-email-template-preview', + skipAssets: true, + locals: { + layout: 'layouts/layout-customer' + }, + }, + 'GET /tables/:tableName': { action: 'view-osquery-table-details', }, diff --git a/website/scripts/generate-html-email-from-article.js b/website/scripts/generate-html-email-from-article.js new file mode 100644 index 000000000000..40bdab085611 --- /dev/null +++ b/website/scripts/generate-html-email-from-article.js @@ -0,0 +1,103 @@ +module.exports = { + + + friendlyName: 'Generate HTML email from article', + + + description: 'Generate an HTML partial for the Fleet newsletter from Markdown files in the articles/ folder of the fleetdm/fleet repo.', + + + inputs: { + articleFilename: {type: 'string', description: 'The filename of the article that will be converted into an HTML email partial', required: true}, + }, + + + fn: async function ({ articleFilename }) { + + let path = require('path'); + + let topLvlRepoPath = path.resolve(sails.config.appPath, '../'); + + let APP_PATH_TO_COMPILED_EMAIL_PARTIALS = 'views/emails/newsletter'; + + + let extensionedArticleFilename = articleFilename; + + // Since this script only handles Markdown files in the articles/ folders, we'll make the file extension optional. + if(!_.endsWith(articleFilename, '.md')) { + // If the file was specified without a file extension, we'll add `.md` to the provided filename, and log a warning. + extensionedArticleFilename = extensionedArticleFilename + '.md'; + sails.log.warn('The filename provided is missing the .md file extension, appending `.md` to the provided articleFilename: '+articleFilename); + } + + // Get the filename without the .md file extension. This will be used to build the final filename + let unextensionedArticleFilename = _.trimRight(extensionedArticleFilename, '.md'); + + // Build the filename for the final HTML partial. + let extensionedFileNameForEmailPartial = 'email-article-'+unextensionedArticleFilename.replace(/\./g, '-')+'.ejs'; + + // Find the Markdown file in the articles folder + let markdownFileToConvert = path.resolve(path.join(topLvlRepoPath, '/articles/'+extensionedArticleFilename)); + + if(!markdownFileToConvert) { // If we couldn't find the file specified, throw an error + throw new Error('Error: No Markdown file in found in the top level articles/ folder with the filename: '+articleFilename); + } + + // If the file specified is not a markdown file, throw an error. + if (path.extname(markdownFileToConvert) !== '.md') { + throw new Error('Error: The specified file ('+articleFilename+') is not a valid Markdown file.'+markdownFileToConvert); + } + + // Get the raw Markdown from the file. + let mdString = await sails.helpers.fs.read(markdownFileToConvert); + + // Get the relative path of the Markdown file we are converting + let pageRelSourcePath = path.relative(path.join(topLvlRepoPath, 'articles/'), path.resolve(markdownFileToConvert)); + + // Remove any meta tags from the Markdown file before we convert it. + mdString = mdString.replace(/]*>/igm, ''); + + // Find and remove any iframe elements in the markdown file + for (let matchedIframe of (mdString.match(/<(iframe)[\s\S]+?<\/iframe>/igm) || [])) { + sails.log.warn('Removing an + + + +
    +
    +

    Mobile (480px width)

    +
    +
    + +
    +
    + +
    +
    +

    Tablet (768px width)

    +
    +
    + +
    +
    + +
    +
    +

    Tablet (1024px width)

    +
    +
    + +
    +
    + +
    +
    +

    Responsive:

    +

    Resize the browser to preview different widths

    +
    +
    + +
    +
    + +<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %> + diff --git a/website/views/pages/admin/email-templates.ejs b/website/views/pages/admin/email-templates.ejs new file mode 100644 index 000000000000..10c979c45ee4 --- /dev/null +++ b/website/views/pages/admin/email-templates.ejs @@ -0,0 +1,19 @@ +
    +
    +

    Preview email templates

    + +
    +

    Preview emails generated from Markdown content

    + +
    +
    +
    +<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>