{"version":"https://jsonfeed.org/version/1","title":"🔥 Updates","home_page_url":"https://lume.land/blog/","feed_url":"https://lume.land/blog/feed.json","description":"A blog to follow the updates of Lume, the static site generator for Deno","items":[{"id":"https://lume.land/blog/posts/lume-3-2-0/","url":"https://lume.land/blog/posts/lume-3-2-0/","title":"Lume 3.2.0 - Rosalía","content_html":"<p>So, you thought that Lume could only generate static sites, right?</p>\n<p><strong>Not anymore!</strong> As of version 3.2, Lume can also <strong>create books</strong> in EPUB\nformat. That's why I wanted to dedicate this version to one of the most\nimportant figures of Galician literature of all time: <strong>Rosalía de Castro</strong>.</p>\n<!--more-->\n<p>You may know <a href=\"https://en.wikipedia.org/wiki/Rosal%C3%ADa\">Rosalía</a>, the popular\nSpanish singer. But in Galicia, we have another Rosalía:\n<a href=\"https://en.wikipedia.org/wiki/Rosal%C3%ADa_de_Castro\">Rosalía de Castro</a>,\nprobably our most important poet and novelist. She was a leading figure in the\nperiod of the resurgence and revitalization of the Galician language in\nliterature during the 19th century (period known as\n<a href=\"https://en.wikipedia.org/wiki/Rexurdimento\">Rexurdimento</a>).</p>\n<p>Some of her poems were set to music by several artists. Do you want an example?\nEnjoy\n<a href=\"https://www.youtube.com/watch?v=q_Nx2nq4oiM\">&quot;Negra sombra&quot; (black shadow)</a>\ninterpreted by Luz Casal.</p>\n<h2 id=\"new-plugin-epub\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#new-plugin-epub\" class=\"header-anchor\">New plugin <code>epub</code></a></h2>\n<p><a href=\"https://www.w3.org/publishing/epub3/\">EPUB</a> is the standard format for ebooks.\nTechnically, it's a zip file containing files in formats like XHTML, CSS, JPEG,\nPNG; and other xml files specific for ebooks (a <code>container.xml</code> manifest file, a\n<code>content.opf</code> with the book structure, etc). Robin Whittleton\n<a href=\"https://www.htmhell.dev/adventcalendar/2025/11/\">wrote a great article</a>\nexplaining how EPUB works.</p>\n<p>Since EPUB is based on web standards that Lume understands, it seems feasible to\nuse Lume to create EPUBs. The only problem was the requirement of\n<a href=\"https://www.w3.org/TR/xhtml11/\">XHTML</a> (HTML is not valid for EPUBs), and this\nwasn't easy to do in previous versions of Lume. But in this version, Lume can\noutput <code>.xhtml</code> files and treat them in the same way as <code>.html</code>, hence we also\nhave an EPUB plugin to help you to generate EPUBs. What this plugin can do?</p>\n<ul>\n<li>Create the <code>container.xml</code>, <code>encryption.xml</code>, <code>mimetype</code>, and <code>content.opf</code>\nmanifest files.</li>\n<li>Create the <code>toc.ncx</code> file with the book structure (using the\n<a href=\"https://lume.land/plugins/nav/\">nav plugin</a> under the hood).</li>\n<li>Convert the code and change the extension of all <code>.html</code> pages to <code>.xhtml</code>.</li>\n<li>Compress all files and create the <code>book.epub</code> file in the <code>dest</code> folder.</li>\n</ul>\n<p>This is an example of using the plugin:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport epub from &quot;lume/plugins/epub.ts&quot;;\n\nconst site = lume({\n  prettyUrls: false, // prettyUrls don't make sense for ebooks\n});\n\nsite.use(epub({\n  // Book metadata\n  metadata: {\n    identifier: &quot;unique identifier of your book&quot;,\n    cover: &quot;/images/cover.png&quot;,\n    title: &quot;My awesome book&quot;,\n    subtitle: &quot;History of my life&quot;,\n    creator: [&quot;Óscar Otero&quot;],\n    publisher: &quot;Lume editions&quot;,\n    language: &quot;en-US&quot;,\n    date: new Date(&quot;2026-01-31T12:18:28Z&quot;),\n  },\n}));\n\nexport default site;\n</code></pre>\n<p>Note that the plugin cannot magically convert any website to an EPUB; you still\nneed to have a proper structure, use some epub specific attributes, etc. But\ndon't worry! the <a href=\"https://github.com/lumeland/simple-epub\">Simple ePub theme</a>\nprovides a nice boilerplate to start publishing books.</p>\n<p>Learn more about this plugin\n<a href=\"https://lume.land/plugins/epub/\">on the documentation page</a>.</p>\n<h2 id=\"new-plugin-image_size\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#new-plugin-image_size\" class=\"header-anchor\">New plugin <code>image_size</code></a></h2>\n<p>This is a recurrent request, and finally, Lume has a plugin to add automatically\nthe <code>width</code> and <code>height</code> values of the images.</p>\n<p>The plugin uses the awesome\n<a href=\"https://github.com/sindresorhus/image-dimensions\">image-dimensions</a> library by\nSindre Sorhus. To use it, just install it like any other plugin:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport imageSize from &quot;lume/plugins/image_size.ts&quot;;\n\nconst site = lume();\n\nsite.use(imageSize());\n\nexport default site;\n</code></pre>\n<p>Add the <code>image-size</code> attribute to the images you want the plugin to calculate\nthe size:</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;/image.png&quot; image-size&gt;\n</code></pre>\n<p>And the plugin automatically adds the <code>width</code> and <code>height</code> attributes:</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;/image.png&quot; width=&quot;600&quot; height=&quot;300&quot;&gt;\n</code></pre>\n<p>More info <a href=\"https://lume.land/plugins/image_size/\">on the documentation page</a>.</p>\n<h2 id=\"new-plugin-extract_order\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#new-plugin-extract_order\" class=\"header-anchor\">New plugin <code>extract_order</code></a></h2>\n<p>Sometimes you have a list of pages that you want to show in a specific order. A\ncommon way to do that is to define an <code>order</code> variable in the front matter:</p>\n<pre><code class=\"language-md\">---\ntitle: Article 3\norder: 3\n---\n\nThis is the article 3\n</code></pre>\n<p>Then, you only have to select the pages in this specific order:</p>\n<pre><code class=\"language-vto\">{{ set pages = search.pages(&quot;type=article&quot;, &quot;order=asc&quot;) }}\n</code></pre>\n<p>This works great, the only problem is that you can't see the pages in the same\norder in your code editor or file system because they are ordered\nalphabetically:</p>\n<pre><code>/article-three.md\n/first-article.md\n/other-article.md\n</code></pre>\n<p>With this plugin you can set the order in the filename, using the format\n<code>{number}.filename</code>, and this value will be used as the <code>order</code> variable. The\nplugin also removes this prefix from the final URL by default (configurable).</p>\n<pre><code>/1.first-article.md\n/2.other-article.md\n/3.article-three.md\n</code></pre>\n<p>This also works great for folders:</p>\n<pre><code>/1.articles/\n   1.first-article.md\n   2.other-article.md\n   3.article-three.md\n/2.notes/\n   1.note-one.md\n   2.note-two.md\n</code></pre>\n<p>To use it:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport extractOrder from &quot;lume/plugins/extract_order.ts&quot;;\n\nconst site = lume();\n\nsite.use(extractOrder());\n\nexport default site;\n</code></pre>\n<p>More info <a href=\"https://lume.land/plugins/extract_order/\">on the documentation page.</a></p>\n<h2 id=\"new-plugin-replace\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#new-plugin-replace\" class=\"header-anchor\">New plugin <code>replace</code></a></h2>\n<p>This simple plugin allows to perform simple text replacements in the site,\nsomething especially useful for documentation sites. For example, let's say you\nwant to display always the last version of your library in a website:</p>\n<pre><code class=\"language-md\">Welcome to Libros 2.3.0, the library to read ebook. To getting started, run the\nfollowing command:\n\ndeno install --global https://deno.land/x/libros@2.3.0/mod.ts\n</code></pre>\n<p>Instead of harcoding the version number everywhere in your site (and remember to\nupdate it after a new version), this plugin allows to use a placeholder:</p>\n<pre><code class=\"language-md\">Welcome to Libros $VERSION, the library to read ebook. To getting started, run\nthe following command:\n\ndeno install --global https://deno.land/x/libros@$VERSION/mod.ts\n</code></pre>\n<p>Now, configure the replacements in the plugin options:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport replace from &quot;lume/plugins/replace.ts&quot;;\n\nconst site = lume();\n\nsite.use(replace({\n  replacements: {\n    &quot;$VERSION&quot;: &quot;2.3.0&quot;,\n  },\n}));\n\nexport default site;\n</code></pre>\n<p>Now you have this value centralized in one place. This is the approach used in\nthe Lume website to\n<a href=\"https://github.com/lumeland/lume.land/blob/055eac5d0a960ab014eedc552492237c6613dbac/_config.ts#L54\">keep the versions of all packages up to date</a>.</p>\n<p>You can use this plugin for any constant value that you want to use globally,\nlike a query parameter for caching CSS and JS files, the hash of the latest\ncommit, the year in the copyright, etc.</p>\n<p>More info <a href=\"https://lume.land/plugins/replace/\">on the documentation page</a>.</p>\n<h2 id=\"parsebasename-can-access-the-parent-values\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#parsebasename-can-access-the-parent-values\" class=\"header-anchor\"><code>parseBasename</code> can access the parent values</a></h2>\n<p>The function\n<a href=\"https://lume.land/docs/core/basename-parsers/\"><code>site.parseBasename</code></a> allows\nregistering functions to extract values from files and folders. In fact, it's\nwhat the <code>extract_order</code> and <code>extract_date</code> plugins use under the hood.</p>\n<p>As of Lume 3.2, the data from the parent folder is added as the second argument.\nThis allows us to compose values contextually using the names of different\nfolders. For example, let's say we have some files with the following paths:</p>\n<pre><code>/2026/01/01/happy-new-year.md\n/2026/01/05/this-year-sucks.md\n</code></pre>\n<p>Now you can compose the final date of each file using the values of the\ndirectories and subdirectories. For example:</p>\n<pre><code class=\"language-js\">site.parseBasename((basename, parent) =&gt; {\n  // Check if the name only contains numbers\n  if (!/^\\d+$/.test(name)) {\n    return;\n  }\n\n  // 4 digits, it's the year\n  if (basename.length === 4) {\n    return { year: basename, basename }\n  }\n\n  // 2 digits, it's the month or day\n  if (basename.length === 2) {\n    // If the month isn't in the parent, this is the month\n    if (!parent.month) {\n      return { month: basename, basename }\n    }\n\n    // This is the day, generate the final date\n    const { year, month } = parent;\n    return {\n      date: `${year}`-${month}`-${basename}`,\n      basename,\n    }\n  }\n})\n</code></pre>\n<h2 id=\"watcher.dependencies-option\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#watcher.dependencies-option\" class=\"header-anchor\"><code>watcher.dependencies</code> option</a></h2>\n<p>The Lume file watcher detects changes in your files in order to rebuild the site\nwith the new content. An important aspect is that only the changed files are\nreloaded, which is way faster than reloading all files every time something\nchanged. This works great in 99% of the cases, but there are some edge cases\nwhere we need to say Lume to reload a file when another file has changed.</p>\n<p>As an example, let's say you have some data stored in a SQLite database and you\nwant to expose some of its data to your pages using a <code>_data.ts</code> file:</p>\n<pre><code class=\"language-js\">// _data.ts\nimport { DatabaseSync } from &quot;node:sqlite&quot;;\n\nconst db = new DatabaseSync(&quot;database.db&quot;);\n\nexport const categories = db.prepare(`\n  SELECT\n    categories.id,\n    categories.name\n  FROM categories\n`).all();\n\ndb.close();\n</code></pre>\n<p>As you can see, we are exporting the <code>categories</code> variable, and this will make\nit available to all pages. If we make changes in the <code>database.db</code> file, Lume\nwill detect that the file has changed, but because <code>_data.ts</code> hasn't changed,\nLume won't re-run this file, so the new changes won't be available. What we\nreally want is to reload the <code>_data.ts</code> file every time the <code>database.db</code> file\nhas changed. And now we can do it thanks to the new <code>watcher.dependencies</code>\noption:</p>\n<pre><code class=\"language-ts\">import lume from &quot;lume/mod.ts&quot;;\n\nconst site = lume({\n  watcher: {\n    dependencies: {\n      &quot;_data.ts&quot;: [&quot;database.db-journal&quot;],\n    },\n  },\n});\n\nexport default site;\n</code></pre>\n<p>Here we are telling Lume that the file <code>_data.ts</code> depends on\n<code>database.db-journal</code> (the extension <code>.db-journal</code> is used by SQLite to create a\ntemporary file during the data transactions). Now Lume knows that every time any\nof its dependencies change, <code>_data.ts</code> will be reloaded too.</p>\n<h2 id=\"better-logs\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#better-logs\" class=\"header-anchor\">Better logs</a></h2>\n<p>When Lume builds a site, the logger outputs messages to the terminal in the same\norder they are generated. One problem with this approach is that there are\nmessages more important than others, and, especially if the site has a lot of\npages, those messages can be lost among others. Another problem is that if the\nsame error is produced by different pages, it's shown once per page, which\nproduces a lot of noise and prevents seeing other important messages. Let's see\nan example:</p>\n<pre><code>WARN [esbuild plugin] No TS, JS, TSX, JSX files found. Use site.add() to add files. For example: site.add(&quot;script.js&quot;)\nERROR SourceError: Unclosed tag\n/_includes/templates/blocks.vto:4:3\n 1 | {{ for block of blocks }}\n 2 |   {{ if !block.hide }}\n 3 |     {{ await comp[block.type]({ block, lang, url }) }}\n 4 |   {{ /if }\n   |   ^ Unclosed tag\nERROR SourceError: Unclosed tag\n/_includes/templates/blocks.vto:4:3\n 1 | {{ for block of blocks }}\n 2 |   {{ if !block.hide }}\n 3 |     {{ await comp[block.type]({ block, lang, url }) }}\n 4 |   {{ /if }\n   |   ^ Unclosed tag\n🔥 /docs/configuration/env-variables/ &lt;- /docs/configuration/env-variables.md\n🔥 /docs/configuration/config-file/ &lt;- /docs/configuration/config-file.md\n🔥 /docs/configuration/add-files/ &lt;- /docs/configuration/add-files.md\n🔥 /img/extend.svg &lt;- /img/extend.svg\n🔥 /img/deploy.svg &lt;- /img/deploy.svg\n...\n🔥 /img/http-imports.svg &lt;- /img/http-imports.svg\n🔥 /init.ts &lt;- /static/init.ts\n🔥 /img/gradient.png &lt;- /img/gradient.png\n🔥 /img/zero-runtime.svg &lt;- /img/zero-runtime.svg\n🔥 /logo.png &lt;- /static/logo.png\nWARN [validate_html plugin] 512 HTML error(s) found. Setup an output file or check the debug bar.\nWARN [seo plugin] 45 SEO error(s) found. Setup an output file or check the debug bar.\n🍾 Site built into ./_site\n  188 files generated in 6.28 seconds\n</code></pre>\n<p>In the example, the Vento error shown twice because it occurred on two pages.\nThere are some WARN errors at the start and others at the end, and a long list\nof generated pages in the middle.</p>\n<p>In order to make the logs clearer, two changes were introduced in this version\nof Lume:</p>\n<ul>\n<li>Messages of type WARN, ERROR, and FATAL are shown at the end, sorted by\nseverity (WARN first, FATAL last). Other levels (TRACE, DEBUG, and INFO) are\nstill shown as they were produced.</li>\n<li>Duplicated logs are removed.</li>\n</ul>\n<p>The example above is shown as follows in the new version:</p>\n<pre><code>...\n🔥 /img/http-imports.svg &lt;- /img/http-imports.svg\n🔥 /init.ts &lt;- /static/init.ts\n🔥 /img/gradient.png &lt;- /img/gradient.png\n🔥 /img/zero-runtime.svg &lt;- /img/zero-runtime.svg\n🔥 /logo.png &lt;- /static/logo.png\n🍾 Site built into ./_site\n  188 files generated in 6.28 seconds\nWARN [validate_html plugin] 512 HTML error(s) found. Setup an output file or check the debug bar.\nWARN [seo plugin] 45 SEO error(s) found. Setup an output file or check the debug bar.\nWARN [esbuild plugin] No TS, JS, TSX, JSX files found. Use site.add() to add files. For example: site.add(&quot;script.js&quot;)\nERROR SourceError: Unclosed tag\n/_includes/templates/blocks.vto:4:3\n 1 | {{ for block of blocks }}\n 2 |   {{ if !block.hide }}\n 3 |     {{ await comp[block.type]({ block, lang, url }) }}\n 4 |   {{ /if }\n   |   ^ Unclosed tag\n</code></pre>\n<p>This hopefully ensures that you won't miss anything important during the build\nprocess!</p>\n<h2 id=\"other-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-2-0/#other-changes\" class=\"header-anchor\">Other changes</a></h2>\n<p>This version also includes some minor changes and several bugfixes. Some of\nthem:</p>\n<ul>\n<li><code>katex</code> plugin supports <code>mhchem</code> extension and includes an option to disable\nthe download of CSS and fonts.</li>\n<li>The <code>date</code> filter registered by <code>date</code> plugin detects the language of the\ncurrent page.</li>\n<li>Some improvements to the LumeCMS integration.</li>\n<li>If you have a <code>script.ts</code> file, it no longer conflicts with the <code>script.js</code>\nfile generated by components.</li>\n<li>Fix globbing on npm/gh specifiers.</li>\n<li>And many more changes that you can see in the\n<a href=\"https://github.com/lumeland/lume/blob/v3.2.0/CHANGELOG.md\">CHANGELOG.md file</a>.</li>\n</ul>\n<p>Finally, I'd like to thank all contributors for helping make Lume so great with\nPR or supporting the project with sponsoring and donations. 🫶</p>\n","date_published":"Mon, 09 Feb 2026 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/new-logo/","url":"https://lume.land/blog/posts/new-logo/","title":"Lume has a new logo!","content_html":"<p>Lume was born 5 years ago as an excuse to try Deno, the new JavaScript runtime.\nAt that moment, the hype around Deno led to many projects featuring a dinosaur\nin their logo. Lume joined this trend, making a joke with the concept of <em>fire</em>\nand <em>Deno</em>.</p>\n<!-- more -->\n<p><img src=\"https://lume.land/uploads/lume-old.png\" alt=\"Image\"></p>\n<p>I've wanted to change the logo for a long time, and finally, I could manage some\ntime to work on that. There are many reasons for this change:</p>\n<ul>\n<li>The Lume logo is based on <a href=\"https://deno.com/artwork\">the original logo</a> of\nDeno, which has changed twice since then.</li>\n<li>Some people think Lume is a Deno product.</li>\n<li>The logo doesn't work for small sizes.</li>\n</ul>\n<h2 id=\"the-vagalume\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/new-logo/#the-vagalume\" class=\"header-anchor\">The <em>vagalume</em></a></h2>\n<p>For the new logo, I wanted to focus on the main concept of Lume: <strong>the fire</strong>. I\ndidn't want to create yet another logo with a flame. Instead, I still wanted a\npet, but a firefly instead of a dinosaur.</p>\n<p>Interestingly enough, in Galician, a firefly is called <em>vagalume</em>, which makes\nsense for a project called Lume.</p>\n<h2 id=\"logo-design\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/new-logo/#logo-design\" class=\"header-anchor\">Logo design</a></h2>\n<p>The new logo is more geometric, created with a leaf shape repeated five times,\nand a head with two antennae. This simple design makes it work better at small\nsizes.</p>\n<p><img src=\"https://lume.land/uploads/logo-outline.png\" alt=\"Image\"></p>\n<p>The red color is used to represent the fire that emits light (using a gradient).\nIt also works in dark and light contexts, just inverting the black and white\ncolors:</p>\n<p><img src=\"https://lume.land/uploads/logo-negativo.png\" alt=\"Image\"> <img src=\"https://lume.land/uploads/logo-positivo.png\" alt=\"Image\"></p>\n<p>The font family used didn't change. It's still\n<a href=\"https://fonts.google.com/specimen/Epilogue?preview.text=Lume\">Epilogue</a>, a\nbeautiful sans-serif font with a great x-height. I only made some kerning tweaks\nand the name is now &quot;Lume&quot; (with upper case L) instead of &quot;lume&quot;.</p>\n<p>Thanks to <a href=\"https://jlantunez.com/\">José Luis Antúnez</a> for the feedback and\nsuggestions, and to Studio Ghibli for the film\n<a href=\"https://en.wikipedia.org/wiki/Grave_of_the_Fireflies\">Grave of the Fireflies</a>\nfrom which I got some inspiration.</p>\n","date_published":"Wed, 05 Nov 2025 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/lume-3-1-0/","url":"https://lume.land/blog/posts/lume-3-1-0/","title":"Lume 3.1.0 - Alexandre Bóveda","content_html":"<p>The first minor version of Lume 3 is dedicated to\n<a href=\"https://en.wikipedia.org/wiki/Alexandre_B%C3%B3veda\"><strong>Alexandre Bóveda</strong></a>, a\nfinancial officer and politician who was executed on 17 August 1936 by Franco's\ndictatorship because of his Galician ideals. The night before his death, he\nwrote three farewell letters, the last one to his brother:</p>\n<blockquote>\n<p>[...] I will die peacefully; I trust that I will be received where we all want\nto get together, and I do it with joy and entrust to God this sacrifice. I\nwanted to do good, I worked for Pontevedra, for Galicia, and for the Republic,\nand the flawed judgment of men (which I forgive and you all must forgive)\ncondemns me.</p>\n<p>Be more of a man now than ever because this is when you should be the most,\nfor our elderly and for the children, to whom, without expecting it, you are\ngoing to be a little father. Comfort them all and try to always be good. Don't\nregret how much good you have done and can still do. [...]</p>\n</blockquote>\n<p>Alexandre is not only a Galician martyr but also a demonstration that there have\nalways been good people in the world. Now, more than ever, we have to remember\nthat.</p>\n<!-- more -->\n<h2 id=\"lume-is-moving-to-jsdelivr\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#lume-is-moving-to-jsdelivr\" class=\"header-anchor\">Lume is moving to jsDelivr</a></h2>\n<p>For the past 5 years, Lume's main distribution channel has been\n<a href=\"https://deno.land/x\">deno.land/x</a>, the CDN created by Deno to distribute\npackages using HTTP imports. This package registry has been deprecated in favor\nof <a href=\"https://jsr.io/\">JSR</a>, a new registry that doesn't support HTTP. Since Deno\ndoesn't want people to use <code>deno.land/x</code> (as indicated in the two\n<a href=\"https://deno.com/add_module\">big yellow banners in the page to add new modules</a>)\nand migration to JSR is not an option, we decided to switch to a different CDN.</p>\n<p><strong>jsDelivr</strong> is the obvious choice for many reasons:</p>\n<ul>\n<li>It's supported by Deno\n<a href=\"https://docs.deno.com/runtime/fundamentals/security/#importing-from-the-web\">without any configuration</a>.</li>\n<li>It's already used to\n<a href=\"https://cdn.jsdelivr.net/gh/lumeland/cms/\">publish LumeCMS</a>, deliver the\ndevelopment versions of Lume, and fetch some assets like icons or CSS code,\nneeded by some plugins.</li>\n<li>It has a nice\n<a href=\"https://www.jsdelivr.com/package/gh/lumeland/lume\">landing page for each package</a>,\nwhere you can see all the files, and statistics, something not possible in\ndeno.land/x. We can even see statistics per file, which lets us know the\nplugins most frequently used.</li>\n<li>The traffic is balanced by different CDN sponsors like Cloudflare, Fastly,\nBunny.net, etc, which ensure performance and reduce risks of relying on a\nsingle CDN.</li>\n<li>All content is permanently cached to ensure reliability. Even if the files get\ndeleted from GitHub, they will continue to work on jsDelivr without breaking\nanything.</li>\n</ul>\n<p>Lume will still be published on <code>deno.land/x</code>; the only difference is that the\nscript to update or initialize a new Lume project will choose jsDelivr by\ndefault. That's one of the many benefits of using HTTP imports: its\ndecentralized nature allows you to change the CDN at any moment with zero\nimpact.</p>\n<h2 id=\"improved-deno.json-file\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#improved-deno.json-file\" class=\"header-anchor\">Improved <code>deno.json</code> file</a></h2>\n<p>Deno added some great improvements to <code>deno.json</code> in recent versions that Lume\nhas adopted for the init and upgrade script:</p>\n<h3 id=\"lume-task-uses-bare-specifier\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#lume-task-uses-bare-specifier\" class=\"header-anchor\"><code>lume</code> task uses bare specifier</a></h3>\n<p>Deno 2.4 added\n<a href=\"https://deno.com/blog/v2.4#deno-run-bare-specifiers\">support for bare specifiers in <code>deno run</code></a>.\nIn older versions, the only way to use a specifier defined in the import map was\nusing <code>deno eval</code> or the ugly:</p>\n<pre><code class=\"language-json\">{\n  &quot;tasks&quot;: {\n    &quot;lume&quot;: &quot;echo \\&quot;import 'lume/cli.ts'\\&quot; | deno run -A -&quot;\n  }\n}\n</code></pre>\n<p>But now, this is supported:</p>\n<pre><code class=\"language-json\">{\n  &quot;tasks&quot;: {\n    &quot;lume&quot;: &quot;deno run -A lume/cli.ts&quot;\n  }\n}\n</code></pre>\n<h3 id=\"lume-tasks-have-descriptions\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#lume-tasks-have-descriptions\" class=\"header-anchor\"><code>lume</code> tasks have descriptions</a></h3>\n<p>To improve the UX, the tasks created by Lume include a description. Run\n<code>deno task</code> to see all available tasks with the description:</p>\n<pre><code class=\"language-json\">{\n  &quot;tasks&quot;: {\n    &quot;lume&quot;: {\n      &quot;description&quot;: &quot;Run Lume command&quot;,\n      &quot;command&quot;: &quot;deno run -A lume/cli.ts&quot;\n    },\n    &quot;build&quot;: {\n      &quot;description&quot;: &quot;Build the site for production&quot;,\n      &quot;command&quot;: &quot;deno task lume&quot;\n    },\n    &quot;serve&quot;: {\n      &quot;description&quot;: &quot;Run and serve the site for development&quot;,\n      &quot;command&quot;: &quot;deno task lume -s&quot;\n    }\n  }\n}\n</code></pre>\n<h3 id=\"permissions\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#permissions\" class=\"header-anchor\">Permissions</a></h3>\n<p>Deno 2.5\n<a href=\"https://deno.com/blog/v2.5\">allows to configure permissions in the <code>deno.json</code> file</a>,\nand Lume adopted this nice feature. Now you have the <code>lume</code> permission preset\nthat is used when running the <code>lume</code> task (previously, it used the <code>-A</code> flag,\nwhich disables the permissions). This will improve the security of your builds\nand will make it easy to edit the permissions to adapt to your needs. This is\nthe default configuration:</p>\n<pre><code class=\"language-json\">{\n  &quot;permissions&quot;: {\n    &quot;lume&quot;: {\n      &quot;read&quot;: true,\n      &quot;write&quot;: [\n        &quot;./&quot;\n      ],\n      &quot;import&quot;: [\n        &quot;cdn.jsdelivr.net:443&quot;,\n        &quot;jsr.io:443&quot;,\n        &quot;deno.land:443&quot;,\n        &quot;esm.sh:443&quot;\n      ],\n      &quot;net&quot;: [\n        &quot;0.0.0.0&quot;,\n        &quot;jsr.io:443&quot;,\n        &quot;cdn.jsdelivr.net:443&quot;,\n        &quot;data.jsdelivr.com:443&quot;,\n        &quot;registry.npmjs.org:443&quot;\n      ],\n      &quot;env&quot;: true,\n      &quot;run&quot;: true,\n      &quot;ffi&quot;: true,\n      &quot;sys&quot;: true\n    }\n  }\n}\n</code></pre>\n<p>As you can see, Deno only has writing permissions for the current folder, net\npermissions to localhost (to run the local server), and net and import\npermissions for JSR, NPM, esm.sh and JsDelivr (to import dependencies). The\nother permissions are granted by default because they are needed for some\nplugins, but you can edit this configuration to make it more restrictive or\nrelaxed.</p>\n<h2 id=\"better-integration-with-lumecms\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#better-integration-with-lumecms\" class=\"header-anchor\">Better integration with LumeCMS</a></h2>\n<p>One of the main challenges with LumeCMS has been integrating it smoothly with\nLume or other static site generators. Previously, LumeCMS relied on <strong>Hono</strong> to\nrun both the CMS and the page preview, which could lead to inconsistencies: a\npage served by Lume (<code>deno task serve</code>) might differ from one served by LumeCMS,\nsince Hono's static server doesn't exactly match Lume's. For example,\nmiddlewares configured for Lume are not available in the CMS, and this affects\nfeatures like live reload, debugbar, etc.</p>\n<h3 id=\"moved-to-middleware\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#moved-to-middleware\" class=\"header-anchor\">Moved to middleware</a></h3>\n<p>With version 0.13, LumeCMS introduces\n<a href=\"https://lume.land/blog/lume-cms-0-13/\">significant changes</a>. Now, LumeCMS <strong>is\na middleware on top of Lume's server</strong>, handling only requests that start with\n<code>/admin/*</code> while delegating page previews to Lume's server. This makes the\nintegration easier, eliminating the need for separate commands\n(<code>deno task serve</code> and <code>deno task cms</code>).</p>\n<p>This means that <strong>the <code>cms</code> task was removed</strong>. Just run <code>deno task serve</code> and,\nif the <code>_cms.ts</code> file is present, the CMS is automatically initialized.</p>\n<h3 id=\"improved-vps-configuration\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#improved-vps-configuration\" class=\"header-anchor\">Improved VPS configuration</a></h3>\n<p>When LumeCMS is running on a VPS, it requires two processes:</p>\n<ul>\n<li>The main process is an HTTP server that is always listening.</li>\n<li>The secondary process runs Lume and LumeCMS.</li>\n</ul>\n<p>The main process starts the secondary process on demand and works as a reverse\nproxy. This allows restarting Lume and LumeCMS after some changes (for example,\nwhen updating the changes with <code>git pull</code> or after changing the git branch)\nwithout closing the server.</p>\n<p>Until now, you needed to use\n<a href=\"https://deno.land/x/lume_cms_adapter\">an external package</a> to setup it. Now\nit's much easier since Lume's main repo includes a module to run the CMS in\nproduction: You only need to run <code>deno serve -A lume/serve.ts</code> and that's all!!</p>\n<h2 id=\"new-plugin%3A-validate_html\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#new-plugin%3A-validate_html\" class=\"header-anchor\">New plugin: <code>validate_html</code></a></h2>\n<p>This plugin checks the HTML code of your site and validates it using\n<a href=\"https://html-validate.org/\">html-validate</a>, a fast NPM package that works\noffline.</p>\n<p>The plugin was originally\n<a href=\"https://git.pyrox.dev/pyrox/new-blog/src/commit/af1de59ce89084064e2973e0d8d3e095e1b2534a/plugins/validateHTML.ts\">created by dish</a>\nand now it's an official Lume plugin, integrated with the Debug bar and with\ndifferent options to export the report.</p>\n<p>The easiest way to use it is to import it into the <code>_config.ts</code> file:</p>\n<pre><code class=\"language-ts\">import lume from &quot;lume/mod.ts&quot;;\nimport validateHtml from &quot;lume/plugins/validate_html.ts&quot;;\n\nconst site = lume();\nsite.use(validateHtml());\n\nexport default site;\n</code></pre>\n<p>Now you will see a new tab in the Debug bar with all HTML errors detected in the\nsite:</p>\n<p><img src=\"https://lume.land/uploads/debugbar_validate_html.png\" alt=\"Image\"></p>\n<p>I hope this plugin will help you to create more standard and bug-free websites.</p>\n<h2 id=\"new-plugin%3A-partytown\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#new-plugin%3A-partytown\" class=\"header-anchor\">New plugin: <code>partytown</code></a></h2>\n<p><a href=\"https://partytown.qwik.dev/\">Partytown</a> is a JavaScript library to run\nthird-party scripts in a web worker. The goal is to dedicate the main thread to\nyour code, and move other resource-intensive third-party scripts, like analytics\nor tracking services to a different thread, making the website faster and more\nsecure.</p>\n<p>The plugin was created originally\n<a href=\"https://github.com/lumeland/experimental-plugins/pull/21\">by kwaa 2 years ago</a>\nas an experimental plugin, and now it has moved to the main repo. To use it, you\nhave to register it in the _config.ts file:</p>\n<pre><code class=\"language-ts\">import lume from &quot;lume/mod.ts&quot;;\nimport partytown from &quot;lume/plugins/partytown.ts&quot;;\n\nconst site = lume();\nsite.use(partytown());\n\nexport default site;\n</code></pre>\n<p>And now add the <code>type=&quot;text/partytown&quot;</code> attribute to all scripts that you want\nto run from the web worker:</p>\n<pre><code class=\"language-html\">&lt;script type=&quot;text/partytown&quot;&gt;...&lt;/script&gt;\n</code></pre>\n<h2 id=\"new-plugin%3A-seo\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#new-plugin%3A-seo\" class=\"header-anchor\">New plugin: <code>seo</code></a></h2>\n<p>This plugin was created by <a href=\"https://github.com/timthepost/\">Tim Post</a> with the\nhelp of <a href=\"https://github.com/RickCogley/\">Rick Cogley</a> for the Japanese language\nsupport (thanks so much, guys!). It's a really interesting plugin that not only\ncan check the SEO basics (titles, descriptions, alt text in images, etc) but\nalso other not very common checks like common words percentage.</p>\n<p>Since Tim couldn't maintain it, we decided to port it to the Lume repo and\nconvert it to an &quot;official plugin&quot;. It was modified in order to align with the\nstyle and conventions of other plugins, and it was simplified a bit to make it\nmore maintainable in the long run (originally, the project was more ambitious).</p>\n<p>The installation is not different from other plugins, just import it into the\n_config.ts file:</p>\n<pre><code class=\"language-ts\">import lume from &quot;lume/mod.ts&quot;;\nimport seo from &quot;lume/plugins/seo.ts&quot;;\n\nconst site = lume();\nsite.use(seo());\n\nexport default site;\n</code></pre>\n<p>Like other validator plugins (<code>check_urls</code>, <code>validate_html</code>, etc), it creates a\nnew tab in the debug bar with all SEO issues found in all pages. It also has an\noption to export the report to a JSON file or any other format by providing a\ncustom export function.</p>\n<p>The plugin is highly customizable. The default options are enough for most\ncases, but you can change how to validate titles, H1 tags, meta descriptions,\nheading orders, duplicated titles, etc. Definitely, this plugin will help you to\ncreate more successful websites!</p>\n<h2 id=\"improved-remote-files\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#improved-remote-files\" class=\"header-anchor\">Improved remote files</a></h2>\n<p>The <a href=\"https://lume.land/docs/core/remote-files/\">function <code>remoteFile</code></a> allows\nthe use of URLs to download the content of a file if it doesn't exist locally.\nFor example:</p>\n<pre><code class=\"language-js\">site.remoteFile(&quot;/styles/styles.css&quot;, &quot;https://example.com/styles/styles.css&quot;);\n</code></pre>\n<p>If the file <code>/styles/styles.css</code> doesn't exist locally, the content of the URL\nwill be used in place. This is very useful for themes because it allows for\nplacing all templates and styles used by a theme remotely.</p>\n<p>The only problem with this function is that it only works for single files. If\nyou have several files, you need to call the function once per file:</p>\n<pre><code class=\"language-js\">const files = [\n  &quot;styles.css&quot;,\n  &quot;components/button.css&quot;,\n  &quot;components/alert.css&quot;,\n  &quot;components/icons.css&quot;,\n];\n\nfor (const file of files) {\n  site.remoteFile(&quot;/styles/&quot; + file, &quot;https://example.com/styles/&quot; + file);\n}\n</code></pre>\n<h3 id=\"new-site.remote()-function\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#new-site.remote()-function\" class=\"header-anchor\">New <code>site.remote()</code> function</a></h3>\n<p>Lume 3.1 includes the new function <code>site.remote()</code> similar to\n<code>site.remoteFile()</code> but allows to register more than one file:</p>\n<pre><code class=\"language-js\">const files = [\n  &quot;styles.css&quot;,\n  &quot;components/button.css&quot;,\n  &quot;components/alert.css&quot;,\n  &quot;components/icons.css&quot;,\n];\n\nsite.remote(&quot;/styles/&quot;, &quot;https://example.com/styles/&quot;, files);\n</code></pre>\n<p>Okay, you may think this isn't a big improvement, it's just a bit simpler. But\nthe good news is that you can use some specifiers that are compatible with glob\npatterns:</p>\n<ul>\n<li><code>npm:</code> to use NPM packages (like <code>npm:lucide-static@0.544.0</code>)</li>\n<li><code>gh:</code> to use GitHub repositories (like <code>gh:lumeland/theme-simple-wiki</code>)</li>\n<li><code>file:</code> to use local files</li>\n<li>All URLs starting with <code>https://cdn.jsdelivr.net/</code>.</li>\n</ul>\n<p>For example, let's say the CSS files are stored in a GitHub repository:</p>\n<pre><code class=\"language-js\">const files = [\n  &quot;/styles/**/*.css&quot;,\n];\n\nsite.remote(&quot;/styles/&quot;, &quot;gh:username/repo@tag&quot;, files);\n</code></pre>\n<p>That's better now! Under the hood, this is possible thanks to the API of\nJsDelivr. Any compatible specifier is converted to JsDelivr equivalent.</p>\n<p>For example, <code>gh:lumeland/theme-simple-wiki@0.14.3</code> is converted to\n<a href=\"https://cdn.jsdelivr.net/gh/lumeland/theme-simple-wiki@0.14.3/\">https://cdn.jsdelivr.net/gh/lumeland/theme-simple-wiki@0.14.3/</a>.\nAnd using the JsDelivr API, we can know the paths of all files in the repository\n(<a href=\"ttps://data.jsdelivr.com/v1/package/gh/lumeland/theme-simple-wiki@v0.14.3?structure=flat\">example</a>),\nso we can filter them using the glob pattern.</p>\n<h3 id=\"create-themes-is-now-easier\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#create-themes-is-now-easier\" class=\"header-anchor\">Create themes is now easier</a></h3>\n<p>Thanks to this new function, it's easier to create Lume themes, since you don't\nhave to worry about forgetting to include a new file that was recently created.\nLet's see this example:</p>\n<pre><code class=\"language-js\">const themeFiles = [\n  &quot;/_includes/**&quot;,\n  &quot;/*.css&quot;,\n  &quot;/*.js&quot;,\n  &quot;/*.vto&quot;,\n];\n\nsite.remote(&quot;/&quot;, import.meta.resolve(&quot;./&quot;), themeFiles);\n</code></pre>\n<p>In local development, <code>import.meta.resolve(&quot;./&quot;)</code> resolves to a <code>file://...</code>\nurl, so the glob patterns work great. If this package is published and imported\nfrom JsDelivr, it's resolved to <code>https://cdn.jsdelivr.net/gh/user/repo@tag</code>, and\nthe glob patterns are still supported thanks to the JsDelivr API. This is\nanother reason to recommend distributing Deno packages on JsDelivr over\ndeno.land/x.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p>For backward compatibility, the function <code>site.remoteFile</code> is still available\nas an alias of <code>site.remote</code>, for example, <code>site.remoteFile(local, remote)</code> is\nan alias of <code>site.remote(local, remote)</code>.</p>\n</div>\n<h2 id=\"inline-plugin-works-with-css-files\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#inline-plugin-works-with-css-files\" class=\"header-anchor\">Inline plugin works with CSS files</a></h2>\n<p>As of Lume 3.1, the <a href=\"https://lume.land/plugins/inline/\">inline</a> plugin not only\nprocesses HTML files but also CSS. This allows for inlining background images\neasily. To use it, just append the <code>?inline</code> parameter to the file URL. For\nexample:</p>\n<pre><code class=\"language-css\">.warning {\n  background-image: url(&quot;/icons/warning.svg?inline&quot;);\n}\n</code></pre>\n<p>Is converted to:</p>\n<pre><code class=\"language-css\">.warning {\n  background-image: url(&quot;data:image/svg+xml;utf8,&lt;svg...&lt;/svg&gt;&quot;);\n}\n</code></pre>\n<h2 id=\"picture-plugin-allows-to-crop-images\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3-1-0/#picture-plugin-allows-to-crop-images\" class=\"header-anchor\">Picture plugin allows to crop images</a></h2>\n<p>Until now, the <a href=\"https://lume.land/plugins/picture/\">picture</a> plugin has only\nallowed the width value to resize images. For example, this configuration\ntransforms the image to 300px and 600px, with versions for 2x resolutions and\nformats AVIF, WebP, and JPG:</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;/flowers.jpg&quot; transform-images=&quot;avif webp jpg 300@2 600@2&quot;&gt;\n</code></pre>\n<p>Now it's possible to specify a height to crop the images to a specific aspect\nratio:</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;/flowers.jpg&quot; transform-images=&quot;avif webp jpg 300x150@2 600x300@2&quot;&gt;\n</code></pre>\n<p>The image will be cropped to 300x150 and 600x300, with a version for 2x\nresolutions. For now, there isn't any way to configure the origin of the crop\n(it's always centered), but this option can be added in future versions.</p>\n<hr>\n<p>See\n<a href=\"https://github.com/lumeland/lume/blob/v3.1.0/CHANGELOG.md\">the CHANGELOG.md file</a>\nto see a complete list of all changes.</p>\n","date_published":"Fri, 17 Oct 2025 00:00:00 GMT"},{"id":"https://lume.land/blog/lume-cms-0-13/","url":"https://lume.land/blog/lume-cms-0-13/","title":"Lume CMS 0.13.0","content_html":"<p>It's been a while (one and a half year!) since\n<a href=\"https://lume.land/posts/lume-cms/\">LumeCMS was announced</a> as an alternative to other existing\nCMS to edit the content of websites.</p>\n<p>During this time, the project was improved in many ways: more field formats,\nmore customization options, light/dark mode, etc.</p>\n<p>But, like many projects in their earlier phases, LumeCMS was a bit &quot;tricky&quot; to\nrun. It was created as a framework-agnostic solution, but in practice, it wasn't\neasy to set up for other frameworks than Lume.</p>\n<p>Version 0.13 has received a lot of changes in order to address this issue, among\nothers. Some of these changes are BREAKING CHANGES (hopefully they are only a\nfew). And even though it's still a development version (the version starts with\n<code>v0.*</code> yet), I think it's an important step towards the future v1.0 version.</p>\n<!-- more -->\n<h2 id=\"new-router\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#new-router\" class=\"header-anchor\">New router</a></h2>\n<p>Since the beginning, LumeCMS has used <a href=\"https://hono.dev/\">Hono</a> under the hood.\nAlthough Hono is a very powerful and popular framework, I found it a bit\ncomplicated to work with. The way to pass data (or contexts) to routers and\nmiddlewares, the rendering system or the use of the custom class <code>HonoRequest</code>\nfor the request instead of the standard <code>Request</code> (that is also available but\nhidden) made me spend more time trying to figure out how to do something in\n&quot;Hono way&quot; than just doing it. In addition to that, it's becoming a\nfull-featured framework with even a client-side JSX library.</p>\n<p>That's why <a href=\"https://github.com/oscarotero/galo\">Galo was created</a>. It's a fast\nand minimalist router that embraces web standards and simplicity without\nsacrificing flexibility. The change from a framework approach to a library like\nthis made LumeCMS much easier to embed in any application running Deno. In fact,\nLumeCMS is now a middleware for your application without any side effects or\ninterference with your existing code.</p>\n<p><img src=\"https://lume.land/uploads/lumecms-galo.png\" alt=\"Image\"></p>\n<h2 id=\"document-types\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#document-types\" class=\"header-anchor\">Document types</a></h2>\n<p>LumeCMS was created assuming that all data would be stored in an object. Let's\nsee this example:</p>\n<pre><code class=\"language-js\">cms.collection({\n  name: &quot;notes&quot;,\n  storage: &quot;src:notes/*.json&quot;,\n  fields: [\n    &quot;title: text&quot;,\n    &quot;text: textarea&quot;,\n  ],\n});\n</code></pre>\n<p>This stores every note in a JSON file in the <code>notes/</code> directory. The stored\nnotes have a structure like this:</p>\n<pre><code class=\"language-json\">{\n  &quot;title&quot;: &quot;Note title&quot;,\n  &quot;text&quot;: &quot;This is the note&quot;\n}\n</code></pre>\n<p>If you want to store all notes in a single JSON file, you can use an\n<code>object-list</code> field to store an array of objects:</p>\n<pre><code class=\"language-js\">cms.document({\n  name: &quot;notes&quot;,\n  storage: &quot;src:notes.json&quot;,\n  fields: [\n    {\n      name: &quot;notes&quot;,\n      type: &quot;object-list&quot;,\n      fields: [\n        &quot;title: text&quot;,\n        &quot;text: textarea&quot;,\n      ],\n    },\n  ],\n});\n</code></pre>\n<p>This configuration produces the following data structure:</p>\n<pre><code class=\"language-json\">{\n  &quot;notes&quot;: [\n    {\n      &quot;title&quot;: &quot;First note&quot;,\n      &quot;text&quot;: &quot;Text of the first note&quot;\n    },\n    {\n      &quot;title&quot;: &quot;Second note&quot;,\n      &quot;text&quot;: &quot;Text of the second note&quot;\n    }\n  ]\n}\n</code></pre>\n<p>As you can see, the root of the data is still an object with the <code>notes</code>\nproperty to hold the array of notes. But what we really want is to store the\narray of notes <strong>as the root element</strong>. Until now, the solution was to change\nthe name of the root value from <code>notes</code> to <code>[]</code>:</p>\n<pre><code class=\"language-js\">cms.document({\n  name: &quot;notes&quot;,\n  storage: &quot;src:notes.json&quot;,\n  fields: [\n    {\n      name: &quot;[]&quot;,\n      type: &quot;object-list&quot;,\n      fields: [\n        &quot;title: text&quot;,\n        &quot;text: textarea&quot;,\n      ],\n    },\n  ],\n});\n</code></pre>\n<p>LumeCMS detected the special name &quot;[]&quot; as an instruction to ignore the element\nand store its content directly. This allows us to store the data as an array:</p>\n<pre><code class=\"language-json\">[\n  {\n    &quot;title&quot;: &quot;First note&quot;,\n    &quot;text&quot;: &quot;Text of the first note&quot;\n  },\n  {\n    &quot;title&quot;: &quot;Second note&quot;,\n    &quot;text&quot;: &quot;Text of the second note&quot;\n  }\n]\n</code></pre>\n<p>The problem with this solution is that it's a bit hacky, verbose, and not very\nflexible. That's why in version 0.13 this feature was replaced with the new\n<code>type</code> option:</p>\n<pre><code class=\"language-js\">cms.document({\n  name: &quot;notes&quot;,\n  storage: &quot;src:notes.json&quot;,\n  type: &quot;object-list&quot;,\n  fields: [\n    &quot;title: text&quot;,\n    &quot;text: textarea&quot;,\n  ],\n});\n</code></pre>\n<p>As you may guess, this option configures the field type used to store the root\ndata. If it's not defined, the default value is <code>object</code>, but other available\nvalues are <code>object-list</code> (to store an array of objects) and <code>choose</code> (to allow\nthe user to choose one structure among a list of options). More types can be\nadded in future versions.</p>\n<h2 id=\"new-previewurl-and-sourcepath-options\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#new-previewurl-and-sourcepath-options\" class=\"header-anchor\">New <code>previewUrl</code> and <code>sourcePath</code> options</a></h2>\n<p>One of the great features of LumeCMS is the ability to preview the changes while\nediting the data. To provide this, we need two things:</p>\n<ul>\n<li>A way to know the URL generated by a file. For example, if we know that the\nfile <code>/posts/hello-world.md</code> produces the URL <code>/posts/hello-world/</code>, we can\ndisplay this URL in the preview panel when the file is being edited.</li>\n<li>A way to know the source file of a URL. If we know that the URL\n<code>/posts/hello-world/</code> is generated by <code>/posts/hello-world.md</code>, we can create a\n&quot;Edit this page&quot; link to go directly to the edit form.</li>\n</ul>\n<p>Until now, the way to get this info was a bit obscure and undocumented. In\nversion 0.13, this is fully configurable, which makes the CMS easier to adapt\nfor other static site generators:</p>\n<pre><code class=\"language-js\">const cms = lumeCMS({\n  previewUrl(path: string, content: Lume.CMS.Content, changed: boolean) {\n    // Return the URL generated by this file\n },\n  sourcePath(url: string, content: Lume.CMS.Content) {\n    // Return the file path that generates this URL\n }\n});\n</code></pre>\n<p>The <code>previewUrl</code> is also customizable at the document or collection level,\nuseful if you're editing a file that doesn't directly produce a URL but can\naffect it (like a <code>_data</code> file):</p>\n<pre><code class=\"language-js\">cms.document({\n  name: &quot;Common data&quot;,\n  storage: &quot;src:_data.yml&quot;,\n  previewUrl: () =&gt; &quot;/&quot;, // preview the homepage\n  fields: [\n    &quot;title: text&quot;,\n    &quot;description: textarea&quot;,\n  ],\n});\n</code></pre>\n<h2 id=\"user-level-permissions\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#user-level-permissions\" class=\"header-anchor\">User-level permissions</a></h2>\n<p>In previous versions, you could configure the permissions to create, edit,\nrename, or delete documents globally. For example, let's say we have a\ncollection of countries that we don't want to remove or create new ones, just\nedit them:</p>\n<pre><code class=\"language-js\">cms.collection({\n  name: &quot;countries&quot;,\n  storage: &quot;src:countries/*.json&quot;,\n  fields: [\n    &quot;name: text&quot;,\n    &quot;description: textarea&quot;,\n  ],\n  create: false,\n  delete: false,\n  rename: false,\n});\n</code></pre>\n<p>With this configuration, all users can edit the countries, but cannot create,\ndelete, or rename files. In version 0.13.0, we can override this configuration\nfor some users:</p>\n<pre><code class=\"language-js\">cms.auth({\n  user1: {\n    password: &quot;password1&quot;,\n    name: &quot;Admin&quot;,\n    permissions: {\n      &quot;countries&quot;: {\n        create: true,\n        delete: true,\n        rename: true,\n      },\n    },\n  },\n  user2: &quot;password2&quot;,\n});\n</code></pre>\n<p>In previous versions, the auth configuration was simply an object with names and\npasswords. Now, we can also use an object to include more options. In this\nexample, the &quot;user1&quot; has a password, the name &quot;Admin&quot; (which is used to show it\nin the interface instead of &quot;user1&quot;), and some special permissions that override\nthe permissions assigned to documents and collections: this user can create,\nrename, and delete files of the countries collection, unlike &quot;user2&quot;.</p>\n<h2 id=\"improved-documentlabel\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#improved-documentlabel\" class=\"header-anchor\">Improved <code>documentLabel</code></a></h2>\n<p>If you see the list of documents in a collection, LumeCMS only displays the file\nnames. Since it doesn't load the documents data, it can't use any value inside\nthem (like <code>title</code> or <code>date</code> properties). The option <code>documentLabel</code> allows\ncustomization of how this document is shown in the interface. For example, it\ncan transform the name <code>my-first-post.md</code> to a more human <code>My First Post</code>. In\nfact, LumeCMS comes with this especific behavior by default, but you can\ncustomize it with your own tranformer:</p>\n<pre><code class=\"language-js\">cms.collection({\n  documentLabel: (filename) =&gt; filename.replace(&quot;.json&quot;, &quot;&quot;),\n  // More options...\n});\n</code></pre>\n<p>As of version 0.13, this function can return an object with the properties\n<code>label</code>, <code>icon</code>, and <code>flags</code> to extract and show more info in the list view.</p>\n<p>Let's say our collection of countries is a folder with the following files:</p>\n<pre><code>/en-spain.json\n/pt-portugal.json\n/fr-france.json\n/it-italy.json\n</code></pre>\n<p>We can configure the collection to show only the country name, and use the flag\nicon (from <a href=\"https://phosphoricons.com/?q=flag\">Phosphor</a>) instead of the default\ndocument icon. The country code is the code is saved in the flags object:</p>\n<pre><code class=\"language-js\">cms.collection({\n  documentLabel: (filename) =&gt; {\n    const [code, name] = filename.replace(&quot;.json&quot;, &quot;&quot;).split(&quot;-&quot;);\n    return {\n      label: name,\n      icon: &quot;flag&quot;\n      flags: { code }\n    }\n  },\n  // ...more options\n});\n</code></pre>\n<h2 id=\"new-relation-and-relation-list-fields\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#new-relation-and-relation-list-fields\" class=\"header-anchor\">New <code>relation</code> and <code>relation-list</code> fields</a></h2>\n<p>This feature is related to the improvements on <code>documentLabel</code> explained above.\nNow that we can extract more info from the filename, we can use this info to\nlink a document to another. For example, let's say we have the &quot;people&quot;\ncollection and we want to assign a country to each person:</p>\n<pre><code class=\"language-js\">cms.collection({\n  name: &quot;people&quot;,\n  storage: &quot;src:people/*.md&quot;,\n  fields: [\n    &quot;name: text&quot;,\n    {\n      name: &quot;country&quot;,\n      type: &quot;relation&quot;,\n\n      // The collection name that we want to relate\n      collection: &quot;countries&quot;,\n\n      // A function to return a label and value for each option\n      option: ({ label, flags }) =&gt; { label, value: flags.code }\n    }\n  ]\n});\n</code></pre>\n<p>Now, this field shows a selector to pick one of the countries and use the <code>code</code>\nflag as the value (<code>en</code> for Spain, <code>pt</code> for Portugal, etc). The <code>relation-list</code>\nfield is similar but allows for storing an array of values.</p>\n<h2 id=\"better-git-commits\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#better-git-commits\" class=\"header-anchor\">Better git commits</a></h2>\n<p>The git commits created by LumeCMS now include the current user name as the\nauthor. It's also possible to include the email by adding the <code>email</code> property\nto the user settings:</p>\n<pre><code class=\"language-js\">cms.auth({\n  user1: {\n    password: &quot;password1&quot;,\n\n    // name &amp; email are included in the commits created by this user.\n    name: &quot;Admin&quot;,\n    email: &quot;user@example.com&quot;,\n  },\n});\n</code></pre>\n<h2 id=\"allow-to-edit-raw-files\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#allow-to-edit-raw-files\" class=\"header-anchor\">Allow to edit raw files</a></h2>\n<p>Now it's possible to configure a document without fields, to show a code editor\ninstead of a form to edit it. It's useful to edit code directly from the CMS:</p>\n<pre><code class=\"language-js\">cms.document({\n  name: &quot;Custom styles&quot;,\n  storage: &quot;src:style.css&quot;,\n});\n</code></pre>\n<h2 id=\"removed-timezone-from-date-and-datetime-fields\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#removed-timezone-from-date-and-datetime-fields\" class=\"header-anchor\">Removed timezone from Date and Datetime fields</a></h2>\n<p>From now on, date and datetime fields don't include the timezone in the YAML\nfiles. Hopefully, it will fix a lot of problems related to unexpected dates due\nto different time zones.</p>\n<pre><code class=\"language-yml\"># before\ndate: 2025-01-11T00:00:00.000Z\n\n# after\ndate: 2025-01-11 00:00:00\n</code></pre>\n<h2 id=\"and-more-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#and-more-changes\" class=\"header-anchor\">And more changes</a></h2>\n<p>There are more changes in this version, like UI improvements, ability to show\nEXIF data from uploaded images, the new <code>cssSelector</code> option to highlight an\nelement in the previewer related with a field, etc.</p>\n<p>Take a look at the\n<a href=\"https://github.com/lumeland/cms/blob/v0.13.0/CHANGELOG.md\">CHANGELOG.md file</a>\nto see the complete list of changes.</p>\n<h2 id=\"lume-compability\" tabindex=\"-1\"><a href=\"https://lume.land/blog/lume-cms-0-13/#lume-compability\" class=\"header-anchor\">Lume compability</a></h2>\n<p>This version isn't compatible with Lume 3.0.x, but it will be in Lume 3.1.</p>\n<p>If you want to try it, upgrade Lume to the latest development version with:</p>\n<pre><code>deno task lume upgrade --dev\n</code></pre>\n<p>Then, run <code>deno task serve</code> and the CMS is automatically created if a <code>_cms.ts</code>\nfile is detected. <strong>You won't need to run <code>deno task cms</code> anymore!</strong></p>\n","date_published":"Tue, 14 Oct 2025 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/vento-2/","url":"https://lume.land/blog/posts/vento-2/","title":"Vento 2 is here!","content_html":"<p><img src=\"https://lume.land/uploads/vento-2.svg\" alt=\"Vento 2\" class=\"vento-logo\"></p>\n<p>Vento was born two years ago as an experiment to create a modern, ergonomic, and\nasync-friendly template engine for JavaScript. Initially, it was a Deno-only\nproject, intended to become the default template engine for <strong>Lume</strong>. But as\nsoon as the <a href=\"https://www.npmjs.com/package/ventojs\">NPM package was available</a>,\nother projects started to use it.</p>\n<!-- more -->\n<p>During this period, a number of people got involved in the development.</p>\n<ul>\n<li><a href=\"https://github.com/wrapperup\">wrap</a> made a wonderful work implementing and\nmaintaining a JavaScript analyzer to automatically resolve the global\nvariables (so you can type <code>{{ somevariable }}</code> instead of\n<code>{{ it.somevariable }}</code>. She's also responsible for the\n<a href=\"https://github.com/ventojs/tree-sitter-vento\">tree-sitter</a> parser to bring\nsupport for Neovim and similar editors.</li>\n<li><a href=\"https://github.com/noelforte\">Noel Forte</a> created the\n<a href=\"https://github.com/noelforte/eleventy-plugin-vento\">11ty plugin</a> that made\nVento popular in the 11ty ecosystem.</li>\n<li><a href=\"https://github.com/dz4k\">Deniz Akşimşek</a> created the\n<a href=\"https://github.com/dz4k/zed-vento\">Zed plugin</a>.</li>\n<li><a href=\"https://github.com/illyrius666\">Illyrius</a> brought\n<a href=\"https://github.com/ventojs/webstorm-vento\">support for WebStorm</a>.</li>\n</ul>\n<p>Vento wouldn't be so awesome without the help of these contributors and\n<a href=\"https://github.com/ventojs/vento/graphs/contributors\">many other</a> that improve\nit with their selfless work. <strong>THANK YOU SO MUCH!</strong></p>\n<h2 id=\"why-create-a-version-2%3F\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#why-create-a-version-2%3F\" class=\"header-anchor\">Why create a version 2?</a></h2>\n<p>Everything started with\n<a href=\"https://vrugtehagel.nl/posts/my-doubts-about-vento/\">this post</a> where\nvrugtehagel exposed some issues detected in Vento. He was kind enough to send me\nan email to let me know about the post whose constructive feedback was very\nhelpful and several issues mentioned were addressed in Vento 1.</p>\n<p>However, vrugtehagel not only limited himself to providing feedback, but he also\nstarted to get involved in the\n<a href=\"https://github.com/ventojs/sublime-vento\">Sublime Text plugin</a> and created a\nbunch of\n<a href=\"https://github.com/ventojs/vento/pulls?q=is%3Apr+is%3Aclosed+author%3Avrugtehagel\">pull requests to the Vento repository</a>,\nleading to some interesting discussions about Vento's philosophy and its\nimplementation approach. The most demanding challenge, for which we made\ndifferent proofs of concept, was to find an alternative way to analyze the\nJavaScript code without using meriyah or any other dependency. This would make\nthe compilation faster and remove all Vento dependencies.</p>\n<p>Thanks to this change, the local footprint was reduced <strong>from 1.8MB to less than\n80Kb</strong> (<strong>18KB</strong> bundled and minified).</p>\n<p>The next step was to convert Vento in an isomorphic library, which makes it to\nwork on browsers and on Node-like runtimes (Node, Deno, Bun) without changes or\nthe need for a compilation step.</p>\n<p>And finally, one of the pain points of Vento, error reporting, was also\naddressed thanks to the\n<a href=\"https://github.com/ventojs/vento/pull/131\">initial work of vrugtehagel</a> and\nsome subsequent changes by me.</p>\n<p>Vento is now a modern, lean, and powerful template engine that can be used on\nany JavaScript runtime and embedded on any framework easily.</p>\n<h2 id=\"main-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#main-changes\" class=\"header-anchor\">Main changes</a></h2>\n<p>After upgrading to Vento 2, almost everything in your .vto files should continue\nworking as usual without changes, although there might be some edge cases that\nnow have a different behavior.</p>\n<h3 id=\"new-variable-resolution\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#new-variable-resolution\" class=\"header-anchor\">New variable resolution</a></h3>\n<p>In Vento 1, when you run <code>Hello {{ name }}</code>, the compiler converts it\nautomatically to <code>Hello {{ it.name }}</code>. This means that, <strong>technically,</strong> you\ncould define a variable directly in the <code>it</code> global variable and it would be\naccessible without the prefix. For example, the following code would print\n<em>&quot;Hello World&quot;</em>:</p>\n<pre><code class=\"language-vto\">{{&gt; it.name = &quot;World&quot; }}\nHello {{ name }}\n</code></pre>\n<p>Vento 2 uses a different approach. When Vento compiles the following template:</p>\n<pre><code class=\"language-vto\">Hello {{ name }}\n</code></pre>\n<p>all variables used are initialized preventively at the begining like this:</p>\n<pre><code class=\"language-js\">var { name } = it;\n</code></pre>\n<p>The variable is not replaced with <code>it.name</code> automatically everywhere but the\nreal variable <code>name</code> is created instead. If you edit the value of <code>it.name</code> in\nyour code directly, <strong>it won't affect <code>name</code></strong>. However, this is more a Vento's\ninternals change and it's unlikely to affect to final users since they never\nshould edit the <code>it</code> variable directly, but use the code\n<code>{{ set name = &quot;other value&quot; }}</code>.</p>\n<h3 id=\"new-error-handler\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#new-error-handler\" class=\"header-anchor\">New error handler</a></h3>\n<p>Any error produced while compiling or running the templates is now converted to\na <code>VentoError</code> class. This class contains all the information required to report\nthe exact point where the error originates. For example, the following template\nproduce an error because we are invoking a function from a <code>null</code> variable:</p>\n<pre><code class=\"language-vto\">{{ set name = null }}\n{{ name.foo() }}\n</code></pre>\n<p>In order to run and pretty-print errors, you can use the <code>printError</code> helper:</p>\n<pre><code class=\"language-js\">import vento from &quot;vento/mod.ts&quot;;\nimport { printError } from &quot;vento/core/error.js&quot;;\n\nconst env = vento();\n\ntry {\n  const result = await env.run(&quot;my-template.vto&quot;);\n} catch (err) {\n  console.error(await printError(err));\n}\n</code></pre>\n<p>This outputs the following:</p>\n<pre><code>TypeError: Cannot read properties of null (reading 'foo')\ntest/main.vto:2:1\n 1 | {{ set name = null }}\n 2 | {{ name.foo() }}\n   | ^\n   | __exports.content += (name.foo()) ?? &quot;&quot;;\n   |                              ^ Cannot read properties of null (reading 'foo')\n</code></pre>\n<p>The error displays the tag where the error occurs and it can show also the error\nin the compiled code (the final JavaScript code) to give more context.</p>\n<p>Note that the error handler doesn't work consistently accross all runtimes, due\ntheir differences providing useful data from the error stack. For example, it\nworks pretty well on Deno, but Node and Bun cannot recover the exact location of\nsome errors so it's not possible to provide detailed info in some cases. There\nmay be also some differences between browsers.</p>\n<p>I hope this feature can be improved in next versions. PR are very appreciated!</p>\n<h3 id=\"removed-sync-mode\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#removed-sync-mode\" class=\"header-anchor\">Removed sync mode</a></h3>\n<p>Vento is <strong>async</strong> by default, it's one of its main selling points. In Vento 1\nthere was also the function <code>runStringSync</code> to run arbitrary code in a\nsynchronous context. For example:\n<code>env.runStringSync(&quot;Hello {{ name }}&quot;, { name: &quot;World&quot;})</code>.</p>\n<p>This mode was originally created because Lume needed it. However, the\nsynchronous mode doesn't fit well with how Vento works internally. Having both\nsync and async modes makes everything more complicated, and the sync mode breaks\nif your template has async tags, like <code>{{ include }}</code>, or runs any async\nfunction or filter.</p>\n<p>Since Lume no longer needs this feature, the function was removed in Vento 2,\nand now all templates are run consistently in an async context.</p>\n<h3 id=\"new-slot-tag\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#new-slot-tag\" class=\"header-anchor\">New <code>slot</code> tag</a></h3>\n<p>The <a href=\"https://vento.js.org/syntax/layout/\"><code>layout</code> tag</a> allows to render a\ntemplate passing extra content. This is great for layouts expecting only one\npiece of content. But if the layout requires more pieces, you have to pass them\nas variables:</p>\n<pre><code class=\"language-vto\">{{ layout &quot;article.vto&quot; { header: &quot;&lt;h1&gt;This is the title&lt;/h1&gt;&quot; } }}\n  &lt;p&gt;This is the content&lt;/p&gt;\n{{ /layout }}\n</code></pre>\n<p>The new <code>slot</code> tag allows to capture and store the content in different\nvariables, similar to what web components do.</p>\n<pre><code class=\"language-vto\">{{ layout &quot;article.vto&quot; }}\n  {{ slot header }}\n    &lt;h1&gt;This is the title&lt;/h1&gt;\n  {{ /slot }}\n\n  &lt;p&gt;This is the content&lt;/p&gt;\n{{ /layout }}\n</code></pre>\n<p>Learn more about\n<a href=\"https://vento.js.org/syntax/layout/#slots\">slots in the documentation site</a>.</p>\n<h3 id=\"browser-support\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#browser-support\" class=\"header-anchor\">Browser support</a></h3>\n<p>As said, Vento 2 works also on browsers, without any compilation step, thanks to\nnot having dependencies and the use of standard APIs. You can download the NPM\npackage or use a CDN like jsDelivr:</p>\n<pre><code class=\"language-js\">import vento from &quot;https://cdn.jsdelivr.net/npm/ventojs@2.0.0/web.js&quot;;\n\nconst env = vento({\n  includes: import.meta.resolve(&quot;./templates&quot;),\n});\n\nconst result = await env.run(&quot;main.vto&quot;);\nconsole.log(result.content);\n</code></pre>\n<p>Note that instead of importing the <code>mod.js</code> module, you have to import <code>web.js</code>.\nThe only difference is that <code>web.js</code> uses the <code>URL</code> loader by default to load\ntemplates using <code>fetch</code>. The <code>includes</code> option defines the base URL to load all\ntemplates.</p>\n<h2 id=\"benchmarks\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#benchmarks\" class=\"header-anchor\">Benchmarks</a></h2>\n<p>Vento 1 was already quite fast, but version 2 is even faster thanks to the new\ncompiler.</p>\n<h3 id=\"vento-1-vs-vento-2\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#vento-1-vs-vento-2\" class=\"header-anchor\">Vento 1 vs Vento 2</a></h3>\n<p>The compiler in Vento 2 is almost <strong>200% faster</strong> as you can see in the\nfollowing benchmark:</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Compilation</th>\n<th style=\"text-align:right\">time/iter (avg)</th>\n<th style=\"text-align:right\">iter/s</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">Vento 2</td>\n<td style=\"text-align:right\">196.0 µs</td>\n<td style=\"text-align:right\">5,102</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">Vento 1</td>\n<td style=\"text-align:right\">378.0 µs</td>\n<td style=\"text-align:right\">2,646</td>\n</tr>\n</tbody>\n</table>\n<p>When comparing the rendering performance of Vento 1 and Vento 2, there are no\nsignificant differences. Vento 1 may be slightly faster, but the difference is\nminimal and not easily noticeable.</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Rendering</th>\n<th style=\"text-align:right\">time/iter (avg)</th>\n<th style=\"text-align:right\">iter/s</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">Vento 2</td>\n<td style=\"text-align:right\">860.9 ns</td>\n<td style=\"text-align:right\">1,162,000</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">Vento 1</td>\n<td style=\"text-align:right\">820.0 ns</td>\n<td style=\"text-align:right\">1,220,000</td>\n</tr>\n</tbody>\n</table>\n<p>Comparing compilation and rendering performance, Vento 2 demonstrates <strong>a 180%\nspeed improvement</strong>. Even if a few milliseconds are lost during rendering, the\noverall performance gain more than compensates for it.</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Comp. &amp; rendering</th>\n<th style=\"text-align:right\">time/iter (avg)</th>\n<th style=\"text-align:right\">iter/s</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">Vento 2</td>\n<td style=\"text-align:right\">186.0 µs</td>\n<td style=\"text-align:right\">5,376</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">Vento 1</td>\n<td style=\"text-align:right\">342.8 µs</td>\n<td style=\"text-align:right\">2,917</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"comparison-with-other-libraries\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#comparison-with-other-libraries\" class=\"header-anchor\">Comparison with other libraries</a></h2>\n<p>To compare the performance of Vento with other template engines, you can\n<a href=\"https://github.com/ventojs/vento/tree/main/bench\">run the bench code</a> in\nVento's repository. As of writing this post, the benchmark data is:</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Compilation</th>\n<th style=\"text-align:right\">time/iter (avg)</th>\n<th style=\"text-align:right\">iter/s</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">1. Eta</td>\n<td style=\"text-align:right\">51.1 µs</td>\n<td style=\"text-align:right\">19,580</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">2. EJS</td>\n<td style=\"text-align:right\">145.2 µs</td>\n<td style=\"text-align:right\">6,888</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">3. Vento</td>\n<td style=\"text-align:right\">196.0 µs</td>\n<td style=\"text-align:right\">5,102</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">4. Nunjucks</td>\n<td style=\"text-align:right\">602.1 µs</td>\n<td style=\"text-align:right\">1,661</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">5. Liquid</td>\n<td style=\"text-align:right\">635.0 µs</td>\n<td style=\"text-align:right\">1,575</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">6. Preact</td>\n<td style=\"text-align:right\">978.4 µs</td>\n<td style=\"text-align:right\">1,022</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">7. Pug</td>\n<td style=\"text-align:right\">4.2 ms</td>\n<td style=\"text-align:right\">237</td>\n</tr>\n</tbody>\n</table>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Rendering</th>\n<th style=\"text-align:right\">time/iter (avg)</th>\n<th style=\"text-align:right\">iter/s</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">1. Vento</td>\n<td style=\"text-align:right\">860.9 ns</td>\n<td style=\"text-align:right\">1,162,000</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">2. Pug</td>\n<td style=\"text-align:right\">1.6 µs</td>\n<td style=\"text-align:right\">632,100</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">3. Preact</td>\n<td style=\"text-align:right\">8.8 µs</td>\n<td style=\"text-align:right\">113,900</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">4. Eta</td>\n<td style=\"text-align:right\">11.2 µs</td>\n<td style=\"text-align:right\">89,600</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">5. EJS</td>\n<td style=\"text-align:right\">14.8 µs</td>\n<td style=\"text-align:right\">67,790</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">6. Liquid</td>\n<td style=\"text-align:right\">351.3 µs</td>\n<td style=\"text-align:right\">2,847</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">7. Nunjucks</td>\n<td style=\"text-align:right\">812.5 µs</td>\n<td style=\"text-align:right\">1,231</td>\n</tr>\n</tbody>\n</table>\n<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Comp. &amp; rendering</th>\n<th style=\"text-align:right\">time/iter (avg)</th>\n<th style=\"text-align:right\">iter/s</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">1. Eta</td>\n<td style=\"text-align:right\">68.0 µs</td>\n<td style=\"text-align:right\">14,700</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">2. EJS</td>\n<td style=\"text-align:right\">177.3 µs</td>\n<td style=\"text-align:right\">5,639</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">3. Vento</td>\n<td style=\"text-align:right\">186.0 µs</td>\n<td style=\"text-align:right\">5,376</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">4. Edge</td>\n<td style=\"text-align:right\">518.0 µs</td>\n<td style=\"text-align:right\">1,931</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">5. Preact</td>\n<td style=\"text-align:right\">706.5 µs</td>\n<td style=\"text-align:right\">1,415</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">6. Liquid</td>\n<td style=\"text-align:right\">1.1 ms</td>\n<td style=\"text-align:right\">937</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">7. Nunjucks</td>\n<td style=\"text-align:right\">1.5 ms</td>\n<td style=\"text-align:right\">676</td>\n</tr>\n<tr>\n<td style=\"text-align:left\">8. Pug</td>\n<td style=\"text-align:right\">4.2 ms</td>\n<td style=\"text-align:right\">236</td>\n</tr>\n</tbody>\n</table>\n<p>Vento delivers exceptional rendering performance. While Eta and EJS offer faster\ncompilation speeds, they struggle with delimiter handling. For example,\n<code>&lt;%= '%&gt;' %&gt;</code> throws an error in EJS, but Vento handles the equivalent,\n<code>{{ '}}' }}</code>, perfectly fine.</p>\n<h2 id=\"update-now\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/vento-2/#update-now\" class=\"header-anchor\">Update now</a></h2>\n<p>Vento 2 is available on NPM (for Node-like runtimes) and HTTP imports (for\nbrowsers and Deno). See the\n<a href=\"https://github.com/ventojs/vento/blob/v2.0.0/CHANGELOG.md\">CHANGELOG file</a> for\nthe full list of changes.</p>\n","date_published":"Mon, 01 Sep 2025 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/lume-3/","url":"https://lume.land/blog/posts/lume-3/","title":"Lume 3 was released - Adolfina Casás","content_html":"<p>After launching Lume 2 almost a year and a half ago, a new major version of Lume\nis here!</p>\n<p><img src=\"https://lume.land/uploads/adolfina-casas.jpeg\" alt=\"Adolfina Casás\"></p>\n<p>This version is dedicated to all Galician <em>cantareiras</em> and\n<em>pandereteiras</em>—women who sing and play the tambourine (or <em>pandeireta</em>), one of\nthe most important instruments in Galician traditional music. One of these\ncantareiras was Adolfina Casás Rama (1912-2009), an ancestor of my friend Miriam\nCasás. She worked in agriculture, where she often sang. She was known for her\nwit and the variety of styles employed.</p>\n<p>Thanks to the <em>recollidas</em>, where musicians and musicologists recorded these\ntraditional songs sung by anonymous women, we can now enjoy this musical\nheritage performed by contemporary musicians with innovative arrangements.</p>\n<p>Some examples include\n<a href=\"https://www.youtube.com/watch?v=CwjZd5ak7xA\">Xabier Díaz &amp; Adufeiras de Salitre</a>,\n<a href=\"https://www.youtube.com/watch?v=Ge9Uu8SeGDE\">Xosé Lois Romero &amp; Aliboria</a>, and\n<a href=\"https://www.youtube.com/watch?v=czMGYX0C2zE\">Berrogüetto</a> (apologies for the\nvideo quality, but I really love that song).</p>\n<p>For more disruptive artists, check out\n<a href=\"https://www.youtube.com/watch?v=qjCeKRoGS8s\">Tanxugueiras</a> or\n<a href=\"https://www.youtube.com/watch?v=9ZM0kou3BPQ\">Baiuca</a>.</p>\n<!-- more -->\n<h2 id=\"why-lume-3%3F\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#why-lume-3%3F\" class=\"header-anchor\">Why Lume 3?</a></h2>\n<p>To many developers, including myself, breaking changes can be frustrating.\nSoftware updates that force you to revisit a project just to ensure it continues\nworking as before often feel like a waste of time. This is one of the reasons I\nenjoy working with Web APIs—they are stable, reliable, and designed to just work\n<a href=\"https://csswizardry.com/2025/01/build-for-the-web-build-on-the-web-build-with-the-web/\">without introducing unnecessary disruptions</a>.</p>\n<p>I strive to bring a similar philosophy to Lume by minimizing breaking changes\nwhenever possible. In fact, I initially had no plans to release a new major\nversion of Lume. However, after receiving numerous reports about certain\nbehaviors and limitations, I realized it was necessary to revisit some design\ndecisions. This effort aims to finally deliver the simple, intuitive static site\ngenerator I have always envisioned, hoping that Lume 4 won't be necesary in a\nlong time, or never.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>TL/DR:</strong> There's\n<a href=\"https://lume.land/docs/advanced/migrate-to-lume3/\">a step-by-step guide to migrate to Lume 3</a>\nin the documentation.</p>\n</div>\n<h2 id=\"the-main-problem\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#the-main-problem\" class=\"header-anchor\">The main problem</a></h2>\n<p>The <code>site.copy()</code> function allows you to copy files from the <code>src</code> folder\nwithout reading the content, which is faster and consumes less memory. But it\nhas one big drawback: the files are not processed.</p>\n<p>For example, let's say you have the following configuration:</p>\n<pre><code class=\"language-js\">site.copy(&quot;/assets&quot;);\nsite.use(postcss());\n</code></pre>\n<p>When Lume builds your site, the files inside the <code>/assets</code> folder are copied\nas-is. If the folder contains CSS files, they <strong>won't be processed by Postcss</strong>.\nLearn more about\n<a href=\"https://github.com/lumeland/lume/issues/571\">this issue on GitHub</a>.</p>\n<p>This behavior is confusing and many people reported this as a bug. And they are\nright: Lume should be clever enough to not delegate the decision of whether a\nfile must be loaded or copied.</p>\n<h2 id=\"the-solution%3A-site.add()\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#the-solution%3A-site.add()\" class=\"header-anchor\">The solution: <code>site.add()</code></a></h2>\n<p>In Lume 3, the <code>site.loadAssets()</code>, <code>site.copyRemainingFiles()</code> and\n<code>site.copy()</code> functions were removed, and now there is a single function for\neverything: <code>site.add()</code>.</p>\n<p>The <code>add()</code> function simply tells Lume that you want to include some files in\nyour site, but without specifying how this file must be treated. Lume will load\nthe file if it needs to (for example, if it needs to be processed), or will copy\nit if no transformations are needed.</p>\n<pre><code class=\"language-js\">site.add(&quot;/assets&quot;);\nsite.use(postcss()); // CSS files in /assets will be processed too!\n</code></pre>\n<p>To upgrade from Lume 2 to Lume 3, just replace the <code>site.loadAssets()</code>,\n<code>site.copyRemainingFiles()</code>, and <code>site.copy()</code> functions with <code>site.add()</code>.</p>\n<p>For example:</p>\n<pre><code class=\"language-js\">// Lume 2\nsite.loadAssets([&quot;.css&quot;]);\nsite.copy(&quot;/assets&quot;, &quot;.&quot;);\nsite.copyRemainingFiles(\n  (path: string) =&gt; path.startsWith(&quot;/articles/&quot;),\n);\n\n// Lume 3\nsite.add([&quot;.css&quot;]);\nsite.add(&quot;/assets&quot;, &quot;.&quot;);\nsite.add(&quot;/articles&quot;);\n</code></pre>\n<blockquote>\n<p><strong>Update:</strong> Some users have reported that <code>site.copy()</code> remains useful in\nspecific scenarios. For instance, if you need to copy a CSS file without\nprocessing it. As a result, the <code>site.copy()</code> function was reintroduced in\nLume 3.0.1 to address these edge cases.</p>\n</blockquote>\n<h3 id=\"copy-remote-files\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#copy-remote-files\" class=\"header-anchor\">Copy remote files</a></h3>\n<p><code>site.add()</code> can add files from the <code>src</code> folder as well as remote files. In\nLume 2, this was possible with the <code>remoteFile</code> function:</p>\n<pre><code class=\"language-js\">// Lume 2\nsite.remoteFile(&quot;styles.css&quot;, &quot;https://example.com/theme/styles.css&quot;);\nsite.copy(&quot;styles.css&quot;);\n</code></pre>\n<p>Lume 3 makes this use case easier:</p>\n<pre><code class=\"language-js\">// Lume 3\nsite.add(&quot;https://example.com/theme/styles.css&quot;, &quot;styles.css&quot;);\n</code></pre>\n<p>The <code>site.add()</code> function also accepts <code>npm</code> specifiers:</p>\n<pre><code class=\"language-js\">site.add(&quot;npm:normalize.css&quot;, &quot;/styles/normalize.css&quot;);\n</code></pre>\n<p>Internally, this uses jsDelivr to download the file. In this example,\n<code>npm:normalize.css</code> is transformed to\n<code>https://cdn.jsdelivr.net/npm/normalize.css</code>. Note that only one file is copied,\nnot all package files.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><code>site.remoteFile</code> is still required in Lume 3 for files not directly exported\nto the dest folder, like <code>_data</code>, <code>_components</code> or <code>_includes</code> files.</p>\n</div>\n<p>More info in the\n<a href=\"https://lume.land/docs/configuration/add-files/\">documentation page</a>.</p>\n<h2 id=\"plugins-no-longer-load-files-automatically\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#plugins-no-longer-load-files-automatically\" class=\"header-anchor\">Plugins no longer load files automatically</a></h2>\n<p>In Lume 2, some plugins configure Lume to load files with a certain extension\nautomatically. For example, Postcss not only processes the CSS code but also\nconfigures Lume to load all CSS files:</p>\n<pre><code class=\"language-js\">// All .css files are loaded and processed\nsite.use(postcss());\n</code></pre>\n<p>In some cases, this is what you want. But if you don't want to load all CSS\nfiles, this behavior makes Lume load everything, and you have to use the\n<code>site.ignore()</code> function or move the unwanted files to a folder starting with\n<code>_</code>.</p>\n<p>In addition to that, this behavior is not fully transparent. You have to read\nthe documentation to know what the plugin is doing.</p>\n<p>In short, this approach causes more harm than good.</p>\n<p>In Lume 3, thanks to the <code>site.add()</code> function, it's very easy to add new files\n(and only the files that you want), so plugins <strong>no longer load files by\ndefault</strong>. You have to explicitly add them, which is more intuitive:</p>\n<pre><code class=\"language-js\">// Lume 2\nsite.use(postcss());\n\n// Lume 3\nsite.add([&quot;.css&quot;]);\nsite.use(postcss());\n</code></pre>\n<p>Another benefit is you have better control of all entry points of your assets.\nFor example, for esbuild:</p>\n<pre><code class=\"language-js\">// Lume 3\nsite.add(&quot;main.ts&quot;);\nsite.use(esbuild()); // Only main.ts is bundled\n</code></pre>\n<p>This change affects the <code>svgo</code>, <code>transform_images</code>, <code>picture</code>, <code>postcss</code>,\n<code>sass</code>, <code>tailwindcss</code>, <code>unocss</code>, <code>esbuild</code> and <code>terser</code> plugins.</p>\n<h2 id=\"jsx\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#jsx\" class=\"header-anchor\">JSX</a></h2>\n<h3 id=\"one-jsx-plugin\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#one-jsx-plugin\" class=\"header-anchor\">One JSX plugin</a></h3>\n<p>Lume started supporting <code>JSX</code> as a template engine thanks to the <code>jsx</code> plugin\nthat uses React under the hood. Later, the <code>jsx_preact</code> plugin was added to use\nPreact, a smaller and more performant alternative to React.</p>\n<p>Having two JSX plugins for the same purpose is useless and adds unnecessary\ncomplexity (for example, combined with the MDX plugin).</p>\n<p>Moreover, both libraries are frontend-first libraries, with features like hooks,\nevent callbacks, hydration, etc, that are not supported at build time, so some\npeople were confused about what they can or cannot do in Lume.</p>\n<p>Lume 3 has only one JSX plugin, and it doesn't use React or Preact but\n<a href=\"https://github.com/oscarotero/ssx/\">SSX</a>, a TypeScript library created\nspecifically for static sites which is faster than React and Preact and more\nergonomic. It allows creating asynchronous components, inserting raw code like\n<code>&lt;!doctype html&gt;</code>, and comes with great documentation including all HTML\nelements and attributes, with links to MDN.</p>\n<p>Lume 3 uses <code>lume/jsx-runtime</code> import source for all JSX and MDX files. So you\nonly have to configure the <code>compilerOptions</code> setting of <code>deno.json</code> as following\n(other options have been omited for brevity):</p>\n<pre><code class=\"language-json\">{\n  &quot;imports&quot;: {\n    &quot;lume/jsx-runtime&quot;: &quot;https://deno.land/x/ssx@v0.1.8/jsx-runtime.ts&quot;\n  },\n  &quot;compilerOptions&quot;: {\n    &quot;jsx&quot;: &quot;react-jsx&quot;,\n    &quot;jsxImportSource&quot;: &quot;lume&quot;\n  }\n}\n</code></pre>\n<p>This allows to upgrade the library (or even replace it with something else)\neasily.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p>With the <code>esbuild</code> plugin you still can use React or Preact in Lume but for\nwhat they were created for: the frontend.</p>\n</div>\n<h3 id=\".page-subextension-for-jsx-and-tsx-pages\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#.page-subextension-for-jsx-and-tsx-pages\" class=\"header-anchor\"><code>.page</code> Subextension for JSX and TSX Pages</a></h3>\n<p>Lume requires the <code>.page</code> subextension for certain file types like <code>.ts</code>, <code>.js</code>,\nor <code>.json</code> to distinguish between files used to generate pages and those\nintended for browser execution. For instance, <code>index.page.js</code> generates the\n<code>index.html</code> page, while <code>index.js</code> is a JavaScript file executed by the\nbrowser.</p>\n<p>Starting with Lume 3, the <code>.page</code> subextension is also applied to <code>.jsx</code> and\n<code>.tsx</code> files. This change allows the <code>.jsx</code> and <code>.tsx</code> extensions to be\nexclusively used for browser-side code (after processing with the <code>esbuild</code>\nplugin).</p>\n<pre><code class=\"language-txt\">Lume 2:\n- /index.jsx\n\nLume 3:\n- /index.page.jsx\n</code></pre>\n<p>If you prefer the Lume 2 behavior (where this differentiation is not required),\nyou can configure the plugin to remove the <code>.page</code> subextension:</p>\n<pre><code class=\"language-js\">site.use(jsx({\n  pageSubExtension: &quot;&quot;, // Reverts to Lume 2 behavior\n}));\n</code></pre>\n<p>More info in <a href=\"https://lume.land/plugins/jsx/\">the plugin documentation</a>.</p>\n<h2 id=\"improved-lume-components\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#improved-lume-components\" class=\"header-anchor\">Improved Lume components</a></h2>\n<h3 id=\"async-components\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#async-components\" class=\"header-anchor\">Async components</a></h3>\n<p>One of the main limitations of Lume 2's components was that they were\nsynchronous. This was to support JSX components that were synchronous with React\nand Preact. With SSX, we don't have this limitation anymore, and all components\nare async.</p>\n<p>For example, you can create a component in JSX that returns a promise:</p>\n<pre><code class=\"language-jsx\">// _components/salute.jsx\n\nexport default async function ({ id }) {\n  const response = await fetch(`https://example.com/api?id=${id}`);\n  const data = await response.json();\n  return &lt;strong&gt;Hello {data.name}&lt;/strong&gt;;\n}\n</code></pre>\n<p>This component can be used in any other template engine, like JSX:</p>\n<pre><code class=\"language-jsx\">export default async function ({ comp }) {\n  return (\n    &lt;p&gt;\n      &lt;comp.Salute id=&quot;23&quot; /&gt;\n    &lt;/p&gt;\n  );\n}\n</code></pre>\n<p>Or Vento:</p>\n<pre><code class=\"language-html\">&lt;p&gt;{{ comp.Salute({ id: 23}) }}&lt;/p&gt;\n</code></pre>\n<h3 id=\"folder-components\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#folder-components\" class=\"header-anchor\">Folder components</a></h3>\n<p>Lume components not only generate HTML code but can also export the CSS and JS\ncode needed to run it on the browser. The code must be exported in the variables\n<code>css</code> and <code>js</code>. For example:</p>\n<pre><code class=\"language-md\">---\ncss: |\n  .mainTitle {\n    color: red;\n  }\n---\n\n&lt;h1 class=&quot;mainTitle&quot;&gt;{{ name }}&lt;/h1&gt;\n</code></pre>\n<p>The problem with this approach is the CSS and JS code is not treated as CSS and\nJS code by your code editor, so there's no syntax highlighting.</p>\n<p>In Lume 3, it's possible to create a component in a folder, with the CSS and JS\ncode in different files. To do that, you use the following structure:</p>\n<pre><code class=\"language-txt\">|_ _components/\n    |_ button/\n        |_ comp.vto\n        |_ style.css\n        |_ script.js\n</code></pre>\n<p>Any folder containing a <code>comp.*</code> file will be loaded as a component using the\nfolder name as the component name, and the <code>style.css</code> and <code>script.js</code> files\nwill be loaded as the CSS and JS code for the component. This makes the creation\nof components more ergonomic, especially for cases with a lot of CSS and JS\ncode.</p>\n<p>Additionally, it's possible to add a <code>script.ts</code> file instead <code>script.js</code> to use\nTypeScript. Lume will compile it to JavaScript automatically.</p>\n<h3 id=\"better-interoperability\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#better-interoperability\" class=\"header-anchor\">Better interoperability</a></h3>\n<p>In Lume 2 components created with text-based engines, like Vento didn't work\nwell for JSX templates. For example, let's say we have the following Vento\ncomponent:</p>\n<pre><code class=\"language-vto\">&lt;button&gt;{{ content }}&lt;/button&gt;\n</code></pre>\n<p>and we want to use it in a JSX page:</p>\n<pre><code class=\"language-jsx\">export default function ({ comp }) {\n  return &lt;comp.Button&gt;Click here&lt;/comp.Button&gt;;\n}\n</code></pre>\n<p>Due JSX escapes the string values, the output code is this:</p>\n<pre><code class=\"language-html\">&amp;lt;button&amp;gt;Click here&amp;lt;/button&amp;gt;\n</code></pre>\n<p>To fix it, we need to create a container element with the\n<code>dangerouslySetInnerHTML</code> attribute:</p>\n<pre><code class=\"language-jsx\">export default function ({ comp }) {\n  return (\n    &lt;div\n      dangerouslySetInnerHTML={{\n        __html: &lt;comp.Button&gt;Click here&lt;/comp.Button&gt;,\n      }}\n    /&gt;\n  );\n}\n</code></pre>\n<p>In Lume 3, thanks to SSX this is no longer necessary. Components are fully\ninteroperable and you can insert JSX components in Vento and viceversa. And to\nmake them even more interchangeable, the <code>content</code> and <code>children</code> variables are\nequivalent.</p>\n<pre><code class=\"language-jsx\">export default function ({ comp }) {\n  return (\n    &lt;&gt;\n      // This works\n      &lt;comp.Button&gt;Click here&lt;/comp.Button&gt;\n\n      // This also works\n      &lt;comp.Button content=&quot;Click here&quot; /&gt;\n    &lt;/&gt;\n  );\n}\n</code></pre>\n<h3 id=\"default-data-in-components\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#default-data-in-components\" class=\"header-anchor\">Default data in components</a></h3>\n<p>In Lume 3, components can have extra data that will be used as default values.\nThis data is like layout data but applied to components.</p>\n<p>Let's see the following <code>_components/title.vto</code> component as an example :</p>\n<pre><code class=\"language-vto\">---\ntitle: Hello world\n---\n\n&lt;h1&gt;{{ title }}&lt;/h1&gt;\n</code></pre>\n<p>Now you can use the component with the default title.</p>\n<pre><code class=\"language-vto\">{{ await comp.title() }}\n&lt;!-- &lt;h1&gt;Hello world&lt;/h1&gt; --&gt;\n</code></pre>\n<p>Or with a custom title</p>\n<pre><code class=\"language-vto\">{{ await comp.title({ title: &quot;New title&quot; }) }}\n&lt;!-- &lt;h1&gt;New title&lt;/h1&gt;  --&gt;\n</code></pre>\n<h2 id=\"global-cssfile%2C-jsfile-and-fontsfolder\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#global-cssfile%2C-jsfile-and-fontsfolder\" class=\"header-anchor\">Global cssFile, jsFile and fontsFolder</a></h2>\n<p>As mentioned, Lume components can output CSS and JS code. However, some plugins\noutput code too. For example, <code>google_fonts</code> generates the CSS code needed to\nload the fonts, <code>prism</code> and <code>code_highlight</code> export the CSS code with the\nthemes, and <code>katex</code> (that didn't generate CSS code in Lume 2) now generates the\nCSS code automatically so you don't need to copy manually the CSS code.</p>\n<p>Additionally, some plugins download also font files (specifically,\n<code>google_fonts</code> and <code>katex</code>).</p>\n<p>In Lume 2, you have to configure how to export the generated code for every\nplugin individually. In Lume 3 there are three global options that will be used\nby default by all plugins and components:</p>\n<pre><code class=\"language-js\">const site = lume({\n  cssFile: &quot;/style.css&quot;, // default value\n  jsFile: &quot;/script.js&quot;, // default value\n  fontsFolder: &quot;/fonts&quot;, // default value\n});\n</code></pre>\n<p>All extra code generated by components and the plugins <code>code_highlight</code>,\n<code>google_fonts</code>, <code>prism</code>, <code>katex</code> and <code>unocss</code> will be stored there.</p>\n<p>Of course, you can still change the code destination for a specific plugin:</p>\n<pre><code class=\"language-js\">site.use(unocss({\n  cssFile: &quot;/unocss-styles.css&quot;,\n}));\n</code></pre>\n<h2 id=\"tailwind-4\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#tailwind-4\" class=\"header-anchor\">Tailwind 4</a></h2>\n<p>The <code>tailwindcss</code> plugin was upgraded to use\n<a href=\"https://tailwindcss.com/blog/tailwindcss-v4\">Tailwind 4</a>. The new version is\nfaster than v3 and no longer needs Postcss to work. There are many changes in\nthe configuration (especially the CSS-first configuration) so take a look at the\n<a href=\"https://tailwindcss.com/docs/upgrade-guide\">upgrade guide</a> if you want to\nupgrade your projects from v3 to v4.</p>\n<pre><code class=\"language-js\">// Lume 2\nsite.use(tailwindcss());\nsite.use(postcss());\n\n// Lume 3\nsite.use(tailwindcss());\nsite.add(&quot;style.css&quot;);\n</code></pre>\n<p>If you don't want to upgrade to v4, it's still possible to continue using\nTailwind 3 with the postcss plugin:</p>\n<pre><code class=\"language-js\">import tailwind from &quot;npm:tailwindcss@^3.4&quot;;\n\nsite.use(\n  postcss({\n    plugins: [tailwind()],\n  }),\n);\n</code></pre>\n<p>More info in the <a href=\"https://lume.land/plugins/tailwindcss/\">plugin documentation</a>.</p>\n<h2 id=\"processors-improvements\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#processors-improvements\" class=\"header-anchor\">Processors improvements</a></h2>\n<p><code>site.process()</code> and <code>site.preprocess()</code> are among Lume's most used features.\nLume 3 brings some improvements here to make them easier to use.</p>\n<h3 id=\"page.document-no-longer-returns-undefined\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#page.document-no-longer-returns-undefined\" class=\"header-anchor\"><code>page.document</code> no longer returns undefined</a></h3>\n<p>One of the many uses of processors is to manipulate HTML pages using the\n<code>page.document</code> property. But this property can return <code>undefined</code> if the page\nis not HTML or cannot be parsed, so you have to check the variable type before\nusing it:</p>\n<pre><code class=\"language-js\">site.process([&quot;.html&quot;], (pages) =&gt; {\n  for (const page of pages) {\n    const document = page.document;\n    if (!document) {\n      continue;\n    }\n    const title = document.querySelector(&quot;title&quot;);\n  }\n});\n</code></pre>\n<p>In Lume 3, <code>page.document</code> always returns a <code>Document</code> instance or throws an\nexception if the page cannot be parsed. This allows us to omit the type check:</p>\n<pre><code class=\"language-js\">site.process([&quot;.html&quot;], (pages) =&gt; {\n  for (const page of pages) {\n    const title = page.document.querySelector(&quot;title&quot;);\n  }\n});\n</code></pre>\n<h3 id=\"new-page-properties\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#new-page-properties\" class=\"header-anchor\">New page properties</a></h3>\n<p>The <code>page.content</code> variable containing the content of the page can be a string\nor a <code>Uint8Array</code>, depending on how this page has been loaded. For example,\nHTML, CSS or JS pages have the content as a string, but images or other binary\nfiles are loaded as <code>Uint8Array</code>.</p>\n<p>To process these files in Lume 2 you have to check the content type:</p>\n<pre><code class=\"language-js\">site.process([&quot;.css&quot;], (pages) =&gt; {\n  for (const page of pages) {\n    const content = page.content;\n\n    if (typeof content === &quot;string&quot;) {\n      page.content = &quot;/* © 2025 */&quot; + content;\n    }\n  }\n});\n</code></pre>\n<p>In Lume 3, pages have two new properties: <code>page.text</code> and <code>page.bytes</code> (inspired\nby the same properties of the\n<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Request#instance_methods\">Request</a>\nobject). As you may guess, <code>page.text</code> allows to work with the page content as\nstrings, making the conversions automatic, and <code>page.bytes</code> does the same but\nfor <code>Uint8Array</code>.</p>\n<pre><code class=\"language-js\">site.process([&quot;.css&quot;], (pages) =&gt; {\n  for (const page of pages) {\n    page.text = &quot;/* © 2025 */&quot; + page.text;\n  }\n});\n</code></pre>\n<h3 id=\"omit-*-wildcard\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#omit-*-wildcard\" class=\"header-anchor\">Omit <code>*</code> wildcard</a></h3>\n<p>In Lume 2, the <code>*</code> wildcard allows you to process all pages:</p>\n<pre><code class=\"language-js\">site.process(&quot;*&quot;, (pages) =&gt; {\n  // Process all pages\n});\n</code></pre>\n<p>In Lume 3, the first argument can be omitted:</p>\n<pre><code class=\"language-js\">site.process((pages) =&gt; {\n  // Process all pages\n});\n</code></pre>\n<h2 id=\"the-order-of-some-plugins-is-now-more-important\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#the-order-of-some-plugins-is-now-more-important\" class=\"header-anchor\">The order of some plugins is now more important</a></h2>\n<p>In Lume 2, the order in which some plugins are registered doesn't matter. Let's\nsee this example from the <code>sitemap</code> plugin:</p>\n<pre><code class=\"language-js\">site.use(sitemap()); // Generate the sitemap file\nsite.use(basePath()); // Add the base path to all URLs\n</code></pre>\n<p>The sitemap plugin is registered before basePath, so you may think the sitemap\nfile is generated before adding the base path prefix to all URLs. But\ninternally, the sitemap plugin is executed using the &quot;beforeSave&quot; event, which\nis triggered at the end, just before saving all files to the _site folder. So\ninternally the basePath plugin is executed before.</p>\n<p>This was designed so you don't have to think about the order of the plugins when\nusing them. But this behavior has two problems:</p>\n<ul>\n<li>There are many plugins in which the order matters. For example, if you combine\nSASS and Postcss you have to process the SCSS files first and pass the result\nto Postcss. This inconsistency makes you wonder in which plugins the order is\nimportant or not.</li>\n<li>It's not possible to use a processor to modify the output of these plugins.\nFor example, if you want to compress the sitemap file with brotli or gzip, is\nnot possible because the sitemap will be always generated at the end.</li>\n</ul>\n<p>To make Lume more transparent and intuitive, many plugins using events were\nchanged to use processors, which respect the order in which they are registered\nin the _config.ts file.</p>\n<p>The affected plugins are: <code>code_highlight</code>, <code>decap_cms</code>, <code>favicon</code>, <code>feed</code>,\n<code>google_fonts</code>, <code>icons</code>, <code>prism</code>, <code>robots</code>, <code>sitemap</code>, and <code>slugify_urls</code>.</p>\n<p>To help with this transition, Lume 3 comes with a\n<a href=\"https://docs.deno.com/runtime/reference/lint_plugins/\">lint plugin</a> that warns\nyou when the order of some plugins is not correct.</p>\n<p><img src=\"https://lume.land/uploads/lint.png\" alt=\"Image\"></p>\n<h2 id=\"esbuild-uses-esbuild-deno-loader-to-resolve-dependencies\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#esbuild-uses-esbuild-deno-loader-to-resolve-dependencies\" class=\"header-anchor\">esbuild uses <code>esbuild-deno-loader</code> to resolve dependencies</a></h2>\n<p>Deno is becoming a complicated runtime, especially for everything related to\nmodule resolution. It supports three completely different types of packages\n(HTTP, NPM, and JSR), with different behaviors, inconsistencies, and\nincompatibilities between them. In addition to the usual complexity of NPM, in\nDeno a package can be located in different places, depending on the variable\n<code>nodeModulesDir</code>, if the file <code>package.json</code> is found, if the <code>node_modules</code>\nfolder exists, etc. JSR is not much better, because the resolution of a package\ndepends on the combination of <code>imports</code>, <code>exports</code>, and <code>patch</code> keys in\ndifferent <code>deno.json</code> and <code>deno.jsonc</code> files. And the addition of workspaces\nadds a new layer of complexity.</p>\n<p>In Lume 2, the <code>esbuild</code> plugin delegates all this complexity to\n<a href=\"https://esm.sh/\">esm.sh</a>, which transforms any NPM or JSR package to simple\nHTTP imports that are easier to manage. But this solution has its problems with\nmultiple configuration options (<code>deps</code>, <code>pin</code>, <code>alias</code>, <code>standalone</code>, <code>exports</code>,\netc) and there are many packages that don't work well after passing them through\nesm.sh.</p>\n<p>In Lume 3 the <code>esbuild</code> plugin uses the\n<a href=\"https://jsr.io/@luca/esbuild-deno-loader\">esbuild-deno-loader</a> plugin created\nby Luca Casonato, a member of the Deno team. This will make your bundled code\nmore reliable and compatible with how Deno works.</p>\n<h2 id=\"basename-improvements\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#basename-improvements\" class=\"header-anchor\"><code>basename</code> improvements</a></h2>\n<p>In Lume 2, the <code>basename</code> variable allows changing the name of a file or\ndirectory. When missing, it's automatically defined by Lume using the page\nfilename. For example, the page <code>/posts/first-post.md</code> has the basename\n<code>first-post</code>.</p>\n<p>In Lume 3 this variable uses the final URL of the page, instead of the source\nfilename. For example, if the <code>/post/first-post.md</code> page generates a different\nURL (say <code>/post/other-name/</code>) the <code>basename</code> is <code>other-name</code>.</p>\n<p>Additionally, the basename no longer accepts &quot;index&quot; as a value. For example,\nthe basename for the <code>/post/hello-world/index.md</code> is <code>hello-world</code> (the folder\nname) instead of <code>index</code> (the filename).</p>\n<p>These changes will make this variable more consistent across all pages, no\nmatter how the URL is generated. It's especially important for the <code>nav</code> plugin\nthat uses this variable to sort pages alphabetically.</p>\n<h2 id=\"date-detection-from-filepath-is-disabled-by-default\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#date-detection-from-filepath-is-disabled-by-default\" class=\"header-anchor\">Date detection from filepath is disabled by default</a></h2>\n<p>Lume 2 detects automatically the <code>date</code> value from the files and folders paths\nand remove it. For example, the file <code>/posts/2020-06-21_hello-world.md</code> outputs\nthe page <code>/posts/hello-world/</code> (without the date).</p>\n<p>Some people don't want this behavior and prefer to keep the date in the output\nURL. Following the Lume's philosophy of having a light core and provide extra\nfeatures through plugins, this feature was removed from the core and the new\n<code>extract_date</code> plugin was created to enable it.</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport extractDate from &quot;lume/plugins/extract_date.ts&quot;;\n\nconst site = lume();\n\nsite.use(extractDate());\n\nexport default site;\n</code></pre>\n<p>By default the plugin provides the same behavior of Lume 2, but it's possible to\nextract the date without removing it from the URL:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport extractDate from &quot;lume/plugins/extract_date.ts&quot;;\n\nconst site = lume();\n\nsite.use(extractDate({\n  remove: false, // Keep the date\n}));\n\nexport default site;\n</code></pre>\n<h2 id=\"removed-plugins\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#removed-plugins\" class=\"header-anchor\">Removed plugins</a></h2>\n<p>In addition to <code>jsx_preact</code>, two more plugins were removed in Lume 3: <code>liquid</code>\nand <code>on_demand</code>.</p>\n<p>Liquid lets you using <a href=\"https://liquidjs.com/\">LiquidJS</a> as a template engine to\nbuild pages. The syntax is very similar to Nunjucks and the library is actively\nmaintained but it has a big limitation: it's not possible to invoke functions.\nThis makes this template engine useless in Lume because it's not possible to use\nhelpers like <code>search</code> or <code>nav</code> to search pages or build the navigation. The\nplugin has been deprecated for a while, and it was removed in Lume 3.</p>\n<p>The <code>on_demand</code> plugin was mainly an experiment to see if it was possible to add\nsome dynamic behavior to Lume sites. But it never worked well, the\nimplementation was a bit hacky to make it work on Deno Deploy, and it was too\nlimited. Lume has the <a href=\"https://lume.land/plugins/router/\">router</a> for simple use\ncases, and for complex cases, maybe you have to use a different framework. The\npurpose of Lume never was to become into one-size-fits-all solution.</p>\n<h2 id=\"removed-some-customization\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#removed-some-customization\" class=\"header-anchor\">Removed some customization</a></h2>\n<p>The following removals aim to improve the stability and interoperability between\nplugins.</p>\n<h3 id=\"extensions-option\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#extensions-option\" class=\"header-anchor\"><code>extensions</code> option</a></h3>\n<p>In Lume 2, some plugins have the <code>extensions</code> option to configure which files\nyou want to process. You rarely need to modify this option because Lume provides\nsensible defaults. For example, the default value for\n<a href=\"https://lume.land/plugins/postcss/\">Postcss</a> plugin is <code>[&quot;.css&quot;]</code>:</p>\n<pre><code class=\"language-js\">site.use(postcss({\n  extensions: [&quot;.css&quot;], // &lt;- You don't need this\n}));\n</code></pre>\n<p>In most cases, this option doesn't make sense, because you can set any value but\nthe plugin expects a specific format, like HTML pages to use DOM API or CSS code\nto process:</p>\n<pre><code class=\"language-js\">site.use(postcss({\n  extensions: [&quot;.html&quot;], // &lt;- This breaks the build\n}));\n</code></pre>\n<p>In Lume 3, this option was removed in many plugins:</p>\n<ul>\n<li>purgecss, postcss, and lightningcss always process <code>.css</code> files.</li>\n<li>sass always processes <code>.scss</code> and <code>.sass</code> files.</li>\n<li>svgo always processes <code>.svg</code> files.</li>\n<li>check_urls, base_path, relative_urls and modify_urls process <code>.css</code> and\n<code>.html</code> files.</li>\n<li>filter_pages processes all extensions.</li>\n<li>code_highlight, fff, inline, json_ld, katex, metas, multilanguage, og_images,\nand prism always process <code>.html</code> pages.</li>\n</ul>\n<h3 id=\"name-option\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#name-option\" class=\"header-anchor\">Name option</a></h3>\n<p>There are other plugins that register filters or helpers that you can use in\nyour pages. In Lume 2 you could customize the name of these elements. For\nexample, it's possible to use a different key to store the data for the <code>metas</code>\nplugin:</p>\n<pre><code class=\"language-js\">site.use(metas({\n  name: &quot;opengraph&quot;,\n}));\n</code></pre>\n<p>Or the filter name of the <code>date</code> plugin:</p>\n<pre><code class=\"language-js\">site.use(date({\n  name: &quot;get_date&quot;,\n}));\n</code></pre>\n<p>Changing the default name of the plugins have two problems:</p>\n<ul>\n<li>The types declared by the plugin don't change, so even if you change the key\n<code>metas</code> to <code>opengraph</code>, <code>Lume.Data.metas</code> still exist.</li>\n<li>This breaks the interoperability between plugins. For example, <code>picture</code> and\n<code>transform_images</code> depend on the same key name. If you change it for only one\nplugin, the other won't work.</li>\n</ul>\n<p>In Lume 3, the <code>name</code> option was removed in the following plugins, so it's no\nlonger possible to change it to something else: <code>date</code>, <code>json_ld</code>, <code>metas</code>,\n<code>nav</code>, <code>paginate</code>, <code>picture</code>, <code>reading_info</code>, <code>search</code>, <code>transform_images</code>,\n<code>url</code> and <code>postcss</code>.</p>\n<h3 id=\"other-options-removed\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#other-options-removed\" class=\"header-anchor\">Other options removed</a></h3>\n<ul>\n<li>cache option in <code>transform_images</code>, <code>favicon</code> and <code>og_images</code></li>\n<li><code>attribute</code> option in <code>inline</code>.</li>\n<li>Components are always in the <code>comp</code> variable. The option to customize this\nvariable name has been removed.</li>\n</ul>\n<p>Most Lume users don't change these options, so most likely these removals don't\naffect your upgrade to Lume 3.</p>\n<h2 id=\"other-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#other-changes\" class=\"header-anchor\">Other changes</a></h2>\n<h3 id=\"temporal-api-enabled-by-default\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#temporal-api-enabled-by-default\" class=\"header-anchor\">Temporal API enabled by default</a></h3>\n<p>The <a href=\"https://github.com/tc39/proposal-temporal\">Temporal proposal</a> provides\nstandard objects and functions for working with dates and times. It's being\nimplemented in all browsers and it's supported by Deno with the\n<code>unstable-temporal</code> flag. Lume 2 uses\n<a href=\"https://www.npmjs.com/package/@js-temporal/polyfill\">a polyfill</a>, but Lume 3\nuses the Deno implementation, which requires to enable it in <code>deno.json</code> file:</p>\n<pre><code class=\"language-json\">{\n  &quot;unstable&quot;: [&quot;temporal&quot;]\n}\n</code></pre>\n<h3 id=\"deno-lts-support\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#deno-lts-support\" class=\"header-anchor\">Deno LTS support</a></h3>\n<p>As of version 3, Lume will support at least the most recent Deno LTS version\n(and probably some older versions too). Lume 3.0 supports Deno 2.1 and greater.\nMore info\n<a href=\"https://docs.deno.com/runtime/fundamentals/stability_and_releases/#long-term-support-(lts)\">about Deno LTS releases</a>.</p>\n<h3 id=\"removed-automatic-doctype\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#removed-automatic-doctype\" class=\"header-anchor\">Removed automatic doctype</a></h3>\n<p>Lume 2 automatically added <code>&lt;!doctype html&gt;</code> to any HTML pages that were missing\nit. The original reason was because JSX doesn't allow adding this directive, so\nit was difficult to create HTML pages with only JSX. However, some users don't\nwant this behavior because they create files with fragments of HTML. In Lume 3,\nit is possible to add the <code>doctype</code> directive in JSX (thanks to SSX) so this\nbehavior is no longer needed and was removed.</p>\n<h3 id=\"more-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#more-changes\" class=\"header-anchor\">More changes</a></h3>\n<p>As always, you can see\n<a href=\"https://github.com/lumeland/lume/blob/v3.0.0/CHANGELOG.md\">the CHANGELOG.md file</a>\nfor a complete list of all changes with more details.</p>\n<h2 id=\"one-last-thing\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#one-last-thing\" class=\"header-anchor\">One last thing</a></h2>\n<p>While the development server is running, Lume 3 features a debug bar that\nprovides valuable insights, including warnings and issues flagged by plugins.</p>\n<p><img src=\"https://lume.land/uploads/debugbar.png\" alt=\"Image\"></p>\n<p>The Lume debug bar can be extended easily by plugins or directly in the\n<code>_config.ts</code>. For example, let's create a simple tab to list all pages without\ntitle:</p>\n<pre><code class=\"language-js\">function createTab() {\n  // Create a collection in the debug bar\n  const collection = site.debugBar?.collection(&quot;Pages without title&quot;);\n\n  // The debug bar is enabled if the collection was created\n  if (collection) {\n    collection.icon = &quot;file&quot;;\n\n    // Add items to the collection\n    collection.items = site.pages\n      .filter((page) =&gt; page.outputPath.endsWith(&quot;.html&quot;)) // Only HTML pages\n      .filter((page) =&gt; !page.data.title) // No title\n      .map((page) =&gt; ({\n        title: page.data.url,\n        actions: [\n          {\n            text: &quot;Visit&quot;,\n            href: page.data.url,\n          },\n        ],\n      }));\n  }\n}\n\n// Run this function after building and updating the site\nsite.addEventListener(&quot;afterBuild&quot;, createTab);\nsite.addEventListener(&quot;afterUpdate&quot;, createTab);\n</code></pre>\n<p>That's it! Our custom tab is now part of the Lume debug bar, and it has already\nidentified two pages without titles!</p>\n<p><img src=\"https://lume.land/uploads/custom-debug-tab.png\" alt=\"Image\"></p>\n<p>The Lume debug bar is still an experimental feature, but it has been very well\nreceived since its introduction in our Discord community. Developers are already\nworking on plugins to enhance the debug bar with features like HTML validator\nreports, accessibility checks, and SEO analysis.</p>\n<p>Keep in mind that the debug bar is only visible when running Lume with\n<code>deno task serve</code>. It is not included in production builds. If you wish to\ndisable this feature completely, you can do so by editing the <code>_config.ts</code> file:</p>\n<pre><code class=\"language-js\">const site = lume({\n  server: {\n    debugBar: false, // disable the debug bar\n  },\n});\n</code></pre>\n<h2 id=\"thanks!\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-3/#thanks!\" class=\"header-anchor\">Thanks!</a></h2>\n<p>All this work wouldn't be possible without the help from all people that\ncontribute to Lume. Thanks to everyone that\n<a href=\"https://opencollective.com/lume\">sponsor Lume</a>\n<a href=\"https://github.com/sponsors/oscarotero\">or directly me</a>. Thanks also to people\nthat have been testing Lume 3 in the latest months or even using it in real\nprojects, reporting bugs and providing feedback (specially\n<a href=\"https://timthepost.deno.dev/\">Tim Post</a> and <a href=\"https://cogley.jp/\">Rick Cogley</a>),\nand thanks to <a href=\"https://pyrox.dev/\">Pyrox</a> for reviewing the grammar of this\npost.</p>\n","date_published":"Wed, 07 May 2025 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/","url":"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/","title":"Lume 2.5.0 - Pedro Días and Muño Vandilaz","content_html":"<p><strong><em>Feliz aninovo</em> 🎄!</strong></p>\n<p>New year and new Lume version! This time, I'd like to dedicate it to Pedro Días\nand Muño Vandilaz, who married on April 16, 1061, almost a thousand years ago.\nThis is the first same-sex marriage documented in Galicia (and the rest of\nSpain).</p>\n<p>The wedding took place in a small Catholic chapel. It's surprising to see how\nhomophobic prejudices have changed since then. If you want to read more about\nthis event take a look at\n<a href=\"https://qnews.com.au/on-this-day-april-16-pedro-diaz-and-muno-vandilaz/\">this Qnews article (English)</a>\nor\n<a href=\"https://www.gciencia.com/tribuna/unha-voda-entre-dous-homes-no-ourense-do-seculo-xi/\">gCiencia post (Galician)</a>.</p>\n<!-- more -->\n<h2 id=\"new-plugin-json_ld\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/#new-plugin-json_ld\" class=\"header-anchor\">New plugin <code>json_ld</code></a></h2>\n<p><a href=\"https://json-ld.org/\">JSON-LD</a> (JSON for Linking Data) is a way to provide\n<a href=\"https://www.schema.org/\">structured data</a> to web pages using JSON format, which\nis easier to parse and doesn't require to modify the HTML code. It's defined\nwith a <code>&lt;script type=&quot;application/ld+json&quot;&gt;</code> element containing the JSON code.\nFor example:</p>\n<pre><code class=\"language-html\">&lt;script type=&quot;application/ld+json&quot;&gt;\n  {\n    &quot;@context&quot;: &quot;https://schema.org&quot;,\n    &quot;@type&quot;: &quot;WebSite&quot;,\n    &quot;url&quot;: &quot;https://oscarotero.com/&quot;,\n    &quot;headline&quot;: &quot;Óscar Otero - Web designer and developer&quot;,\n    &quot;name&quot;: &quot;Óscar Otero&quot;,\n    &quot;description&quot;: &quot;I’m just a designer and web developer&quot;,\n    &quot;author&quot;: {\n      &quot;@type&quot;: &quot;Person&quot;,\n      &quot;name&quot;: &quot;Óscar Otero&quot;\n    }\n  }\n&lt;/script&gt;\n</code></pre>\n<p>The <code>json_ld</code> plugin, created by <a href=\"https://github.com/shuaixr\">Shuaixr</a>, makes\neasier to work with this structured data. Edit your <code>_config</code> file to install\nit:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport jsonLd from &quot;lume/plugins/json_ld.ts&quot;;\n\nconst site = lume();\nsite.use(jsonLd());\n\nexport default site;\n</code></pre>\n<p>Then, you can create the <code>jsonLd</code> variable in your pages. For example:</p>\n<pre><code class=\"language-yml\">jsonLd:\n  &quot;@type&quot;: WebSite\n  url: /\n  headline: Óscar Otero - Web designer and developer\n  name: Óscar Otero\n  description: I’m just a designer and web developer\n  author:\n    &quot;@type&quot;: Person\n    name: Óscar Otero\n</code></pre>\n<p>Note the following:</p>\n<ul>\n<li>The plugin automatically adds the <code>@context</code> property if it's missing</li>\n<li>URLs can omit the protocol and host. The plugin automatically resolves all\nURLs based on the <code>location</code> of the site.</li>\n</ul>\n<p>Like with other similar plugins like <a href=\"https://lume.land/plugins/metas/\">metas</a>,\nyou can use field aliases:</p>\n<pre><code class=\"language-yml\">title: Óscar Otero - Web designer and developer\nheader:\n  title: Óscar Otero\n  description: I’m just a designer and web developer\n\njsonLd:\n  &quot;@type&quot;: WebSite\n  url: /\n  headline: =title\n  name: =header.title\n  description: =header.description\n  author:\n    &quot;@type&quot;: Person\n    name: =header.title\n</code></pre>\n<h3 id=\"typescript\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/#typescript\" class=\"header-anchor\">TypeScript</a></h3>\n<p>If you want to use TypeScript, there's the <code>Lume.Data[&quot;jsonLd&quot;]</code> type (powered\nby <a href=\"https://www.npmjs.com/package/schema-dts\">schema-dts</a> package):</p>\n<pre><code class=\"language-ts\">export const jsonLd: Lume.Data[&quot;jsonLd&quot;] = {\n  &quot;@type&quot;: &quot;WebSite&quot;,\n  url: &quot;/&quot;,\n  headline: &quot;Óscar Otero - Web designer and developer&quot;,\n  description: &quot;I’m just a designer and web developer&quot;,\n  name: &quot;Óscar Otero&quot;,\n  author: {\n    &quot;@type&quot;: &quot;Person&quot;,\n    name: &quot;Óscar Otero&quot;,\n  },\n};\n</code></pre>\n<p>More info in the\n<a href=\"https://lume.land/plugins/json_ld/\">plugin documentation page</a>.</p>\n<h2 id=\"new-plugin-purgecss\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/#new-plugin-purgecss\" class=\"header-anchor\">New plugin <code>purgecss</code></a></h2>\n<p><a href=\"https://purgecss.com/\">PurgeCSS</a> is a utility to remove unused CSS code, making\nyour CSS files smaller to improve the site performance. The tool provides a\nPostcss plugin so, in theory, it can also be used in Lume. Now it has its own\nplugin (big thanks to <a href=\"https://github.com/into-the-v0id\"><em>into-the-v0id</em></a>) which\nhas some advantages:</p>\n<ul>\n<li>Scan generated HTML pages by Lume</li>\n<li>Scan bundled JS dependencies (bootstrap, etc)</li>\n<li>Only include CSS that is necessary (don't include drafts or conditional HTML\nthat does not make it into the build)</li>\n</ul>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport purgecss from &quot;lume/plugins/purgecss.ts&quot;;\n\nconst site = lume();\nsite.use(purgecss());\n\nexport default site;\n</code></pre>\n<p>Go to the <a href=\"https://lume.land/plugins/purgecss/\">plugin documentation page</a> for\nmore info.</p>\n<h2 id=\"new-router-middleware\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/#new-router-middleware\" class=\"header-anchor\">New <code>router</code> middleware</a></h2>\n<p>Lume is a static site generator (and always will be). But sometimes you need\nsome server-side logic to handle small things. For example, to handle the data a\nuser sends from an HTML form, or maybe you need a small API to provide dynamic\ndata.</p>\n<p>For sites requiring front and back, you have great options like\n<a href=\"https://fresh.deno.dev/\">Fresh</a>, <a href=\"https://astro.build/\">Astro</a> or\n<a href=\"https://hono.dev/\">Hono</a>. But if you only need a couple of entry points, you\nmay consider using something simpler like <code>router</code> middleware, which is a\nminimal router that works great with Lume's server.</p>\n<pre><code class=\"language-js\">import Server from &quot;lume/core/server.ts&quot;;\nimport Router from &quot;lume/middlewares/router.ts&quot;;\n\n// Create the router\nconst router = new Router();\n\nrouter.get(&quot;/hello/:name&quot;, ({ name }) =&gt; {\n  return new Response(`Hello ${name}`);\n});\n\n// Create the server:\nconst server = new Server();\n\nserver.use(router.middleware());\n\nserver.start();\n</code></pre>\n<p>That's all. Now the <code>/hello/laura</code> request will return a <code>Hello laura</code> response!</p>\n<p>The router uses the standard\n<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/URLPattern\">URLPattern</a> under\nthe hood that creates an object with all variables captured in the path and\npasses it as the first argument of the route handler.</p>\n<p>In addition to the captured variables, you have the <code>request</code> property with the\nRequest instance:</p>\n<pre><code class=\"language-js\">router.get(&quot;/search&quot;, ({ request }) =&gt; {\n  const { searchParams } = new URL(request.url);\n\n  const query = searchParams.get(&quot;query&quot;);\n  return new Response(`Searching by ${query}`);\n});\n</code></pre>\n<p>Note that to use this middleware in production, you need a hosting service\nrunning Deno like <a href=\"https://deno.com/deploy\">Deno Deploy</a> or similar.</p>\n<h2 id=\"new-plaintext-plugin\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/#new-plaintext-plugin\" class=\"header-anchor\">New <code>plaintext</code> plugin</a></h2>\n<p>Sometimes you have your content in Markdown or HTML, but also need a plain text\nversion. Let's see the following example:</p>\n<pre><code class=\"language-vto\">---\ntitle: Welcome to **my site**\n---\n&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n  &lt;head&gt;\n    &lt;title&gt;{{ title }}&lt;/title&gt;\n  &lt;/head&gt;\n\n  &lt;body&gt;\n    &lt;h1&gt;{{ title |&gt; md(true) }}&lt;/h1&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>The <code>title</code> variable uses Markdown syntax to render\n<code>Welcome to &lt;strong&gt;my site&lt;/strong&gt;</code>. But this also affects the <code>&lt;title&gt;</code>\nelement of the page which contains the asterisks.</p>\n<p>The new <code>plaintext</code> plugin registers the <code>plaintext</code> filter, that not only\nremoves any Markdown and HTML syntax but also linebreaks and extra spaces:</p>\n<pre><code class=\"language-vto\">---\ntitle: Welcome to **my site**\n---\n&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n  &lt;head&gt;\n    &lt;title&gt;{{ title |&gt; plaintext }}&lt;/title&gt;\n  &lt;/head&gt;\n\n  &lt;body&gt;\n    &lt;h1&gt;{{ title |&gt; md(true) }}&lt;/h1&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>The plugin is disabled by default so you need to import it to your _config.ts\nfile:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport plaintext from &quot;lume/plugins/plaintext.ts&quot;;\n\nconst site = lume();\nsite.use(plaintext());\n\nexport default site;\n</code></pre>\n<p>More info in the\n<a href=\"https://lume.land/plugins/plaintext/\">plugin documentation page</a>.</p>\n<h2 id=\"better-control-of-the-generated-css-code\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/#better-control-of-the-generated-css-code\" class=\"header-anchor\">Better control of the generated CSS code</a></h2>\n<p>Some plugins like <a href=\"https://lume.land/plugins/google_fonts/\"><code>google_fonts</code></a>,\n<a href=\"https://lume.land/plugins/prism/\"><code>prism</code></a> or\n<a href=\"https://lume.land/plugins/code_highlight/\"><code>code_highlight</code></a> can generate CSS\ncode. The way this code is generated is different for each plugin.</p>\n<p>Google Fonts plugin has the <code>cssFile</code> option to configure the filename to output\nthe CSS code. If the css file doesn't exist, it's created. If it already exists,\nthe code is appended at the end. You can use the <code>placeholder</code> option to insert\nthe code at some point in the middle of the file.</p>\n<pre><code class=\"language-js\">site.use(googleFonts({\n  cssFile: &quot;styles.css&quot;,\n  placeholder: &quot;/* insert-google-fonts-here */&quot;,\n  // ...more options\n}));\n</code></pre>\n<p>Prism and Code Highlight plugins output the theme's CSS code differently. The\n<code>theme</code> option has the <code>path</code> property but it's not the <strong>output</strong> css file but\nthe <strong>source file</strong>:</p>\n<pre><code class=\"language-js\">site.use(prism({\n  theme: {\n    name: &quot;funky&quot;,\n    path: &quot;/_includes/css/code_theme.css&quot;,\n  },\n}));\n</code></pre>\n<p>This means that if you set the path as <code>/_includes/css/code_theme.css</code>, this\nfile must be imported somewhere in your CSS code in order to be visible:</p>\n<pre><code class=\"language-css\">@import &quot;css/code_theme.css&quot;;\n</code></pre>\n<p>The problem with this approach is it requires two steps: first, configure the\nsource file name in the plugin, and then import the file in your CSS file (or\ncopy it with <code>site.copy()</code>).</p>\n<p>The Google fonts approach is more straightforward.</p>\n<p>In order to make Lume more consistent across all plugins, I want to unify the\nway the CSS code is generated everywhere. That's why the <code>theme.path</code> option of\nPrism and Code Highlight plugins are now deprecated and the new <code>theme.cssFile</code>\nand <code>theme.placeholder</code> options were added.</p>\n<pre><code class=\"language-js\">site.use(prism({\n  theme: {\n    name: &quot;funky&quot;,\n    cssFile: &quot;styles.css&quot;,\n    placeholder: &quot;/* prism-theme-here */&quot;,\n  },\n}));\n</code></pre>\n<p>This change is also aligned with the\n<a href=\"https://lume.land/docs/configuration/config-file/#components-options\"><code>components.placeholder</code> option</a>\nintroduced in Lume 2.4.</p>\n<h2 id=\"other-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-5-0-pedro-munho/#other-changes\" class=\"header-anchor\">Other changes</a></h2>\n<ul>\n<li>Added <code>subset</code> options to\n<a href=\"https://lume.land/plugins/google_fonts/\">Google fonts</a> plugin.</li>\n<li>Added <code>ui.globalVariable</code> option to\n<a href=\"https://lume.land/plugins/pagefind/\">Pagefind</a> plugin to store the pagefind\ninstance in a global variable for future manipulation.</li>\n<li>Hot reload inline script includes the integrity hash, to avoid CSP issues.</li>\n<li>Files with extension <code>.d.ts</code> are ignored by Lume, to avoid generating empty\nfiles.</li>\n<li>Updated the default browser versions supported by\n<a href=\"https://lume.land/plugins/lightningcss/\">LightningCSS</a> plugin.</li>\n</ul>\n<p>See\n<a href=\"https://github.com/lumeland/lume/blob/v2.5.0/CHANGELOG.md\">the CHANGELOG.md file</a>\nto see a list of all changes with more detail.</p>\n","date_published":"Sat, 11 Jan 2025 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/","url":"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/","title":"Lume 2.4.0 - Maruja Mallo","content_html":"<p>Ola 👋!</p>\n<p>This new version of Lume is dedicated to\n<a href=\"https://en.wikipedia.org/wiki/Maruja_Mallo\">Maruja Mallo,</a> an extraordinary\nsurrealist painter born in Galicia in 1902 who gained international fame.\n<a href=\"https://edspace.american.edu/marujamalloheadsofwomen/maruja-mallo-biography/\">Learn more about Maruja</a>.</p>\n<!-- more -->\n<h2 id=\"new-plugin%3A-check_urls\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#new-plugin%3A-check_urls\" class=\"header-anchor\">New plugin: <code>check_urls</code></a></h2>\n<p>Broken links are one of the biggest issues on the Web. A recent study detected\nthat\n<a href=\"https://medium.com/@tonywangcn/27-6-of-the-top-10-million-sites-are-dead-6bc7805efa85\">27.6% of the top 10 million sites are dead</a>.\nAnd for those sites that are still alive, they are likely to change the URLs at\nsome point, after a redesign or content updates, causing a lot of broken links.</p>\n<p>The new plugin <code>check_urls</code> will help you to keep your links healthy, by\nchecking all links in your website (not only to HTML pages but also files like\nimages, JavaScript or CSS). This plugin already existed for some time as\n<a href=\"https://github.com/lumeland/experimental-plugins\">experimental plugin</a> thanks\nto <a href=\"https://github.com/iacore\">iacore</a>, but it was moved to the main Lume repo\nand was improved with additional features.</p>\n<p>The basic way to use it is like any other plugin. No big surprises here!</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport checkUrls from &quot;lume/plugins/check_urls.ts&quot;;\n\nconst site = lume();\nsite.use(checkUrls());\n\nexport default site;\n</code></pre>\n<p>The default configuration will check all your internal links and warns you when\na broken link is found. This plugin is compatible with\n<a href=\"https://lume.land/plugins/redirects/\">redirects</a>: when a link to a non-existing\npage is found, but it redirects to an existing page, the url is valid.</p>\n<h3 id=\"strict-mode\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#strict-mode\" class=\"header-anchor\">Strict mode</a></h3>\n<p>There's a mode for a more <em>strict</em> detection:</p>\n<pre><code class=\"language-js\">site.use(checkUrls({\n  strict: true,\n}));\n</code></pre>\n<p>In the <em>strict</em> mode the <strong>redirects are not allowed,</strong> all links must go to the\nfinal page. This also affects to the trailing slashes: for example <code>/about-me</code>\nis invalid but <code>/about-me/</code> is valid.</p>\n<h3 id=\"external-urls\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#external-urls\" class=\"header-anchor\">External URLs</a></h3>\n<p>By default, the plugin only checks internal links. But you can configure it to\ncheck links to external domains:</p>\n<pre><code class=\"language-js\">site.use(checkUrls({\n  external: true,\n}));\n</code></pre>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\">Warning</p>\n<p>This option can make the build slower, specially if you have many external\nlinks, so probably it's a good idea to enable it only occasionally.</p>\n</div>\n<p>Learn more about this plugin\n<a href=\"https://lume.land/plugins/check_urls/\">in the documentation page</a>.</p>\n<h2 id=\"new-plugin%3A-icons\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#new-plugin%3A-icons\" class=\"header-anchor\">New plugin: <code>icons</code></a></h2>\n<p>Nowadays, most websites are using icons to a greater or lesser extent. The\n<code>icons</code> plugin allows to use some of the most popular SVG icon libraries. The\ninstallation can't be easier:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport icons from &quot;lume/plugins/icons.ts&quot;;\n\nconst site = lume();\nsite.use(icons());\n\nexport default site;\n</code></pre>\n<p>To import an icon, just use the <code>icon</code> filter which returns the path of the\nicon's svg file.</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;{{ &quot;acorn&quot; |&gt; icon(&quot;phosphor&quot;) }}&quot;&gt;\n</code></pre>\n<p>Lume will download the &quot;acorn&quot; icon from the popular\n<a href=\"https://phosphoricons.com/\">Phosphor</a> library into <code>/icons/phosphor/acorn.svg</code>\n(the output folder is configurable) and return the path.</p>\n<p>Some icons have different variations that you can configure with the\n<code>name:variation</code> syntax:</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;{{ &quot;acorn:duotone&quot; |&gt; icon(&quot;phosphor&quot;) }}&quot;&gt;\n</code></pre>\n<p>Alternatively, you can set the variation in the second argument of the filter:</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;{{ &quot;acorn&quot; |&gt; icon(&quot;phosphor&quot;, &quot;duotone&quot;) }}&quot;&gt;\n</code></pre>\n<p>You can use <a href=\"https://lume.land/plugins/inline/\"><code>inline</code> plugin</a> to inline the\nSVG code in the HTML.</p>\n<pre><code class=\"language-html\">&lt;img src=&quot;{{ &quot;acorn&quot; |&gt; icon(&quot;phosphor&quot;) }}&quot; inline&gt;\n</code></pre>\n<p>The icon plugin supports the following icon collections and it's easily\nextensible with more.</p>\n<ul>\n<li><a href=\"https://ant.design/components/icon\">Ant</a></li>\n<li><a href=\"https://icons.getbootstrap.com/\">Bootstrap</a></li>\n<li><a href=\"https://boxicons.com/\">Boxicons</a></li>\n<li><a href=\"https://react.fluentui.dev/?path=/docs/icons-catalog--docs\">Fluent</a></li>\n<li><a href=\"https://heroicons.com/\">Heroicons</a></li>\n<li><a href=\"https://iconoir.com/\">Iconoir</a></li>\n<li><a href=\"https://lucide.dev/\">Lucide</a></li>\n<li><a href=\"https://fonts.google.com/icons?icon.set=Material+Icons\">Material Icons</a></li>\n<li><a href=\"https://fonts.google.com/icons?icon.set=Material+Symbols\">Material Symbols</a></li>\n<li><a href=\"https://www.mingcute.com/\">Mingcute</a></li>\n<li><a href=\"https://mynaui.com/icons\">Myna</a></li>\n<li><a href=\"https://primer.style/foundations/icons\">Octicons</a></li>\n<li><a href=\"https://openmoji.org/\">Openmoji</a></li>\n<li><a href=\"https://phosphoricons.com/\">Phosphor</a></li>\n<li><a href=\"https://remixicon.com/\">Remix icons</a></li>\n<li><a href=\"https://sargamicons.com/\">Sargam</a></li>\n<li><a href=\"https://simpleicons.org/\">Simpleicons</a></li>\n<li><a href=\"https://tabler.io/icons\">Tabler</a></li>\n</ul>\n<p>Learn more about this plugin\n<a href=\"https://lume.land/plugins/icons/\">in the documentation page</a>.</p>\n<h2 id=\"new-plugin-google_fonts\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#new-plugin-google_fonts\" class=\"header-anchor\">New plugin <code>google_fonts</code></a></h2>\n<p>Another common asset used to build sites is webfonts.\n<a href=\"https://fonts.google.com/\">Google Fonts</a> is a fantastic resource for open\nsource fonts, but loading the fonts from the Google Fonts CDN is not the best\noption, not only for privacy and GDPR compliance, but also\n<a href=\"https://github.com/HTTPArchive/almanac.httparchive.org/pull/607\">for performance</a>.</p>\n<p>The <code>google_fonts</code> plugin downloads the optimized font files from Google fonts\nautomatically into the <code>/fonts</code> directory (configurable) and generates the\n<code>/fonts.css</code> file (also configurable) with the <code>@font-face</code> declarations.</p>\n<p>To use it, just register the plugin passing the sharing URL of your font\nselection. For example, let's say we want to use\n<a href=\"https://fonts.google.com/share?selection.family=Playfair+Display:ital,wght@0,400..900;1,400..900\">Playfair Display</a>:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport googleFonts from &quot;lume/plugins/google_fonts.ts&quot;;\n\nconst site = lume();\n\nsite.use(googleFonts({\n  fonts:\n    &quot;https://fonts.google.com/share?selection.family=Playfair+Display:ital,wght@0,400..900;1,400..900&quot;,\n}));\n\nexport default site;\n</code></pre>\n<p>It's possible to rename the fonts, useful if you want to change a font without\nchanging the code:</p>\n<pre><code class=\"language-js\">site.use(googleFonts({\n  fonts: {\n    display: &quot;https://fonts.google.com/share?selection.family=Playfair+Display:ital,wght@0,400..900;1,400..900&quot;,\n    text: &quot;https://fonts.google.com/share?selection.family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&quot;\n}));\n</code></pre>\n<p>In the example above, the <strong>Playfair Display</strong> font is renamed to &quot;display&quot; and\n<strong>Roboto</strong> to &quot;text&quot;, so this allows the use of the fonts in the CSS code with\nthese names:</p>\n<pre><code class=\"language-css\">h1 {\n  font-family: display;\n}\nbody {\n  font-family: text;\n}\n</code></pre>\n<p><a href=\"https://lume.land/plugins/google_fonts/\">Go to the documentation page</a> to learn\nmore about the Google Fonts plugin!</p>\n<h2 id=\"new-plugins-brotli-and-gzip\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#new-plugins-brotli-and-gzip\" class=\"header-anchor\">New plugins <code>brotli</code> and <code>gzip</code></a></h2>\n<p>Thanks to <a href=\"https://github.com/into-the-v0id\">Into the V0id</a> for adding these two\nplugins to Lume. They are useful for compressing text-based files (like HTML,\nJavaScript, SVG, or CSS files) using the Gzip and Brotli algorithms and output\nfiles with the same name but with <code>.gz</code> or <code>.br</code> extensions. For example, in\naddition to the <code>/index.html</code> page, the plugins generate also <code>/index.html.gz</code>\n(for Gzip) and <code>/index.html.br</code> (for Brotli).</p>\n<p>I think it's not necessary to show how to activate the plugin, but just to\ndemonstrate how predictable and &quot;boring&quot; Lume is:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport brotli from &quot;lume/plugins/brotli.ts&quot;;\n\nconst site = lume();\n\nsite.use(brotli());\n\nexport default site;\n</code></pre>\n<h3 id=\"new-precompress-middleware\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#new-precompress-middleware\" class=\"header-anchor\">New <code>precompress</code> middleware</a></h3>\n<p><code>brotli</code> and <code>gzip</code> plugins can be combined with\n<a href=\"https://lume.land/docs/core/server/#precompress\">the new <code>precompress</code> middleware</a>\nif you're using <a href=\"https://lume.land/docs/core/server/\">Lume server</a> to serve your\nstatic files (for example in Deno Deploy). This middleware checks the\n<code>Accept-Encoding</code> header and if the browser accepts <code>br</code> or <code>gzip</code> values, it\nwill serve the precompressed file.</p>\n<pre><code class=\"language-js\">import Server from &quot;lume/core/server.ts&quot;;\nimport precompress from &quot;lume/middlewares/precompress.ts&quot;;\n\nconst server = new Server();\n\nserver.use(precompress());\n\nserver.start();\n</code></pre>\n<p>Learn more about these plugins in the\n<a href=\"https://lume.land/plugins/brotli/\">brotli</a> and\n<a href=\"https://lume.land/plugins/gzip/\">gzip</a> documentation pages.</p>\n<h2 id=\"modify_urls-supports-css-files\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#modify_urls-supports-css-files\" class=\"header-anchor\"><code>modify_urls</code> supports CSS files</a></h2>\n<p>The <a href=\"https://lume.land/plugins/modify_urls/\"><code>modify_urls</code> plugin</a> now can\nsearch and modify urls in CSS files. This is not important only for this plugin\nbut also for other plugins that use <code>modify_urls</code> under the hood, like\n<a href=\"https://lume.land/plugins/base_path/\"><code>base_path</code></a> and\n<a href=\"https://lume.land/plugins/relative_urls/\"><code>relative_urls</code></a>.</p>\n<h3 id=\"example-with-base_path\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#example-with-base_path\" class=\"header-anchor\">Example with <code>base_path</code></a></h3>\n<p><code>base_path</code> is one of Lume's most useful plugins because it adds a prefix to all\nabsolute URLs of your site. This is important if your site is hosted in a\nsubdirectory.</p>\n<p>For example, let's say you want to host your blog in the location\n<code>https://my-site.com/blog/</code> and you have this HTML code:</p>\n<pre><code class=\"language-html\">&lt;a href=&quot;/posts/hello-world/&quot;&gt;Hello world&lt;/a&gt;\n</code></pre>\n<p>The plugin automatically fixes the URL to add the <code>/blog/</code> prefix:</p>\n<pre><code class=\"language-html\">&lt;a href=&quot;/blog/posts/hello-world/&quot;&gt;Hello world&lt;/a&gt;\n</code></pre>\n<p>Until now, the plugin only transformed URLs in HTML pages. If your site has this\nCSS code:</p>\n<pre><code class=\"language-css\">.background {\n  background-image: url(&quot;/img/bg.png&quot;);\n}\n</code></pre>\n<p>The background image will fail because the <code>/blog/</code> prefix is missing. As of\nLume 2.4.0, this plugin can transform also CSS files. This option is disabled by\ndefault, it requires to configure it in the _config.ts file:</p>\n<pre><code class=\"language-js\">site.use(basePath({\n  extensions: [&quot;.html&quot;, &quot;.css&quot;],\n}));\n</code></pre>\n<p>Now not only HTML pages but also CSS files will be processed:</p>\n<pre><code class=\"language-css\">.background {\n  background-image: url(&quot;/blog/img/bg.png&quot;);\n}\n</code></pre>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\">Important</p>\n<p>Keep in mind that Lume only processes files that are loaded. To transform CSS\nfiles they must be loaded before. If you're using any styling plugin like\n<a href=\"https://lume.land/plugins/postcss/\"><code>postcss</code></a>,\n<a href=\"https://lume.land/plugins/lightningcss/\"><code>lightningcss</code></a>, or\n<a href=\"https://lume.land/plugins/sass/\"><code>sass</code></a>, you don't need to do anything else.\nBut if you are copying the css files with <code>site.copy([&quot;.css&quot;])</code> or\n<code>site.copy(&quot;/styles&quot;)</code> they won't be processed. To fix it, you have to use\n<code>site.loadAssets([&quot;.css&quot;])</code>.</p>\n</div>\n<h2 id=\"fallbacks-for-metas-and-feed-plugins\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#fallbacks-for-metas-and-feed-plugins\" class=\"header-anchor\">Fallbacks for <code>metas</code> and <code>feed</code> plugins</a></h2>\n<p>Some plugins like <code>metas</code> and <code>feed</code> allow to\n<a href=\"https://lume.land/plugins/metas/#field-aliases\">define aliases</a> to other\nvariables. For example, if we want to use the variable <code>title</code> inside\n<code>metas.title</code>:</p>\n<pre><code class=\"language-yml\">title: Page title\nmetas:\n  title: =title\n</code></pre>\n<p>As of Lume 1.4, it's possible to define fallbacks to other variables or provide\na default variable:</p>\n<pre><code class=\"language-yml\">metas:\n  title: =title || =header.title || Default title\n</code></pre>\n<p>In this example, the title used in metas is the <code>title</code> variable. If it's not\ndefined, the <code>header.title</code> variable is used. And if it's doesn't exist, the\nstring &quot;Default title&quot; will be used.</p>\n<h2 id=\"support-for-author-in-feed-plugin\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#support-for-author-in-feed-plugin\" class=\"header-anchor\">Support for author in <code>feed</code> plugin</a></h2>\n<p>In addition to fallbacks, the <a href=\"https://lume.land/plugins/feed/\"><code>feed</code> plugin</a>\nhas added support for the author name and author URL variables:</p>\n<pre><code class=\"language-js\">site.use(feed({\n  output: [&quot;/posts.rss&quot;, &quot;/posts.json&quot;],\n  query: &quot;type=post&quot;,\n  info: {\n    title: &quot;=site.title&quot;,\n    description: &quot;=site.description&quot;,\n    authorName: &quot;=site.author.name&quot;,\n    authorUrl: &quot;=site.author.url&quot;,\n  },\n  items: {\n    title: &quot;=title&quot;,\n    description: &quot;=excerpt&quot;,\n    authorName: &quot;=author.name&quot;,\n    authorUrl: &quot;=author.url&quot;,\n  },\n}));\n</code></pre>\n<h2 id=\"other-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/#other-changes\" class=\"header-anchor\">Other changes</a></h2>\n<ul>\n<li>Several improvements to <a href=\"https://lume.land/plugins/esbuild/\"><code>esbuild</code> plugin</a>\nby <a href=\"https://github.com/into-the-v0id\">Into the V0id</a>.</li>\n<li>Added the new variable <code>fediverse</code> to the\n<a href=\"https://lume.land/plugins/metas/\"><code>metas</code> plugin</a>, to generate the\n<code>&lt;meta name=&quot;fediverse:creator&quot; content=&quot;...&quot;&gt;</code> tag\n<a href=\"https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/\">added to Mastodon</a>.</li>\n<li>Fixed some bugs related to Windows support and CJK characters.</li>\n<li>New option <code>placeholder</code> in the\n<a href=\"https://lume.land/plugins/unocss/\"><code>unocss</code> plugin</a> to insert the generated\ncode in a specific place.</li>\n<li>New option <code>placeholder</code> in the\n<a href=\"https://lume.land/docs/core/components/\">components configuration</a> to insert\nthe generated CSS and JavaScript code in a specific place.</li>\n<li>Updated all dependencies to their latest version.</li>\n</ul>\n<p>And more changes. See the\n<a href=\"https://github.com/lumeland/lume/blob/v2.4.0/CHANGELOG.md\">CHANGELOG file</a> for\nmore details.</p>\n<p><strong>Happy Luming!</strong></p>\n","date_published":"Wed, 06 Nov 2024 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/","url":"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/","title":"Lume 2.3.0 - Andrés do Barro","content_html":"<p>Lume 2.3.0 is dedicated to\n<a href=\"https://en.wikipedia.org/wiki/Andr%C3%A9s_do_Barro\">Andrés do Barro</a>, a\nGalician singer and songwriter who was one of the first artists who achieved\nsuccess singing in Galego out of Galicia. Among his songs, we can find\n<a href=\"https://www.youtube.com/watch?v=4feqklaMDR8\">Pandeirada</a> and\n<a href=\"https://www.youtube.com/watch?v=CUAOwBknH5I\">O trén</a>.</p>\n<!--more -->\n<h2 id=\"tl%2Fdr\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#tl%2Fdr\" class=\"header-anchor\">TL/DR</a></h2>\n<p>If you are using the <code>nav</code> plugin, <a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#nav-plugin-changes\">see this</a>.</p>\n<h2 id=\"new-function-parsebasename\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#new-function-parsebasename\" class=\"header-anchor\">New function <code>parseBasename</code></a></h2>\n<p>When Lume loads a page file, the basename (the filename after removing the\nextension) is parsed to extract additional data. This feature makes it possible\nthat, for instance, if the basename starts with <code>yyyy-mm-dd_*</code>, Lume extracts\nthis value\n<a href=\"https://lume.land/docs/creating-pages/page-files/#page-date\">to set the page date</a>,\nand remove it from the final name, so the file <code>/2020-06-21_hello-world.md</code>\ngenerates the page <code>/hello-world/</code>.</p>\n<p>As of Lume 2.3.0, you can add additional parsers with the new function\n<code>site.parseBasename</code>.</p>\n<p>Let's say we want to use a variable named <code>order</code> to sort pages in a menu, and\nwe want to extract this value from the file name. For example the file\n<code>/12.hello-world.md</code> outputs the page <code>/hello-world/</code> and sets <code>12</code> as the\n<code>order</code> variable. We can achieve this with the following function:</p>\n<pre><code class=\"language-js\">site.parseBasename((basename) =&gt; {\n  // Regexp to detect the order pattern\n  const match = basename.match(/(\\d+)\\.(.+)/);\n\n  if (match) {\n    const [, order, basename] = match;\n\n    // Return the order value and the new basename without the prefix\n    return {\n      order: parseInt(order),\n      basename,\n    };\n  }\n});\n</code></pre>\n<p>As you can see, the function is simple: it receives the basename and return an\nobject with the parsed values. Note that the returned object contains the\nbasename without the prefix, in order to be removed from the final URL.</p>\n<p>See more info in the\n<a href=\"https://lume.land/docs/core/basename-parsers/\">documentation page</a>.</p>\n<h2 id=\"restart-after-modifying-the-_config.ts-and-_cms.ts-files\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#restart-after-modifying-the-_config.ts-and-_cms.ts-files\" class=\"header-anchor\">Restart after modifying the <code>_config.ts</code> and <code>_cms.ts</code> files</a></h2>\n<p>Until now, if you modify the <code>_config.ts</code> file during the server mode, you must\nstop the process and start it again to see the changes. This is very\ninconvenient, especially in the early phases of development, when you may want\nto try different plugins or make changes to the Lume configuration.</p>\n<p>From now on, the building process is run inside a Worker. This change allows us\nto stop the build and restart it again without stopping the main process (under\nthe hood, it is done by closing the Worker and creating a new one).</p>\n<p>For now, the rebuild is triggered every time a change in the <code>_config</code> file is\ndetected. In the next versions, we can add additional triggers.</p>\n<p>In CMS mode (with <code>deno task cms</code>), the process is also restarted if the\n<code>_cms.ts</code> file is modified, which is useful when you are configuring the\ndocuments and collections.</p>\n<h2 id=\"new-sorting-methods-asc-locale-and-desc-locale\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#new-sorting-methods-asc-locale-and-desc-locale\" class=\"header-anchor\">New sorting methods <code>asc-locale</code> and <code>desc-locale</code></a></h2>\n<p>When searching pages using the\n<a href=\"https://lume.land/plugins/search/\"><code>search</code> helper</a>, you can sort them by any\nfield, for example, the title:</p>\n<pre><code class=\"language-js\">const pages = search.pages(&quot;type=post&quot;, &quot;title=asc&quot;);\n</code></pre>\n<p>Under the hood, Lume sorts the pages with\n<a href=\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort\">array sort</a>\nusing basic comparison operators (<code>&gt;</code> and <code>&lt;</code>):</p>\n<pre><code class=\"language-js\">pages.sort((a, b) =&gt; a == 0 ? 0 : a.title &gt; b.title ? 1 : -1);\n</code></pre>\n<p>This works fine in many cases, but not when you have strings with accents,\ndifferent cases, etc. In these cases\n<a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare\">localeCompare</a>\nworks much better. In this version, we have introduced two new locale methods:\n<code>asc-locale</code> and <code>desc-locale</code>. So the previous example can be improved with:</p>\n<pre><code class=\"language-js\">const pages = search.pages(&quot;type=post&quot;, &quot;title=asc-locale&quot;);\n</code></pre>\n<h2 id=\"new-plugin-sri\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#new-plugin-sri\" class=\"header-anchor\">New plugin <code>SRI</code></a></h2>\n<p><abbr>SRI</abbr> (Subresource Integrity) is a browser feature to protect your\nsite and your users from compromised code loaded from external CDN. It verifies\nthe code loaded by the browser is exactly the same code that you got during the\nbuild process, without unexpected manipulations. You can\n<a href=\"https://developer.mozilla.org/en-US/blog/securing-cdn-using-sri-why-how/\">learn more about SRI in the MDN article</a>.</p>\n<p>Lume had\n<a href=\"https://github.com/lumeland/experimental-plugins\">an experimental SRI plugin</a>\nthat has been moved to the main repo, so now it's part of the official plugins\ncollection:</p>\n<pre><code class=\"language-ts\">import lume from &quot;lume/mod.ts&quot;;\nimport sri from &quot;lume/plugins/sri.ts&quot;;\n\nconst site = lume();\nsite.use(sri());\n\nexport default site;\n</code></pre>\n<p>The plugin searches for <code>&lt;script&gt;</code> and <code>&lt;link rel=&quot;stylesheet&quot;&gt;</code> elements in\nyour pages that load resources from other domains and add the <code>integrity</code> and\n<code>crossorigin</code> attributes automatically. For example, if you have this code:</p>\n<pre><code class=\"language-html\">&lt;script src=&quot;https://code.jquery.com/jquery-3.7.0.slim.min.js&quot;&gt;&lt;/script&gt;\n</code></pre>\n<p>The plugin outputs the following:</p>\n<pre><code class=\"language-html\">&lt;script\n  src=&quot;https://code.jquery.com/jquery-3.7.0.slim.min.js&quot;\n  integrity=&quot;sha256-tG5mcZUtJsZvyKAxYLVXrmjKBVLd6VpVccqz/r4ypFE=&quot;\n  crossorigin=&quot;anonymous&quot;\n&gt;&lt;/script&gt;\n</code></pre>\n<p>See more info\n<a href=\"https://lume.land/plugins/sri/\">in the plugin documentation page</a>.</p>\n<h2 id=\"nav-plugin-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#nav-plugin-changes\" class=\"header-anchor\"><code>nav</code> plugin changes</a></h2>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\">Important</p>\n<p>In this version, the nav plugin got a <strong>small BREAKING CHANGE</strong> (sorry for\nthat).</p>\n</div>\n<p>The <a href=\"https://lume.land/plugins/nav/\">nav plugin</a> is useful for creating menus at\nmultiple levels.</p>\n<h3 id=\"breaking-change%3A-changed-the-tree-data-interface\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#breaking-change%3A-changed-the-tree-data-interface\" class=\"header-anchor\">BREAKING CHANGE: Changed the tree data interface</a></h3>\n<p>The <code>nav.menu()</code> function returns an object tree using the pages' URL. Every\nobject in the tree is a page or a directory and in previous Lume versions, it\ncould have the following properties:</p>\n<ul>\n<li><code>item.slug</code> The name of the page or folder.</li>\n<li><code>item.data</code> If the element is a page, this is the data object of the page. If\nit's a folder, this value is undefined.</li>\n<li><code>item.children</code> An array of sub-pages and sub-folders.</li>\n</ul>\n<p>These properties didn't fit well to order the elements, especially the\nsub-folder items. In the new structure, <s>the <code>slug</code> property has been removed</s>\n(<strong>Edit:</strong> it was restored in Lume 2.3.1 due some bugs) and this value is stored\nin <code>data.basename</code>.</p>\n<p>This change affects to how this tree is iterated in your template. For instance,\nif in Lume 2.2 we have the following code:</p>\n<pre><code class=\"language-js\">if (item.data) {\n  // item.data exists, it's a page\n  return `&lt;a href=&quot;{{ item.data.url }}&quot;&gt;{{ item.data.title }}&lt;/a&gt;`;\n} else {\n  // It's a folder\n  return `&lt;strong&gt;{{ item.slug }}&lt;/strong&gt;`;\n}\n</code></pre>\n<p>With the changes in Lume 2.3, the code must be changed to:</p>\n<pre><code class=\"language-js\">if (item.data.url) {\n  // item.data.url exists, it's a page\n  return `&lt;a href=&quot;{{ item.data.url }}&quot;&gt;{{ item.data.title }}&lt;/a&gt;`;\n} else {\n  // It's a folder\n  return `&lt;strong&gt;{{ item.slug }}&lt;/strong&gt;`;\n}\n</code></pre>\n<p>Now both pages and folder items store the <code>basename</code> in the same place\n(<code>data.basename</code>), and it's easy to sort the elements alphabetically:</p>\n<pre><code class=\"language-js\">const menu = nav.menu(&quot;/&quot;, &quot;&quot;, &quot;basename=asc&quot;);\n</code></pre>\n<p>And even use the new locale sorting methods:</p>\n<pre><code class=\"language-js\">const menu = nav.menu(&quot;/&quot;, &quot;&quot;, &quot;basename=asc-locale&quot;);\n</code></pre>\n<h3 id=\"added-functions-to-get-the-next-and-previous-pages\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#added-functions-to-get-the-next-and-previous-pages\" class=\"header-anchor\">Added functions to get the next and previous pages</a></h3>\n<p>In this version the functions <code>nav.nextPage()</code> and <code>navPreviousPage()</code> have been\nadded, to ease the navigation to the next and previous pages.</p>\n<p>For example, let's say we have created the following tree structure with the\nfunction <code>nav.menu()</code>:</p>\n<pre><code class=\"language-txt\">docs\n  |__ getting-started\n        |__ installation\n        |__ configuration\n  |__ plugins\n        |__ prettier\n</code></pre>\n<p>The new function <code>nav.nextPage</code> returns the next page relative to the provided\nURL. For example:</p>\n<pre><code class=\"language-js\">const nextPage = nav.nextPage(&quot;/docs/getting-started/installation/&quot;);\nconsole.log(nextPage.url); // /docs/getting-started/configuration/\n</code></pre>\n<p>If the page is the last sibling of the current section, it returns the first\npage of the next section:</p>\n<pre><code class=\"language-js\">const nextPage = nav.nextPage(&quot;/docs/getting-started/configuration/&quot;);\nconsole.log(nextPage.url); // /docs/plugins/\n</code></pre>\n<p>If the current section has children, it returns the first child:</p>\n<pre><code class=\"language-js\">const nextPage = nav.nextPage(&quot;/docs/plugins/&quot;);\nconsole.log(nextPage.url); // /docs/plugins/prettier/\n</code></pre>\n<p>The <code>nav.previousPage()</code> works similarly but in reverse order.</p>\n<h2 id=\"other-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/#other-changes\" class=\"header-anchor\">Other changes</a></h2>\n<ul>\n<li>\n<p>Plugins and middlewares can be imported using named imports, in addition to\nthe default exports:</p>\n<pre><code class=\"language-js\">import basePath from &quot;lume/plugins/base_path.ts&quot;;\n// it's the same as\nimport { basePath } from &quot;lume/plugins/base_path.ts&quot;;\n</code></pre>\n</li>\n<li>\n<p>Several bug fixes and improvements have made to the watcher and live reload.</p>\n</li>\n</ul>\n<p>And there are many more changes that you can see in the\n<a href=\"https://github.com/lumeland/lume/blob/v2.3.0/CHANGELOG.md\">CHANGELOG file</a>.</p>\n","date_published":"Fri, 30 Aug 2024 00:00:00 GMT"},{"id":"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/","url":"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/","title":"Lume 2.2.0 - Luísa Villalta","content_html":"<p>From now on, every new minor version of Lume will be dedicated to a relevant\ngalician person. Today —17 May— is the\n<a href=\"https://en.wikipedia.org/wiki/Galician_Literature_Day\">Galician Literature Day</a>\nand this year it was honored to the great poet\n<a href=\"https://galicianliterature.com/villalta/\"><strong>Luísa Villalta</strong></a>. This Lume\nversion is dedicated to her.</p>\n<!--more -->\n<h2 id=\"tl%2Fdr\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#tl%2Fdr\" class=\"header-anchor\">TL/DR</a></h2>\n<p>Cases requiring some manual changes after updating to Lume 2.2:</p>\n<ul>\n<li>If you have a <code>_cache</code> folder in your <code>src</code> directory,\n<a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#_cache-folder-relative-to-root-directory\">see this</a>.</li>\n<li>If you importing LumeCMS from <code>/lume/cms.ts</code>, <a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#cms-import-changes\">see this</a>.</li>\n</ul>\n<h2 id=\"esbuild-improvements\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#esbuild-improvements\" class=\"header-anchor\">Esbuild improvements</a></h2>\n<p>The <a href=\"https://lume.land/plugins/esbuild/\">Esbuild plugin</a> got the following\nimprovements:</p>\n<ul>\n<li>\n<p><strong>JSR support:</strong> Now you can use <a href=\"https://jsr.io/\"><code>jsr:</code></a> specifiers in your\ncode. They are handled using esm.sh (the same as <code>npm:</code> specifiers).</p>\n</li>\n<li>\n<p><strong>Fixed <code>npm:</code> resolution.</strong> In previous versions, importing a bare specifier\nmapped to <code>npm:</code> like the following would fail:</p>\n<pre><code class=\"language-json\">{\n  &quot;imports&quot;: {\n    &quot;bar&quot;: &quot;npm:bar&quot;\n  }\n}\n</code></pre>\n<pre><code class=\"language-js\">import foo from &quot;bar&quot;;\n</code></pre>\n<p>This has been fixed and it works fine now.</p>\n</li>\n<li>\n<p><strong>Using esbuild without bundler:</strong> Let's say you want to compile the following\ncode with the <code>bundle</code> option to <code>false</code>:</p>\n<pre><code class=\"language-js\">import foo from &quot;./bar.ts&quot;;\n</code></pre>\n<p>Esbuild converts <code>bar.ts</code> to <code>bar.js</code>, but\n<a href=\"https://github.com/evanw/esbuild/issues/2435\">it doesn't change the file extension in the import</a>.\nThe Lume plugin tries to fix this.\n<a href=\"https://github.com/lumeland/lume/issues/594\">More info</a>.</p>\n</li>\n</ul>\n<h2 id=\"cms-import-changes\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#cms-import-changes\" class=\"header-anchor\">CMS import changes</a></h2>\n<p><a href=\"https://lume.land/cms/\">LumeCMS</a> is a simple CMS to manage the content of the\nsites. To configure it, you have to create the <code>_cms.ts</code> file and import the CMS\nfrom the <code>lume/cms.ts</code> specifier:</p>\n<pre><code class=\"language-js\">import lumeCMS from &quot;lume/cms.ts&quot;;\n\nconst cms = lumeCMS();\n\n// Configuration here\n\nexport default cms;\n</code></pre>\n<p>The file <code>lume/cms.ts</code>, provided by Lume, exports everything you need from\nLumeCMS. But this has also the following issues:</p>\n<ul>\n<li>Lume and LumeCMS have different update paces. It's not easy to update LumeCMS\nif it's coupled to Lume.</li>\n<li>Providing everything in a single file makes Deno download all LumeCMS modules\neven those that you don't need. For example, GitHub storage has some NPM\ndependencies like Octokit that you may not need.</li>\n</ul>\n<p>The best way to import LumeCMS is using the import map. This is something that\n<a href=\"https://lume.land/docs/overview/installation/#setup-lume\">the <code>init</code> script</a>\nhas been doing for a while. For example:</p>\n<pre><code class=\"language-json\">{\n  &quot;imports&quot;: {\n    &quot;lume/&quot;: &quot;https://deno.land/x/lume@v2.2.0/&quot;,\n    &quot;lume/cms/&quot;: &quot;https://cdn.jsdelivr.net/gh/lumeland/cms@0.4.1/&quot;\n  }\n}\n</code></pre>\n<pre><code class=\"language-js\">import lumeCMS from &quot;lume/cms/mod.ts&quot;;\n\nconst cms = lumeCMS();\n\n// Configuration here\n\nexport default cms;\n</code></pre>\n<p>This allows you to import only the specific modules of Lume that you need and\nupdate LumeCMS at your own pace.</p>\n<p>I know this is a BREAKING CHANGE and sorry for that 🙏. But I believe the\ncurrent situation wasn't easy to maintain.</p>\n<h2 id=\"new-middleware-shutdown\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#new-middleware-shutdown\" class=\"header-anchor\">New middleware <code>shutdown</code></a></h2>\n<p>If you have a site on Deno Deploy that you want to shut down for reasons, this\nnew middleware can be useful:</p>\n<ul>\n<li>All HTML requests will return the content of the <code>/503.html</code> page with the\n<code>503</code> status code.</li>\n<li>It also sends the <code>Retry-After</code> header with the default value of 24 hours\n(customizable).</li>\n</ul>\n<pre><code class=\"language-js\">import Server from &quot;lume/core/server.ts&quot;;\nimport shutdown from &quot;lume/middlewares/shutdown.ts&quot;;\n\nconst server = new Server();\nserver.use(shutdown());\n\nserver.start();\n</code></pre>\n<h2 id=\"new-theme-option-for-prism-and-highlight.js\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#new-theme-option-for-prism-and-highlight.js\" class=\"header-anchor\">New <code>theme</code> option for Prism and Highlight.js</a></h2>\n<p>The plugins <code>prism</code> and <code>code_highlight</code> highlight the syntax of your code\nautomatically using the libraries <a href=\"https://prismjs.com/\">Prism</a> and\n<a href=\"https://highlightjs.org/\">Highlight.js</a> respectively. These libraries provide\nsome nice premade themes that you can use but need to be loaded manually.</p>\n<p>A new option <code>theme</code> was introduced to ease this step, so the themes are\ndownloaded automatically. The option works the same for both plugins, this is an\nexample with <code>prism</code>:</p>\n<pre><code class=\"language-js\">site.use(prism({\n  theme: {\n    name: &quot;funky&quot;,\n    path: &quot;_includes/css/code_theme.css&quot;,\n  },\n}));\n</code></pre>\n<p>The CSS file for the\n<a href=\"https://github.com/PrismJS/prism/blob/master/themes/prism-funky.css\">Funky theme</a>\nis downloaded automatically (using\n<a href=\"https://lume.land/docs/core/remote-files/\">remoteFile</a> under the hood) with the\nlocal path <code>_includes/css/code_theme.css</code> so you can import it in your CSS file\nwith:</p>\n<pre><code class=\"language-css\">@import &quot;css/code_theme.css&quot;;\n</code></pre>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p>If you're not using any CSS plugin (like postcss or lightningcss), you have to\nconfigure Lume to copy or load the file. Example:</p>\n</div>\n<pre><code class=\"language-js\">site.use(prism({\n  theme: {\n    name: &quot;funky&quot;,\n    path: &quot;/css/code_theme.css&quot;,\n  },\n}));\n\n// Copy the file\nsite.copy(&quot;/css/code_theme.css&quot;);\n</code></pre>\n<h2 id=\"extra-meta-tags\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#extra-meta-tags\" class=\"header-anchor\">Extra <code>meta</code> tags</a></h2>\n<p>The <a href=\"https://lume.land/plugins/metas/\"><code>metas</code> plugin</a> allows to include custom\nmeta tags. Useful to insert metas like <code>twitter:label1</code>, <code>twitter:data1</code>, etc.</p>\n<p>In addition to the regular values, the <code>metas</code> object accepts additional values\nthat are treated as custom meta tags. Example:</p>\n<pre><code class=\"language-yml\">title: Lume is awesome\nauthor: Dark Vader\nmetas:\n  title: =title\n  &quot;twitter:label1&quot;: Reading time\n  &quot;twitter:data1&quot;: 1 minute\n  &quot;twitter:label2&quot;: Written by\n  &quot;twitter:data1&quot;: =author\n</code></pre>\n<p>This configuration generates the following code:</p>\n<pre><code class=\"language-html\">&lt;meta name=&quot;title&quot; content=&quot;Lume is awesome&quot; /&gt;\n&lt;meta name=&quot;twitter:label1&quot; content=&quot;Reading time&quot; /&gt;\n&lt;meta name=&quot;twitter:data1&quot; content=&quot;1 minute&quot; /&gt;\n&lt;meta name=&quot;twitter:label2&quot; content=&quot;Written by&quot; /&gt;\n&lt;meta name=&quot;twitter:data2&quot; content=&quot;Dark Vader&quot; /&gt;\n</code></pre>\n<h2 id=\"feed-plugin-accepts-images\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#feed-plugin-accepts-images\" class=\"header-anchor\">Feed plugin accepts images</a></h2>\n<p>The <a href=\"https://lume.land/plugins/feed/\"><code>feed</code> plugin</a> has been extended with the\nnew <code>image</code> key that allows one to place an image per item. For example:</p>\n<pre><code class=\"language-js\">site.use(feed({\n  output: &quot;/feed.xml&quot;,\n  query: &quot;type=articles&quot;,\n  items: {\n    title: &quot;=title&quot;,\n    description: &quot;=excerpt&quot;,\n    image: &quot;=cover&quot;,\n  },\n}));\n</code></pre>\n<h2 id=\"liquid-deprecation\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#liquid-deprecation\" class=\"header-anchor\">Liquid deprecation</a></h2>\n<p><a href=\"https://lume.land/plugins/liquid/\">Liquid plugin</a> allows to use\n<a href=\"https://liquidjs.com/\">Liquidjs</a> library as a template engine in Lume.</p>\n<p>Liquidjs is a great library but it has a limitation incompatible with Lume:\n<a href=\"https://github.com/harttle/liquidjs/discussions/580\">it's not possible to invoke functions</a>.\nThis is very unfortunate because it's not possible to use the\n<a href=\"https://lume.land/docs/core/searching/\"><code>search</code> helper</a> inside a liquid\ntemplate to loop through the pages. For example, the following code doesn't\nwork:</p>\n<pre><code class=\"language-html\">&lt;ul&gt;\n  {% for item in search.pages('post') %}\n  &lt;li&gt;{{item.title}}&lt;/li&gt;\n  {% endfor %}\n&lt;/ul&gt;\n</code></pre>\n<p>Lume has support for Nunjucks which is a good replacement because has a very\nsimilar syntax to Liquid and allows you to run functions, so I decided to\ndeprecate the Liquid plugin and recommend Nunjucks instead. It will still be\navailable in Lume 2 but probably be removed in Lume 3 (in the distant future).</p>\n<h2 id=\"_cache-folder-relative-to-root-directory\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#_cache-folder-relative-to-root-directory\" class=\"header-anchor\"><code>_cache</code> folder relative to root directory</a></h2>\n<p>The <code>_cache</code> folder is created by some plugins like\n<a href=\"https://lume.land/plugins/transform_images/\"><code>transform_images</code></a> in the source\nfolder. For example, if your source folder is <code>./src/</code> the cache folder is\n<code>./src/_cache/</code>.</p>\n<p>As of Lume 2.2.0, this folder is created <strong>in the root directory</strong> (the same\ndirectory where the <code>_config.ts</code> file is). This makes its location more\npredictable, especially to add it to <code>.gitignore</code>.</p>\n<p>After updating Lume, if you are using a subdirectory as the source folder, the\n<code>_cache</code> folder should be moved to the root.</p>\n<h2 id=\"removed-nesting-plugin-in-postcss\" tabindex=\"-1\"><a href=\"https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#removed-nesting-plugin-in-postcss\" class=\"header-anchor\">Removed nesting plugin in PostCSS</a></h2>\n<p>The <a href=\"https://lume.land/plugins/postcss/\">postcss plugin</a> comes with the plugins\n<a href=\"https://www.npmjs.com/package/postcss-nesting\">postcss-nesting</a> and\n<a href=\"https://www.npmjs.com/package/autoprefixer\">autoprefixer</a> enabled by default.</p>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector\">CSS nesting</a>\nis now available in all browsers and this plugin is no longer needed, so it's no\nlonger enabled by default in Lume.</p>\n<p>If you still want to use it, you have to import it in the _config.ts file:</p>\n<pre><code class=\"language-js\">import lume from &quot;lume/mod.ts&quot;;\nimport postcss from &quot;lume/plugins/postcss.ts&quot;;\nimport nesting from &quot;npm:postcss-nesting&quot;;\n\nconst site = lume();\n\nsite.use(postcss({\n  plugins: [nesting()],\n}));\n\nexport default site;\n</code></pre>\n<hr>\n<p>And there are many more changes that you can see in the\n<a href=\"https://github.com/lumeland/lume/blob/v2.2.0/CHANGELOG.md\">CHANGELOG file.</a></p>\n","date_published":"Fri, 17 May 2024 00:00:00 GMT"}]}