maizzle / framework

Quickly build HTML emails with Tailwind CSS.
https://maizzle.com
MIT License
1.2k stars 48 forks source link

NodeJS package not generating most of the styles #565

Closed ChronicStone closed 2 years ago

ChronicStone commented 2 years ago

When using the NodeJS version, a big part of the CSS is not rendered. When using inline mode, more than half classes are simply ignored, and when not inlined, the CSS associated to most classes is not generated as well.

NODEJS CONFIG & SETUP : image

And using the exact same config on Node and with Maizzle CLI, but the output of the builds is totally different :

MAIZZLE CLI BUILD: image

NODEJS BUILD: image

Here's an archive with the HTML outputs of the email template generated through Node and Maizzle CLI HTML BUILDS.zip

cossssmin commented 2 years ago

Thanks for the detailed description and for attaching the files.

I'm not seeing any <style> tag in the Node build, so I'm thinking all CSS gets purged right in Tailwind.

Can you share your Tailwind and Maizzle configs?

It should not purge classes that you have in the template string, because it's automatically handled:

https://github.com/maizzle/framework/blob/53c69eab387695026a18a97080703783f926adde/src/generators/tailwindcss.js#L33-L43

cossssmin commented 2 years ago

Also, you do have {{ page.css }} in your layout, right?

https://maizzle.com/docs/nodejs/#css-output

ChronicStone commented 2 years ago

Hi @cossssmin

I do have the {{{ page.css }}} in my layout. I also do not have any purging config defined on my tailwind config. Here's all you requested :

My maizzle config on node :

{
      env: 'node',
      // inlineCSS: true,
      build: {
          components: {
              root: EMAILS_ROOT_PATH,
          },
          layouts: {
              root: EMAILS_ROOT_PATH,
          }
      }
  }

My tailwindcss config :

module.exports = {
  mode: 'jit',
  theme: {
    screens: {
      sm: {max: '600px'},
      md: {max: '800px'},
      lg: {max: '1000px'},
      xl: {max: '1200px'},
    },
    fontFamily: {
      body: ['bilo', 'sans-serif']
    },
    extend: {
      colors: {
        primary: '#fff',
        secondary: '#168a99',
        secondaryDark: '#116b77'
      },
      width: {
        '9/10': '90%'
      },
      spacing: {
        screen: '100vw',
        full: '100%',
        px: '1px',
        0: '0',
        2: '2px',
        3: '3px',
        4: '4px',
        5: '5px',
        6: '6px',
        7: '7px',
        8: '8px',
        9: '9px',
        10: '10px',
        11: '11px',
        12: '12px',
        14: '14px',
        16: '16px',
        20: '20px',
        24: '24px',
        28: '28px',
        32: '32px',
        36: '36px',
        40: '40px',
        44: '44px',
        48: '48px',
        52: '52px',
        56: '56px',
        60: '60px',
        64: '64px',
        72: '72px',
        80: '80px',
        96: '96px',
        600: '600px',
        '1/2': '50%',
        '1/3': '33.333333%',
        '2/3': '66.666667%',
        '1/4': '25%',
        '2/4': '50%',
        '3/4': '75%',
        '1/5': '20%',
        '2/5': '40%',
        '3/5': '60%',
        '4/5': '80%',
        '1/6': '16.666667%',
        '2/6': '33.333333%',
        '3/6': '50%',
        '4/6': '66.666667%',
        '5/6': '83.333333%',
        '1/12': '8.333333%',
        '2/12': '16.666667%',
        '3/12': '25%',
        '4/12': '33.333333%',
        '5/12': '41.666667%',
        '6/12': '50%',
        '7/12': '58.333333%',
        '8/12': '66.666667%',
        '9/12': '75%',
        '10/12': '83.333333%',
        '11/12': '91.666667%',
      },
      borderRadius: {
        none: '0px',
        sm: '2px',
        DEFAULT: '4px',
        md: '6px',
        lg: '8px',
        xl: '12px',
        '2xl': '16px',
        '3xl': '24px',
        full: '9999px',
      },
      fontFamily: {
        sans: ['ui-sans-serif', 'system-ui', '-apple-system', '"Segoe UI"', 'sans-serif'],
        serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
        mono: ['ui-monospace', 'Menlo', 'Consolas', 'monospace'],
      },
      fontSize: {
        0: '0',
        xs: '12px',
        sm: '14px',
        base: '16px',
        lg: '18px',
        xl: '20px',
        '2xl': '24px',
        '3xl': '30px',
        '4xl': '36px',
        '5xl': '48px',
        '6xl': '60px',
        '7xl': '72px',
        '8xl': '96px',
        '9xl': '128px',
      },
      inset: theme => ({
        ...theme('spacing'),
      }),
      letterSpacing: theme => ({
        ...theme('spacing'),
      }),
      lineHeight: theme => ({
        ...theme('spacing'),
      }),
      maxHeight: theme => ({
        ...theme('spacing'),
      }),
      maxWidth: theme => ({
        ...theme('spacing'),
        xs: '160px',
        sm: '192px',
        md: '224px',
        lg: '256px',
        xl: '288px',
        '2xl': '336px',
        '3xl': '384px',
        '4xl': '448px',
        '5xl': '512px',
        '6xl': '576px',
        '7xl': '640px',
      }),
      minHeight: theme => ({
        ...theme('spacing'),
      }),
      minWidth: theme => ({
        ...theme('spacing'),
      }),
    },
  },
  corePlugins: {
    animation: false,
    backgroundOpacity: false,
    borderOpacity: true,
    divideOpacity: true,
    placeholderOpacity: false,
    textOpacity: false,
  },
}

My layout file :

<!DOCTYPE {{{ page.doctype || 'html' }}}>
<html lang="{{ page.language || 'en' }}" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
  <meta charset="{{ page.charset || 'utf-8' }}">
  <meta name="x-apple-disable-message-reformatting">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
  <link rel="stylesheet" href="https://use.typekit.net/dil2ent.css" />
  <!--[if mso]>
  <noscript>
    <xml>
      <o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
        <o:PixelsPerInch>96</o:PixelsPerInch>
      </o:OfficeDocumentSettings>
    </xml>
  </noscript>
  <style>
    td,th,div,p,a,h1,h2,h3,h4,h5,h6 {font-family: "Segoe UI", sans-serif; mso-line-height-rule: exactly;}
  </style>
  <![endif]-->
  <if condition="page.title">
    <title>{{{ page.title }}}</title>
  </if>
  <if condition="page.css">
    <style>{{ page.css }}</style>
  </if>
  <block name="head"></block>
</head>
<body class="{{ page.bodyClass }} font-body">
  <if condition="page.preheader">
    <div class="hidden">{{{ page.preheader }}}&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
      &#160;&#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &#847; &zwnj;
      &#160;&#847; &#847; &#847; &#847; &#847; </div>
  </if>
  <div role="article" class="w-full h-full justify-center text-black" aria-roledescription="email" aria-label="{{{ page.title || '' }}}" lang="{{ page.language || 'en' }}">
    <table class="w-full">
      <tr>
        <td  align="center">
          <div class="w-2/4 md:w-9/10">
            <table class="text-black pt-32 ml-20 text-left">
              <tr>
                <td align="center">
                  <!-- HEADER -->
                  <tr class=" justify-between w-full items-center">
                    <img src="images/logo_vtest.png" class="h-48"/>
                  </tr>

                  <if condition="page.title" class="text-center">
                    <table class="text-3xl w-full font-bold  mt-16">
                      <tr>
                        <td align="center">
                          <div class="max-w-4xl text-center uppercase">
                            {{{ page.title }}}
                          </div>
                          <if condition="page.icon">
                            <img src="images/{{page.icon}}.png" class="w-40 my-20">
                          </if>
                        </td>
                      </tr>

                    </table>
                  </if>

                  <!-- CONTENT -->
                  <table>
                    <tr>
                      <td>
                        <block name="template"></block>
                      </td>
                    </tr>
                  </table>

                  <hr class="border-0 bg-gray-200 text-gray-200 h-px w-full my-28"/>

                  <!-- FOOTER -->
                  <div class="text-center  -col gap-16 text-sm">
                    <!-- 'RECEIVED AS CANDIDATE' MENTION -->
                    <if condition="page.target === 'candidate'">
                      <span class="text-secondary font-bold text-lg">$t('LAYOUT.EMAIL_TARGET')</span>
                    </condition>

                    <!-- TEST CENTER INFORMATION -->
                    <div class="mt-16 text-black">
                      <div class="font-bold mb-2">$t('LAYOUT.TEST_CENTER_CONTACT_SENTENCE')</div>
                      <div><%test_center.name%></span>
                      <div><a href="" class="text-black no-underline hover:text-secondary"><%test_center.email%></a> - <%test_center.phone_number%></span>
                      <div><%test_center.address%></span>
                    </div>

                    <hr class="border-0 bg-gray-200 text-gray-200 h-px w-3/4 self-center my-12"/>

                    <div class=" -col mb-16">
                      <div>$t('LAYOUT.KNOW_MORE.PART_1') <a href="https://www.vtest.com/privacy-policy/" class="text-black no-underline hover:text-secondary">VTEST privacy policy</a> - Copyright 2021 - VTEST</div>
                      <div>
                        <a href="https://vtest.com" class="text-black no-underline hover:text-secondary">www.vtest.com</a>
                      </div>
                    </div>

                  </div>

                </td>
              </tr>
            </table> 
          </div>
        </td>
      </tr>
    </table>

  </div>
</body>
</html>
cossssmin commented 2 years ago

Hmm, I wonder if it's because of the environment variables for layouts/components.

I've just tested this simple scenario and it works fine:

// node-test.js
const Maizzle = require('@maizzle/framework')

const template = `
<!DOCTYPE html>
<html>
  <head>
    <if condition="page.css">
      <style>{{{ page.css }}}</style>
    </if>
  </head>
  <body>
    <div class="text-base sm:text-sm">test</div>
  </body>
</html>
`

Maizzle.render(
  template,
  {
    maizzle: {
      env: 'node',
      inlineCSS: true,
      removeUnusedCSS: true,
    },
    tailwind: {
      css: '@tailwind utilities;',
      config: import('./tailwind.config.js'), // default Tailwind config from https://github.com/maizzle/maizzle
    }
  }
)
  .then(({html}) => {
    console.log(html)
  })
node node-test.js

Result:

<!DOCTYPE html>
<html>
  <head>
    <style>
      @media (max-width: 600px) {
        .sm-text-sm {
          font-size: 14px !important;
        }
      }
    </style>
  </head>
  <body>
    <div class="sm-text-sm" style="font-size: 16px;">test</div>
  </body>
</html>

I'll test some more with layouts and components, though that should work fine too.

Btw, what are you using for translations?

cossssmin commented 2 years ago

Added a Node.js example repo:

https://github.com/maizzle/example-nodejs

ChronicStone commented 2 years ago

Thanks for the example. Unfortunately I used the exact same Maizzle config as you did in the example, and I still get the same result. I also tried switching to different versions of Maizzle, but it did not help either.

Here's a template example of one of my templates, with its variables :

---
template_name: exam_done
title: $t('TEMPLATE.EMAIL_TITLE')
preheader: 
bodyClass: bg-gray-100
icon: done
target: candidate
layout: exam
---

<extends src="src/layouts/exam.html">
  <block name="template">
    <div class="w-full">
      <!-- INTRO SECTION -->
      <div>
        $t('TEMPLATE.EXAM_COMPLETION_SENTENCE.PART_1')
        $t('TEMPLATE.EXAM_COMPLETION_SENTENCE.PART_2')
      </div>

      <!-- EXAM INFORMATION SECTION -->
      <div class="mt-16">
        $t('TEMPLATE.RESULTS_ACCESS_INSTRUCTIONS')
      </div>

      <table class="w-full my-16">
        <tr>
          <td align="center">
             <!-- CTA SECTION -->
              <div>
                <span class="font-bold uppercase">$t('TEMPLATE.SECURE_CODE') :</span> @{{assessment.secure_code}}
              </div>

              <div class="mt-12">
                <component locals='{"link": "@{{resources.result_app_url}}"}' src="src/components/button.html">
                  $t('TEMPLATE.ACCESS_RESULTS_BUTTON')
                </component>
              </div>
          </td>
        </tr>
      </table>

      <!-- SECTION -->
      <div>
        $t('TEMPLATE.RESULT_PAGE_CONTENT')
      </div>

      <!-- END SECTION -->
      <div class="mt-16">
        $t('TEMPLATE.THANKS_MESSAGE')
      </div>
    </div>
  </block>
</extends>

About translations, what I needed for this project was really simple so I custom-made my locales injection system : (Small explanation, the template & layout variables of the destructured object in parameters contains the translations for the targetted locale, and the default-prefixed variables contains the default ones. Pretty simple implementation that makes sure if a translation / translation key is not available in the selected language, we display the default one) carbon

cossssmin commented 2 years ago

Does the example work for you as-is? Wondering if it has to do with something else in your code...

digitalmaster commented 2 years ago

I think this might be related to the issue i'm having as well which is that styles don't get inlined when using templates. Could you create a node example that uses templating?

Was definitely a struggle to drill down to this being the issue (validated that it works so long as you don't try to extend a template). I have a feeling it's related to this line in the docs: image

gyto commented 2 years ago

@cossssmin looks like @digitalmaster is right about the templates not rendering the extended blocks.

  1. Template is getting all the changes it needs like class transform to styles.
  2. if the template is extending the layout it uses it not getting any styles applied to the file itself

I feel like this is something to do with the render of the layout which doesn't get any styles applied to it and it removes all unused CSS after the render.

Honesty I have the same issue right, I love the render but looks like I will need to build production emails and then serve them via render

cossssmin commented 2 years ago

Could you provide a repo that reproduces the issue? Can’t help much without one, sorry.

digitalmaster commented 2 years ago

For me this issue was related to the fact that Maizzle is nested in a subdirectory ~/emails/.

This is fine for maizzle serve because we run it from this emails directory (meaning working directory is correctly set to ~/emails).

However, when we trigger Maizzle.render(), the working directory is that of the Node process that triggered it (which is the root of my NextJs project ~/). This bad root causes all CSS to be purged since it can't find any HTML to tell it what to keep.

The fix was pretty straight forward: Just configure root values:

commit 450201badf7fe4e2be35e368bab30fbd83308901
Author: Jose Browne <josebrowne@gmail.com>
Date:   Mon Feb 7 11:15:04 2022 -0400

    Set Template Root Directory Based on Environment

diff --git a/curriculum/curriculum/emails/config.js b/curriculum/curriculum/emails/config.js
index c59407f..442859e 100644
--- a/curriculum/curriculum/emails/config.js
+++ b/curriculum/curriculum/emails/config.js
@@ -10,6 +10,8 @@
 */
 const fs = require('fs/promises');

+const isLocalEmailDevEnv = process.env.NODE_ENV === 'local';
+
 module.exports = {
   build: {
     templates: {
@@ -22,6 +24,14 @@ module.exports = {
         destination: 'images',
       },
     },
+    ...(!isLocalEmailDevEnv && {
+      layouts: {
+        root: 'emails/',
+      },
+      components: {
+        root: 'emails/',
+      },
+    }),
     browsersync: {
       port: 3001,
       ui: { port: 3002 },

hope this helps 🙏

Davounet commented 2 years ago

I am having a similar problem. Like @digitalmaster all my ressources (layouts, css, components, tailwind.config.js) are in a subdirectory named emails.

My render function looks like this :

const { html } = await Maizzle.render(template, {
    maizzle: {
      env: 'node',
      inlineCSS: {
        mergeLonghand: true
      },
      prettify: {
        ocd: true
      },
      build: {
        components: {
          root: './emails'
        },
        layouts: {
          root: './emails'
        }
      }
    },
    tailwind: {
      css: `@tailwind utilities;`,
      config: import('../emails/tailwind.config.js')
    }
  })

The problem is that my layout and components are not handled by maizzle/tailwind. Every class stays in its place, neither purged nor inlined, which in the end produces a template with no styling for those layout and components.

The funny thing is that everything works fine when this subdirectory is named src (and that the corresponding paths are updated). The render function then looks like this :

const { html } = await Maizzle.render(template, {
    maizzle: {
      env: 'node',
      inlineCSS: {
        mergeLonghand: true
      },
      prettify: {
        ocd: true
      },
      build: {
        components: {
          root: './'
        },
        layouts: {
          root: './'
        }
      }
    },
    tailwind: {
      css: `@tailwind utilities;`,
      config: import('../src/tailwind.config.js')
    }
  })

I fail to see where the problem comes from, I lost hope and currently have my directory called src but would really like to find a solution !