<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:webfeeds="http://webfeeds.org/rss/1.0" version="2.0">
  <channel>
    <title>🔥 Updates</title>
    <link>https://lume.land/blog/</link>
    <atom:link href="https://lume.land/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>A blog to follow the updates of Lume, the static site generator for Deno</description>
    <lastBuildDate>Mon, 09 Feb 2026 18:43:28 GMT</lastBuildDate>
    <language>en</language>
    <generator>Lume 3.1.4</generator>
    <item>
      <title>Lume 3.2.0 - Rosalía</title>
      <link>https://lume.land/blog/posts/lume-3-2-0/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/lume-3-2-0/</guid>
      <content:encoded>
        <![CDATA[<p>So, you thought that Lume could only generate static sites, right?</p>
<p><strong>Not anymore!</strong> As of version 3.2, Lume can also <strong>create books</strong> in EPUB
format. That's why I wanted to dedicate this version to one of the most
important figures of Galician literature of all time: <strong>Rosalía de Castro</strong>.</p>
<!--more-->
<p>You may know <a href="https://en.wikipedia.org/wiki/Rosal%C3%ADa">Rosalía</a>, the popular
Spanish singer. But in Galicia, we have another Rosalía:
<a href="https://en.wikipedia.org/wiki/Rosal%C3%ADa_de_Castro">Rosalía de Castro</a>,
probably our most important poet and novelist. She was a leading figure in the
period of the resurgence and revitalization of the Galician language in
literature during the 19th century (period known as
<a href="https://en.wikipedia.org/wiki/Rexurdimento">Rexurdimento</a>).</p>
<p>Some of her poems were set to music by several artists. Do you want an example?
Enjoy
<a href="https://www.youtube.com/watch?v=q_Nx2nq4oiM">&quot;Negra sombra&quot; (black shadow)</a>
interpreted by Luz Casal.</p>
<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>
<p><a href="https://www.w3.org/publishing/epub3/">EPUB</a> is the standard format for ebooks.
Technically, it's a zip file containing files in formats like XHTML, CSS, JPEG,
PNG; and other xml files specific for ebooks (a <code>container.xml</code> manifest file, a
<code>content.opf</code> with the book structure, etc). Robin Whittleton
<a href="https://www.htmhell.dev/adventcalendar/2025/11/">wrote a great article</a>
explaining how EPUB works.</p>
<p>Since EPUB is based on web standards that Lume understands, it seems feasible to
use Lume to create EPUBs. The only problem was the requirement of
<a href="https://www.w3.org/TR/xhtml11/">XHTML</a> (HTML is not valid for EPUBs), and this
wasn't easy to do in previous versions of Lume. But in this version, Lume can
output <code>.xhtml</code> files and treat them in the same way as <code>.html</code>, hence we also
have an EPUB plugin to help you to generate EPUBs. What this plugin can do?</p>
<ul>
<li>Create the <code>container.xml</code>, <code>encryption.xml</code>, <code>mimetype</code>, and <code>content.opf</code>
manifest files.</li>
<li>Create the <code>toc.ncx</code> file with the book structure (using the
<a href="https://lume.land/plugins/nav/">nav plugin</a> under the hood).</li>
<li>Convert the code and change the extension of all <code>.html</code> pages to <code>.xhtml</code>.</li>
<li>Compress all files and create the <code>book.epub</code> file in the <code>dest</code> folder.</li>
</ul>
<p>This is an example of using the plugin:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import epub from &quot;lume/plugins/epub.ts&quot;;

const site = lume({
  prettyUrls: false, // prettyUrls don't make sense for ebooks
});

site.use(epub({
  // Book metadata
  metadata: {
    identifier: &quot;unique identifier of your book&quot;,
    cover: &quot;/images/cover.png&quot;,
    title: &quot;My awesome book&quot;,
    subtitle: &quot;History of my life&quot;,
    creator: [&quot;Óscar Otero&quot;],
    publisher: &quot;Lume editions&quot;,
    language: &quot;en-US&quot;,
    date: new Date(&quot;2026-01-31T12:18:28Z&quot;),
  },
}));

export default site;
</code></pre>
<p>Note that the plugin cannot magically convert any website to an EPUB; you still
need to have a proper structure, use some epub specific attributes, etc. But
don't worry! the <a href="https://github.com/lumeland/simple-epub">Simple ePub theme</a>
provides a nice boilerplate to start publishing books.</p>
<p>Learn more about this plugin
<a href="https://lume.land/plugins/epub/">on the documentation page</a>.</p>
<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>
<p>This is a recurrent request, and finally, Lume has a plugin to add automatically
the <code>width</code> and <code>height</code> values of the images.</p>
<p>The plugin uses the awesome
<a href="https://github.com/sindresorhus/image-dimensions">image-dimensions</a> library by
Sindre Sorhus. To use it, just install it like any other plugin:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import imageSize from &quot;lume/plugins/image_size.ts&quot;;

const site = lume();

site.use(imageSize());

export default site;
</code></pre>
<p>Add the <code>image-size</code> attribute to the images you want the plugin to calculate
the size:</p>
<pre><code class="language-html">&lt;img src=&quot;/image.png&quot; image-size&gt;
</code></pre>
<p>And the plugin automatically adds the <code>width</code> and <code>height</code> attributes:</p>
<pre><code class="language-html">&lt;img src=&quot;/image.png&quot; width=&quot;600&quot; height=&quot;300&quot;&gt;
</code></pre>
<p>More info <a href="https://lume.land/plugins/image_size/">on the documentation page</a>.</p>
<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>
<p>Sometimes you have a list of pages that you want to show in a specific order. A
common way to do that is to define an <code>order</code> variable in the front matter:</p>
<pre><code class="language-md">---
title: Article 3
order: 3
---

This is the article 3
</code></pre>
<p>Then, you only have to select the pages in this specific order:</p>
<pre><code class="language-vto">{{ set pages = search.pages(&quot;type=article&quot;, &quot;order=asc&quot;) }}
</code></pre>
<p>This works great, the only problem is that you can't see the pages in the same
order in your code editor or file system because they are ordered
alphabetically:</p>
<pre><code>/article-three.md
/first-article.md
/other-article.md
</code></pre>
<p>With this plugin you can set the order in the filename, using the format
<code>{number}.filename</code>, and this value will be used as the <code>order</code> variable. The
plugin also removes this prefix from the final URL by default (configurable).</p>
<pre><code>/1.first-article.md
/2.other-article.md
/3.article-three.md
</code></pre>
<p>This also works great for folders:</p>
<pre><code>/1.articles/
   1.first-article.md
   2.other-article.md
   3.article-three.md
/2.notes/
   1.note-one.md
   2.note-two.md
</code></pre>
<p>To use it:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import extractOrder from &quot;lume/plugins/extract_order.ts&quot;;

const site = lume();

site.use(extractOrder());

export default site;
</code></pre>
<p>More info <a href="https://lume.land/plugins/extract_order/">on the documentation page.</a></p>
<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>
<p>This simple plugin allows to perform simple text replacements in the site,
something especially useful for documentation sites. For example, let's say you
want to display always the last version of your library in a website:</p>
<pre><code class="language-md">Welcome to Libros 2.3.0, the library to read ebook. To getting started, run the
following command:

deno install --global https://deno.land/x/libros@2.3.0/mod.ts
</code></pre>
<p>Instead of harcoding the version number everywhere in your site (and remember to
update it after a new version), this plugin allows to use a placeholder:</p>
<pre><code class="language-md">Welcome to Libros $VERSION, the library to read ebook. To getting started, run
the following command:

deno install --global https://deno.land/x/libros@$VERSION/mod.ts
</code></pre>
<p>Now, configure the replacements in the plugin options:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import replace from &quot;lume/plugins/replace.ts&quot;;

const site = lume();

site.use(replace({
  replacements: {
    &quot;$VERSION&quot;: &quot;2.3.0&quot;,
  },
}));

export default site;
</code></pre>
<p>Now you have this value centralized in one place. This is the approach used in
the Lume website to
<a href="https://github.com/lumeland/lume.land/blob/055eac5d0a960ab014eedc552492237c6613dbac/_config.ts#L54">keep the versions of all packages up to date</a>.</p>
<p>You can use this plugin for any constant value that you want to use globally,
like a query parameter for caching CSS and JS files, the hash of the latest
commit, the year in the copyright, etc.</p>
<p>More info <a href="https://lume.land/plugins/replace/">on the documentation page</a>.</p>
<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>
<p>The function
<a href="https://lume.land/docs/core/basename-parsers/"><code>site.parseBasename</code></a> allows
registering functions to extract values from files and folders. In fact, it's
what the <code>extract_order</code> and <code>extract_date</code> plugins use under the hood.</p>
<p>As of Lume 3.2, the data from the parent folder is added as the second argument.
This allows us to compose values contextually using the names of different
folders. For example, let's say we have some files with the following paths:</p>
<pre><code>/2026/01/01/happy-new-year.md
/2026/01/05/this-year-sucks.md
</code></pre>
<p>Now you can compose the final date of each file using the values of the
directories and subdirectories. For example:</p>
<pre><code class="language-js">site.parseBasename((basename, parent) =&gt; {
  // Check if the name only contains numbers
  if (!/^\d+$/.test(name)) {
    return;
  }

  // 4 digits, it's the year
  if (basename.length === 4) {
    return { year: basename, basename }
  }

  // 2 digits, it's the month or day
  if (basename.length === 2) {
    // If the month isn't in the parent, this is the month
    if (!parent.month) {
      return { month: basename, basename }
    }

    // This is the day, generate the final date
    const { year, month } = parent;
    return {
      date: `${year}`-${month}`-${basename}`,
      basename,
    }
  }
})
</code></pre>
<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>
<p>The Lume file watcher detects changes in your files in order to rebuild the site
with the new content. An important aspect is that only the changed files are
reloaded, which is way faster than reloading all files every time something
changed. This works great in 99% of the cases, but there are some edge cases
where we need to say Lume to reload a file when another file has changed.</p>
<p>As an example, let's say you have some data stored in a SQLite database and you
want to expose some of its data to your pages using a <code>_data.ts</code> file:</p>
<pre><code class="language-js">// _data.ts
import { DatabaseSync } from &quot;node:sqlite&quot;;

const db = new DatabaseSync(&quot;database.db&quot;);

export const categories = db.prepare(`
  SELECT
    categories.id,
    categories.name
  FROM categories
`).all();

db.close();
</code></pre>
<p>As you can see, we are exporting the <code>categories</code> variable, and this will make
it available to all pages. If we make changes in the <code>database.db</code> file, Lume
will detect that the file has changed, but because <code>_data.ts</code> hasn't changed,
Lume won't re-run this file, so the new changes won't be available. What we
really want is to reload the <code>_data.ts</code> file every time the <code>database.db</code> file
has changed. And now we can do it thanks to the new <code>watcher.dependencies</code>
option:</p>
<pre><code class="language-ts">import lume from &quot;lume/mod.ts&quot;;

const site = lume({
  watcher: {
    dependencies: {
      &quot;_data.ts&quot;: [&quot;database.db-journal&quot;],
    },
  },
});

export default site;
</code></pre>
<p>Here we are telling Lume that the file <code>_data.ts</code> depends on
<code>database.db-journal</code> (the extension <code>.db-journal</code> is used by SQLite to create a
temporary file during the data transactions). Now Lume knows that every time any
of its dependencies change, <code>_data.ts</code> will be reloaded too.</p>
<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>
<p>When Lume builds a site, the logger outputs messages to the terminal in the same
order they are generated. One problem with this approach is that there are
messages more important than others, and, especially if the site has a lot of
pages, those messages can be lost among others. Another problem is that if the
same error is produced by different pages, it's shown once per page, which
produces a lot of noise and prevents seeing other important messages. Let's see
an example:</p>
<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;)
ERROR SourceError: Unclosed tag
/_includes/templates/blocks.vto:4:3
 1 | {{ for block of blocks }}
 2 |   {{ if !block.hide }}
 3 |     {{ await comp[block.type]({ block, lang, url }) }}
 4 |   {{ /if }
   |   ^ Unclosed tag
ERROR SourceError: Unclosed tag
/_includes/templates/blocks.vto:4:3
 1 | {{ for block of blocks }}
 2 |   {{ if !block.hide }}
 3 |     {{ await comp[block.type]({ block, lang, url }) }}
 4 |   {{ /if }
   |   ^ Unclosed tag
🔥 /docs/configuration/env-variables/ &lt;- /docs/configuration/env-variables.md
🔥 /docs/configuration/config-file/ &lt;- /docs/configuration/config-file.md
🔥 /docs/configuration/add-files/ &lt;- /docs/configuration/add-files.md
🔥 /img/extend.svg &lt;- /img/extend.svg
🔥 /img/deploy.svg &lt;- /img/deploy.svg
...
🔥 /img/http-imports.svg &lt;- /img/http-imports.svg
🔥 /init.ts &lt;- /static/init.ts
🔥 /img/gradient.png &lt;- /img/gradient.png
🔥 /img/zero-runtime.svg &lt;- /img/zero-runtime.svg
🔥 /logo.png &lt;- /static/logo.png
WARN [validate_html plugin] 512 HTML error(s) found. Setup an output file or check the debug bar.
WARN [seo plugin] 45 SEO error(s) found. Setup an output file or check the debug bar.
🍾 Site built into ./_site
  188 files generated in 6.28 seconds
</code></pre>
<p>In the example, the Vento error shown twice because it occurred on two pages.
There are some WARN errors at the start and others at the end, and a long list
of generated pages in the middle.</p>
<p>In order to make the logs clearer, two changes were introduced in this version
of Lume:</p>
<ul>
<li>Messages of type WARN, ERROR, and FATAL are shown at the end, sorted by
severity (WARN first, FATAL last). Other levels (TRACE, DEBUG, and INFO) are
still shown as they were produced.</li>
<li>Duplicated logs are removed.</li>
</ul>
<p>The example above is shown as follows in the new version:</p>
<pre><code>...
🔥 /img/http-imports.svg &lt;- /img/http-imports.svg
🔥 /init.ts &lt;- /static/init.ts
🔥 /img/gradient.png &lt;- /img/gradient.png
🔥 /img/zero-runtime.svg &lt;- /img/zero-runtime.svg
🔥 /logo.png &lt;- /static/logo.png
🍾 Site built into ./_site
  188 files generated in 6.28 seconds
WARN [validate_html plugin] 512 HTML error(s) found. Setup an output file or check the debug bar.
WARN [seo plugin] 45 SEO error(s) found. Setup an output file or check the debug bar.
WARN [esbuild plugin] No TS, JS, TSX, JSX files found. Use site.add() to add files. For example: site.add(&quot;script.js&quot;)
ERROR SourceError: Unclosed tag
/_includes/templates/blocks.vto:4:3
 1 | {{ for block of blocks }}
 2 |   {{ if !block.hide }}
 3 |     {{ await comp[block.type]({ block, lang, url }) }}
 4 |   {{ /if }
   |   ^ Unclosed tag
</code></pre>
<p>This hopefully ensures that you won't miss anything important during the build
process!</p>
<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>
<p>This version also includes some minor changes and several bugfixes. Some of
them:</p>
<ul>
<li><code>katex</code> plugin supports <code>mhchem</code> extension and includes an option to disable
the download of CSS and fonts.</li>
<li>The <code>date</code> filter registered by <code>date</code> plugin detects the language of the
current page.</li>
<li>Some improvements to the LumeCMS integration.</li>
<li>If you have a <code>script.ts</code> file, it no longer conflicts with the <code>script.js</code>
file generated by components.</li>
<li>Fix globbing on npm/gh specifiers.</li>
<li>And many more changes that you can see in the
<a href="https://github.com/lumeland/lume/blob/v3.2.0/CHANGELOG.md">CHANGELOG.md file</a>.</li>
</ul>
<p>Finally, I'd like to thank all contributors for helping make Lume so great with
PR or supporting the project with sponsoring and donations. 🫶</p>
]]>
      </content:encoded>
      <pubDate>Mon, 09 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume has a new logo!</title>
      <link>https://lume.land/blog/posts/new-logo/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/new-logo/</guid>
      <content:encoded>
        <![CDATA[<p>Lume was born 5 years ago as an excuse to try Deno, the new JavaScript runtime.
At that moment, the hype around Deno led to many projects featuring a dinosaur
in their logo. Lume joined this trend, making a joke with the concept of <em>fire</em>
and <em>Deno</em>.</p>
<!-- more -->
<p><img src="https://lume.land/uploads/lume-old.png" alt="Image"></p>
<p>I've wanted to change the logo for a long time, and finally, I could manage some
time to work on that. There are many reasons for this change:</p>
<ul>
<li>The Lume logo is based on <a href="https://deno.com/artwork">the original logo</a> of
Deno, which has changed twice since then.</li>
<li>Some people think Lume is a Deno product.</li>
<li>The logo doesn't work for small sizes.</li>
</ul>
<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>
<p>For the new logo, I wanted to focus on the main concept of Lume: <strong>the fire</strong>. I
didn't want to create yet another logo with a flame. Instead, I still wanted a
pet, but a firefly instead of a dinosaur.</p>
<p>Interestingly enough, in Galician, a firefly is called <em>vagalume</em>, which makes
sense for a project called Lume.</p>
<h2 id="logo-design" tabindex="-1"><a href="https://lume.land/blog/posts/new-logo/#logo-design" class="header-anchor">Logo design</a></h2>
<p>The new logo is more geometric, created with a leaf shape repeated five times,
and a head with two antennae. This simple design makes it work better at small
sizes.</p>
<p><img src="https://lume.land/uploads/logo-outline.png" alt="Image"></p>
<p>The red color is used to represent the fire that emits light (using a gradient).
It also works in dark and light contexts, just inverting the black and white
colors:</p>
<p><img src="https://lume.land/uploads/logo-negativo.png" alt="Image"> <img src="https://lume.land/uploads/logo-positivo.png" alt="Image"></p>
<p>The font family used didn't change. It's still
<a href="https://fonts.google.com/specimen/Epilogue?preview.text=Lume">Epilogue</a>, a
beautiful sans-serif font with a great x-height. I only made some kerning tweaks
and the name is now &quot;Lume&quot; (with upper case L) instead of &quot;lume&quot;.</p>
<p>Thanks to <a href="https://jlantunez.com/">José Luis Antúnez</a> for the feedback and
suggestions, and to Studio Ghibli for the film
<a href="https://en.wikipedia.org/wiki/Grave_of_the_Fireflies">Grave of the Fireflies</a>
from which I got some inspiration.</p>
]]>
      </content:encoded>
      <pubDate>Wed, 05 Nov 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume 3.1.0 - Alexandre Bóveda</title>
      <link>https://lume.land/blog/posts/lume-3-1-0/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/lume-3-1-0/</guid>
      <content:encoded>
        <![CDATA[<p>The first minor version of Lume 3 is dedicated to
<a href="https://en.wikipedia.org/wiki/Alexandre_B%C3%B3veda"><strong>Alexandre Bóveda</strong></a>, a
financial officer and politician who was executed on 17 August 1936 by Franco's
dictatorship because of his Galician ideals. The night before his death, he
wrote three farewell letters, the last one to his brother:</p>
<blockquote>
<p>[...] I will die peacefully; I trust that I will be received where we all want
to get together, and I do it with joy and entrust to God this sacrifice. I
wanted to do good, I worked for Pontevedra, for Galicia, and for the Republic,
and the flawed judgment of men (which I forgive and you all must forgive)
condemns me.</p>
<p>Be more of a man now than ever because this is when you should be the most,
for our elderly and for the children, to whom, without expecting it, you are
going to be a little father. Comfort them all and try to always be good. Don't
regret how much good you have done and can still do. [...]</p>
</blockquote>
<p>Alexandre is not only a Galician martyr but also a demonstration that there have
always been good people in the world. Now, more than ever, we have to remember
that.</p>
<!-- more -->
<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>
<p>For the past 5 years, Lume's main distribution channel has been
<a href="https://deno.land/x">deno.land/x</a>, the CDN created by Deno to distribute
packages using HTTP imports. This package registry has been deprecated in favor
of <a href="https://jsr.io/">JSR</a>, a new registry that doesn't support HTTP. Since Deno
doesn't want people to use <code>deno.land/x</code> (as indicated in the two
<a href="https://deno.com/add_module">big yellow banners in the page to add new modules</a>)
and migration to JSR is not an option, we decided to switch to a different CDN.</p>
<p><strong>jsDelivr</strong> is the obvious choice for many reasons:</p>
<ul>
<li>It's supported by Deno
<a href="https://docs.deno.com/runtime/fundamentals/security/#importing-from-the-web">without any configuration</a>.</li>
<li>It's already used to
<a href="https://cdn.jsdelivr.net/gh/lumeland/cms/">publish LumeCMS</a>, deliver the
development versions of Lume, and fetch some assets like icons or CSS code,
needed by some plugins.</li>
<li>It has a nice
<a href="https://www.jsdelivr.com/package/gh/lumeland/lume">landing page for each package</a>,
where you can see all the files, and statistics, something not possible in
deno.land/x. We can even see statistics per file, which lets us know the
plugins most frequently used.</li>
<li>The traffic is balanced by different CDN sponsors like Cloudflare, Fastly,
Bunny.net, etc, which ensure performance and reduce risks of relying on a
single CDN.</li>
<li>All content is permanently cached to ensure reliability. Even if the files get
deleted from GitHub, they will continue to work on jsDelivr without breaking
anything.</li>
</ul>
<p>Lume will still be published on <code>deno.land/x</code>; the only difference is that the
script to update or initialize a new Lume project will choose jsDelivr by
default. That's one of the many benefits of using HTTP imports: its
decentralized nature allows you to change the CDN at any moment with zero
impact.</p>
<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>
<p>Deno added some great improvements to <code>deno.json</code> in recent versions that Lume
has adopted for the init and upgrade script:</p>
<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>
<p>Deno 2.4 added
<a href="https://deno.com/blog/v2.4#deno-run-bare-specifiers">support for bare specifiers in <code>deno run</code></a>.
In older versions, the only way to use a specifier defined in the import map was
using <code>deno eval</code> or the ugly:</p>
<pre><code class="language-json">{
  &quot;tasks&quot;: {
    &quot;lume&quot;: &quot;echo \&quot;import 'lume/cli.ts'\&quot; | deno run -A -&quot;
  }
}
</code></pre>
<p>But now, this is supported:</p>
<pre><code class="language-json">{
  &quot;tasks&quot;: {
    &quot;lume&quot;: &quot;deno run -A lume/cli.ts&quot;
  }
}
</code></pre>
<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>
<p>To improve the UX, the tasks created by Lume include a description. Run
<code>deno task</code> to see all available tasks with the description:</p>
<pre><code class="language-json">{
  &quot;tasks&quot;: {
    &quot;lume&quot;: {
      &quot;description&quot;: &quot;Run Lume command&quot;,
      &quot;command&quot;: &quot;deno run -A lume/cli.ts&quot;
    },
    &quot;build&quot;: {
      &quot;description&quot;: &quot;Build the site for production&quot;,
      &quot;command&quot;: &quot;deno task lume&quot;
    },
    &quot;serve&quot;: {
      &quot;description&quot;: &quot;Run and serve the site for development&quot;,
      &quot;command&quot;: &quot;deno task lume -s&quot;
    }
  }
}
</code></pre>
<h3 id="permissions" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3-1-0/#permissions" class="header-anchor">Permissions</a></h3>
<p>Deno 2.5
<a href="https://deno.com/blog/v2.5">allows to configure permissions in the <code>deno.json</code> file</a>,
and Lume adopted this nice feature. Now you have the <code>lume</code> permission preset
that is used when running the <code>lume</code> task (previously, it used the <code>-A</code> flag,
which disables the permissions). This will improve the security of your builds
and will make it easy to edit the permissions to adapt to your needs. This is
the default configuration:</p>
<pre><code class="language-json">{
  &quot;permissions&quot;: {
    &quot;lume&quot;: {
      &quot;read&quot;: true,
      &quot;write&quot;: [
        &quot;./&quot;
      ],
      &quot;import&quot;: [
        &quot;cdn.jsdelivr.net:443&quot;,
        &quot;jsr.io:443&quot;,
        &quot;deno.land:443&quot;,
        &quot;esm.sh:443&quot;
      ],
      &quot;net&quot;: [
        &quot;0.0.0.0&quot;,
        &quot;jsr.io:443&quot;,
        &quot;cdn.jsdelivr.net:443&quot;,
        &quot;data.jsdelivr.com:443&quot;,
        &quot;registry.npmjs.org:443&quot;
      ],
      &quot;env&quot;: true,
      &quot;run&quot;: true,
      &quot;ffi&quot;: true,
      &quot;sys&quot;: true
    }
  }
}
</code></pre>
<p>As you can see, Deno only has writing permissions for the current folder, net
permissions to localhost (to run the local server), and net and import
permissions for JSR, NPM, esm.sh and JsDelivr (to import dependencies). The
other permissions are granted by default because they are needed for some
plugins, but you can edit this configuration to make it more restrictive or
relaxed.</p>
<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>
<p>One of the main challenges with LumeCMS has been integrating it smoothly with
Lume or other static site generators. Previously, LumeCMS relied on <strong>Hono</strong> to
run both the CMS and the page preview, which could lead to inconsistencies: a
page served by Lume (<code>deno task serve</code>) might differ from one served by LumeCMS,
since Hono's static server doesn't exactly match Lume's. For example,
middlewares configured for Lume are not available in the CMS, and this affects
features like live reload, debugbar, etc.</p>
<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>
<p>With version 0.13, LumeCMS introduces
<a href="https://lume.land/blog/lume-cms-0-13/">significant changes</a>. Now, LumeCMS <strong>is
a middleware on top of Lume's server</strong>, handling only requests that start with
<code>/admin/*</code> while delegating page previews to Lume's server. This makes the
integration easier, eliminating the need for separate commands
(<code>deno task serve</code> and <code>deno task cms</code>).</p>
<p>This means that <strong>the <code>cms</code> task was removed</strong>. Just run <code>deno task serve</code> and,
if the <code>_cms.ts</code> file is present, the CMS is automatically initialized.</p>
<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>
<p>When LumeCMS is running on a VPS, it requires two processes:</p>
<ul>
<li>The main process is an HTTP server that is always listening.</li>
<li>The secondary process runs Lume and LumeCMS.</li>
</ul>
<p>The main process starts the secondary process on demand and works as a reverse
proxy. This allows restarting Lume and LumeCMS after some changes (for example,
when updating the changes with <code>git pull</code> or after changing the git branch)
without closing the server.</p>
<p>Until now, you needed to use
<a href="https://deno.land/x/lume_cms_adapter">an external package</a> to setup it. Now
it's much easier since Lume's main repo includes a module to run the CMS in
production: You only need to run <code>deno serve -A lume/serve.ts</code> and that's all!!</p>
<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>
<p>This plugin checks the HTML code of your site and validates it using
<a href="https://html-validate.org/">html-validate</a>, a fast NPM package that works
offline.</p>
<p>The plugin was originally
<a href="https://git.pyrox.dev/pyrox/new-blog/src/commit/af1de59ce89084064e2973e0d8d3e095e1b2534a/plugins/validateHTML.ts">created by dish</a>
and now it's an official Lume plugin, integrated with the Debug bar and with
different options to export the report.</p>
<p>The easiest way to use it is to import it into the <code>_config.ts</code> file:</p>
<pre><code class="language-ts">import lume from &quot;lume/mod.ts&quot;;
import validateHtml from &quot;lume/plugins/validate_html.ts&quot;;

const site = lume();
site.use(validateHtml());

export default site;
</code></pre>
<p>Now you will see a new tab in the Debug bar with all HTML errors detected in the
site:</p>
<p><img src="https://lume.land/uploads/debugbar_validate_html.png" alt="Image"></p>
<p>I hope this plugin will help you to create more standard and bug-free websites.</p>
<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>
<p><a href="https://partytown.qwik.dev/">Partytown</a> is a JavaScript library to run
third-party scripts in a web worker. The goal is to dedicate the main thread to
your code, and move other resource-intensive third-party scripts, like analytics
or tracking services to a different thread, making the website faster and more
secure.</p>
<p>The plugin was created originally
<a href="https://github.com/lumeland/experimental-plugins/pull/21">by kwaa 2 years ago</a>
as an experimental plugin, and now it has moved to the main repo. To use it, you
have to register it in the _config.ts file:</p>
<pre><code class="language-ts">import lume from &quot;lume/mod.ts&quot;;
import partytown from &quot;lume/plugins/partytown.ts&quot;;

const site = lume();
site.use(partytown());

export default site;
</code></pre>
<p>And now add the <code>type=&quot;text/partytown&quot;</code> attribute to all scripts that you want
to run from the web worker:</p>
<pre><code class="language-html">&lt;script type=&quot;text/partytown&quot;&gt;...&lt;/script&gt;
</code></pre>
<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>
<p>This plugin was created by <a href="https://github.com/timthepost/">Tim Post</a> with the
help of <a href="https://github.com/RickCogley/">Rick Cogley</a> for the Japanese language
support (thanks so much, guys!). It's a really interesting plugin that not only
can check the SEO basics (titles, descriptions, alt text in images, etc) but
also other not very common checks like common words percentage.</p>
<p>Since Tim couldn't maintain it, we decided to port it to the Lume repo and
convert it to an &quot;official plugin&quot;. It was modified in order to align with the
style and conventions of other plugins, and it was simplified a bit to make it
more maintainable in the long run (originally, the project was more ambitious).</p>
<p>The installation is not different from other plugins, just import it into the
_config.ts file:</p>
<pre><code class="language-ts">import lume from &quot;lume/mod.ts&quot;;
import seo from &quot;lume/plugins/seo.ts&quot;;

const site = lume();
site.use(seo());

export default site;
</code></pre>
<p>Like other validator plugins (<code>check_urls</code>, <code>validate_html</code>, etc), it creates a
new tab in the debug bar with all SEO issues found in all pages. It also has an
option to export the report to a JSON file or any other format by providing a
custom export function.</p>
<p>The plugin is highly customizable. The default options are enough for most
cases, but you can change how to validate titles, H1 tags, meta descriptions,
heading orders, duplicated titles, etc. Definitely, this plugin will help you to
create more successful websites!</p>
<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>
<p>The <a href="https://lume.land/docs/core/remote-files/">function <code>remoteFile</code></a> allows
the use of URLs to download the content of a file if it doesn't exist locally.
For example:</p>
<pre><code class="language-js">site.remoteFile(&quot;/styles/styles.css&quot;, &quot;https://example.com/styles/styles.css&quot;);
</code></pre>
<p>If the file <code>/styles/styles.css</code> doesn't exist locally, the content of the URL
will be used in place. This is very useful for themes because it allows for
placing all templates and styles used by a theme remotely.</p>
<p>The only problem with this function is that it only works for single files. If
you have several files, you need to call the function once per file:</p>
<pre><code class="language-js">const files = [
  &quot;styles.css&quot;,
  &quot;components/button.css&quot;,
  &quot;components/alert.css&quot;,
  &quot;components/icons.css&quot;,
];

for (const file of files) {
  site.remoteFile(&quot;/styles/&quot; + file, &quot;https://example.com/styles/&quot; + file);
}
</code></pre>
<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>
<p>Lume 3.1 includes the new function <code>site.remote()</code> similar to
<code>site.remoteFile()</code> but allows to register more than one file:</p>
<pre><code class="language-js">const files = [
  &quot;styles.css&quot;,
  &quot;components/button.css&quot;,
  &quot;components/alert.css&quot;,
  &quot;components/icons.css&quot;,
];

site.remote(&quot;/styles/&quot;, &quot;https://example.com/styles/&quot;, files);
</code></pre>
<p>Okay, you may think this isn't a big improvement, it's just a bit simpler. But
the good news is that you can use some specifiers that are compatible with glob
patterns:</p>
<ul>
<li><code>npm:</code> to use NPM packages (like <code>npm:lucide-static@0.544.0</code>)</li>
<li><code>gh:</code> to use GitHub repositories (like <code>gh:lumeland/theme-simple-wiki</code>)</li>
<li><code>file:</code> to use local files</li>
<li>All URLs starting with <code>https://cdn.jsdelivr.net/</code>.</li>
</ul>
<p>For example, let's say the CSS files are stored in a GitHub repository:</p>
<pre><code class="language-js">const files = [
  &quot;/styles/**/*.css&quot;,
];

site.remote(&quot;/styles/&quot;, &quot;gh:username/repo@tag&quot;, files);
</code></pre>
<p>That's better now! Under the hood, this is possible thanks to the API of
JsDelivr. Any compatible specifier is converted to JsDelivr equivalent.</p>
<p>For example, <code>gh:lumeland/theme-simple-wiki@0.14.3</code> is converted to
<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>.
And using the JsDelivr API, we can know the paths of all files in the repository
(<a href="ttps://data.jsdelivr.com/v1/package/gh/lumeland/theme-simple-wiki@v0.14.3?structure=flat">example</a>),
so we can filter them using the glob pattern.</p>
<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>
<p>Thanks to this new function, it's easier to create Lume themes, since you don't
have to worry about forgetting to include a new file that was recently created.
Let's see this example:</p>
<pre><code class="language-js">const themeFiles = [
  &quot;/_includes/**&quot;,
  &quot;/*.css&quot;,
  &quot;/*.js&quot;,
  &quot;/*.vto&quot;,
];

site.remote(&quot;/&quot;, import.meta.resolve(&quot;./&quot;), themeFiles);
</code></pre>
<p>In local development, <code>import.meta.resolve(&quot;./&quot;)</code> resolves to a <code>file://...</code>
url, so the glob patterns work great. If this package is published and imported
from JsDelivr, it's resolved to <code>https://cdn.jsdelivr.net/gh/user/repo@tag</code>, and
the glob patterns are still supported thanks to the JsDelivr API. This is
another reason to recommend distributing Deno packages on JsDelivr over
deno.land/x.</p>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>For backward compatibility, the function <code>site.remoteFile</code> is still available
as an alias of <code>site.remote</code>, for example, <code>site.remoteFile(local, remote)</code> is
an alias of <code>site.remote(local, remote)</code>.</p>
</div>
<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>
<p>As of Lume 3.1, the <a href="https://lume.land/plugins/inline/">inline</a> plugin not only
processes HTML files but also CSS. This allows for inlining background images
easily. To use it, just append the <code>?inline</code> parameter to the file URL. For
example:</p>
<pre><code class="language-css">.warning {
  background-image: url(&quot;/icons/warning.svg?inline&quot;);
}
</code></pre>
<p>Is converted to:</p>
<pre><code class="language-css">.warning {
  background-image: url(&quot;data:image/svg+xml;utf8,&lt;svg...&lt;/svg&gt;&quot;);
}
</code></pre>
<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>
<p>Until now, the <a href="https://lume.land/plugins/picture/">picture</a> plugin has only
allowed the width value to resize images. For example, this configuration
transforms the image to 300px and 600px, with versions for 2x resolutions and
formats AVIF, WebP, and JPG:</p>
<pre><code class="language-html">&lt;img src=&quot;/flowers.jpg&quot; transform-images=&quot;avif webp jpg 300@2 600@2&quot;&gt;
</code></pre>
<p>Now it's possible to specify a height to crop the images to a specific aspect
ratio:</p>
<pre><code class="language-html">&lt;img src=&quot;/flowers.jpg&quot; transform-images=&quot;avif webp jpg 300x150@2 600x300@2&quot;&gt;
</code></pre>
<p>The image will be cropped to 300x150 and 600x300, with a version for 2x
resolutions. For now, there isn't any way to configure the origin of the crop
(it's always centered), but this option can be added in future versions.</p>
<hr>
<p>See
<a href="https://github.com/lumeland/lume/blob/v3.1.0/CHANGELOG.md">the CHANGELOG.md file</a>
to see a complete list of all changes.</p>
]]>
      </content:encoded>
      <pubDate>Fri, 17 Oct 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume CMS 0.13.0</title>
      <link>https://lume.land/blog/lume-cms-0-13/</link>
      <guid isPermaLink="false">https://lume.land/blog/lume-cms-0-13/</guid>
      <content:encoded>
        <![CDATA[<p>It's been a while (one and a half year!) since
<a href="https://lume.land/posts/lume-cms/">LumeCMS was announced</a> as an alternative to other existing
CMS to edit the content of websites.</p>
<p>During this time, the project was improved in many ways: more field formats,
more customization options, light/dark mode, etc.</p>
<p>But, like many projects in their earlier phases, LumeCMS was a bit &quot;tricky&quot; to
run. It was created as a framework-agnostic solution, but in practice, it wasn't
easy to set up for other frameworks than Lume.</p>
<p>Version 0.13 has received a lot of changes in order to address this issue, among
others. Some of these changes are BREAKING CHANGES (hopefully they are only a
few). And even though it's still a development version (the version starts with
<code>v0.*</code> yet), I think it's an important step towards the future v1.0 version.</p>
<!-- more -->
<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>
<p>Since the beginning, LumeCMS has used <a href="https://hono.dev/">Hono</a> under the hood.
Although Hono is a very powerful and popular framework, I found it a bit
complicated to work with. The way to pass data (or contexts) to routers and
middlewares, the rendering system or the use of the custom class <code>HonoRequest</code>
for the request instead of the standard <code>Request</code> (that is also available but
hidden) made me spend more time trying to figure out how to do something in
&quot;Hono way&quot; than just doing it. In addition to that, it's becoming a
full-featured framework with even a client-side JSX library.</p>
<p>That's why <a href="https://github.com/oscarotero/galo">Galo was created</a>. It's a fast
and minimalist router that embraces web standards and simplicity without
sacrificing flexibility. The change from a framework approach to a library like
this made LumeCMS much easier to embed in any application running Deno. In fact,
LumeCMS is now a middleware for your application without any side effects or
interference with your existing code.</p>
<p><img src="https://lume.land/uploads/lumecms-galo.png" alt="Image"></p>
<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>
<p>LumeCMS was created assuming that all data would be stored in an object. Let's
see this example:</p>
<pre><code class="language-js">cms.collection({
  name: &quot;notes&quot;,
  storage: &quot;src:notes/*.json&quot;,
  fields: [
    &quot;title: text&quot;,
    &quot;text: textarea&quot;,
  ],
});
</code></pre>
<p>This stores every note in a JSON file in the <code>notes/</code> directory. The stored
notes have a structure like this:</p>
<pre><code class="language-json">{
  &quot;title&quot;: &quot;Note title&quot;,
  &quot;text&quot;: &quot;This is the note&quot;
}
</code></pre>
<p>If you want to store all notes in a single JSON file, you can use an
<code>object-list</code> field to store an array of objects:</p>
<pre><code class="language-js">cms.document({
  name: &quot;notes&quot;,
  storage: &quot;src:notes.json&quot;,
  fields: [
    {
      name: &quot;notes&quot;,
      type: &quot;object-list&quot;,
      fields: [
        &quot;title: text&quot;,
        &quot;text: textarea&quot;,
      ],
    },
  ],
});
</code></pre>
<p>This configuration produces the following data structure:</p>
<pre><code class="language-json">{
  &quot;notes&quot;: [
    {
      &quot;title&quot;: &quot;First note&quot;,
      &quot;text&quot;: &quot;Text of the first note&quot;
    },
    {
      &quot;title&quot;: &quot;Second note&quot;,
      &quot;text&quot;: &quot;Text of the second note&quot;
    }
  ]
}
</code></pre>
<p>As you can see, the root of the data is still an object with the <code>notes</code>
property to hold the array of notes. But what we really want is to store the
array of notes <strong>as the root element</strong>. Until now, the solution was to change
the name of the root value from <code>notes</code> to <code>[]</code>:</p>
<pre><code class="language-js">cms.document({
  name: &quot;notes&quot;,
  storage: &quot;src:notes.json&quot;,
  fields: [
    {
      name: &quot;[]&quot;,
      type: &quot;object-list&quot;,
      fields: [
        &quot;title: text&quot;,
        &quot;text: textarea&quot;,
      ],
    },
  ],
});
</code></pre>
<p>LumeCMS detected the special name &quot;[]&quot; as an instruction to ignore the element
and store its content directly. This allows us to store the data as an array:</p>
<pre><code class="language-json">[
  {
    &quot;title&quot;: &quot;First note&quot;,
    &quot;text&quot;: &quot;Text of the first note&quot;
  },
  {
    &quot;title&quot;: &quot;Second note&quot;,
    &quot;text&quot;: &quot;Text of the second note&quot;
  }
]
</code></pre>
<p>The problem with this solution is that it's a bit hacky, verbose, and not very
flexible. That's why in version 0.13 this feature was replaced with the new
<code>type</code> option:</p>
<pre><code class="language-js">cms.document({
  name: &quot;notes&quot;,
  storage: &quot;src:notes.json&quot;,
  type: &quot;object-list&quot;,
  fields: [
    &quot;title: text&quot;,
    &quot;text: textarea&quot;,
  ],
});
</code></pre>
<p>As you may guess, this option configures the field type used to store the root
data. If it's not defined, the default value is <code>object</code>, but other available
values are <code>object-list</code> (to store an array of objects) and <code>choose</code> (to allow
the user to choose one structure among a list of options). More types can be
added in future versions.</p>
<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>
<p>One of the great features of LumeCMS is the ability to preview the changes while
editing the data. To provide this, we need two things:</p>
<ul>
<li>A way to know the URL generated by a file. For example, if we know that the
file <code>/posts/hello-world.md</code> produces the URL <code>/posts/hello-world/</code>, we can
display this URL in the preview panel when the file is being edited.</li>
<li>A way to know the source file of a URL. If we know that the URL
<code>/posts/hello-world/</code> is generated by <code>/posts/hello-world.md</code>, we can create a
&quot;Edit this page&quot; link to go directly to the edit form.</li>
</ul>
<p>Until now, the way to get this info was a bit obscure and undocumented. In
version 0.13, this is fully configurable, which makes the CMS easier to adapt
for other static site generators:</p>
<pre><code class="language-js">const cms = lumeCMS({
  previewUrl(path: string, content: Lume.CMS.Content, changed: boolean) {
    // Return the URL generated by this file
 },
  sourcePath(url: string, content: Lume.CMS.Content) {
    // Return the file path that generates this URL
 }
});
</code></pre>
<p>The <code>previewUrl</code> is also customizable at the document or collection level,
useful if you're editing a file that doesn't directly produce a URL but can
affect it (like a <code>_data</code> file):</p>
<pre><code class="language-js">cms.document({
  name: &quot;Common data&quot;,
  storage: &quot;src:_data.yml&quot;,
  previewUrl: () =&gt; &quot;/&quot;, // preview the homepage
  fields: [
    &quot;title: text&quot;,
    &quot;description: textarea&quot;,
  ],
});
</code></pre>
<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>
<p>In previous versions, you could configure the permissions to create, edit,
rename, or delete documents globally. For example, let's say we have a
collection of countries that we don't want to remove or create new ones, just
edit them:</p>
<pre><code class="language-js">cms.collection({
  name: &quot;countries&quot;,
  storage: &quot;src:countries/*.json&quot;,
  fields: [
    &quot;name: text&quot;,
    &quot;description: textarea&quot;,
  ],
  create: false,
  delete: false,
  rename: false,
});
</code></pre>
<p>With this configuration, all users can edit the countries, but cannot create,
delete, or rename files. In version 0.13.0, we can override this configuration
for some users:</p>
<pre><code class="language-js">cms.auth({
  user1: {
    password: &quot;password1&quot;,
    name: &quot;Admin&quot;,
    permissions: {
      &quot;countries&quot;: {
        create: true,
        delete: true,
        rename: true,
      },
    },
  },
  user2: &quot;password2&quot;,
});
</code></pre>
<p>In previous versions, the auth configuration was simply an object with names and
passwords. Now, we can also use an object to include more options. In this
example, the &quot;user1&quot; has a password, the name &quot;Admin&quot; (which is used to show it
in the interface instead of &quot;user1&quot;), and some special permissions that override
the permissions assigned to documents and collections: this user can create,
rename, and delete files of the countries collection, unlike &quot;user2&quot;.</p>
<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>
<p>If you see the list of documents in a collection, LumeCMS only displays the file
names. Since it doesn't load the documents data, it can't use any value inside
them (like <code>title</code> or <code>date</code> properties). The option <code>documentLabel</code> allows
customization of how this document is shown in the interface. For example, it
can transform the name <code>my-first-post.md</code> to a more human <code>My First Post</code>. In
fact, LumeCMS comes with this especific behavior by default, but you can
customize it with your own tranformer:</p>
<pre><code class="language-js">cms.collection({
  documentLabel: (filename) =&gt; filename.replace(&quot;.json&quot;, &quot;&quot;),
  // More options...
});
</code></pre>
<p>As of version 0.13, this function can return an object with the properties
<code>label</code>, <code>icon</code>, and <code>flags</code> to extract and show more info in the list view.</p>
<p>Let's say our collection of countries is a folder with the following files:</p>
<pre><code>/en-spain.json
/pt-portugal.json
/fr-france.json
/it-italy.json
</code></pre>
<p>We can configure the collection to show only the country name, and use the flag
icon (from <a href="https://phosphoricons.com/?q=flag">Phosphor</a>) instead of the default
document icon. The country code is the code is saved in the flags object:</p>
<pre><code class="language-js">cms.collection({
  documentLabel: (filename) =&gt; {
    const [code, name] = filename.replace(&quot;.json&quot;, &quot;&quot;).split(&quot;-&quot;);
    return {
      label: name,
      icon: &quot;flag&quot;
      flags: { code }
    }
  },
  // ...more options
});
</code></pre>
<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>
<p>This feature is related to the improvements on <code>documentLabel</code> explained above.
Now that we can extract more info from the filename, we can use this info to
link a document to another. For example, let's say we have the &quot;people&quot;
collection and we want to assign a country to each person:</p>
<pre><code class="language-js">cms.collection({
  name: &quot;people&quot;,
  storage: &quot;src:people/*.md&quot;,
  fields: [
    &quot;name: text&quot;,
    {
      name: &quot;country&quot;,
      type: &quot;relation&quot;,

      // The collection name that we want to relate
      collection: &quot;countries&quot;,

      // A function to return a label and value for each option
      option: ({ label, flags }) =&gt; { label, value: flags.code }
    }
  ]
});
</code></pre>
<p>Now, this field shows a selector to pick one of the countries and use the <code>code</code>
flag as the value (<code>en</code> for Spain, <code>pt</code> for Portugal, etc). The <code>relation-list</code>
field is similar but allows for storing an array of values.</p>
<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>
<p>The git commits created by LumeCMS now include the current user name as the
author. It's also possible to include the email by adding the <code>email</code> property
to the user settings:</p>
<pre><code class="language-js">cms.auth({
  user1: {
    password: &quot;password1&quot;,

    // name &amp; email are included in the commits created by this user.
    name: &quot;Admin&quot;,
    email: &quot;user@example.com&quot;,
  },
});
</code></pre>
<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>
<p>Now it's possible to configure a document without fields, to show a code editor
instead of a form to edit it. It's useful to edit code directly from the CMS:</p>
<pre><code class="language-js">cms.document({
  name: &quot;Custom styles&quot;,
  storage: &quot;src:style.css&quot;,
});
</code></pre>
<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>
<p>From now on, date and datetime fields don't include the timezone in the YAML
files. Hopefully, it will fix a lot of problems related to unexpected dates due
to different time zones.</p>
<pre><code class="language-yml"># before
date: 2025-01-11T00:00:00.000Z

# after
date: 2025-01-11 00:00:00
</code></pre>
<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>
<p>There are more changes in this version, like UI improvements, ability to show
EXIF data from uploaded images, the new <code>cssSelector</code> option to highlight an
element in the previewer related with a field, etc.</p>
<p>Take a look at the
<a href="https://github.com/lumeland/cms/blob/v0.13.0/CHANGELOG.md">CHANGELOG.md file</a>
to see the complete list of changes.</p>
<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>
<p>This version isn't compatible with Lume 3.0.x, but it will be in Lume 3.1.</p>
<p>If you want to try it, upgrade Lume to the latest development version with:</p>
<pre><code>deno task lume upgrade --dev
</code></pre>
<p>Then, run <code>deno task serve</code> and the CMS is automatically created if a <code>_cms.ts</code>
file is detected. <strong>You won't need to run <code>deno task cms</code> anymore!</strong></p>
]]>
      </content:encoded>
      <pubDate>Tue, 14 Oct 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Vento 2 is here!</title>
      <link>https://lume.land/blog/posts/vento-2/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/vento-2/</guid>
      <content:encoded>
        <![CDATA[<p><img src="https://lume.land/uploads/vento-2.svg" alt="Vento 2" class="vento-logo"></p>
<p>Vento was born two years ago as an experiment to create a modern, ergonomic, and
async-friendly template engine for JavaScript. Initially, it was a Deno-only
project, intended to become the default template engine for <strong>Lume</strong>. But as
soon as the <a href="https://www.npmjs.com/package/ventojs">NPM package was available</a>,
other projects started to use it.</p>
<!-- more -->
<p>During this period, a number of people got involved in the development.</p>
<ul>
<li><a href="https://github.com/wrapperup">wrap</a> made a wonderful work implementing and
maintaining a JavaScript analyzer to automatically resolve the global
variables (so you can type <code>{{ somevariable }}</code> instead of
<code>{{ it.somevariable }}</code>. She's also responsible for the
<a href="https://github.com/ventojs/tree-sitter-vento">tree-sitter</a> parser to bring
support for Neovim and similar editors.</li>
<li><a href="https://github.com/noelforte">Noel Forte</a> created the
<a href="https://github.com/noelforte/eleventy-plugin-vento">11ty plugin</a> that made
Vento popular in the 11ty ecosystem.</li>
<li><a href="https://github.com/dz4k">Deniz Akşimşek</a> created the
<a href="https://github.com/dz4k/zed-vento">Zed plugin</a>.</li>
<li><a href="https://github.com/illyrius666">Illyrius</a> brought
<a href="https://github.com/ventojs/webstorm-vento">support for WebStorm</a>.</li>
</ul>
<p>Vento wouldn't be so awesome without the help of these contributors and
<a href="https://github.com/ventojs/vento/graphs/contributors">many other</a> that improve
it with their selfless work. <strong>THANK YOU SO MUCH!</strong></p>
<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>
<p>Everything started with
<a href="https://vrugtehagel.nl/posts/my-doubts-about-vento/">this post</a> where
vrugtehagel exposed some issues detected in Vento. He was kind enough to send me
an email to let me know about the post whose constructive feedback was very
helpful and several issues mentioned were addressed in Vento 1.</p>
<p>However, vrugtehagel not only limited himself to providing feedback, but he also
started to get involved in the
<a href="https://github.com/ventojs/sublime-vento">Sublime Text plugin</a> and created a
bunch of
<a href="https://github.com/ventojs/vento/pulls?q=is%3Apr+is%3Aclosed+author%3Avrugtehagel">pull requests to the Vento repository</a>,
leading to some interesting discussions about Vento's philosophy and its
implementation approach. The most demanding challenge, for which we made
different proofs of concept, was to find an alternative way to analyze the
JavaScript code without using meriyah or any other dependency. This would make
the compilation faster and remove all Vento dependencies.</p>
<p>Thanks to this change, the local footprint was reduced <strong>from 1.8MB to less than
80Kb</strong> (<strong>18KB</strong> bundled and minified).</p>
<p>The next step was to convert Vento in an isomorphic library, which makes it to
work on browsers and on Node-like runtimes (Node, Deno, Bun) without changes or
the need for a compilation step.</p>
<p>And finally, one of the pain points of Vento, error reporting, was also
addressed thanks to the
<a href="https://github.com/ventojs/vento/pull/131">initial work of vrugtehagel</a> and
some subsequent changes by me.</p>
<p>Vento is now a modern, lean, and powerful template engine that can be used on
any JavaScript runtime and embedded on any framework easily.</p>
<h2 id="main-changes" tabindex="-1"><a href="https://lume.land/blog/posts/vento-2/#main-changes" class="header-anchor">Main changes</a></h2>
<p>After upgrading to Vento 2, almost everything in your .vto files should continue
working as usual without changes, although there might be some edge cases that
now have a different behavior.</p>
<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>
<p>In Vento 1, when you run <code>Hello {{ name }}</code>, the compiler converts it
automatically to <code>Hello {{ it.name }}</code>. This means that, <strong>technically,</strong> you
could define a variable directly in the <code>it</code> global variable and it would be
accessible without the prefix. For example, the following code would print
<em>&quot;Hello World&quot;</em>:</p>
<pre><code class="language-vto">{{&gt; it.name = &quot;World&quot; }}
Hello {{ name }}
</code></pre>
<p>Vento 2 uses a different approach. When Vento compiles the following template:</p>
<pre><code class="language-vto">Hello {{ name }}
</code></pre>
<p>all variables used are initialized preventively at the begining like this:</p>
<pre><code class="language-js">var { name } = it;
</code></pre>
<p>The variable is not replaced with <code>it.name</code> automatically everywhere but the
real variable <code>name</code> is created instead. If you edit the value of <code>it.name</code> in
your code directly, <strong>it won't affect <code>name</code></strong>. However, this is more a Vento's
internals change and it's unlikely to affect to final users since they never
should edit the <code>it</code> variable directly, but use the code
<code>{{ set name = &quot;other value&quot; }}</code>.</p>
<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>
<p>Any error produced while compiling or running the templates is now converted to
a <code>VentoError</code> class. This class contains all the information required to report
the exact point where the error originates. For example, the following template
produce an error because we are invoking a function from a <code>null</code> variable:</p>
<pre><code class="language-vto">{{ set name = null }}
{{ name.foo() }}
</code></pre>
<p>In order to run and pretty-print errors, you can use the <code>printError</code> helper:</p>
<pre><code class="language-js">import vento from &quot;vento/mod.ts&quot;;
import { printError } from &quot;vento/core/error.js&quot;;

const env = vento();

try {
  const result = await env.run(&quot;my-template.vto&quot;);
} catch (err) {
  console.error(await printError(err));
}
</code></pre>
<p>This outputs the following:</p>
<pre><code>TypeError: Cannot read properties of null (reading 'foo')
test/main.vto:2:1
 1 | {{ set name = null }}
 2 | {{ name.foo() }}
   | ^
   | __exports.content += (name.foo()) ?? &quot;&quot;;
   |                              ^ Cannot read properties of null (reading 'foo')
</code></pre>
<p>The error displays the tag where the error occurs and it can show also the error
in the compiled code (the final JavaScript code) to give more context.</p>
<p>Note that the error handler doesn't work consistently accross all runtimes, due
their differences providing useful data from the error stack. For example, it
works pretty well on Deno, but Node and Bun cannot recover the exact location of
some errors so it's not possible to provide detailed info in some cases. There
may be also some differences between browsers.</p>
<p>I hope this feature can be improved in next versions. PR are very appreciated!</p>
<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>
<p>Vento is <strong>async</strong> by default, it's one of its main selling points. In Vento 1
there was also the function <code>runStringSync</code> to run arbitrary code in a
synchronous context. For example:
<code>env.runStringSync(&quot;Hello {{ name }}&quot;, { name: &quot;World&quot;})</code>.</p>
<p>This mode was originally created because Lume needed it. However, the
synchronous mode doesn't fit well with how Vento works internally. Having both
sync and async modes makes everything more complicated, and the sync mode breaks
if your template has async tags, like <code>{{ include }}</code>, or runs any async
function or filter.</p>
<p>Since Lume no longer needs this feature, the function was removed in Vento 2,
and now all templates are run consistently in an async context.</p>
<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>
<p>The <a href="https://vento.js.org/syntax/layout/"><code>layout</code> tag</a> allows to render a
template passing extra content. This is great for layouts expecting only one
piece of content. But if the layout requires more pieces, you have to pass them
as variables:</p>
<pre><code class="language-vto">{{ layout &quot;article.vto&quot; { header: &quot;&lt;h1&gt;This is the title&lt;/h1&gt;&quot; } }}
  &lt;p&gt;This is the content&lt;/p&gt;
{{ /layout }}
</code></pre>
<p>The new <code>slot</code> tag allows to capture and store the content in different
variables, similar to what web components do.</p>
<pre><code class="language-vto">{{ layout &quot;article.vto&quot; }}
  {{ slot header }}
    &lt;h1&gt;This is the title&lt;/h1&gt;
  {{ /slot }}

  &lt;p&gt;This is the content&lt;/p&gt;
{{ /layout }}
</code></pre>
<p>Learn more about
<a href="https://vento.js.org/syntax/layout/#slots">slots in the documentation site</a>.</p>
<h3 id="browser-support" tabindex="-1"><a href="https://lume.land/blog/posts/vento-2/#browser-support" class="header-anchor">Browser support</a></h3>
<p>As said, Vento 2 works also on browsers, without any compilation step, thanks to
not having dependencies and the use of standard APIs. You can download the NPM
package or use a CDN like jsDelivr:</p>
<pre><code class="language-js">import vento from &quot;https://cdn.jsdelivr.net/npm/ventojs@2.0.0/web.js&quot;;

const env = vento({
  includes: import.meta.resolve(&quot;./templates&quot;),
});

const result = await env.run(&quot;main.vto&quot;);
console.log(result.content);
</code></pre>
<p>Note that instead of importing the <code>mod.js</code> module, you have to import <code>web.js</code>.
The only difference is that <code>web.js</code> uses the <code>URL</code> loader by default to load
templates using <code>fetch</code>. The <code>includes</code> option defines the base URL to load all
templates.</p>
<h2 id="benchmarks" tabindex="-1"><a href="https://lume.land/blog/posts/vento-2/#benchmarks" class="header-anchor">Benchmarks</a></h2>
<p>Vento 1 was already quite fast, but version 2 is even faster thanks to the new
compiler.</p>
<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>
<p>The compiler in Vento 2 is almost <strong>200% faster</strong> as you can see in the
following benchmark:</p>
<table>
<thead>
<tr>
<th style="text-align:left">Compilation</th>
<th style="text-align:right">time/iter (avg)</th>
<th style="text-align:right">iter/s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Vento 2</td>
<td style="text-align:right">196.0 µs</td>
<td style="text-align:right">5,102</td>
</tr>
<tr>
<td style="text-align:left">Vento 1</td>
<td style="text-align:right">378.0 µs</td>
<td style="text-align:right">2,646</td>
</tr>
</tbody>
</table>
<p>When comparing the rendering performance of Vento 1 and Vento 2, there are no
significant differences. Vento 1 may be slightly faster, but the difference is
minimal and not easily noticeable.</p>
<table>
<thead>
<tr>
<th style="text-align:left">Rendering</th>
<th style="text-align:right">time/iter (avg)</th>
<th style="text-align:right">iter/s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Vento 2</td>
<td style="text-align:right">860.9 ns</td>
<td style="text-align:right">1,162,000</td>
</tr>
<tr>
<td style="text-align:left">Vento 1</td>
<td style="text-align:right">820.0 ns</td>
<td style="text-align:right">1,220,000</td>
</tr>
</tbody>
</table>
<p>Comparing compilation and rendering performance, Vento 2 demonstrates <strong>a 180%
speed improvement</strong>. Even if a few milliseconds are lost during rendering, the
overall performance gain more than compensates for it.</p>
<table>
<thead>
<tr>
<th style="text-align:left">Comp. &amp; rendering</th>
<th style="text-align:right">time/iter (avg)</th>
<th style="text-align:right">iter/s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Vento 2</td>
<td style="text-align:right">186.0 µs</td>
<td style="text-align:right">5,376</td>
</tr>
<tr>
<td style="text-align:left">Vento 1</td>
<td style="text-align:right">342.8 µs</td>
<td style="text-align:right">2,917</td>
</tr>
</tbody>
</table>
<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>
<p>To compare the performance of Vento with other template engines, you can
<a href="https://github.com/ventojs/vento/tree/main/bench">run the bench code</a> in
Vento's repository. As of writing this post, the benchmark data is:</p>
<table>
<thead>
<tr>
<th style="text-align:left">Compilation</th>
<th style="text-align:right">time/iter (avg)</th>
<th style="text-align:right">iter/s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">1. Eta</td>
<td style="text-align:right">51.1 µs</td>
<td style="text-align:right">19,580</td>
</tr>
<tr>
<td style="text-align:left">2. EJS</td>
<td style="text-align:right">145.2 µs</td>
<td style="text-align:right">6,888</td>
</tr>
<tr>
<td style="text-align:left">3. Vento</td>
<td style="text-align:right">196.0 µs</td>
<td style="text-align:right">5,102</td>
</tr>
<tr>
<td style="text-align:left">4. Nunjucks</td>
<td style="text-align:right">602.1 µs</td>
<td style="text-align:right">1,661</td>
</tr>
<tr>
<td style="text-align:left">5. Liquid</td>
<td style="text-align:right">635.0 µs</td>
<td style="text-align:right">1,575</td>
</tr>
<tr>
<td style="text-align:left">6. Preact</td>
<td style="text-align:right">978.4 µs</td>
<td style="text-align:right">1,022</td>
</tr>
<tr>
<td style="text-align:left">7. Pug</td>
<td style="text-align:right">4.2 ms</td>
<td style="text-align:right">237</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th style="text-align:left">Rendering</th>
<th style="text-align:right">time/iter (avg)</th>
<th style="text-align:right">iter/s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">1. Vento</td>
<td style="text-align:right">860.9 ns</td>
<td style="text-align:right">1,162,000</td>
</tr>
<tr>
<td style="text-align:left">2. Pug</td>
<td style="text-align:right">1.6 µs</td>
<td style="text-align:right">632,100</td>
</tr>
<tr>
<td style="text-align:left">3. Preact</td>
<td style="text-align:right">8.8 µs</td>
<td style="text-align:right">113,900</td>
</tr>
<tr>
<td style="text-align:left">4. Eta</td>
<td style="text-align:right">11.2 µs</td>
<td style="text-align:right">89,600</td>
</tr>
<tr>
<td style="text-align:left">5. EJS</td>
<td style="text-align:right">14.8 µs</td>
<td style="text-align:right">67,790</td>
</tr>
<tr>
<td style="text-align:left">6. Liquid</td>
<td style="text-align:right">351.3 µs</td>
<td style="text-align:right">2,847</td>
</tr>
<tr>
<td style="text-align:left">7. Nunjucks</td>
<td style="text-align:right">812.5 µs</td>
<td style="text-align:right">1,231</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th style="text-align:left">Comp. &amp; rendering</th>
<th style="text-align:right">time/iter (avg)</th>
<th style="text-align:right">iter/s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">1. Eta</td>
<td style="text-align:right">68.0 µs</td>
<td style="text-align:right">14,700</td>
</tr>
<tr>
<td style="text-align:left">2. EJS</td>
<td style="text-align:right">177.3 µs</td>
<td style="text-align:right">5,639</td>
</tr>
<tr>
<td style="text-align:left">3. Vento</td>
<td style="text-align:right">186.0 µs</td>
<td style="text-align:right">5,376</td>
</tr>
<tr>
<td style="text-align:left">4. Edge</td>
<td style="text-align:right">518.0 µs</td>
<td style="text-align:right">1,931</td>
</tr>
<tr>
<td style="text-align:left">5. Preact</td>
<td style="text-align:right">706.5 µs</td>
<td style="text-align:right">1,415</td>
</tr>
<tr>
<td style="text-align:left">6. Liquid</td>
<td style="text-align:right">1.1 ms</td>
<td style="text-align:right">937</td>
</tr>
<tr>
<td style="text-align:left">7. Nunjucks</td>
<td style="text-align:right">1.5 ms</td>
<td style="text-align:right">676</td>
</tr>
<tr>
<td style="text-align:left">8. Pug</td>
<td style="text-align:right">4.2 ms</td>
<td style="text-align:right">236</td>
</tr>
</tbody>
</table>
<p>Vento delivers exceptional rendering performance. While Eta and EJS offer faster
compilation speeds, they struggle with delimiter handling. For example,
<code>&lt;%= '%&gt;' %&gt;</code> throws an error in EJS, but Vento handles the equivalent,
<code>{{ '}}' }}</code>, perfectly fine.</p>
<h2 id="update-now" tabindex="-1"><a href="https://lume.land/blog/posts/vento-2/#update-now" class="header-anchor">Update now</a></h2>
<p>Vento 2 is available on NPM (for Node-like runtimes) and HTTP imports (for
browsers and Deno). See the
<a href="https://github.com/ventojs/vento/blob/v2.0.0/CHANGELOG.md">CHANGELOG file</a> for
the full list of changes.</p>
]]>
      </content:encoded>
      <pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume 3 was released - Adolfina Casás</title>
      <link>https://lume.land/blog/posts/lume-3/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/lume-3/</guid>
      <content:encoded>
        <![CDATA[<p>After launching Lume 2 almost a year and a half ago, a new major version of Lume
is here!</p>
<p><img src="https://lume.land/uploads/adolfina-casas.jpeg" alt="Adolfina Casás"></p>
<p>This version is dedicated to all Galician <em>cantareiras</em> and
<em>pandereteiras</em>—women who sing and play the tambourine (or <em>pandeireta</em>), one of
the most important instruments in Galician traditional music. One of these
cantareiras was Adolfina Casás Rama (1912-2009), an ancestor of my friend Miriam
Casás. She worked in agriculture, where she often sang. She was known for her
wit and the variety of styles employed.</p>
<p>Thanks to the <em>recollidas</em>, where musicians and musicologists recorded these
traditional songs sung by anonymous women, we can now enjoy this musical
heritage performed by contemporary musicians with innovative arrangements.</p>
<p>Some examples include
<a href="https://www.youtube.com/watch?v=CwjZd5ak7xA">Xabier Díaz &amp; Adufeiras de Salitre</a>,
<a href="https://www.youtube.com/watch?v=Ge9Uu8SeGDE">Xosé Lois Romero &amp; Aliboria</a>, and
<a href="https://www.youtube.com/watch?v=czMGYX0C2zE">Berrogüetto</a> (apologies for the
video quality, but I really love that song).</p>
<p>For more disruptive artists, check out
<a href="https://www.youtube.com/watch?v=qjCeKRoGS8s">Tanxugueiras</a> or
<a href="https://www.youtube.com/watch?v=9ZM0kou3BPQ">Baiuca</a>.</p>
<!-- more -->
<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>
<p>To many developers, including myself, breaking changes can be frustrating.
Software updates that force you to revisit a project just to ensure it continues
working as before often feel like a waste of time. This is one of the reasons I
enjoy working with Web APIs—they are stable, reliable, and designed to just work
<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>
<p>I strive to bring a similar philosophy to Lume by minimizing breaking changes
whenever possible. In fact, I initially had no plans to release a new major
version of Lume. However, after receiving numerous reports about certain
behaviors and limitations, I realized it was necessary to revisit some design
decisions. This effort aims to finally deliver the simple, intuitive static site
generator I have always envisioned, hoping that Lume 4 won't be necesary in a
long time, or never.</p>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p><strong>TL/DR:</strong> There's
<a href="https://lume.land/docs/advanced/migrate-to-lume3/">a step-by-step guide to migrate to Lume 3</a>
in the documentation.</p>
</div>
<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>
<p>The <code>site.copy()</code> function allows you to copy files from the <code>src</code> folder
without reading the content, which is faster and consumes less memory. But it
has one big drawback: the files are not processed.</p>
<p>For example, let's say you have the following configuration:</p>
<pre><code class="language-js">site.copy(&quot;/assets&quot;);
site.use(postcss());
</code></pre>
<p>When Lume builds your site, the files inside the <code>/assets</code> folder are copied
as-is. If the folder contains CSS files, they <strong>won't be processed by Postcss</strong>.
Learn more about
<a href="https://github.com/lumeland/lume/issues/571">this issue on GitHub</a>.</p>
<p>This behavior is confusing and many people reported this as a bug. And they are
right: Lume should be clever enough to not delegate the decision of whether a
file must be loaded or copied.</p>
<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>
<p>In Lume 3, the <code>site.loadAssets()</code>, <code>site.copyRemainingFiles()</code> and
<code>site.copy()</code> functions were removed, and now there is a single function for
everything: <code>site.add()</code>.</p>
<p>The <code>add()</code> function simply tells Lume that you want to include some files in
your site, but without specifying how this file must be treated. Lume will load
the file if it needs to (for example, if it needs to be processed), or will copy
it if no transformations are needed.</p>
<pre><code class="language-js">site.add(&quot;/assets&quot;);
site.use(postcss()); // CSS files in /assets will be processed too!
</code></pre>
<p>To upgrade from Lume 2 to Lume 3, just replace the <code>site.loadAssets()</code>,
<code>site.copyRemainingFiles()</code>, and <code>site.copy()</code> functions with <code>site.add()</code>.</p>
<p>For example:</p>
<pre><code class="language-js">// Lume 2
site.loadAssets([&quot;.css&quot;]);
site.copy(&quot;/assets&quot;, &quot;.&quot;);
site.copyRemainingFiles(
  (path: string) =&gt; path.startsWith(&quot;/articles/&quot;),
);

// Lume 3
site.add([&quot;.css&quot;]);
site.add(&quot;/assets&quot;, &quot;.&quot;);
site.add(&quot;/articles&quot;);
</code></pre>
<blockquote>
<p><strong>Update:</strong> Some users have reported that <code>site.copy()</code> remains useful in
specific scenarios. For instance, if you need to copy a CSS file without
processing it. As a result, the <code>site.copy()</code> function was reintroduced in
Lume 3.0.1 to address these edge cases.</p>
</blockquote>
<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>
<p><code>site.add()</code> can add files from the <code>src</code> folder as well as remote files. In
Lume 2, this was possible with the <code>remoteFile</code> function:</p>
<pre><code class="language-js">// Lume 2
site.remoteFile(&quot;styles.css&quot;, &quot;https://example.com/theme/styles.css&quot;);
site.copy(&quot;styles.css&quot;);
</code></pre>
<p>Lume 3 makes this use case easier:</p>
<pre><code class="language-js">// Lume 3
site.add(&quot;https://example.com/theme/styles.css&quot;, &quot;styles.css&quot;);
</code></pre>
<p>The <code>site.add()</code> function also accepts <code>npm</code> specifiers:</p>
<pre><code class="language-js">site.add(&quot;npm:normalize.css&quot;, &quot;/styles/normalize.css&quot;);
</code></pre>
<p>Internally, this uses jsDelivr to download the file. In this example,
<code>npm:normalize.css</code> is transformed to
<code>https://cdn.jsdelivr.net/npm/normalize.css</code>. Note that only one file is copied,
not all package files.</p>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p><code>site.remoteFile</code> is still required in Lume 3 for files not directly exported
to the dest folder, like <code>_data</code>, <code>_components</code> or <code>_includes</code> files.</p>
</div>
<p>More info in the
<a href="https://lume.land/docs/configuration/add-files/">documentation page</a>.</p>
<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>
<p>In Lume 2, some plugins configure Lume to load files with a certain extension
automatically. For example, Postcss not only processes the CSS code but also
configures Lume to load all CSS files:</p>
<pre><code class="language-js">// All .css files are loaded and processed
site.use(postcss());
</code></pre>
<p>In some cases, this is what you want. But if you don't want to load all CSS
files, this behavior makes Lume load everything, and you have to use the
<code>site.ignore()</code> function or move the unwanted files to a folder starting with
<code>_</code>.</p>
<p>In addition to that, this behavior is not fully transparent. You have to read
the documentation to know what the plugin is doing.</p>
<p>In short, this approach causes more harm than good.</p>
<p>In Lume 3, thanks to the <code>site.add()</code> function, it's very easy to add new files
(and only the files that you want), so plugins <strong>no longer load files by
default</strong>. You have to explicitly add them, which is more intuitive:</p>
<pre><code class="language-js">// Lume 2
site.use(postcss());

// Lume 3
site.add([&quot;.css&quot;]);
site.use(postcss());
</code></pre>
<p>Another benefit is you have better control of all entry points of your assets.
For example, for esbuild:</p>
<pre><code class="language-js">// Lume 3
site.add(&quot;main.ts&quot;);
site.use(esbuild()); // Only main.ts is bundled
</code></pre>
<p>This change affects the <code>svgo</code>, <code>transform_images</code>, <code>picture</code>, <code>postcss</code>,
<code>sass</code>, <code>tailwindcss</code>, <code>unocss</code>, <code>esbuild</code> and <code>terser</code> plugins.</p>
<h2 id="jsx" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#jsx" class="header-anchor">JSX</a></h2>
<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>
<p>Lume started supporting <code>JSX</code> as a template engine thanks to the <code>jsx</code> plugin
that uses React under the hood. Later, the <code>jsx_preact</code> plugin was added to use
Preact, a smaller and more performant alternative to React.</p>
<p>Having two JSX plugins for the same purpose is useless and adds unnecessary
complexity (for example, combined with the MDX plugin).</p>
<p>Moreover, both libraries are frontend-first libraries, with features like hooks,
event callbacks, hydration, etc, that are not supported at build time, so some
people were confused about what they can or cannot do in Lume.</p>
<p>Lume 3 has only one JSX plugin, and it doesn't use React or Preact but
<a href="https://github.com/oscarotero/ssx/">SSX</a>, a TypeScript library created
specifically for static sites which is faster than React and Preact and more
ergonomic. It allows creating asynchronous components, inserting raw code like
<code>&lt;!doctype html&gt;</code>, and comes with great documentation including all HTML
elements and attributes, with links to MDN.</p>
<p>Lume 3 uses <code>lume/jsx-runtime</code> import source for all JSX and MDX files. So you
only have to configure the <code>compilerOptions</code> setting of <code>deno.json</code> as following
(other options have been omited for brevity):</p>
<pre><code class="language-json">{
  &quot;imports&quot;: {
    &quot;lume/jsx-runtime&quot;: &quot;https://deno.land/x/ssx@v0.1.8/jsx-runtime.ts&quot;
  },
  &quot;compilerOptions&quot;: {
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;jsxImportSource&quot;: &quot;lume&quot;
  }
}
</code></pre>
<p>This allows to upgrade the library (or even replace it with something else)
easily.</p>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>With the <code>esbuild</code> plugin you still can use React or Preact in Lume but for
what they were created for: the frontend.</p>
</div>
<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>
<p>Lume requires the <code>.page</code> subextension for certain file types like <code>.ts</code>, <code>.js</code>,
or <code>.json</code> to distinguish between files used to generate pages and those
intended for browser execution. For instance, <code>index.page.js</code> generates the
<code>index.html</code> page, while <code>index.js</code> is a JavaScript file executed by the
browser.</p>
<p>Starting with Lume 3, the <code>.page</code> subextension is also applied to <code>.jsx</code> and
<code>.tsx</code> files. This change allows the <code>.jsx</code> and <code>.tsx</code> extensions to be
exclusively used for browser-side code (after processing with the <code>esbuild</code>
plugin).</p>
<pre><code class="language-txt">Lume 2:
- /index.jsx

Lume 3:
- /index.page.jsx
</code></pre>
<p>If you prefer the Lume 2 behavior (where this differentiation is not required),
you can configure the plugin to remove the <code>.page</code> subextension:</p>
<pre><code class="language-js">site.use(jsx({
  pageSubExtension: &quot;&quot;, // Reverts to Lume 2 behavior
}));
</code></pre>
<p>More info in <a href="https://lume.land/plugins/jsx/">the plugin documentation</a>.</p>
<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>
<h3 id="async-components" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#async-components" class="header-anchor">Async components</a></h3>
<p>One of the main limitations of Lume 2's components was that they were
synchronous. This was to support JSX components that were synchronous with React
and Preact. With SSX, we don't have this limitation anymore, and all components
are async.</p>
<p>For example, you can create a component in JSX that returns a promise:</p>
<pre><code class="language-jsx">// _components/salute.jsx

export default async function ({ id }) {
  const response = await fetch(`https://example.com/api?id=${id}`);
  const data = await response.json();
  return &lt;strong&gt;Hello {data.name}&lt;/strong&gt;;
}
</code></pre>
<p>This component can be used in any other template engine, like JSX:</p>
<pre><code class="language-jsx">export default async function ({ comp }) {
  return (
    &lt;p&gt;
      &lt;comp.Salute id=&quot;23&quot; /&gt;
    &lt;/p&gt;
  );
}
</code></pre>
<p>Or Vento:</p>
<pre><code class="language-html">&lt;p&gt;{{ comp.Salute({ id: 23}) }}&lt;/p&gt;
</code></pre>
<h3 id="folder-components" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#folder-components" class="header-anchor">Folder components</a></h3>
<p>Lume components not only generate HTML code but can also export the CSS and JS
code needed to run it on the browser. The code must be exported in the variables
<code>css</code> and <code>js</code>. For example:</p>
<pre><code class="language-md">---
css: |
  .mainTitle {
    color: red;
  }
---

&lt;h1 class=&quot;mainTitle&quot;&gt;{{ name }}&lt;/h1&gt;
</code></pre>
<p>The problem with this approach is the CSS and JS code is not treated as CSS and
JS code by your code editor, so there's no syntax highlighting.</p>
<p>In Lume 3, it's possible to create a component in a folder, with the CSS and JS
code in different files. To do that, you use the following structure:</p>
<pre><code class="language-txt">|_ _components/
    |_ button/
        |_ comp.vto
        |_ style.css
        |_ script.js
</code></pre>
<p>Any folder containing a <code>comp.*</code> file will be loaded as a component using the
folder name as the component name, and the <code>style.css</code> and <code>script.js</code> files
will be loaded as the CSS and JS code for the component. This makes the creation
of components more ergonomic, especially for cases with a lot of CSS and JS
code.</p>
<p>Additionally, it's possible to add a <code>script.ts</code> file instead <code>script.js</code> to use
TypeScript. Lume will compile it to JavaScript automatically.</p>
<h3 id="better-interoperability" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#better-interoperability" class="header-anchor">Better interoperability</a></h3>
<p>In Lume 2 components created with text-based engines, like Vento didn't work
well for JSX templates. For example, let's say we have the following Vento
component:</p>
<pre><code class="language-vto">&lt;button&gt;{{ content }}&lt;/button&gt;
</code></pre>
<p>and we want to use it in a JSX page:</p>
<pre><code class="language-jsx">export default function ({ comp }) {
  return &lt;comp.Button&gt;Click here&lt;/comp.Button&gt;;
}
</code></pre>
<p>Due JSX escapes the string values, the output code is this:</p>
<pre><code class="language-html">&amp;lt;button&amp;gt;Click here&amp;lt;/button&amp;gt;
</code></pre>
<p>To fix it, we need to create a container element with the
<code>dangerouslySetInnerHTML</code> attribute:</p>
<pre><code class="language-jsx">export default function ({ comp }) {
  return (
    &lt;div
      dangerouslySetInnerHTML={{
        __html: &lt;comp.Button&gt;Click here&lt;/comp.Button&gt;,
      }}
    /&gt;
  );
}
</code></pre>
<p>In Lume 3, thanks to SSX this is no longer necessary. Components are fully
interoperable and you can insert JSX components in Vento and viceversa. And to
make them even more interchangeable, the <code>content</code> and <code>children</code> variables are
equivalent.</p>
<pre><code class="language-jsx">export default function ({ comp }) {
  return (
    &lt;&gt;
      // This works
      &lt;comp.Button&gt;Click here&lt;/comp.Button&gt;

      // This also works
      &lt;comp.Button content=&quot;Click here&quot; /&gt;
    &lt;/&gt;
  );
}
</code></pre>
<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>
<p>In Lume 3, components can have extra data that will be used as default values.
This data is like layout data but applied to components.</p>
<p>Let's see the following <code>_components/title.vto</code> component as an example :</p>
<pre><code class="language-vto">---
title: Hello world
---

&lt;h1&gt;{{ title }}&lt;/h1&gt;
</code></pre>
<p>Now you can use the component with the default title.</p>
<pre><code class="language-vto">{{ await comp.title() }}
&lt;!-- &lt;h1&gt;Hello world&lt;/h1&gt; --&gt;
</code></pre>
<p>Or with a custom title</p>
<pre><code class="language-vto">{{ await comp.title({ title: &quot;New title&quot; }) }}
&lt;!-- &lt;h1&gt;New title&lt;/h1&gt;  --&gt;
</code></pre>
<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>
<p>As mentioned, Lume components can output CSS and JS code. However, some plugins
output code too. For example, <code>google_fonts</code> generates the CSS code needed to
load the fonts, <code>prism</code> and <code>code_highlight</code> export the CSS code with the
themes, and <code>katex</code> (that didn't generate CSS code in Lume 2) now generates the
CSS code automatically so you don't need to copy manually the CSS code.</p>
<p>Additionally, some plugins download also font files (specifically,
<code>google_fonts</code> and <code>katex</code>).</p>
<p>In Lume 2, you have to configure how to export the generated code for every
plugin individually. In Lume 3 there are three global options that will be used
by default by all plugins and components:</p>
<pre><code class="language-js">const site = lume({
  cssFile: &quot;/style.css&quot;, // default value
  jsFile: &quot;/script.js&quot;, // default value
  fontsFolder: &quot;/fonts&quot;, // default value
});
</code></pre>
<p>All extra code generated by components and the plugins <code>code_highlight</code>,
<code>google_fonts</code>, <code>prism</code>, <code>katex</code> and <code>unocss</code> will be stored there.</p>
<p>Of course, you can still change the code destination for a specific plugin:</p>
<pre><code class="language-js">site.use(unocss({
  cssFile: &quot;/unocss-styles.css&quot;,
}));
</code></pre>
<h2 id="tailwind-4" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#tailwind-4" class="header-anchor">Tailwind 4</a></h2>
<p>The <code>tailwindcss</code> plugin was upgraded to use
<a href="https://tailwindcss.com/blog/tailwindcss-v4">Tailwind 4</a>. The new version is
faster than v3 and no longer needs Postcss to work. There are many changes in
the configuration (especially the CSS-first configuration) so take a look at the
<a href="https://tailwindcss.com/docs/upgrade-guide">upgrade guide</a> if you want to
upgrade your projects from v3 to v4.</p>
<pre><code class="language-js">// Lume 2
site.use(tailwindcss());
site.use(postcss());

// Lume 3
site.use(tailwindcss());
site.add(&quot;style.css&quot;);
</code></pre>
<p>If you don't want to upgrade to v4, it's still possible to continue using
Tailwind 3 with the postcss plugin:</p>
<pre><code class="language-js">import tailwind from &quot;npm:tailwindcss@^3.4&quot;;

site.use(
  postcss({
    plugins: [tailwind()],
  }),
);
</code></pre>
<p>More info in the <a href="https://lume.land/plugins/tailwindcss/">plugin documentation</a>.</p>
<h2 id="processors-improvements" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#processors-improvements" class="header-anchor">Processors improvements</a></h2>
<p><code>site.process()</code> and <code>site.preprocess()</code> are among Lume's most used features.
Lume 3 brings some improvements here to make them easier to use.</p>
<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>
<p>One of the many uses of processors is to manipulate HTML pages using the
<code>page.document</code> property. But this property can return <code>undefined</code> if the page
is not HTML or cannot be parsed, so you have to check the variable type before
using it:</p>
<pre><code class="language-js">site.process([&quot;.html&quot;], (pages) =&gt; {
  for (const page of pages) {
    const document = page.document;
    if (!document) {
      continue;
    }
    const title = document.querySelector(&quot;title&quot;);
  }
});
</code></pre>
<p>In Lume 3, <code>page.document</code> always returns a <code>Document</code> instance or throws an
exception if the page cannot be parsed. This allows us to omit the type check:</p>
<pre><code class="language-js">site.process([&quot;.html&quot;], (pages) =&gt; {
  for (const page of pages) {
    const title = page.document.querySelector(&quot;title&quot;);
  }
});
</code></pre>
<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>
<p>The <code>page.content</code> variable containing the content of the page can be a string
or a <code>Uint8Array</code>, depending on how this page has been loaded. For example,
HTML, CSS or JS pages have the content as a string, but images or other binary
files are loaded as <code>Uint8Array</code>.</p>
<p>To process these files in Lume 2 you have to check the content type:</p>
<pre><code class="language-js">site.process([&quot;.css&quot;], (pages) =&gt; {
  for (const page of pages) {
    const content = page.content;

    if (typeof content === &quot;string&quot;) {
      page.content = &quot;/* © 2025 */&quot; + content;
    }
  }
});
</code></pre>
<p>In Lume 3, pages have two new properties: <code>page.text</code> and <code>page.bytes</code> (inspired
by the same properties of the
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Request#instance_methods">Request</a>
object). As you may guess, <code>page.text</code> allows to work with the page content as
strings, making the conversions automatic, and <code>page.bytes</code> does the same but
for <code>Uint8Array</code>.</p>
<pre><code class="language-js">site.process([&quot;.css&quot;], (pages) =&gt; {
  for (const page of pages) {
    page.text = &quot;/* © 2025 */&quot; + page.text;
  }
});
</code></pre>
<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>
<p>In Lume 2, the <code>*</code> wildcard allows you to process all pages:</p>
<pre><code class="language-js">site.process(&quot;*&quot;, (pages) =&gt; {
  // Process all pages
});
</code></pre>
<p>In Lume 3, the first argument can be omitted:</p>
<pre><code class="language-js">site.process((pages) =&gt; {
  // Process all pages
});
</code></pre>
<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>
<p>In Lume 2, the order in which some plugins are registered doesn't matter. Let's
see this example from the <code>sitemap</code> plugin:</p>
<pre><code class="language-js">site.use(sitemap()); // Generate the sitemap file
site.use(basePath()); // Add the base path to all URLs
</code></pre>
<p>The sitemap plugin is registered before basePath, so you may think the sitemap
file is generated before adding the base path prefix to all URLs. But
internally, the sitemap plugin is executed using the &quot;beforeSave&quot; event, which
is triggered at the end, just before saving all files to the _site folder. So
internally the basePath plugin is executed before.</p>
<p>This was designed so you don't have to think about the order of the plugins when
using them. But this behavior has two problems:</p>
<ul>
<li>There are many plugins in which the order matters. For example, if you combine
SASS and Postcss you have to process the SCSS files first and pass the result
to Postcss. This inconsistency makes you wonder in which plugins the order is
important or not.</li>
<li>It's not possible to use a processor to modify the output of these plugins.
For example, if you want to compress the sitemap file with brotli or gzip, is
not possible because the sitemap will be always generated at the end.</li>
</ul>
<p>To make Lume more transparent and intuitive, many plugins using events were
changed to use processors, which respect the order in which they are registered
in the _config.ts file.</p>
<p>The affected plugins are: <code>code_highlight</code>, <code>decap_cms</code>, <code>favicon</code>, <code>feed</code>,
<code>google_fonts</code>, <code>icons</code>, <code>prism</code>, <code>robots</code>, <code>sitemap</code>, and <code>slugify_urls</code>.</p>
<p>To help with this transition, Lume 3 comes with a
<a href="https://docs.deno.com/runtime/reference/lint_plugins/">lint plugin</a> that warns
you when the order of some plugins is not correct.</p>
<p><img src="https://lume.land/uploads/lint.png" alt="Image"></p>
<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>
<p>Deno is becoming a complicated runtime, especially for everything related to
module resolution. It supports three completely different types of packages
(HTTP, NPM, and JSR), with different behaviors, inconsistencies, and
incompatibilities between them. In addition to the usual complexity of NPM, in
Deno a package can be located in different places, depending on the variable
<code>nodeModulesDir</code>, if the file <code>package.json</code> is found, if the <code>node_modules</code>
folder exists, etc. JSR is not much better, because the resolution of a package
depends on the combination of <code>imports</code>, <code>exports</code>, and <code>patch</code> keys in
different <code>deno.json</code> and <code>deno.jsonc</code> files. And the addition of workspaces
adds a new layer of complexity.</p>
<p>In Lume 2, the <code>esbuild</code> plugin delegates all this complexity to
<a href="https://esm.sh/">esm.sh</a>, which transforms any NPM or JSR package to simple
HTTP imports that are easier to manage. But this solution has its problems with
multiple configuration options (<code>deps</code>, <code>pin</code>, <code>alias</code>, <code>standalone</code>, <code>exports</code>,
etc) and there are many packages that don't work well after passing them through
esm.sh.</p>
<p>In Lume 3 the <code>esbuild</code> plugin uses the
<a href="https://jsr.io/@luca/esbuild-deno-loader">esbuild-deno-loader</a> plugin created
by Luca Casonato, a member of the Deno team. This will make your bundled code
more reliable and compatible with how Deno works.</p>
<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>
<p>In Lume 2, the <code>basename</code> variable allows changing the name of a file or
directory. When missing, it's automatically defined by Lume using the page
filename. For example, the page <code>/posts/first-post.md</code> has the basename
<code>first-post</code>.</p>
<p>In Lume 3 this variable uses the final URL of the page, instead of the source
filename. For example, if the <code>/post/first-post.md</code> page generates a different
URL (say <code>/post/other-name/</code>) the <code>basename</code> is <code>other-name</code>.</p>
<p>Additionally, the basename no longer accepts &quot;index&quot; as a value. For example,
the basename for the <code>/post/hello-world/index.md</code> is <code>hello-world</code> (the folder
name) instead of <code>index</code> (the filename).</p>
<p>These changes will make this variable more consistent across all pages, no
matter how the URL is generated. It's especially important for the <code>nav</code> plugin
that uses this variable to sort pages alphabetically.</p>
<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>
<p>Lume 2 detects automatically the <code>date</code> value from the files and folders paths
and remove it. For example, the file <code>/posts/2020-06-21_hello-world.md</code> outputs
the page <code>/posts/hello-world/</code> (without the date).</p>
<p>Some people don't want this behavior and prefer to keep the date in the output
URL. Following the Lume's philosophy of having a light core and provide extra
features through plugins, this feature was removed from the core and the new
<code>extract_date</code> plugin was created to enable it.</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import extractDate from &quot;lume/plugins/extract_date.ts&quot;;

const site = lume();

site.use(extractDate());

export default site;
</code></pre>
<p>By default the plugin provides the same behavior of Lume 2, but it's possible to
extract the date without removing it from the URL:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import extractDate from &quot;lume/plugins/extract_date.ts&quot;;

const site = lume();

site.use(extractDate({
  remove: false, // Keep the date
}));

export default site;
</code></pre>
<h2 id="removed-plugins" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#removed-plugins" class="header-anchor">Removed plugins</a></h2>
<p>In addition to <code>jsx_preact</code>, two more plugins were removed in Lume 3: <code>liquid</code>
and <code>on_demand</code>.</p>
<p>Liquid lets you using <a href="https://liquidjs.com/">LiquidJS</a> as a template engine to
build pages. The syntax is very similar to Nunjucks and the library is actively
maintained but it has a big limitation: it's not possible to invoke functions.
This makes this template engine useless in Lume because it's not possible to use
helpers like <code>search</code> or <code>nav</code> to search pages or build the navigation. The
plugin has been deprecated for a while, and it was removed in Lume 3.</p>
<p>The <code>on_demand</code> plugin was mainly an experiment to see if it was possible to add
some dynamic behavior to Lume sites. But it never worked well, the
implementation was a bit hacky to make it work on Deno Deploy, and it was too
limited. Lume has the <a href="https://lume.land/plugins/router/">router</a> for simple use
cases, and for complex cases, maybe you have to use a different framework. The
purpose of Lume never was to become into one-size-fits-all solution.</p>
<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>
<p>The following removals aim to improve the stability and interoperability between
plugins.</p>
<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>
<p>In Lume 2, some plugins have the <code>extensions</code> option to configure which files
you want to process. You rarely need to modify this option because Lume provides
sensible defaults. For example, the default value for
<a href="https://lume.land/plugins/postcss/">Postcss</a> plugin is <code>[&quot;.css&quot;]</code>:</p>
<pre><code class="language-js">site.use(postcss({
  extensions: [&quot;.css&quot;], // &lt;- You don't need this
}));
</code></pre>
<p>In most cases, this option doesn't make sense, because you can set any value but
the plugin expects a specific format, like HTML pages to use DOM API or CSS code
to process:</p>
<pre><code class="language-js">site.use(postcss({
  extensions: [&quot;.html&quot;], // &lt;- This breaks the build
}));
</code></pre>
<p>In Lume 3, this option was removed in many plugins:</p>
<ul>
<li>purgecss, postcss, and lightningcss always process <code>.css</code> files.</li>
<li>sass always processes <code>.scss</code> and <code>.sass</code> files.</li>
<li>svgo always processes <code>.svg</code> files.</li>
<li>check_urls, base_path, relative_urls and modify_urls process <code>.css</code> and
<code>.html</code> files.</li>
<li>filter_pages processes all extensions.</li>
<li>code_highlight, fff, inline, json_ld, katex, metas, multilanguage, og_images,
and prism always process <code>.html</code> pages.</li>
</ul>
<h3 id="name-option" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#name-option" class="header-anchor">Name option</a></h3>
<p>There are other plugins that register filters or helpers that you can use in
your pages. In Lume 2 you could customize the name of these elements. For
example, it's possible to use a different key to store the data for the <code>metas</code>
plugin:</p>
<pre><code class="language-js">site.use(metas({
  name: &quot;opengraph&quot;,
}));
</code></pre>
<p>Or the filter name of the <code>date</code> plugin:</p>
<pre><code class="language-js">site.use(date({
  name: &quot;get_date&quot;,
}));
</code></pre>
<p>Changing the default name of the plugins have two problems:</p>
<ul>
<li>The types declared by the plugin don't change, so even if you change the key
<code>metas</code> to <code>opengraph</code>, <code>Lume.Data.metas</code> still exist.</li>
<li>This breaks the interoperability between plugins. For example, <code>picture</code> and
<code>transform_images</code> depend on the same key name. If you change it for only one
plugin, the other won't work.</li>
</ul>
<p>In Lume 3, the <code>name</code> option was removed in the following plugins, so it's no
longer possible to change it to something else: <code>date</code>, <code>json_ld</code>, <code>metas</code>,
<code>nav</code>, <code>paginate</code>, <code>picture</code>, <code>reading_info</code>, <code>search</code>, <code>transform_images</code>,
<code>url</code> and <code>postcss</code>.</p>
<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>
<ul>
<li>cache option in <code>transform_images</code>, <code>favicon</code> and <code>og_images</code></li>
<li><code>attribute</code> option in <code>inline</code>.</li>
<li>Components are always in the <code>comp</code> variable. The option to customize this
variable name has been removed.</li>
</ul>
<p>Most Lume users don't change these options, so most likely these removals don't
affect your upgrade to Lume 3.</p>
<h2 id="other-changes" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#other-changes" class="header-anchor">Other changes</a></h2>
<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>
<p>The <a href="https://github.com/tc39/proposal-temporal">Temporal proposal</a> provides
standard objects and functions for working with dates and times. It's being
implemented in all browsers and it's supported by Deno with the
<code>unstable-temporal</code> flag. Lume 2 uses
<a href="https://www.npmjs.com/package/@js-temporal/polyfill">a polyfill</a>, but Lume 3
uses the Deno implementation, which requires to enable it in <code>deno.json</code> file:</p>
<pre><code class="language-json">{
  &quot;unstable&quot;: [&quot;temporal&quot;]
}
</code></pre>
<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>
<p>As of version 3, Lume will support at least the most recent Deno LTS version
(and probably some older versions too). Lume 3.0 supports Deno 2.1 and greater.
More info
<a href="https://docs.deno.com/runtime/fundamentals/stability_and_releases/#long-term-support-(lts)">about Deno LTS releases</a>.</p>
<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>
<p>Lume 2 automatically added <code>&lt;!doctype html&gt;</code> to any HTML pages that were missing
it. The original reason was because JSX doesn't allow adding this directive, so
it was difficult to create HTML pages with only JSX. However, some users don't
want this behavior because they create files with fragments of HTML. In Lume 3,
it is possible to add the <code>doctype</code> directive in JSX (thanks to SSX) so this
behavior is no longer needed and was removed.</p>
<h3 id="more-changes" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#more-changes" class="header-anchor">More changes</a></h3>
<p>As always, you can see
<a href="https://github.com/lumeland/lume/blob/v3.0.0/CHANGELOG.md">the CHANGELOG.md file</a>
for a complete list of all changes with more details.</p>
<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>
<p>While the development server is running, Lume 3 features a debug bar that
provides valuable insights, including warnings and issues flagged by plugins.</p>
<p><img src="https://lume.land/uploads/debugbar.png" alt="Image"></p>
<p>The Lume debug bar can be extended easily by plugins or directly in the
<code>_config.ts</code>. For example, let's create a simple tab to list all pages without
title:</p>
<pre><code class="language-js">function createTab() {
  // Create a collection in the debug bar
  const collection = site.debugBar?.collection(&quot;Pages without title&quot;);

  // The debug bar is enabled if the collection was created
  if (collection) {
    collection.icon = &quot;file&quot;;

    // Add items to the collection
    collection.items = site.pages
      .filter((page) =&gt; page.outputPath.endsWith(&quot;.html&quot;)) // Only HTML pages
      .filter((page) =&gt; !page.data.title) // No title
      .map((page) =&gt; ({
        title: page.data.url,
        actions: [
          {
            text: &quot;Visit&quot;,
            href: page.data.url,
          },
        ],
      }));
  }
}

// Run this function after building and updating the site
site.addEventListener(&quot;afterBuild&quot;, createTab);
site.addEventListener(&quot;afterUpdate&quot;, createTab);
</code></pre>
<p>That's it! Our custom tab is now part of the Lume debug bar, and it has already
identified two pages without titles!</p>
<p><img src="https://lume.land/uploads/custom-debug-tab.png" alt="Image"></p>
<p>The Lume debug bar is still an experimental feature, but it has been very well
received since its introduction in our Discord community. Developers are already
working on plugins to enhance the debug bar with features like HTML validator
reports, accessibility checks, and SEO analysis.</p>
<p>Keep in mind that the debug bar is only visible when running Lume with
<code>deno task serve</code>. It is not included in production builds. If you wish to
disable this feature completely, you can do so by editing the <code>_config.ts</code> file:</p>
<pre><code class="language-js">const site = lume({
  server: {
    debugBar: false, // disable the debug bar
  },
});
</code></pre>
<h2 id="thanks!" tabindex="-1"><a href="https://lume.land/blog/posts/lume-3/#thanks!" class="header-anchor">Thanks!</a></h2>
<p>All this work wouldn't be possible without the help from all people that
contribute to Lume. Thanks to everyone that
<a href="https://opencollective.com/lume">sponsor Lume</a>
<a href="https://github.com/sponsors/oscarotero">or directly me</a>. Thanks also to people
that have been testing Lume 3 in the latest months or even using it in real
projects, reporting bugs and providing feedback (specially
<a href="https://timthepost.deno.dev/">Tim Post</a> and <a href="https://cogley.jp/">Rick Cogley</a>),
and thanks to <a href="https://pyrox.dev/">Pyrox</a> for reviewing the grammar of this
post.</p>
]]>
      </content:encoded>
      <pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume 2.5.0 - Pedro Días and Muño Vandilaz</title>
      <link>https://lume.land/blog/posts/lume-2-5-0-pedro-munho/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/lume-2-5-0-pedro-munho/</guid>
      <content:encoded>
        <![CDATA[<p><strong><em>Feliz aninovo</em> 🎄!</strong></p>
<p>New year and new Lume version! This time, I'd like to dedicate it to Pedro Días
and Muño Vandilaz, who married on April 16, 1061, almost a thousand years ago.
This is the first same-sex marriage documented in Galicia (and the rest of
Spain).</p>
<p>The wedding took place in a small Catholic chapel. It's surprising to see how
homophobic prejudices have changed since then. If you want to read more about
this event take a look at
<a href="https://qnews.com.au/on-this-day-april-16-pedro-diaz-and-muno-vandilaz/">this Qnews article (English)</a>
or
<a href="https://www.gciencia.com/tribuna/unha-voda-entre-dous-homes-no-ourense-do-seculo-xi/">gCiencia post (Galician)</a>.</p>
<!-- more -->
<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>
<p><a href="https://json-ld.org/">JSON-LD</a> (JSON for Linking Data) is a way to provide
<a href="https://www.schema.org/">structured data</a> to web pages using JSON format, which
is easier to parse and doesn't require to modify the HTML code. It's defined
with a <code>&lt;script type=&quot;application/ld+json&quot;&gt;</code> element containing the JSON code.
For example:</p>
<pre><code class="language-html">&lt;script type=&quot;application/ld+json&quot;&gt;
  {
    &quot;@context&quot;: &quot;https://schema.org&quot;,
    &quot;@type&quot;: &quot;WebSite&quot;,
    &quot;url&quot;: &quot;https://oscarotero.com/&quot;,
    &quot;headline&quot;: &quot;Óscar Otero - Web designer and developer&quot;,
    &quot;name&quot;: &quot;Óscar Otero&quot;,
    &quot;description&quot;: &quot;I’m just a designer and web developer&quot;,
    &quot;author&quot;: {
      &quot;@type&quot;: &quot;Person&quot;,
      &quot;name&quot;: &quot;Óscar Otero&quot;
    }
  }
&lt;/script&gt;
</code></pre>
<p>The <code>json_ld</code> plugin, created by <a href="https://github.com/shuaixr">Shuaixr</a>, makes
easier to work with this structured data. Edit your <code>_config</code> file to install
it:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import jsonLd from &quot;lume/plugins/json_ld.ts&quot;;

const site = lume();
site.use(jsonLd());

export default site;
</code></pre>
<p>Then, you can create the <code>jsonLd</code> variable in your pages. For example:</p>
<pre><code class="language-yml">jsonLd:
  &quot;@type&quot;: WebSite
  url: /
  headline: Óscar Otero - Web designer and developer
  name: Óscar Otero
  description: I’m just a designer and web developer
  author:
    &quot;@type&quot;: Person
    name: Óscar Otero
</code></pre>
<p>Note the following:</p>
<ul>
<li>The plugin automatically adds the <code>@context</code> property if it's missing</li>
<li>URLs can omit the protocol and host. The plugin automatically resolves all
URLs based on the <code>location</code> of the site.</li>
</ul>
<p>Like with other similar plugins like <a href="https://lume.land/plugins/metas/">metas</a>,
you can use field aliases:</p>
<pre><code class="language-yml">title: Óscar Otero - Web designer and developer
header:
  title: Óscar Otero
  description: I’m just a designer and web developer

jsonLd:
  &quot;@type&quot;: WebSite
  url: /
  headline: =title
  name: =header.title
  description: =header.description
  author:
    &quot;@type&quot;: Person
    name: =header.title
</code></pre>
<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>
<p>If you want to use TypeScript, there's the <code>Lume.Data[&quot;jsonLd&quot;]</code> type (powered
by <a href="https://www.npmjs.com/package/schema-dts">schema-dts</a> package):</p>
<pre><code class="language-ts">export const jsonLd: Lume.Data[&quot;jsonLd&quot;] = {
  &quot;@type&quot;: &quot;WebSite&quot;,
  url: &quot;/&quot;,
  headline: &quot;Óscar Otero - Web designer and developer&quot;,
  description: &quot;I’m just a designer and web developer&quot;,
  name: &quot;Óscar Otero&quot;,
  author: {
    &quot;@type&quot;: &quot;Person&quot;,
    name: &quot;Óscar Otero&quot;,
  },
};
</code></pre>
<p>More info in the
<a href="https://lume.land/plugins/json_ld/">plugin documentation page</a>.</p>
<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>
<p><a href="https://purgecss.com/">PurgeCSS</a> is a utility to remove unused CSS code, making
your CSS files smaller to improve the site performance. The tool provides a
Postcss plugin so, in theory, it can also be used in Lume. Now it has its own
plugin (big thanks to <a href="https://github.com/into-the-v0id"><em>into-the-v0id</em></a>) which
has some advantages:</p>
<ul>
<li>Scan generated HTML pages by Lume</li>
<li>Scan bundled JS dependencies (bootstrap, etc)</li>
<li>Only include CSS that is necessary (don't include drafts or conditional HTML
that does not make it into the build)</li>
</ul>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import purgecss from &quot;lume/plugins/purgecss.ts&quot;;

const site = lume();
site.use(purgecss());

export default site;
</code></pre>
<p>Go to the <a href="https://lume.land/plugins/purgecss/">plugin documentation page</a> for
more info.</p>
<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>
<p>Lume is a static site generator (and always will be). But sometimes you need
some server-side logic to handle small things. For example, to handle the data a
user sends from an HTML form, or maybe you need a small API to provide dynamic
data.</p>
<p>For sites requiring front and back, you have great options like
<a href="https://fresh.deno.dev/">Fresh</a>, <a href="https://astro.build/">Astro</a> or
<a href="https://hono.dev/">Hono</a>. But if you only need a couple of entry points, you
may consider using something simpler like <code>router</code> middleware, which is a
minimal router that works great with Lume's server.</p>
<pre><code class="language-js">import Server from &quot;lume/core/server.ts&quot;;
import Router from &quot;lume/middlewares/router.ts&quot;;

// Create the router
const router = new Router();

router.get(&quot;/hello/:name&quot;, ({ name }) =&gt; {
  return new Response(`Hello ${name}`);
});

// Create the server:
const server = new Server();

server.use(router.middleware());

server.start();
</code></pre>
<p>That's all. Now the <code>/hello/laura</code> request will return a <code>Hello laura</code> response!</p>
<p>The router uses the standard
<a href="https://developer.mozilla.org/en-US/docs/Web/API/URLPattern">URLPattern</a> under
the hood that creates an object with all variables captured in the path and
passes it as the first argument of the route handler.</p>
<p>In addition to the captured variables, you have the <code>request</code> property with the
Request instance:</p>
<pre><code class="language-js">router.get(&quot;/search&quot;, ({ request }) =&gt; {
  const { searchParams } = new URL(request.url);

  const query = searchParams.get(&quot;query&quot;);
  return new Response(`Searching by ${query}`);
});
</code></pre>
<p>Note that to use this middleware in production, you need a hosting service
running Deno like <a href="https://deno.com/deploy">Deno Deploy</a> or similar.</p>
<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>
<p>Sometimes you have your content in Markdown or HTML, but also need a plain text
version. Let's see the following example:</p>
<pre><code class="language-vto">---
title: Welcome to **my site**
---
&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;{{ title }}&lt;/title&gt;
  &lt;/head&gt;

  &lt;body&gt;
    &lt;h1&gt;{{ title |&gt; md(true) }}&lt;/h1&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The <code>title</code> variable uses Markdown syntax to render
<code>Welcome to &lt;strong&gt;my site&lt;/strong&gt;</code>. But this also affects the <code>&lt;title&gt;</code>
element of the page which contains the asterisks.</p>
<p>The new <code>plaintext</code> plugin registers the <code>plaintext</code> filter, that not only
removes any Markdown and HTML syntax but also linebreaks and extra spaces:</p>
<pre><code class="language-vto">---
title: Welcome to **my site**
---
&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;{{ title |&gt; plaintext }}&lt;/title&gt;
  &lt;/head&gt;

  &lt;body&gt;
    &lt;h1&gt;{{ title |&gt; md(true) }}&lt;/h1&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The plugin is disabled by default so you need to import it to your _config.ts
file:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import plaintext from &quot;lume/plugins/plaintext.ts&quot;;

const site = lume();
site.use(plaintext());

export default site;
</code></pre>
<p>More info in the
<a href="https://lume.land/plugins/plaintext/">plugin documentation page</a>.</p>
<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>
<p>Some plugins like <a href="https://lume.land/plugins/google_fonts/"><code>google_fonts</code></a>,
<a href="https://lume.land/plugins/prism/"><code>prism</code></a> or
<a href="https://lume.land/plugins/code_highlight/"><code>code_highlight</code></a> can generate CSS
code. The way this code is generated is different for each plugin.</p>
<p>Google Fonts plugin has the <code>cssFile</code> option to configure the filename to output
the CSS code. If the css file doesn't exist, it's created. If it already exists,
the code is appended at the end. You can use the <code>placeholder</code> option to insert
the code at some point in the middle of the file.</p>
<pre><code class="language-js">site.use(googleFonts({
  cssFile: &quot;styles.css&quot;,
  placeholder: &quot;/* insert-google-fonts-here */&quot;,
  // ...more options
}));
</code></pre>
<p>Prism and Code Highlight plugins output the theme's CSS code differently. The
<code>theme</code> option has the <code>path</code> property but it's not the <strong>output</strong> css file but
the <strong>source file</strong>:</p>
<pre><code class="language-js">site.use(prism({
  theme: {
    name: &quot;funky&quot;,
    path: &quot;/_includes/css/code_theme.css&quot;,
  },
}));
</code></pre>
<p>This means that if you set the path as <code>/_includes/css/code_theme.css</code>, this
file must be imported somewhere in your CSS code in order to be visible:</p>
<pre><code class="language-css">@import &quot;css/code_theme.css&quot;;
</code></pre>
<p>The problem with this approach is it requires two steps: first, configure the
source file name in the plugin, and then import the file in your CSS file (or
copy it with <code>site.copy()</code>).</p>
<p>The Google fonts approach is more straightforward.</p>
<p>In order to make Lume more consistent across all plugins, I want to unify the
way the CSS code is generated everywhere. That's why the <code>theme.path</code> option of
Prism and Code Highlight plugins are now deprecated and the new <code>theme.cssFile</code>
and <code>theme.placeholder</code> options were added.</p>
<pre><code class="language-js">site.use(prism({
  theme: {
    name: &quot;funky&quot;,
    cssFile: &quot;styles.css&quot;,
    placeholder: &quot;/* prism-theme-here */&quot;,
  },
}));
</code></pre>
<p>This change is also aligned with the
<a href="https://lume.land/docs/configuration/config-file/#components-options"><code>components.placeholder</code> option</a>
introduced in Lume 2.4.</p>
<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>
<ul>
<li>Added <code>subset</code> options to
<a href="https://lume.land/plugins/google_fonts/">Google fonts</a> plugin.</li>
<li>Added <code>ui.globalVariable</code> option to
<a href="https://lume.land/plugins/pagefind/">Pagefind</a> plugin to store the pagefind
instance in a global variable for future manipulation.</li>
<li>Hot reload inline script includes the integrity hash, to avoid CSP issues.</li>
<li>Files with extension <code>.d.ts</code> are ignored by Lume, to avoid generating empty
files.</li>
<li>Updated the default browser versions supported by
<a href="https://lume.land/plugins/lightningcss/">LightningCSS</a> plugin.</li>
</ul>
<p>See
<a href="https://github.com/lumeland/lume/blob/v2.5.0/CHANGELOG.md">the CHANGELOG.md file</a>
to see a list of all changes with more detail.</p>
]]>
      </content:encoded>
      <pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume 2.4.0 - Maruja Mallo</title>
      <link>https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/lume-2-4-0-maruja-mallo/</guid>
      <content:encoded>
        <![CDATA[<p>Ola 👋!</p>
<p>This new version of Lume is dedicated to
<a href="https://en.wikipedia.org/wiki/Maruja_Mallo">Maruja Mallo,</a> an extraordinary
surrealist painter born in Galicia in 1902 who gained international fame.
<a href="https://edspace.american.edu/marujamalloheadsofwomen/maruja-mallo-biography/">Learn more about Maruja</a>.</p>
<!-- more -->
<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>
<p>Broken links are one of the biggest issues on the Web. A recent study detected
that
<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>.
And for those sites that are still alive, they are likely to change the URLs at
some point, after a redesign or content updates, causing a lot of broken links.</p>
<p>The new plugin <code>check_urls</code> will help you to keep your links healthy, by
checking all links in your website (not only to HTML pages but also files like
images, JavaScript or CSS). This plugin already existed for some time as
<a href="https://github.com/lumeland/experimental-plugins">experimental plugin</a> thanks
to <a href="https://github.com/iacore">iacore</a>, but it was moved to the main Lume repo
and was improved with additional features.</p>
<p>The basic way to use it is like any other plugin. No big surprises here!</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import checkUrls from &quot;lume/plugins/check_urls.ts&quot;;

const site = lume();
site.use(checkUrls());

export default site;
</code></pre>
<p>The default configuration will check all your internal links and warns you when
a broken link is found. This plugin is compatible with
<a href="https://lume.land/plugins/redirects/">redirects</a>: when a link to a non-existing
page is found, but it redirects to an existing page, the url is valid.</p>
<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>
<p>There's a mode for a more <em>strict</em> detection:</p>
<pre><code class="language-js">site.use(checkUrls({
  strict: true,
}));
</code></pre>
<p>In the <em>strict</em> mode the <strong>redirects are not allowed,</strong> all links must go to the
final page. This also affects to the trailing slashes: for example <code>/about-me</code>
is invalid but <code>/about-me/</code> is valid.</p>
<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>
<p>By default, the plugin only checks internal links. But you can configure it to
check links to external domains:</p>
<pre><code class="language-js">site.use(checkUrls({
  external: true,
}));
</code></pre>
<div class="markdown-alert markdown-alert-warning">
<p class="markdown-alert-title">Warning</p>
<p>This option can make the build slower, specially if you have many external
links, so probably it's a good idea to enable it only occasionally.</p>
</div>
<p>Learn more about this plugin
<a href="https://lume.land/plugins/check_urls/">in the documentation page</a>.</p>
<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>
<p>Nowadays, most websites are using icons to a greater or lesser extent. The
<code>icons</code> plugin allows to use some of the most popular SVG icon libraries. The
installation can't be easier:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import icons from &quot;lume/plugins/icons.ts&quot;;

const site = lume();
site.use(icons());

export default site;
</code></pre>
<p>To import an icon, just use the <code>icon</code> filter which returns the path of the
icon's svg file.</p>
<pre><code class="language-html">&lt;img src=&quot;{{ &quot;acorn&quot; |&gt; icon(&quot;phosphor&quot;) }}&quot;&gt;
</code></pre>
<p>Lume will download the &quot;acorn&quot; icon from the popular
<a href="https://phosphoricons.com/">Phosphor</a> library into <code>/icons/phosphor/acorn.svg</code>
(the output folder is configurable) and return the path.</p>
<p>Some icons have different variations that you can configure with the
<code>name:variation</code> syntax:</p>
<pre><code class="language-html">&lt;img src=&quot;{{ &quot;acorn:duotone&quot; |&gt; icon(&quot;phosphor&quot;) }}&quot;&gt;
</code></pre>
<p>Alternatively, you can set the variation in the second argument of the filter:</p>
<pre><code class="language-html">&lt;img src=&quot;{{ &quot;acorn&quot; |&gt; icon(&quot;phosphor&quot;, &quot;duotone&quot;) }}&quot;&gt;
</code></pre>
<p>You can use <a href="https://lume.land/plugins/inline/"><code>inline</code> plugin</a> to inline the
SVG code in the HTML.</p>
<pre><code class="language-html">&lt;img src=&quot;{{ &quot;acorn&quot; |&gt; icon(&quot;phosphor&quot;) }}&quot; inline&gt;
</code></pre>
<p>The icon plugin supports the following icon collections and it's easily
extensible with more.</p>
<ul>
<li><a href="https://ant.design/components/icon">Ant</a></li>
<li><a href="https://icons.getbootstrap.com/">Bootstrap</a></li>
<li><a href="https://boxicons.com/">Boxicons</a></li>
<li><a href="https://react.fluentui.dev/?path=/docs/icons-catalog--docs">Fluent</a></li>
<li><a href="https://heroicons.com/">Heroicons</a></li>
<li><a href="https://iconoir.com/">Iconoir</a></li>
<li><a href="https://lucide.dev/">Lucide</a></li>
<li><a href="https://fonts.google.com/icons?icon.set=Material+Icons">Material Icons</a></li>
<li><a href="https://fonts.google.com/icons?icon.set=Material+Symbols">Material Symbols</a></li>
<li><a href="https://www.mingcute.com/">Mingcute</a></li>
<li><a href="https://mynaui.com/icons">Myna</a></li>
<li><a href="https://primer.style/foundations/icons">Octicons</a></li>
<li><a href="https://openmoji.org/">Openmoji</a></li>
<li><a href="https://phosphoricons.com/">Phosphor</a></li>
<li><a href="https://remixicon.com/">Remix icons</a></li>
<li><a href="https://sargamicons.com/">Sargam</a></li>
<li><a href="https://simpleicons.org/">Simpleicons</a></li>
<li><a href="https://tabler.io/icons">Tabler</a></li>
</ul>
<p>Learn more about this plugin
<a href="https://lume.land/plugins/icons/">in the documentation page</a>.</p>
<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>
<p>Another common asset used to build sites is webfonts.
<a href="https://fonts.google.com/">Google Fonts</a> is a fantastic resource for open
source fonts, but loading the fonts from the Google Fonts CDN is not the best
option, not only for privacy and GDPR compliance, but also
<a href="https://github.com/HTTPArchive/almanac.httparchive.org/pull/607">for performance</a>.</p>
<p>The <code>google_fonts</code> plugin downloads the optimized font files from Google fonts
automatically into the <code>/fonts</code> directory (configurable) and generates the
<code>/fonts.css</code> file (also configurable) with the <code>@font-face</code> declarations.</p>
<p>To use it, just register the plugin passing the sharing URL of your font
selection. For example, let's say we want to use
<a href="https://fonts.google.com/share?selection.family=Playfair+Display:ital,wght@0,400..900;1,400..900">Playfair Display</a>:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import googleFonts from &quot;lume/plugins/google_fonts.ts&quot;;

const site = lume();

site.use(googleFonts({
  fonts:
    &quot;https://fonts.google.com/share?selection.family=Playfair+Display:ital,wght@0,400..900;1,400..900&quot;,
}));

export default site;
</code></pre>
<p>It's possible to rename the fonts, useful if you want to change a font without
changing the code:</p>
<pre><code class="language-js">site.use(googleFonts({
  fonts: {
    display: &quot;https://fonts.google.com/share?selection.family=Playfair+Display:ital,wght@0,400..900;1,400..900&quot;,
    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;
}));
</code></pre>
<p>In the example above, the <strong>Playfair Display</strong> font is renamed to &quot;display&quot; and
<strong>Roboto</strong> to &quot;text&quot;, so this allows the use of the fonts in the CSS code with
these names:</p>
<pre><code class="language-css">h1 {
  font-family: display;
}
body {
  font-family: text;
}
</code></pre>
<p><a href="https://lume.land/plugins/google_fonts/">Go to the documentation page</a> to learn
more about the Google Fonts plugin!</p>
<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>
<p>Thanks to <a href="https://github.com/into-the-v0id">Into the V0id</a> for adding these two
plugins to Lume. They are useful for compressing text-based files (like HTML,
JavaScript, SVG, or CSS files) using the Gzip and Brotli algorithms and output
files with the same name but with <code>.gz</code> or <code>.br</code> extensions. For example, in
addition to the <code>/index.html</code> page, the plugins generate also <code>/index.html.gz</code>
(for Gzip) and <code>/index.html.br</code> (for Brotli).</p>
<p>I think it's not necessary to show how to activate the plugin, but just to
demonstrate how predictable and &quot;boring&quot; Lume is:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import brotli from &quot;lume/plugins/brotli.ts&quot;;

const site = lume();

site.use(brotli());

export default site;
</code></pre>
<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>
<p><code>brotli</code> and <code>gzip</code> plugins can be combined with
<a href="https://lume.land/docs/core/server/#precompress">the new <code>precompress</code> middleware</a>
if you're using <a href="https://lume.land/docs/core/server/">Lume server</a> to serve your
static files (for example in Deno Deploy). This middleware checks the
<code>Accept-Encoding</code> header and if the browser accepts <code>br</code> or <code>gzip</code> values, it
will serve the precompressed file.</p>
<pre><code class="language-js">import Server from &quot;lume/core/server.ts&quot;;
import precompress from &quot;lume/middlewares/precompress.ts&quot;;

const server = new Server();

server.use(precompress());

server.start();
</code></pre>
<p>Learn more about these plugins in the
<a href="https://lume.land/plugins/brotli/">brotli</a> and
<a href="https://lume.land/plugins/gzip/">gzip</a> documentation pages.</p>
<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>
<p>The <a href="https://lume.land/plugins/modify_urls/"><code>modify_urls</code> plugin</a> now can
search and modify urls in CSS files. This is not important only for this plugin
but also for other plugins that use <code>modify_urls</code> under the hood, like
<a href="https://lume.land/plugins/base_path/"><code>base_path</code></a> and
<a href="https://lume.land/plugins/relative_urls/"><code>relative_urls</code></a>.</p>
<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>
<p><code>base_path</code> is one of Lume's most useful plugins because it adds a prefix to all
absolute URLs of your site. This is important if your site is hosted in a
subdirectory.</p>
<p>For example, let's say you want to host your blog in the location
<code>https://my-site.com/blog/</code> and you have this HTML code:</p>
<pre><code class="language-html">&lt;a href=&quot;/posts/hello-world/&quot;&gt;Hello world&lt;/a&gt;
</code></pre>
<p>The plugin automatically fixes the URL to add the <code>/blog/</code> prefix:</p>
<pre><code class="language-html">&lt;a href=&quot;/blog/posts/hello-world/&quot;&gt;Hello world&lt;/a&gt;
</code></pre>
<p>Until now, the plugin only transformed URLs in HTML pages. If your site has this
CSS code:</p>
<pre><code class="language-css">.background {
  background-image: url(&quot;/img/bg.png&quot;);
}
</code></pre>
<p>The background image will fail because the <code>/blog/</code> prefix is missing. As of
Lume 2.4.0, this plugin can transform also CSS files. This option is disabled by
default, it requires to configure it in the _config.ts file:</p>
<pre><code class="language-js">site.use(basePath({
  extensions: [&quot;.html&quot;, &quot;.css&quot;],
}));
</code></pre>
<p>Now not only HTML pages but also CSS files will be processed:</p>
<pre><code class="language-css">.background {
  background-image: url(&quot;/blog/img/bg.png&quot;);
}
</code></pre>
<div class="markdown-alert markdown-alert-important">
<p class="markdown-alert-title">Important</p>
<p>Keep in mind that Lume only processes files that are loaded. To transform CSS
files they must be loaded before. If you're using any styling plugin like
<a href="https://lume.land/plugins/postcss/"><code>postcss</code></a>,
<a href="https://lume.land/plugins/lightningcss/"><code>lightningcss</code></a>, or
<a href="https://lume.land/plugins/sass/"><code>sass</code></a>, you don't need to do anything else.
But if you are copying the css files with <code>site.copy([&quot;.css&quot;])</code> or
<code>site.copy(&quot;/styles&quot;)</code> they won't be processed. To fix it, you have to use
<code>site.loadAssets([&quot;.css&quot;])</code>.</p>
</div>
<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>
<p>Some plugins like <code>metas</code> and <code>feed</code> allow to
<a href="https://lume.land/plugins/metas/#field-aliases">define aliases</a> to other
variables. For example, if we want to use the variable <code>title</code> inside
<code>metas.title</code>:</p>
<pre><code class="language-yml">title: Page title
metas:
  title: =title
</code></pre>
<p>As of Lume 1.4, it's possible to define fallbacks to other variables or provide
a default variable:</p>
<pre><code class="language-yml">metas:
  title: =title || =header.title || Default title
</code></pre>
<p>In this example, the title used in metas is the <code>title</code> variable. If it's not
defined, the <code>header.title</code> variable is used. And if it's doesn't exist, the
string &quot;Default title&quot; will be used.</p>
<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>
<p>In addition to fallbacks, the <a href="https://lume.land/plugins/feed/"><code>feed</code> plugin</a>
has added support for the author name and author URL variables:</p>
<pre><code class="language-js">site.use(feed({
  output: [&quot;/posts.rss&quot;, &quot;/posts.json&quot;],
  query: &quot;type=post&quot;,
  info: {
    title: &quot;=site.title&quot;,
    description: &quot;=site.description&quot;,
    authorName: &quot;=site.author.name&quot;,
    authorUrl: &quot;=site.author.url&quot;,
  },
  items: {
    title: &quot;=title&quot;,
    description: &quot;=excerpt&quot;,
    authorName: &quot;=author.name&quot;,
    authorUrl: &quot;=author.url&quot;,
  },
}));
</code></pre>
<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>
<ul>
<li>Several improvements to <a href="https://lume.land/plugins/esbuild/"><code>esbuild</code> plugin</a>
by <a href="https://github.com/into-the-v0id">Into the V0id</a>.</li>
<li>Added the new variable <code>fediverse</code> to the
<a href="https://lume.land/plugins/metas/"><code>metas</code> plugin</a>, to generate the
<code>&lt;meta name=&quot;fediverse:creator&quot; content=&quot;...&quot;&gt;</code> tag
<a href="https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/">added to Mastodon</a>.</li>
<li>Fixed some bugs related to Windows support and CJK characters.</li>
<li>New option <code>placeholder</code> in the
<a href="https://lume.land/plugins/unocss/"><code>unocss</code> plugin</a> to insert the generated
code in a specific place.</li>
<li>New option <code>placeholder</code> in the
<a href="https://lume.land/docs/core/components/">components configuration</a> to insert
the generated CSS and JavaScript code in a specific place.</li>
<li>Updated all dependencies to their latest version.</li>
</ul>
<p>And more changes. See the
<a href="https://github.com/lumeland/lume/blob/v2.4.0/CHANGELOG.md">CHANGELOG file</a> for
more details.</p>
<p><strong>Happy Luming!</strong></p>
]]>
      </content:encoded>
      <pubDate>Wed, 06 Nov 2024 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume 2.3.0 - Andrés do Barro</title>
      <link>https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/lume-2-3-0-andres-do-barro/</guid>
      <content:encoded>
        <![CDATA[<p>Lume 2.3.0 is dedicated to
<a href="https://en.wikipedia.org/wiki/Andr%C3%A9s_do_Barro">Andrés do Barro</a>, a
Galician singer and songwriter who was one of the first artists who achieved
success singing in Galego out of Galicia. Among his songs, we can find
<a href="https://www.youtube.com/watch?v=4feqklaMDR8">Pandeirada</a> and
<a href="https://www.youtube.com/watch?v=CUAOwBknH5I">O trén</a>.</p>
<!--more -->
<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>
<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>
<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>
<p>When Lume loads a page file, the basename (the filename after removing the
extension) is parsed to extract additional data. This feature makes it possible
that, for instance, if the basename starts with <code>yyyy-mm-dd_*</code>, Lume extracts
this value
<a href="https://lume.land/docs/creating-pages/page-files/#page-date">to set the page date</a>,
and remove it from the final name, so the file <code>/2020-06-21_hello-world.md</code>
generates the page <code>/hello-world/</code>.</p>
<p>As of Lume 2.3.0, you can add additional parsers with the new function
<code>site.parseBasename</code>.</p>
<p>Let's say we want to use a variable named <code>order</code> to sort pages in a menu, and
we want to extract this value from the file name. For example the file
<code>/12.hello-world.md</code> outputs the page <code>/hello-world/</code> and sets <code>12</code> as the
<code>order</code> variable. We can achieve this with the following function:</p>
<pre><code class="language-js">site.parseBasename((basename) =&gt; {
  // Regexp to detect the order pattern
  const match = basename.match(/(\d+)\.(.+)/);

  if (match) {
    const [, order, basename] = match;

    // Return the order value and the new basename without the prefix
    return {
      order: parseInt(order),
      basename,
    };
  }
});
</code></pre>
<p>As you can see, the function is simple: it receives the basename and return an
object with the parsed values. Note that the returned object contains the
basename without the prefix, in order to be removed from the final URL.</p>
<p>See more info in the
<a href="https://lume.land/docs/core/basename-parsers/">documentation page</a>.</p>
<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>
<p>Until now, if you modify the <code>_config.ts</code> file during the server mode, you must
stop the process and start it again to see the changes. This is very
inconvenient, especially in the early phases of development, when you may want
to try different plugins or make changes to the Lume configuration.</p>
<p>From now on, the building process is run inside a Worker. This change allows us
to stop the build and restart it again without stopping the main process (under
the hood, it is done by closing the Worker and creating a new one).</p>
<p>For now, the rebuild is triggered every time a change in the <code>_config</code> file is
detected. In the next versions, we can add additional triggers.</p>
<p>In CMS mode (with <code>deno task cms</code>), the process is also restarted if the
<code>_cms.ts</code> file is modified, which is useful when you are configuring the
documents and collections.</p>
<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>
<p>When searching pages using the
<a href="https://lume.land/plugins/search/"><code>search</code> helper</a>, you can sort them by any
field, for example, the title:</p>
<pre><code class="language-js">const pages = search.pages(&quot;type=post&quot;, &quot;title=asc&quot;);
</code></pre>
<p>Under the hood, Lume sorts the pages with
<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">array sort</a>
using basic comparison operators (<code>&gt;</code> and <code>&lt;</code>):</p>
<pre><code class="language-js">pages.sort((a, b) =&gt; a == 0 ? 0 : a.title &gt; b.title ? 1 : -1);
</code></pre>
<p>This works fine in many cases, but not when you have strings with accents,
different cases, etc. In these cases
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare">localeCompare</a>
works much better. In this version, we have introduced two new locale methods:
<code>asc-locale</code> and <code>desc-locale</code>. So the previous example can be improved with:</p>
<pre><code class="language-js">const pages = search.pages(&quot;type=post&quot;, &quot;title=asc-locale&quot;);
</code></pre>
<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>
<p><abbr>SRI</abbr> (Subresource Integrity) is a browser feature to protect your
site and your users from compromised code loaded from external CDN. It verifies
the code loaded by the browser is exactly the same code that you got during the
build process, without unexpected manipulations. You can
<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>
<p>Lume had
<a href="https://github.com/lumeland/experimental-plugins">an experimental SRI plugin</a>
that has been moved to the main repo, so now it's part of the official plugins
collection:</p>
<pre><code class="language-ts">import lume from &quot;lume/mod.ts&quot;;
import sri from &quot;lume/plugins/sri.ts&quot;;

const site = lume();
site.use(sri());

export default site;
</code></pre>
<p>The plugin searches for <code>&lt;script&gt;</code> and <code>&lt;link rel=&quot;stylesheet&quot;&gt;</code> elements in
your pages that load resources from other domains and add the <code>integrity</code> and
<code>crossorigin</code> attributes automatically. For example, if you have this code:</p>
<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;
</code></pre>
<p>The plugin outputs the following:</p>
<pre><code class="language-html">&lt;script
  src=&quot;https://code.jquery.com/jquery-3.7.0.slim.min.js&quot;
  integrity=&quot;sha256-tG5mcZUtJsZvyKAxYLVXrmjKBVLd6VpVccqz/r4ypFE=&quot;
  crossorigin=&quot;anonymous&quot;
&gt;&lt;/script&gt;
</code></pre>
<p>See more info
<a href="https://lume.land/plugins/sri/">in the plugin documentation page</a>.</p>
<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>
<div class="markdown-alert markdown-alert-important">
<p class="markdown-alert-title">Important</p>
<p>In this version, the nav plugin got a <strong>small BREAKING CHANGE</strong> (sorry for
that).</p>
</div>
<p>The <a href="https://lume.land/plugins/nav/">nav plugin</a> is useful for creating menus at
multiple levels.</p>
<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>
<p>The <code>nav.menu()</code> function returns an object tree using the pages' URL. Every
object in the tree is a page or a directory and in previous Lume versions, it
could have the following properties:</p>
<ul>
<li><code>item.slug</code> The name of the page or folder.</li>
<li><code>item.data</code> If the element is a page, this is the data object of the page. If
it's a folder, this value is undefined.</li>
<li><code>item.children</code> An array of sub-pages and sub-folders.</li>
</ul>
<p>These properties didn't fit well to order the elements, especially the
sub-folder items. In the new structure, <s>the <code>slug</code> property has been removed</s>
(<strong>Edit:</strong> it was restored in Lume 2.3.1 due some bugs) and this value is stored
in <code>data.basename</code>.</p>
<p>This change affects to how this tree is iterated in your template. For instance,
if in Lume 2.2 we have the following code:</p>
<pre><code class="language-js">if (item.data) {
  // item.data exists, it's a page
  return `&lt;a href=&quot;{{ item.data.url }}&quot;&gt;{{ item.data.title }}&lt;/a&gt;`;
} else {
  // It's a folder
  return `&lt;strong&gt;{{ item.slug }}&lt;/strong&gt;`;
}
</code></pre>
<p>With the changes in Lume 2.3, the code must be changed to:</p>
<pre><code class="language-js">if (item.data.url) {
  // item.data.url exists, it's a page
  return `&lt;a href=&quot;{{ item.data.url }}&quot;&gt;{{ item.data.title }}&lt;/a&gt;`;
} else {
  // It's a folder
  return `&lt;strong&gt;{{ item.slug }}&lt;/strong&gt;`;
}
</code></pre>
<p>Now both pages and folder items store the <code>basename</code> in the same place
(<code>data.basename</code>), and it's easy to sort the elements alphabetically:</p>
<pre><code class="language-js">const menu = nav.menu(&quot;/&quot;, &quot;&quot;, &quot;basename=asc&quot;);
</code></pre>
<p>And even use the new locale sorting methods:</p>
<pre><code class="language-js">const menu = nav.menu(&quot;/&quot;, &quot;&quot;, &quot;basename=asc-locale&quot;);
</code></pre>
<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>
<p>In this version the functions <code>nav.nextPage()</code> and <code>navPreviousPage()</code> have been
added, to ease the navigation to the next and previous pages.</p>
<p>For example, let's say we have created the following tree structure with the
function <code>nav.menu()</code>:</p>
<pre><code class="language-txt">docs
  |__ getting-started
        |__ installation
        |__ configuration
  |__ plugins
        |__ prettier
</code></pre>
<p>The new function <code>nav.nextPage</code> returns the next page relative to the provided
URL. For example:</p>
<pre><code class="language-js">const nextPage = nav.nextPage(&quot;/docs/getting-started/installation/&quot;);
console.log(nextPage.url); // /docs/getting-started/configuration/
</code></pre>
<p>If the page is the last sibling of the current section, it returns the first
page of the next section:</p>
<pre><code class="language-js">const nextPage = nav.nextPage(&quot;/docs/getting-started/configuration/&quot;);
console.log(nextPage.url); // /docs/plugins/
</code></pre>
<p>If the current section has children, it returns the first child:</p>
<pre><code class="language-js">const nextPage = nav.nextPage(&quot;/docs/plugins/&quot;);
console.log(nextPage.url); // /docs/plugins/prettier/
</code></pre>
<p>The <code>nav.previousPage()</code> works similarly but in reverse order.</p>
<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>
<ul>
<li>
<p>Plugins and middlewares can be imported using named imports, in addition to
the default exports:</p>
<pre><code class="language-js">import basePath from &quot;lume/plugins/base_path.ts&quot;;
// it's the same as
import { basePath } from &quot;lume/plugins/base_path.ts&quot;;
</code></pre>
</li>
<li>
<p>Several bug fixes and improvements have made to the watcher and live reload.</p>
</li>
</ul>
<p>And there are many more changes that you can see in the
<a href="https://github.com/lumeland/lume/blob/v2.3.0/CHANGELOG.md">CHANGELOG file</a>.</p>
]]>
      </content:encoded>
      <pubDate>Fri, 30 Aug 2024 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Lume 2.2.0 - Luísa Villalta</title>
      <link>https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/</link>
      <guid isPermaLink="false">https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/</guid>
      <content:encoded>
        <![CDATA[<p>From now on, every new minor version of Lume will be dedicated to a relevant
galician person. Today —17 May— is the
<a href="https://en.wikipedia.org/wiki/Galician_Literature_Day">Galician Literature Day</a>
and this year it was honored to the great poet
<a href="https://galicianliterature.com/villalta/"><strong>Luísa Villalta</strong></a>. This Lume
version is dedicated to her.</p>
<!--more -->
<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>
<p>Cases requiring some manual changes after updating to Lume 2.2:</p>
<ul>
<li>If you have a <code>_cache</code> folder in your <code>src</code> directory,
<a href="https://lume.land/blog/posts/lume-2-2-0-luisa-villalta/#_cache-folder-relative-to-root-directory">see this</a>.</li>
<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>
</ul>
<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>
<p>The <a href="https://lume.land/plugins/esbuild/">Esbuild plugin</a> got the following
improvements:</p>
<ul>
<li>
<p><strong>JSR support:</strong> Now you can use <a href="https://jsr.io/"><code>jsr:</code></a> specifiers in your
code. They are handled using esm.sh (the same as <code>npm:</code> specifiers).</p>
</li>
<li>
<p><strong>Fixed <code>npm:</code> resolution.</strong> In previous versions, importing a bare specifier
mapped to <code>npm:</code> like the following would fail:</p>
<pre><code class="language-json">{
  &quot;imports&quot;: {
    &quot;bar&quot;: &quot;npm:bar&quot;
  }
}
</code></pre>
<pre><code class="language-js">import foo from &quot;bar&quot;;
</code></pre>
<p>This has been fixed and it works fine now.</p>
</li>
<li>
<p><strong>Using esbuild without bundler:</strong> Let's say you want to compile the following
code with the <code>bundle</code> option to <code>false</code>:</p>
<pre><code class="language-js">import foo from &quot;./bar.ts&quot;;
</code></pre>
<p>Esbuild converts <code>bar.ts</code> to <code>bar.js</code>, but
<a href="https://github.com/evanw/esbuild/issues/2435">it doesn't change the file extension in the import</a>.
The Lume plugin tries to fix this.
<a href="https://github.com/lumeland/lume/issues/594">More info</a>.</p>
</li>
</ul>
<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>
<p><a href="https://lume.land/cms/">LumeCMS</a> is a simple CMS to manage the content of the
sites. To configure it, you have to create the <code>_cms.ts</code> file and import the CMS
from the <code>lume/cms.ts</code> specifier:</p>
<pre><code class="language-js">import lumeCMS from &quot;lume/cms.ts&quot;;

const cms = lumeCMS();

// Configuration here

export default cms;
</code></pre>
<p>The file <code>lume/cms.ts</code>, provided by Lume, exports everything you need from
LumeCMS. But this has also the following issues:</p>
<ul>
<li>Lume and LumeCMS have different update paces. It's not easy to update LumeCMS
if it's coupled to Lume.</li>
<li>Providing everything in a single file makes Deno download all LumeCMS modules
even those that you don't need. For example, GitHub storage has some NPM
dependencies like Octokit that you may not need.</li>
</ul>
<p>The best way to import LumeCMS is using the import map. This is something that
<a href="https://lume.land/docs/overview/installation/#setup-lume">the <code>init</code> script</a>
has been doing for a while. For example:</p>
<pre><code class="language-json">{
  &quot;imports&quot;: {
    &quot;lume/&quot;: &quot;https://deno.land/x/lume@v2.2.0/&quot;,
    &quot;lume/cms/&quot;: &quot;https://cdn.jsdelivr.net/gh/lumeland/cms@0.4.1/&quot;
  }
}
</code></pre>
<pre><code class="language-js">import lumeCMS from &quot;lume/cms/mod.ts&quot;;

const cms = lumeCMS();

// Configuration here

export default cms;
</code></pre>
<p>This allows you to import only the specific modules of Lume that you need and
update LumeCMS at your own pace.</p>
<p>I know this is a BREAKING CHANGE and sorry for that 🙏. But I believe the
current situation wasn't easy to maintain.</p>
<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>
<p>If you have a site on Deno Deploy that you want to shut down for reasons, this
new middleware can be useful:</p>
<ul>
<li>All HTML requests will return the content of the <code>/503.html</code> page with the
<code>503</code> status code.</li>
<li>It also sends the <code>Retry-After</code> header with the default value of 24 hours
(customizable).</li>
</ul>
<pre><code class="language-js">import Server from &quot;lume/core/server.ts&quot;;
import shutdown from &quot;lume/middlewares/shutdown.ts&quot;;

const server = new Server();
server.use(shutdown());

server.start();
</code></pre>
<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>
<p>The plugins <code>prism</code> and <code>code_highlight</code> highlight the syntax of your code
automatically using the libraries <a href="https://prismjs.com/">Prism</a> and
<a href="https://highlightjs.org/">Highlight.js</a> respectively. These libraries provide
some nice premade themes that you can use but need to be loaded manually.</p>
<p>A new option <code>theme</code> was introduced to ease this step, so the themes are
downloaded automatically. The option works the same for both plugins, this is an
example with <code>prism</code>:</p>
<pre><code class="language-js">site.use(prism({
  theme: {
    name: &quot;funky&quot;,
    path: &quot;_includes/css/code_theme.css&quot;,
  },
}));
</code></pre>
<p>The CSS file for the
<a href="https://github.com/PrismJS/prism/blob/master/themes/prism-funky.css">Funky theme</a>
is downloaded automatically (using
<a href="https://lume.land/docs/core/remote-files/">remoteFile</a> under the hood) with the
local path <code>_includes/css/code_theme.css</code> so you can import it in your CSS file
with:</p>
<pre><code class="language-css">@import &quot;css/code_theme.css&quot;;
</code></pre>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>If you're not using any CSS plugin (like postcss or lightningcss), you have to
configure Lume to copy or load the file. Example:</p>
</div>
<pre><code class="language-js">site.use(prism({
  theme: {
    name: &quot;funky&quot;,
    path: &quot;/css/code_theme.css&quot;,
  },
}));

// Copy the file
site.copy(&quot;/css/code_theme.css&quot;);
</code></pre>
<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>
<p>The <a href="https://lume.land/plugins/metas/"><code>metas</code> plugin</a> allows to include custom
meta tags. Useful to insert metas like <code>twitter:label1</code>, <code>twitter:data1</code>, etc.</p>
<p>In addition to the regular values, the <code>metas</code> object accepts additional values
that are treated as custom meta tags. Example:</p>
<pre><code class="language-yml">title: Lume is awesome
author: Dark Vader
metas:
  title: =title
  &quot;twitter:label1&quot;: Reading time
  &quot;twitter:data1&quot;: 1 minute
  &quot;twitter:label2&quot;: Written by
  &quot;twitter:data1&quot;: =author
</code></pre>
<p>This configuration generates the following code:</p>
<pre><code class="language-html">&lt;meta name=&quot;title&quot; content=&quot;Lume is awesome&quot; /&gt;
&lt;meta name=&quot;twitter:label1&quot; content=&quot;Reading time&quot; /&gt;
&lt;meta name=&quot;twitter:data1&quot; content=&quot;1 minute&quot; /&gt;
&lt;meta name=&quot;twitter:label2&quot; content=&quot;Written by&quot; /&gt;
&lt;meta name=&quot;twitter:data2&quot; content=&quot;Dark Vader&quot; /&gt;
</code></pre>
<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>
<p>The <a href="https://lume.land/plugins/feed/"><code>feed</code> plugin</a> has been extended with the
new <code>image</code> key that allows one to place an image per item. For example:</p>
<pre><code class="language-js">site.use(feed({
  output: &quot;/feed.xml&quot;,
  query: &quot;type=articles&quot;,
  items: {
    title: &quot;=title&quot;,
    description: &quot;=excerpt&quot;,
    image: &quot;=cover&quot;,
  },
}));
</code></pre>
<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>
<p><a href="https://lume.land/plugins/liquid/">Liquid plugin</a> allows to use
<a href="https://liquidjs.com/">Liquidjs</a> library as a template engine in Lume.</p>
<p>Liquidjs is a great library but it has a limitation incompatible with Lume:
<a href="https://github.com/harttle/liquidjs/discussions/580">it's not possible to invoke functions</a>.
This is very unfortunate because it's not possible to use the
<a href="https://lume.land/docs/core/searching/"><code>search</code> helper</a> inside a liquid
template to loop through the pages. For example, the following code doesn't
work:</p>
<pre><code class="language-html">&lt;ul&gt;
  {% for item in search.pages('post') %}
  &lt;li&gt;{{item.title}}&lt;/li&gt;
  {% endfor %}
&lt;/ul&gt;
</code></pre>
<p>Lume has support for Nunjucks which is a good replacement because has a very
similar syntax to Liquid and allows you to run functions, so I decided to
deprecate the Liquid plugin and recommend Nunjucks instead. It will still be
available in Lume 2 but probably be removed in Lume 3 (in the distant future).</p>
<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>
<p>The <code>_cache</code> folder is created by some plugins like
<a href="https://lume.land/plugins/transform_images/"><code>transform_images</code></a> in the source
folder. For example, if your source folder is <code>./src/</code> the cache folder is
<code>./src/_cache/</code>.</p>
<p>As of Lume 2.2.0, this folder is created <strong>in the root directory</strong> (the same
directory where the <code>_config.ts</code> file is). This makes its location more
predictable, especially to add it to <code>.gitignore</code>.</p>
<p>After updating Lume, if you are using a subdirectory as the source folder, the
<code>_cache</code> folder should be moved to the root.</p>
<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>
<p>The <a href="https://lume.land/plugins/postcss/">postcss plugin</a> comes with the plugins
<a href="https://www.npmjs.com/package/postcss-nesting">postcss-nesting</a> and
<a href="https://www.npmjs.com/package/autoprefixer">autoprefixer</a> enabled by default.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector">CSS nesting</a>
is now available in all browsers and this plugin is no longer needed, so it's no
longer enabled by default in Lume.</p>
<p>If you still want to use it, you have to import it in the _config.ts file:</p>
<pre><code class="language-js">import lume from &quot;lume/mod.ts&quot;;
import postcss from &quot;lume/plugins/postcss.ts&quot;;
import nesting from &quot;npm:postcss-nesting&quot;;

const site = lume();

site.use(postcss({
  plugins: [nesting()],
}));

export default site;
</code></pre>
<hr>
<p>And there are many more changes that you can see in the
<a href="https://github.com/lumeland/lume/blob/v2.2.0/CHANGELOG.md">CHANGELOG file.</a></p>
]]>
      </content:encoded>
      <pubDate>Fri, 17 May 2024 00:00:00 GMT</pubDate>
    </item>
  </channel>
</rss>