eleventy-plugin-vento 2.6.0 → 3.0.1

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 (88) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/LICENSE +1 -1
  3. package/package.json +61 -56
  4. package/readme.md +98 -76
  5. package/src/engine.js +84 -0
  6. package/src/index.js +130 -0
  7. package/src/modules/create-vento-tag.js +56 -0
  8. package/src/modules/ignore-tag.js +15 -0
  9. package/src/modules/utils.js +25 -0
  10. package/.editorconfig +0 -16
  11. package/.github/actions/setup/action.yml +0 -25
  12. package/.github/renovate.json +0 -13
  13. package/.github/workflows/ci.yml +0 -15
  14. package/.github/workflows/release.yml +0 -33
  15. package/.node-version +0 -1
  16. package/.vscode/settings.json +0 -4
  17. package/.vscode/tests.code-snippets +0 -16
  18. package/VentoPlugin.js +0 -140
  19. package/eslint.config.js +0 -67
  20. package/jsconfig.json +0 -11
  21. package/lib/ssr.js +0 -23
  22. package/release.config.js +0 -31
  23. package/tests/autotrim/__snapshots__/autotrim-all.html +0 -53
  24. package/tests/autotrim/__snapshots__/autotrim-single.html +0 -75
  25. package/tests/autotrim/autotrim.test.js +0 -17
  26. package/tests/autotrim/build/autotrim-all.html +0 -53
  27. package/tests/autotrim/build/autotrim-single.html +0 -75
  28. package/tests/autotrim/index.vto +0 -23
  29. package/tests/basic/__snapshots__/template-basic.html +0 -4
  30. package/tests/basic/basic.test.js +0 -9
  31. package/tests/basic/index.vto +0 -7
  32. package/tests/computed-data/__snapshots__/from-local-key.html +0 -4
  33. package/tests/computed-data/computed-data.test.js +0 -9
  34. package/tests/computed-data/from-local-key.vto +0 -9
  35. package/tests/engine-overrides/__snapshots__/as-html-engine.html +0 -4
  36. package/tests/engine-overrides/__snapshots__/as-md-engine.html +0 -2
  37. package/tests/engine-overrides/as-html-engine.html +0 -7
  38. package/tests/engine-overrides/as-md-engine.md +0 -7
  39. package/tests/engine-overrides/eleventy.config.js +0 -4
  40. package/tests/engine-overrides/engine-overrides.test.js +0 -14
  41. package/tests/filters/__snapshots__/filters-arguments.html +0 -1
  42. package/tests/filters/__snapshots__/filters-basic.html +0 -2
  43. package/tests/filters/basic.vto +0 -5
  44. package/tests/filters/eleventy.config.js +0 -13
  45. package/tests/filters/filters.test.js +0 -19
  46. package/tests/filters/with-arguments.vto +0 -1
  47. package/tests/filters/with-this.vto +0 -1
  48. package/tests/get-instance.js +0 -38
  49. package/tests/includes/__snapshots__/includes-basic.html +0 -8
  50. package/tests/includes/__snapshots__/includes-data.html +0 -8
  51. package/tests/includes/_includes/partial.vto +0 -4
  52. package/tests/includes/basic.vto +0 -7
  53. package/tests/includes/includes.test.js +0 -14
  54. package/tests/includes/with-data.vto +0 -7
  55. package/tests/layouts/__snapshots__/example-layout-1.html +0 -27
  56. package/tests/layouts/__snapshots__/example-layout-2.html +0 -24
  57. package/tests/layouts/_includes/layout1.vto +0 -20
  58. package/tests/layouts/_includes/layout2.vto +0 -17
  59. package/tests/layouts/layouts.test.js +0 -14
  60. package/tests/layouts/page1.vto +0 -11
  61. package/tests/layouts/page2.vto +0 -11
  62. package/tests/permalinks/__snapshots__/permalink-nested.html +0 -15
  63. package/tests/permalinks/__snapshots__/permalink-root.html +0 -15
  64. package/tests/permalinks/_includes/layout.vto +0 -13
  65. package/tests/permalinks/at-nested.vto +0 -7
  66. package/tests/permalinks/at-root.vto +0 -7
  67. package/tests/permalinks/dynamic/dynamic-permalink.vto +0 -6
  68. package/tests/permalinks/dynamic/dynamic.11tydata.json +0 -3
  69. package/tests/permalinks/dynamic-js/dynamic-js.11tydata.js +0 -5
  70. package/tests/permalinks/dynamic-js/dynamic-permalink-js.vto +0 -6
  71. package/tests/permalinks/permalinks.test.js +0 -25
  72. package/tests/permalinks/skip.vto +0 -7
  73. package/tests/shortcodes/__snapshots__/shortcode-with-arguments.html +0 -2
  74. package/tests/shortcodes/basic.vto +0 -1
  75. package/tests/shortcodes/eleventy.config.js +0 -4
  76. package/tests/shortcodes/shortcodes.test.js +0 -14
  77. package/tests/shortcodes/with-arguments.vto +0 -2
  78. package/tests/ssr/__snapshots__/ssr-data.html +0 -1
  79. package/tests/ssr/__snapshots__/ssr-echo.html +0 -7
  80. package/tests/ssr/__snapshots__/ssr-exec.html +0 -1
  81. package/tests/ssr/__snapshots__/ssr-for.html +0 -3
  82. package/tests/ssr/__snapshots__/ssr-if.html +0 -5
  83. package/tests/ssr/data.vto +0 -1
  84. package/tests/ssr/echo.vto +0 -7
  85. package/tests/ssr/exec.vto +0 -1
  86. package/tests/ssr/for.vto +0 -3
  87. package/tests/ssr/if.vto +0 -5
  88. package/tests/ssr/ssr.test.js +0 -29
package/release.config.js DELETED
@@ -1,31 +0,0 @@
1
- const presetConfig = {
2
- types: [
3
- { type: 'feat', section: 'New Features' },
4
- { type: 'fix', section: 'Bug Fixes' },
5
- { type: 'refactor', section: 'Refactors' },
6
- { type: 'upstream', section: 'Dependency Updates' },
7
- { type: 'docs', hidden: true },
8
- { type: 'chore', hidden: true },
9
- { type: 'perf', hidden: true },
10
- { type: 'style', hidden: true },
11
- { type: 'test', hidden: true },
12
- ],
13
- };
14
-
15
- const releaseRules = [
16
- { breaking: true, release: 'major' },
17
- { type: 'upstream', release: 'minor' },
18
- ];
19
-
20
- /** @type {import('semantic-release').Options} */
21
- export default {
22
- preset: 'conventionalcommits',
23
- presetConfig,
24
- releaseRules,
25
- plugins: [
26
- ['@semantic-release/commit-analyzer'],
27
- '@semantic-release/release-notes-generator',
28
- '@semantic-release/npm',
29
- '@semantic-release/github',
30
- ],
31
- };
@@ -1,53 +0,0 @@
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>Document</title>
7
- </head>
8
- <body>
9
- <main>
10
- <section>
11
- <h1>1</h1>
12
- <p>This is section 1</p>
13
- </section>
14
- <section>
15
- <h1>2</h1>
16
- <p>This is section 2</p>
17
- </section>
18
- <section>
19
- <h1>3</h1>
20
- <p>This is section 3</p>
21
- </section>
22
- <section>
23
- <h1>4</h1>
24
- <p>This is section 4</p>
25
- </section>
26
- Section 5 is next!!
27
- <section>
28
- <h1>5</h1>
29
- <p>This is section 5</p>
30
- </section>
31
- <section>
32
- <h1>6</h1>
33
- <p>This is section 6</p>
34
- </section>
35
- <section>
36
- <h1>7</h1>
37
- <p>This is section 7</p>
38
- </section>
39
- <section>
40
- <h1>8</h1>
41
- <p>This is section 8</p>
42
- </section>
43
- <section>
44
- <h1>9</h1>
45
- <p>This is section 9</p>
46
- </section>
47
- <section>
48
- <h1>10</h1>
49
- <p>This is section 10</p>
50
- </section>
51
- </main>
52
- </body>
53
- </html>
@@ -1,75 +0,0 @@
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>Document</title>
7
- </head>
8
- <body>
9
- <main>
10
-
11
-
12
- <section>
13
- <h1>1</h1>
14
- <p>This is section 1</p>
15
- </section>
16
-
17
-
18
- <section>
19
- <h1>2</h1>
20
- <p>This is section 2</p>
21
- </section>
22
-
23
-
24
- <section>
25
- <h1>3</h1>
26
- <p>This is section 3</p>
27
- </section>
28
-
29
-
30
- <section>
31
- <h1>4</h1>
32
- <p>This is section 4</p>
33
- </section>
34
-
35
-
36
- Section 5 is next!!
37
-
38
- <section>
39
- <h1>5</h1>
40
- <p>This is section 5</p>
41
- </section>
42
-
43
-
44
- <section>
45
- <h1>6</h1>
46
- <p>This is section 6</p>
47
- </section>
48
-
49
-
50
- <section>
51
- <h1>7</h1>
52
- <p>This is section 7</p>
53
- </section>
54
-
55
-
56
- <section>
57
- <h1>8</h1>
58
- <p>This is section 8</p>
59
- </section>
60
-
61
-
62
- <section>
63
- <h1>9</h1>
64
- <p>This is section 9</p>
65
- </section>
66
-
67
-
68
- <section>
69
- <h1>10</h1>
70
- <p>This is section 10</p>
71
- </section>
72
-
73
- </main>
74
- </body>
75
- </html>
@@ -1,17 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { runBuild } from '#test-instance';
3
-
4
- const [trimSingle, trimAll] = await Promise.all([
5
- runBuild(import.meta.dirname, { vento: { trimTags: ['set'] } }),
6
- runBuild(import.meta.dirname, { vento: { trimTags: true } }),
7
- ]);
8
-
9
- test('should trim a single tag', async () => {
10
- const page = trimSingle[0];
11
- await expect(page.content).toMatchFileSnapshot('./build/autotrim-single.html');
12
- });
13
-
14
- test('should trim all tags', async () => {
15
- const page = trimAll[0];
16
- await expect(page.content).toMatchFileSnapshot('./build/autotrim-all.html');
17
- });
@@ -1,53 +0,0 @@
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>Document</title>
7
- </head>
8
- <body>
9
- <main>
10
- <section>
11
- <h1>1</h1>
12
- <p>This is section 1</p>
13
- </section>
14
- <section>
15
- <h1>2</h1>
16
- <p>This is section 2</p>
17
- </section>
18
- <section>
19
- <h1>3</h1>
20
- <p>This is section 3</p>
21
- </section>
22
- <section>
23
- <h1>4</h1>
24
- <p>This is section 4</p>
25
- </section>
26
- Section 5 is next!!
27
- <section>
28
- <h1>5</h1>
29
- <p>This is section 5</p>
30
- </section>
31
- <section>
32
- <h1>6</h1>
33
- <p>This is section 6</p>
34
- </section>
35
- <section>
36
- <h1>7</h1>
37
- <p>This is section 7</p>
38
- </section>
39
- <section>
40
- <h1>8</h1>
41
- <p>This is section 8</p>
42
- </section>
43
- <section>
44
- <h1>9</h1>
45
- <p>This is section 9</p>
46
- </section>
47
- <section>
48
- <h1>10</h1>
49
- <p>This is section 10</p>
50
- </section>
51
- </main>
52
- </body>
53
- </html>
@@ -1,75 +0,0 @@
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>Document</title>
7
- </head>
8
- <body>
9
- <main>
10
-
11
-
12
- <section>
13
- <h1>1</h1>
14
- <p>This is section 1</p>
15
- </section>
16
-
17
-
18
- <section>
19
- <h1>2</h1>
20
- <p>This is section 2</p>
21
- </section>
22
-
23
-
24
- <section>
25
- <h1>3</h1>
26
- <p>This is section 3</p>
27
- </section>
28
-
29
-
30
- <section>
31
- <h1>4</h1>
32
- <p>This is section 4</p>
33
- </section>
34
-
35
-
36
- Section 5 is next!!
37
-
38
- <section>
39
- <h1>5</h1>
40
- <p>This is section 5</p>
41
- </section>
42
-
43
-
44
- <section>
45
- <h1>6</h1>
46
- <p>This is section 6</p>
47
- </section>
48
-
49
-
50
- <section>
51
- <h1>7</h1>
52
- <p>This is section 7</p>
53
- </section>
54
-
55
-
56
- <section>
57
- <h1>8</h1>
58
- <p>This is section 8</p>
59
- </section>
60
-
61
-
62
- <section>
63
- <h1>9</h1>
64
- <p>This is section 9</p>
65
- </section>
66
-
67
-
68
- <section>
69
- <h1>10</h1>
70
- <p>This is section 10</p>
71
- </section>
72
-
73
- </main>
74
- </body>
75
- </html>
@@ -1,23 +0,0 @@
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>Document</title>
7
- </head>
8
- <body>
9
- {{ set numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }}
10
- <main>
11
- {{ for num of numbers }}
12
- {{ set current = `section ${num}` }}
13
- {{ if num === 5 }}
14
- Section 5 is next!!
15
- {{ /if }}
16
- <section>
17
- <h1>{{ num }}</h1>
18
- <p>This is {{ current }}</p>
19
- </section>
20
- {{ /for }}
21
- </main>
22
- </body>
23
- </html>
@@ -1,4 +0,0 @@
1
-
2
- <h1>Basic Page</h1>
3
-
4
- This is a standalone page.
@@ -1,9 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { runBuild } from '#test-instance';
3
-
4
- const results = await runBuild(import.meta.dirname);
5
-
6
- test('renders a basic template', async () => {
7
- const page = results[0];
8
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/template-basic.html');
9
- });
@@ -1,7 +0,0 @@
1
- ---
2
- title: Basic Page
3
- ---
4
-
5
- <h1>{{ title }}</h1>
6
-
7
- This is a standalone page.
@@ -1,4 +0,0 @@
1
-
2
- <h1>Dynamically generated from a custom key</h1>
3
-
4
- <p>a custom key</p>
@@ -1,9 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { runBuild } from '#test-instance';
3
-
4
- const results = await runBuild(import.meta.dirname);
5
-
6
- test('renders computed data from local key', async () => {
7
- const page = results.find(({ url }) => url === '/from-local-key/');
8
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/from-local-key.html');
9
- });
@@ -1,9 +0,0 @@
1
- ---
2
- eleventyComputed:
3
- title: "Dynamically generated from {{ customKey }}"
4
- customKey: "a custom key"
5
- ---
6
-
7
- <h1>{{ title }}</h1>
8
-
9
- <p>{{ customKey }}</p>
@@ -1,4 +0,0 @@
1
-
2
- <p>2 + 2 = 4</p>
3
-
4
- <p>This is a cool page</p>
@@ -1,2 +0,0 @@
1
- <p>2 + 2 = 4</p>
2
- <p>This is a cool page</p>
@@ -1,7 +0,0 @@
1
- ---
2
- title: This is a cool page
3
- ---
4
-
5
- <p>2 + 2 = {{ 2 + 2 }}</p>
6
-
7
- <p>{{ title }}</p>
@@ -1,7 +0,0 @@
1
- ---
2
- title: This is a cool page
3
- ---
4
-
5
- 2 + 2 = {{ 2 + 2 }}
6
-
7
- {{ title }}
@@ -1,4 +0,0 @@
1
- export const config = {
2
- markdownTemplateEngine: 'vto',
3
- htmlTemplateEngine: 'vto',
4
- };
@@ -1,14 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { runBuild } from '#test-instance';
3
-
4
- export const results = await runBuild(import.meta.dirname);
5
-
6
- test('renders markdown file with Vento', async () => {
7
- const page = results.find(({ url }) => url === '/as-md-engine/');
8
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/as-md-engine.html');
9
- });
10
-
11
- test('renders html file with Vento', async () => {
12
- const page = results.find(({ url }) => url === '/as-html-engine/');
13
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/as-html-engine.html');
14
- });
@@ -1 +0,0 @@
1
- This is a <strong>really cool</strong> filter.
@@ -1,2 +0,0 @@
1
-
2
- Page title is 'A BASIC PAGE'
@@ -1,5 +0,0 @@
1
- ---
2
- title: A basic page
3
- ---
4
-
5
- Page title is '{{ title |> uppercase }}'
@@ -1,13 +0,0 @@
1
- export default function (eleventyConfig) {
2
- eleventyConfig.addFilter(
3
- 'uppercase',
4
- /** @param {string} content */
5
- (content) => content.toUpperCase()
6
- );
7
-
8
- eleventyConfig.addFilter('wrapWith', (content, tag) => `<${tag}>${content}</${tag}>`);
9
-
10
- eleventyConfig.addFilter('pageUrlCompare', function (url) {
11
- return url === this.page.url;
12
- });
13
- }
@@ -1,19 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { runBuild } from '#test-instance';
3
-
4
- const results = await runBuild(import.meta.dirname);
5
-
6
- test('transforms string with built-in method', async () => {
7
- const page = results.find(({ url }) => url === '/basic/');
8
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/filters-basic.html');
9
- });
10
-
11
- test('transforms string with passed arguments', async () => {
12
- const page = results.find(({ url }) => url === '/with-arguments/');
13
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/filters-arguments.html');
14
- });
15
-
16
- test('verify page context in scoped data', async () => {
17
- const page = results.find(({ url }) => url === '/with-this/');
18
- expect(page.content).toBe('true\n');
19
- });
@@ -1 +0,0 @@
1
- This is a {{ 'really cool' |> wrapWith('strong') }} filter.
@@ -1 +0,0 @@
1
- {{ page.url |> pageUrlCompare }}
@@ -1,38 +0,0 @@
1
- import path from 'node:path';
2
- import { VentoPlugin } from 'eleventy-plugin-vento';
3
- import Eleventy from '@11ty/eleventy';
4
-
5
- /**
6
- * @typedef {import('@11ty/eleventy/src/UserConfig.js').default} EleventyUserConfig
7
- *
8
- * @typedef {object} ExtensionsOptions
9
- * @property {import('eleventy-plugin-vento').VentoPluginOptions} vento
10
- * @property {(configApi: EleventyUserConfig) => void} eleventy
11
- *
12
- * @typedef {{ url: string, inputPath: string, outputPath: string, rawInput: string, content: string }} PageObject
13
- * @typedef {PageObject[]} EleventyOutput
14
- */
15
-
16
- /**
17
- * @param {string} fromInput
18
- * @param {ExtensionsOptions} [extensions={}]
19
- */
20
- export async function runBuild(fromInput, extensions = {}) {
21
- const instance = new Eleventy(fromInput, '_site', {
22
- quiet: true,
23
-
24
- configPath: path.join(fromInput, 'eleventy.config.js'),
25
-
26
- /** @param {EleventyUserConfig} eleventyConfig */
27
- config(eleventyConfig) {
28
- eleventyConfig.addPlugin(VentoPlugin, extensions.vento || {});
29
-
30
- eleventyConfig.ignores.add('**/__snapshots__/**');
31
- },
32
- });
33
-
34
- /** @type {Promise<EleventyOutput>} */
35
- const results = await instance.toJSON();
36
-
37
- return results;
38
- }
@@ -1,8 +0,0 @@
1
-
2
- <h1>Page with includes</h1>
3
-
4
- <section class="include">
5
- <p>This content is included.</p>
6
- <p>Passed data for `extra` key: ''</p>
7
- </section>
8
-
@@ -1,8 +0,0 @@
1
-
2
- <h1>Page with includes (data passing)</h1>
3
-
4
- <section class="include">
5
- <p>This content is included.</p>
6
- <p>Passed data for `extra` key: 'super fancy data'</p>
7
- </section>
8
-
@@ -1,4 +0,0 @@
1
- <section class="include">
2
- <p>This content is included.</p>
3
- <p>Passed data for `extra` key: '{{ extra }}'</p>
4
- </section>
@@ -1,7 +0,0 @@
1
- ---
2
- title: Page with includes
3
- ---
4
-
5
- <h1>{{ title }}</h1>
6
-
7
- {{ include 'partial.vto' }}
@@ -1,14 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { runBuild } from '#test-instance';
3
-
4
- const results = await runBuild(import.meta.dirname);
5
-
6
- test('include a page', async () => {
7
- const page = results.find(({ url }) => url === '/basic/');
8
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/includes-basic.html');
9
- });
10
-
11
- test('include a page with data passing', async () => {
12
- const page = results.find(({ url }) => url === '/with-data/');
13
- await expect(page.content).toMatchFileSnapshot('./__snapshots__/includes-data.html');
14
- });
@@ -1,7 +0,0 @@
1
- ---
2
- title: Page with includes (data passing)
3
- ---
4
-
5
- <h1>{{ title }}</h1>
6
-
7
- {{ include 'partial.vto' { extra: 'super fancy data' } }}
@@ -1,27 +0,0 @@
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>Page with layout</title>
7
- </head>
8
- <body>
9
- <header>
10
- <h1>Page title: Page with layout</h1>
11
- </header>
12
- <main>
13
- <section>This is an alternative layout.</section>
14
- <section>
15
- <p>This is a page that uses the first layout.</p>
16
-
17
- <p>This is another paragraph.</p>
18
- <p>This is another paragraph.</p>
19
- <p>This is another paragraph.</p>
20
-
21
- </section>
22
- </main>
23
- <footer>
24
- <p>I am a footer, outside the layout</p>
25
- </footer>
26
- </body>
27
- </html>
@@ -1,24 +0,0 @@
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>Page with layout</title>
7
- </head>
8
- <body>
9
- <header>
10
- <h1>Page title: Page with layout</h1>
11
- </header>
12
- <main>
13
- <p>This is a page that uses the second layout.</p>
14
-
15
- <p>This is another paragraph.</p>
16
- <p>This is another paragraph.</p>
17
- <p>This is another paragraph.</p>
18
-
19
- </main>
20
- <footer>
21
- <p>I am a footer, outside the layout</p>
22
- </footer>
23
- </body>
24
- </html>
@@ -1,20 +0,0 @@
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>{{ title }}</title>
7
- </head>
8
- <body>
9
- <header>
10
- <h1>Page title: {{ title }}</h1>
11
- </header>
12
- <main>
13
- <section>This is an alternative layout.</section>
14
- <section>{{ content }}</section>
15
- </main>
16
- <footer>
17
- <p>I am a footer, outside the layout</p>
18
- </footer>
19
- </body>
20
- </html>