@maizzle/framework 5.0.0-beta.2 → 5.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,71 @@ app.use(hmrRoute)
40
40
 
41
41
  let viewing = ''
42
42
  const spinner = ora()
43
+ let templatePaths = []
44
+
45
+ function getTemplateFolders(config) {
46
+ return Array.isArray(get(config, 'build.content'))
47
+ ? config.build.content
48
+ : [config.build.content]
49
+ }
50
+
51
+ async function getTemplatePaths(templateFolders) {
52
+ return await fg.glob([...new Set(templateFolders)])
53
+ }
54
+
55
+ async function getUpdatedRoutes(app, config) {
56
+ return getTemplatePaths(getTemplateFolders(config))
57
+ }
58
+
59
+ async function renderUpdatedFile(file, config) {
60
+ try {
61
+ const startTime = Date.now()
62
+ spinner.start('Building...')
63
+
64
+ // beforeCreate event
65
+ if (typeof config.beforeCreate === 'function') {
66
+ await config.beforeCreate(config)
67
+ }
68
+
69
+ // Read the file
70
+ const fileContent = await fs.readFile(file, 'utf8')
71
+
72
+ // Set a `dev` flag on the config
73
+ config._dev = true
74
+
75
+ // Render the file with PostHTML
76
+ let { html } = await render(fileContent, config)
77
+
78
+ // Update console message
79
+ const shouldReportFileSize = get(config, 'server.reportFileSize', false)
80
+
81
+ spinner.succeed(
82
+ `Done in ${formatTime(Date.now() - startTime)}`
83
+ + `${pico.gray(` [${path.relative(cwd(), file)}]`)}`
84
+ + `${shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
85
+ )
86
+
87
+ /**
88
+ * Inject HMR script
89
+ */
90
+ html = injectScript(html, '<script src="/hmr.js"></script>')
91
+
92
+ // Notify connected websocket clients about the change
93
+ wss.clients.forEach(client => {
94
+ if (client.readyState === WebSocket.OPEN) {
95
+ client.send(JSON.stringify({
96
+ type: 'change',
97
+ content: html,
98
+ scrollSync: get(config, 'server.scrollSync', false),
99
+ hmr: get(config, 'server.hmr', true),
100
+ }))
101
+ }
102
+ })
103
+ } catch (error) {
104
+ spinner.fail('Failed to render template.')
105
+ throw error
106
+ }
107
+ }
43
108
 
44
109
  export default async (config = {}) => {
45
110
  // Read the Maizzle config file
@@ -47,7 +112,10 @@ export default async (config = {}) => {
47
112
 
48
113
  /**
49
114
  * Dev server settings
50
- */
115
+ */
116
+ spinner.spinner = get(config, 'server.spinner', 'circleHalves')
117
+ spinner.start('Starting server...')
118
+
51
119
  const shouldScroll = get(config, 'server.scrollSync', false)
52
120
  const useHmr = get(config, 'server.hmr', true)
53
121
 
@@ -62,16 +130,18 @@ export default async (config = {}) => {
62
130
  */
63
131
  initWebSockets(wss, { scrollSync: shouldScroll, hmr: useHmr })
64
132
 
65
- // Get a list of all template paths
66
- const templateFolders = Array.isArray(get(config, 'build.content'))
67
- ? config.build.content
68
- : [config.build.content]
133
+ // Register routes
134
+ templatePaths = await getUpdatedRoutes(app, config)
69
135
 
70
- const templatePaths = await fg.glob([...new Set(templateFolders)])
71
-
72
- // Set the template paths on the app, we use them in the index view
136
+ /**
137
+ * Store template paths on the request object
138
+ *
139
+ * We use it in the index view to list all templates.
140
+ * */
73
141
  app.request.templatePaths = templatePaths
74
142
 
143
+ // await updateRoutes(app, config)
144
+
75
145
  /**
76
146
  * Create route pattern
77
147
  * Only allow files with the following extensions
@@ -83,7 +153,7 @@ export default async (config = {}) => {
83
153
  )
84
154
  ].join('|')
85
155
 
86
- const routePattern = Array.isArray(templateFolders)
156
+ const routePattern = Array.isArray(getTemplateFolders(config))
87
157
  ? `*/:file.(${extensions})`
88
158
  : `:file.(${extensions})`
89
159
 
@@ -125,99 +195,82 @@ export default async (config = {}) => {
125
195
  })
126
196
  })
127
197
 
128
- // Error-handling middleware
129
- app.use(async (error, req, res, next) => { // eslint-disable-line
130
- console.error(error)
131
-
132
- const view = await fs.readFile(path.join(__dirname, 'views', 'error.html'), 'utf8')
133
- const { html } = await render(view, {
134
- method: req.method,
135
- url: req.url,
136
- error
137
- })
138
-
139
- res.status(500).send(html)
140
- })
141
-
142
198
  /**
143
199
  * Components watcher
144
200
  *
145
201
  * Watches for changes in the configured Templates and Components paths
146
202
  */
203
+ let isWatcherReady = false
147
204
  chokidar
148
- .watch([...templatePaths, ...get(config, 'components.folders', defaultComponentsConfig.folders) ])
205
+ .watch(
206
+ [
207
+ ...templatePaths,
208
+ ...get(config, 'components.folders', defaultComponentsConfig.folders)
209
+ ],
210
+ {
211
+ ignoreInitial: true,
212
+ awaitWriteFinish: {
213
+ stabilityThreshold: 150,
214
+ pollInterval: 25,
215
+ },
216
+ }
217
+ )
149
218
  .on('change', async () => {
150
- // Not viewing a component in the browser, no need to rebuild
151
- if (!viewing) {
152
- return
219
+ if (viewing) {
220
+ await renderUpdatedFile(viewing, config)
153
221
  }
154
-
155
- try {
156
- const startTime = Date.now()
157
- spinner.start('Building...')
158
-
159
- // beforeCreate event
160
- if (typeof config.beforeCreate === 'function') {
161
- await config.beforeCreate(config)
162
- }
163
-
164
- // Read the file
165
- const fileContent = await fs.readFile(viewing, 'utf8')
166
-
167
- // Set a `dev` flag on the config
168
- config._dev = true
169
-
170
- // Render the file with PostHTML
171
- let { html } = await render(fileContent, config)
172
-
173
- // Update console message
174
- const shouldReportFileSize = get(config, 'server.reportFileSize', false)
175
-
176
- spinner.succeed(
177
- `Done in ${formatTime(Date.now() - startTime)}`
178
- + `${pico.gray(` [${path.relative(cwd(), viewing)}]`)}`
179
- + `${ shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
180
- )
181
-
182
- /**
183
- * Inject HMR script
184
- */
185
- html = injectScript(html, '<script src="/hmr.js"></script>')
186
-
187
- // Notify connected websocket clients about the change
188
- wss.clients.forEach(client => {
189
- if (client.readyState === WebSocket.OPEN) {
190
- client.send(JSON.stringify({
191
- type: 'change',
192
- content: html,
193
- scrollSync: get(config, 'server.scrollSync', false),
194
- hmr: get(config, 'server.hmr', true),
195
- }))
196
- }
197
- })
198
- } catch (error) {
199
- spinner.fail('Failed to render template.')
200
- throw error
222
+ })
223
+ .on('ready', () => {
224
+ /**
225
+ * `add` fires immediately when the watcher is created,
226
+ * so we use this trick to detect new files added
227
+ * after it has started.
228
+ */
229
+ isWatcherReady = true
230
+ })
231
+ .on('add', async () => {
232
+ if (isWatcherReady) {
233
+ templatePaths = await getUpdatedRoutes(app, config)
234
+ app.request.templatePaths = templatePaths
235
+ }
236
+ })
237
+ .on('unlink', async () => {
238
+ if (isWatcherReady) {
239
+ templatePaths = await getUpdatedRoutes(app, config)
240
+ app.request.templatePaths = templatePaths
201
241
  }
202
242
  })
203
243
 
204
244
  /**
205
245
  * Global watcher
206
246
  *
207
- * Watch for changes in the config file, Tailwind CSS config, and CSS files
247
+ * Watch for changes in the config files, Tailwind CSS config, CSS files,
248
+ * configured static assets, and user-defined watch paths.
208
249
  */
209
250
  const globalWatchedPaths = new Set([
210
- 'config*.js',
211
- 'maizzle.config*.js',
212
- 'tailwind*.config.js',
251
+ 'config*.{js,cjs,ts}',
252
+ 'maizzle.config*.{js,cjs,ts}',
253
+ 'tailwind*.config.{js,ts}',
213
254
  '**/*.css',
214
- ...get(config, 'server.watch', [])
255
+ ...get(config, 'build.static.source', []),
256
+ ...get(config, 'server.watch', []),
215
257
  ])
216
258
 
217
259
  async function globalPathsHandler(file, eventType) {
260
+ // Update express.static to serve new files
261
+ if (eventType === 'add') {
262
+ app.use(express.static(path.dirname(file)))
263
+ }
264
+
265
+ // Stop serving deleted files
266
+ if (eventType === 'unlink') {
267
+ app._router.stack = app._router.stack.filter(
268
+ layer => layer.regexp.source !== path.dirname(file).replace(/\\/g, '/')
269
+ )
270
+ }
271
+
218
272
  // Not viewing a component in the browser, no need to rebuild
219
273
  if (!viewing) {
220
- spinner.info(`file ${eventType}: ${file}`)
221
274
  return
222
275
  }
223
276
 
@@ -254,7 +307,7 @@ export default async (config = {}) => {
254
307
  spinner.succeed(
255
308
  `Done in ${formatTime(Date.now() - startTime)}`
256
309
  + `${pico.gray(` [${path.relative(cwd(), filePath)}]`)}`
257
- + `${ shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
310
+ + `${shouldReportFileSize ? ' · ' + getColorizedFileSize(html) : ''}`
258
311
  )
259
312
 
260
313
  /**
@@ -274,7 +327,7 @@ export default async (config = {}) => {
274
327
  }
275
328
  })
276
329
  } catch (error) {
277
- spinner.fail('Failed to render template.')
330
+ spinner.fail(`Failed to render template: ${file}`)
278
331
  throw error
279
332
  }
280
333
  }
@@ -286,6 +339,10 @@ export default async (config = {}) => {
286
339
  get(config, 'build.output.path', 'build_production'),
287
340
  ],
288
341
  ignoreInitial: true,
342
+ awaitWriteFinish: {
343
+ stabilityThreshold: 150,
344
+ pollInterval: 25,
345
+ },
289
346
  })
290
347
  .on('change', async file => await globalPathsHandler(file, 'change'))
291
348
  .on('add', async file => await globalPathsHandler(file, 'add'))
@@ -293,25 +350,33 @@ export default async (config = {}) => {
293
350
 
294
351
  /**
295
352
  * Serve all folders in the cwd as static files
296
- *
297
- * TODO: change to include build.assets or build.static, which may be outside cwd
298
353
  */
299
354
  const srcFoldersList = await fg.glob(
300
355
  [
301
356
  '**/*/',
302
357
  ...get(config, 'build.static.source', [])
303
358
  ], {
304
- onlyFiles: false,
305
- ignore: [
306
- 'node_modules',
307
- get(config, 'build.output.path', 'build_*'),
308
- ]
309
- })
359
+ onlyFiles: false,
360
+ ignore: [
361
+ 'node_modules',
362
+ get(config, 'build.output.path', 'build_*'),
363
+ ]
364
+ })
310
365
 
311
366
  srcFoldersList.forEach(folder => {
312
367
  app.use(express.static(path.join(config.cwd, folder)))
313
368
  })
314
369
 
370
+ // Error-handling middleware
371
+ app.use(async (req, res) => {
372
+ const view = await fs.readFile(path.join(__dirname, 'views', '404.html'), 'utf8')
373
+ const { html } = await render(view, {
374
+ url: req.url,
375
+ })
376
+
377
+ res.status(404).send(html)
378
+ })
379
+
315
380
  /**
316
381
  * Start the server
317
382
  */
@@ -321,8 +386,6 @@ export default async (config = {}) => {
321
386
 
322
387
  function startServer(port) {
323
388
  const serverStartTime = Date.now()
324
- spinner.start('Starting server...')
325
-
326
389
  const server = createServer(app)
327
390
 
328
391
  /**
@@ -0,0 +1,59 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>404 - Template not found</title>
7
+ <style>
8
+ html, body {
9
+ font-family: Helvetica, Arial, sans-serif;
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ height: 100%;
14
+ }
15
+
16
+ .container {
17
+ box-sizing: border-box;
18
+ height: 100vh;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ position: relative;
23
+ z-index: 1;
24
+ padding: 24px;
25
+ }
26
+
27
+ .error-code {
28
+ font-size: 25rem;
29
+ font-weight: 700;
30
+ color: #f1f5f9;
31
+ position: fixed;
32
+ top: -1.5rem;
33
+ left: -3rem;
34
+ user-select: none;
35
+ }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <span class="error-code">404</span>
40
+
41
+ <div class="container">
42
+ <div style="text-align: center;">
43
+ <h1 style="font-size: 3rem; color: #0F172A; margin: 2.25rem 0">
44
+ Template Not Found
45
+ </h1>
46
+ <p style="margin: 0 0 2.25rem; font-size: 1.25rem; line-height: 1.5; color: #64748B;">
47
+ The Template you are looking for was not found:
48
+ </p>
49
+ <p style="margin: 1rem 0 0; font-size: 1rem; line-height: 1.5; font-weight: 600; color: #334155;">
50
+ {{ page.url }}
51
+ </p>
52
+ </div>
53
+ </div>
54
+
55
+ <div style="position: fixed; bottom: 0; right: 0; pointer-events: none; user-select: none;">
56
+ <svg width="883" height="536" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1100" height="536"><path fill="#D9D9D9" d="M0 .955h1100V536H0z"/></mask><g mask="url(#a)" stroke="#94A3B8" stroke-miterlimit="10"><path d="M1056.93 92.587c0-50.03-43.95-90.587-98.168-90.587-54.22 0-98.174 40.557-98.174 90.587v483.125c0 50.029 43.954 90.586 98.174 90.586 54.218 0 98.168-40.557 98.168-90.586V92.587ZM646.241 92.587C646.241 42.556 602.287 2 548.067 2c-54.219 0-98.173 40.557-98.173 90.587v483.125c0 50.029 43.954 90.586 98.173 90.586 54.22 0 98.174-40.557 98.174-90.586V92.587Z"/><path d="M1036.18 148.383c33.41-39.402 25.88-96.336-16.82-127.164C976.657-9.61 914.955-2.66 881.544 36.742L471.586 520.215c-33.411 39.402-25.879 96.336 16.824 127.164 42.702 30.829 104.404 23.879 137.815-15.523l409.955-483.473ZM625.093 148.396c33.411-39.403 25.878-96.336-16.824-127.164C565.567-9.597 503.865-2.647 470.454 36.755L60.495 520.228c-33.41 39.402-25.878 96.336 16.825 127.164 42.702 30.829 104.404 23.879 137.815-15.523l409.958-483.473Z"/></g></svg>
57
+ </div>
58
+ </body>
59
+ </html>
@@ -11,7 +11,7 @@ export default function posthtmlPlugin(attributes = {}) {
11
11
  role: 'none'
12
12
  },
13
13
  img: {
14
- alt: ''
14
+ alt: true,
15
15
  }
16
16
  }
17
17
 
@@ -37,7 +37,7 @@ const posthtmlPlugin = options => tree => {
37
37
 
38
38
  const { result: html } = emailComb(render(tree), options)
39
39
 
40
- return parse(html)
40
+ return parse(html, posthtmlConfig)
41
41
  }
42
42
 
43
43
  export default posthtmlPlugin
@@ -11,6 +11,18 @@ const posthtmlPlugin = (config = {}) => tree => {
11
11
  node.content = ['']
12
12
  }
13
13
 
14
+ /**
15
+ * Custom attributes to prevent inlining CSS from <style> tags
16
+ */
17
+ if (
18
+ node.tag === 'style'
19
+ && (node.attrs?.['no-inline'] || node.attrs?.embed)
20
+ ) {
21
+ node.attrs['no-inline'] = false
22
+ node.attrs.embed = false
23
+ node.attrs['data-embed'] = true
24
+ }
25
+
14
26
  return node
15
27
  }
16
28
 
@@ -2,12 +2,14 @@ import posthtml from 'posthtml'
2
2
  import get from 'lodash-es/get.js'
3
3
  import { defu as merge } from 'defu'
4
4
 
5
+ import core from './core.js'
5
6
  import comb from './comb.js'
6
7
  import sixHex from './sixHex.js'
7
8
  import minify from './minify.js'
8
9
  import baseUrl from './baseUrl.js'
9
10
  import inlineCSS from './inline.js'
10
11
  import prettify from './prettify.js'
12
+ import templateTag from './template.js'
11
13
  import filters from './filters/index.js'
12
14
  import markdown from 'posthtml-markdownit'
13
15
  import posthtmlMso from './posthtmlMso.js'
@@ -20,19 +22,17 @@ import replaceStrings from './replaceStrings.js'
20
22
  import attributeToStyle from './attributeToStyle.js'
21
23
  import removeAttributes from './removeAttributes.js'
22
24
 
23
- import coreTransformers from './core.js'
24
-
25
25
  import defaultPosthtmlConfig from '../posthtml/defaultConfig.js'
26
26
 
27
27
  /**
28
28
  * Use Maizzle Transformers on an HTML string.
29
29
  *
30
- * Only Transformers that are enabled in the `config` will be used.
30
+ * Only Transformers that are enabled in the passed
31
+ * `config` parameter will be used.
31
32
  *
32
33
  * @param {string} html The HTML content
33
34
  * @param {object} config The Maizzle config object
34
- * @returns {Promise<{ original: string, config: object, html: string }>}
35
- * A Promise resolving to an object containing the original HTML, modified HTML, and the config
35
+ * @returns {Promise<{ html: string }>} A Promise resolving to an object containing the modified HTML
36
36
  */
37
37
  export async function run(html = '', config = {}) {
38
38
  const posthtmlPlugins = []
@@ -45,27 +45,26 @@ export async function run(html = '', config = {}) {
45
45
  /**
46
46
  * 1. Core transformers
47
47
  *
48
- * Transformers that are always enabled
48
+ * Transformers that are always enabled.
49
49
  *
50
50
  */
51
- posthtmlPlugins.push(coreTransformers(config))
51
+ posthtmlPlugins.push(core(config))
52
52
 
53
53
  /**
54
54
  * 2. Safe class names
55
55
  *
56
56
  * Rewrite Tailwind CSS class names to email-safe alternatives,
57
- * unless explicitly disabled
57
+ * unless explicitly disabled.
58
58
  */
59
- if (get(config, 'css.safeClassNames') !== false) {
59
+ if (get(config, 'css.safe') !== false) {
60
60
  posthtmlPlugins.push(
61
- safeClassNames(get(config, 'css.safeClassNames', {}))
61
+ safeClassNames(get(config, 'css.safe', {}))
62
62
  )
63
63
  }
64
64
 
65
65
  /**
66
66
  * 3. Filters
67
67
  *
68
- * Apply filters to HTML.
69
68
  * Filters are always applied, unless explicitly disabled.
70
69
  */
71
70
  if (get(config, 'filters') !== false) {
@@ -77,7 +76,7 @@ export async function run(html = '', config = {}) {
77
76
  /**
78
77
  * 4. Markdown
79
78
  *
80
- * Convert Markdown to HTML with Markdown-it, unless explicitly disabled
79
+ * Convert Markdown to HTML with markdown-it, unless explicitly disabled.
81
80
  */
82
81
  if (get(config, 'markdown') !== false) {
83
82
  posthtmlPlugins.push(
@@ -87,7 +86,9 @@ export async function run(html = '', config = {}) {
87
86
 
88
87
  /**
89
88
  * 5. Prevent widow words
90
- * Always runs, unless explicitly disabled
89
+ *
90
+ * Enabled by default, will prevent widow words in elements
91
+ * wrapped with a `prevent-widows` attribute.
91
92
  */
92
93
  if (get(config, 'widowWords') !== false) {
93
94
  posthtmlPlugins.push(
@@ -109,7 +110,7 @@ export async function run(html = '', config = {}) {
109
110
  /**
110
111
  * 7. Inline CSS
111
112
  *
112
- * Inline CSS into HTML
113
+ * Inline CSS into HTML.
113
114
  */
114
115
  if (get(config, 'css.inline')) {
115
116
  posthtmlPlugins.push(inlineCSS(
@@ -149,7 +150,7 @@ export async function run(html = '', config = {}) {
149
150
  /**
150
151
  * 10. Shorthand CSS
151
152
  *
152
- * Convert longhand CSS properties to shorthand in `style` attributes
153
+ * Convert longhand CSS properties to shorthand in `style` attributes.
153
154
  */
154
155
  if (get(config, 'css.shorthand')) {
155
156
  posthtmlPlugins.push(
@@ -160,7 +161,7 @@ export async function run(html = '', config = {}) {
160
161
  /**
161
162
  * 11. Add attributes
162
163
  *
163
- * Add attributes to HTML tags
164
+ * Add attributes to HTML tags.
164
165
  */
165
166
  if (get(config, 'attributes.add') !== false) {
166
167
  posthtmlPlugins.push(
@@ -171,33 +172,18 @@ export async function run(html = '', config = {}) {
171
172
  /**
172
173
  * 12. Base URL
173
174
  *
174
- * Add a base URL to relative paths
175
+ * Add a base URL to relative paths.
175
176
  */
176
177
  if (get(config, 'baseURL', get(config, 'baseUrl'))) {
177
178
  posthtmlPlugins.push(
178
179
  baseUrl(get(config, 'baseURL', get(config, 'baseUrl', {})))
179
180
  )
180
- } else {
181
- /**
182
- * Set baseURL to `build.static.destination` if it's not already set
183
- */
184
- const destination = get(config, 'build.static.destination', '')
185
- if (destination && !config._dev) {
186
- posthtmlPlugins.push(
187
- baseUrl({
188
- url: destination,
189
- allTags: true,
190
- styleTag: true,
191
- inlineCss: true,
192
- })
193
- )
194
- }
195
181
  }
196
182
 
197
183
  /**
198
184
  * 13. URL parameters
199
185
  *
200
- * Add parameters to URLs
186
+ * Add parameters to URLs.
201
187
  */
202
188
  if (get(config, 'urlParameters')) {
203
189
  posthtmlPlugins.push(
@@ -208,8 +194,7 @@ export async function run(html = '', config = {}) {
208
194
  /**
209
195
  * 14. Six-digit HEX
210
196
  *
211
- * Convert three-digit HEX colors to six-digit
212
- * Always runs, unless explicitly disabled
197
+ * Enabled by default, converts three-digit HEX colors to six-digit.
213
198
  */
214
199
  if (get(config, 'css.sixHex') !== false) {
215
200
  posthtmlPlugins.push(
@@ -220,7 +205,7 @@ export async function run(html = '', config = {}) {
220
205
  /**
221
206
  * 15. PostHTML MSO
222
207
  *
223
- * Simplify writing MSO conditionals for Outlook
208
+ * Enabled by default, simplifies writing MSO conditionals for Outlook.
224
209
  */
225
210
  if (get(config, 'outlook') !== false) {
226
211
  posthtmlPlugins.push(
@@ -231,7 +216,7 @@ export async function run(html = '', config = {}) {
231
216
  /**
232
217
  * 16. Prettify
233
218
  *
234
- * Pretty-print HTML using js-beautify
219
+ * Pretty-print HTML using js-beautify.
235
220
  */
236
221
  if (get(config, 'prettify')) {
237
222
  posthtmlPlugins.push(
@@ -242,7 +227,7 @@ export async function run(html = '', config = {}) {
242
227
  /**
243
228
  * 17. Minify
244
229
  *
245
- * Minify HTML using html-crush
230
+ * Minify HTML using html-crush.
246
231
  */
247
232
  if (get(config, 'minify')) {
248
233
  posthtmlPlugins.push(
@@ -251,9 +236,16 @@ export async function run(html = '', config = {}) {
251
236
  }
252
237
 
253
238
  /**
254
- * 18. Replace strings
239
+ * 18. <template> tags
240
+ *
241
+ * Replace <template> tags with their content.
242
+ */
243
+ posthtmlPlugins.push(templateTag())
244
+
245
+ /**
246
+ * 19. Replace strings
255
247
  *
256
- * Replace strings through regular expressions
248
+ * Replace strings through regular expressions.
257
249
  */
258
250
  if (get(config, 'replaceStrings')) {
259
251
  posthtmlPlugins.push(