@maizzle/framework 5.0.0-beta.3 → 5.0.0-beta.30

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.
Files changed (46) hide show
  1. package/package.json +32 -30
  2. package/src/commands/build.js +146 -73
  3. package/src/generators/plaintext.js +26 -23
  4. package/src/generators/render.js +15 -14
  5. package/src/posthtml/defaultComponentsConfig.js +2 -2
  6. package/src/posthtml/defaultConfig.js +13 -3
  7. package/src/posthtml/index.js +38 -9
  8. package/src/posthtml/plugins/envAttributes.js +32 -0
  9. package/src/posthtml/plugins/envTags.js +33 -0
  10. package/src/server/index.js +159 -96
  11. package/src/server/routes/index.js +51 -13
  12. package/src/server/views/404.html +59 -0
  13. package/src/server/views/index.html +162 -14
  14. package/src/transformers/addAttributes.js +2 -3
  15. package/src/transformers/attributeToStyle.js +1 -3
  16. package/src/transformers/baseUrl.js +6 -6
  17. package/src/transformers/comb.js +7 -6
  18. package/src/transformers/core.js +12 -0
  19. package/src/transformers/filters/index.js +1 -2
  20. package/src/transformers/index.js +56 -67
  21. package/src/transformers/inline.js +53 -16
  22. package/src/transformers/markdown.js +14 -7
  23. package/src/transformers/minify.js +4 -3
  24. package/src/transformers/posthtmlMso.js +1 -3
  25. package/src/transformers/prettify.js +4 -3
  26. package/src/transformers/preventWidows.js +15 -65
  27. package/src/transformers/removeAttributes.js +3 -4
  28. package/src/transformers/replaceStrings.js +7 -5
  29. package/src/transformers/safeClassNames.js +1 -2
  30. package/src/transformers/shorthandCss.js +1 -3
  31. package/src/transformers/sixHex.js +1 -3
  32. package/src/transformers/template.js +26 -0
  33. package/src/transformers/urlParameters.js +1 -3
  34. package/src/transformers/useAttributeSizes.js +1 -3
  35. package/src/utils/string.js +89 -0
  36. package/types/build.d.ts +53 -24
  37. package/types/config.d.ts +60 -49
  38. package/types/css/inline.d.ts +2 -0
  39. package/types/css/purge.d.ts +1 -1
  40. package/types/events.d.ts +153 -5
  41. package/types/index.d.ts +4 -3
  42. package/types/posthtml.d.ts +3 -3
  43. package/types/urlParameters.d.ts +1 -1
  44. package/types/widowWords.d.ts +16 -36
  45. package/types/components.d.ts +0 -195
  46. package/types/expressions.d.ts +0 -100
@@ -3,10 +3,13 @@ import { defu as merge } from 'defu'
3
3
 
4
4
  // PostHTML
5
5
  import posthtml from 'posthtml'
6
+ import posthtmlFetch from 'posthtml-fetch'
7
+ import envTags from './plugins/envTags.js'
6
8
  import components from 'posthtml-component'
7
9
  import posthtmlPostcss from 'posthtml-postcss'
8
- import defaultPosthtmlConfig from './defaultConfig.js'
9
10
  import expandLinkTag from './plugins/expandLinkTag.js'
11
+ import envAttributes from './plugins/envAttributes.js'
12
+ import { getPosthtmlOptions } from './defaultConfig.js'
10
13
 
11
14
  // PostCSS
12
15
  import tailwindcss from 'tailwindcss'
@@ -33,12 +36,12 @@ export async function process(html = '', config = {}) {
33
36
  )
34
37
  )
35
38
 
36
- const posthtmlOptions = merge(get(config, 'posthtml.options', {}), defaultPosthtmlConfig)
39
+ const posthtmlOptions = getPosthtmlOptions(get(config, 'posthtml.options', {}))
37
40
 
38
41
  const componentsUserOptions = get(config, 'components', {})
39
42
 
40
43
  const expressionsOptions = merge(
41
- get(config, 'build.expressions', get(config, 'posthtml.expressions', {})),
44
+ get(config, 'expressions', get(config, 'posthtml.expressions', {})),
42
45
  get(componentsUserOptions, 'expressions', {}),
43
46
  )
44
47
 
@@ -48,24 +51,50 @@ export async function process(html = '', config = {}) {
48
51
  { page: config },
49
52
  )
50
53
 
54
+ const fetchPlugin = posthtmlFetch(
55
+ merge(
56
+ {
57
+ expressions: merge(
58
+ { locals },
59
+ expressionsOptions,
60
+ {
61
+ missingLocal: '{local}',
62
+ strictMode: false,
63
+ },
64
+ ),
65
+ },
66
+ get(config, 'fetch', {})
67
+ )
68
+ )
69
+
51
70
  return posthtml([
52
71
  ...get(config, 'posthtml.plugins.before', []),
72
+ envTags(config.env),
73
+ envAttributes(config.env),
53
74
  expandLinkTag,
54
75
  postcssPlugin,
76
+ fetchPlugin,
55
77
  components(
56
- merge({
57
- expressions: {
58
- locals,
59
- }
60
- }, defaultComponentsConfig)
78
+ merge(
79
+ {
80
+ expressions: merge(
81
+ { locals },
82
+ expressionsOptions,
83
+ )
84
+ },
85
+ componentsUserOptions,
86
+ defaultComponentsConfig
87
+ )
61
88
  ),
62
89
  expandLinkTag,
63
90
  postcssPlugin,
91
+ envTags(config.env),
92
+ envAttributes(config.env),
64
93
  ...get(config, 'posthtml.plugins.after', get(config, 'posthtml.plugins', []))
65
94
  ])
66
95
  .process(html, posthtmlOptions)
67
96
  .then(result => ({
68
- config,
97
+ config: merge(config, { page: config }),
69
98
  html: result.html,
70
99
  }))
71
100
  .catch(error => {
@@ -0,0 +1,32 @@
1
+ const plugin = (env => tree => {
2
+ const process = node => {
3
+ // Return the original node if no environment is set
4
+ if (!env) {
5
+ return node
6
+ }
7
+
8
+ if (node.attrs) {
9
+ for (const attr in node.attrs) {
10
+ const suffix = `-${env}`
11
+
12
+ // Find attributes on this node that have this suffix
13
+ if (attr.endsWith(suffix)) {
14
+ const key = attr.slice(0, -suffix.length)
15
+ const value = node.attrs[attr]
16
+
17
+ // Change the attribute without the suffix to have the value of the suffixed attribute
18
+ node.attrs[key] = value
19
+
20
+ // Remove the attribute with the suffix
21
+ node.attrs[attr] = false
22
+ }
23
+ }
24
+ }
25
+
26
+ return node
27
+ }
28
+
29
+ return tree.walk(process)
30
+ })
31
+
32
+ export default plugin
@@ -0,0 +1,33 @@
1
+ const plugin = (env => tree => {
2
+ const process = node => {
3
+ env = env || 'local'
4
+
5
+ // Return the original node if it doesn't have a tag
6
+ if (!node.tag) {
7
+ return node
8
+ }
9
+
10
+ const tagEnv = node.tag.split(':').pop()
11
+
12
+ // Tag targets current env, remove it and return its content
13
+ if (node.tag === `env:${env}`) {
14
+ node.tag = false
15
+ }
16
+
17
+ // Tag doesn't target current env, remove it completely
18
+ if (
19
+ typeof node.tag === 'string'
20
+ && node.tag.startsWith('env:')
21
+ && tagEnv !== env
22
+ ) {
23
+ node.content = []
24
+ node.tag = false
25
+ }
26
+
27
+ return node
28
+ }
29
+
30
+ return tree.walk(process)
31
+ })
32
+
33
+ export default plugin
@@ -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
 
@@ -60,18 +128,20 @@ export default async (config = {}) => {
60
128
  * Initialize WebSocket server
61
129
  * Used to send messages between the server and the browser
62
130
  */
63
- initWebSockets(wss, { scrollSync: shouldScroll, hmr: useHmr })
131
+ initWebSockets(wss, { shouldScroll, 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
+ app.request.maizzleConfig = 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
 
@@ -94,11 +164,11 @@ export default async (config = {}) => {
94
164
  app.get(routePattern, async (req, res, next) => {
95
165
  // Run beforeCreate event
96
166
  if (typeof config.beforeCreate === 'function') {
97
- config.beforeCreate(config)
167
+ await config.beforeCreate({ config })
98
168
  }
99
169
 
100
170
  try {
101
- const filePath = templatePaths.find(t => t.endsWith(req.url.slice(1)))
171
+ const filePath = templatePaths.find(t => t.endsWith(decodeURI(req.url.slice(1))))
102
172
 
103
173
  // Set the file being viewed
104
174
  viewing = filePath
@@ -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
 
@@ -235,7 +288,7 @@ export default async (config = {}) => {
235
288
 
236
289
  // Run beforeCreate event
237
290
  if (typeof config.beforeCreate === 'function') {
238
- await config.beforeCreate(config)
291
+ await config.beforeCreate({ config })
239
292
  }
240
293
 
241
294
  // Read the file
@@ -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
  /**
@@ -7,27 +7,65 @@ import { fileURLToPath } from 'node:url'
7
7
  import expressions from 'posthtml-expressions'
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
9
 
10
- route.get(['/', '/index.html'], async (req, res) => {
11
- const view = await fs.readFile(path.join(__dirname, '../views', 'index.html'), 'utf8')
10
+ function groupFilesByDirectories(globs, files) {
11
+ const result = {}
12
+ let current = {}
12
13
 
13
- // Group by `dir`
14
- const groupedByDir = {}
14
+ globs.forEach(glob => {
15
+ const rootPath = glob.split(/[\*\!\{\}]/)[0].replace(/\/+$/, '')
15
16
 
16
- req.templatePaths
17
- .map(t => path.parse(t))
18
- .forEach(file => {
19
- if (!groupedByDir[file.dir]) {
20
- groupedByDir[file.dir] = []
21
- }
17
+ files.forEach(file => {
18
+ if (file.startsWith(rootPath)) {
19
+ const relativePath = file.slice(rootPath.length + 1)
20
+ const parts = relativePath.split('/')
21
+ current = result[rootPath] = result[rootPath] || {}
22
+
23
+ for (let i = 0; i < parts.length - 1; i++) {
24
+ current = current[parts[i]] = current[parts[i]] || {}
25
+ }
22
26
 
23
- file.href = [file.dir.replace(file.root, ''), file.base].join('/')
24
- groupedByDir[file.dir].push(file)
27
+ const fileName = parts[parts.length - 1]
28
+ current[fileName] = {
29
+ name: fileName,
30
+ href: encodeURI(file),
31
+ }
32
+ }
25
33
  })
34
+ })
35
+
36
+ return result
37
+ }
38
+
39
+ function flattenPaths(paths, parentPath = '', currentDepth = 0) {
40
+ const flatArray = []
41
+
42
+ for (const [key, value] of Object.entries(paths)) {
43
+ const fullPath = parentPath ? `${parentPath}/${key}` : key
44
+
45
+ if (value && typeof value === 'object' && !value.name) {
46
+ // If it's a folder, add it with the current depth and recurse into its contents
47
+ flatArray.push({ name: key, path: fullPath, depth: currentDepth, type: 'folder' })
48
+ flatArray.push(...flattenPaths(value, fullPath, currentDepth + 1))
49
+ } else if (value && typeof value === 'object' && value.name) {
50
+ // If it's a file, add it with the current depth
51
+ flatArray.push({ name: value.name, href: value.href, path: fullPath, depth: currentDepth, type: 'file' })
52
+ }
53
+ }
54
+
55
+ return flatArray
56
+ }
57
+
58
+ route.get(['/', '/index.html'], async (req, res) => {
59
+ const view = await fs.readFile(path.join(__dirname, '../views', 'index.html'), 'utf8')
60
+
61
+ const content = new Set(req.maizzleConfig.build.content)
62
+
63
+ const groupedByDir = groupFilesByDirectories(content, req.templatePaths)
26
64
 
27
65
  const { html } = await posthtml()
28
66
  .use(expressions({
29
67
  locals: {
30
- templates: groupedByDir
68
+ paths: flattenPaths(groupedByDir)
31
69
  }
32
70
  }))
33
71
  .process(view)
@@ -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>