Lume CMS 0.13.0
by Óscar Otero
7 min read
It's been a while (one and a half year!) since LumeCMS was announced as an alternative to other existing CMS to edit the content of websites.
During this time, the project was improved in many ways: more field formats, more customization options, light/dark mode, etc.
But, like many projects in their earlier phases, LumeCMS was a bit "tricky" 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.
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
v0.* yet), I think it's an important step towards the future v1.0 version.
New router
Since the beginning, LumeCMS has used Hono 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 HonoRequest
for the request instead of the standard Request (that is also available but
hidden) made me spend more time trying to figure out how to do something in
"Hono way" than just doing it. In addition to that, it's becoming a
full-featured framework with even a client-side JSX library.
That's why Galo was created. 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.

Document types
LumeCMS was created assuming that all data would be stored in an object. Let's see this example:
cms.collection({
name: "notes",
storage: "src:notes/*.json",
fields: [
"title: text",
"text: textarea",
],
});
This stores every note in a JSON file in the notes/ directory. The stored
notes have a structure like this:
{
"title": "Note title",
"text": "This is the note"
}
If you want to store all notes in a single JSON file, you can use an
object-list field to store an array of objects:
cms.document({
name: "notes",
storage: "src:notes.json",
fields: [
{
name: "notes",
type: "object-list",
fields: [
"title: text",
"text: textarea",
],
},
],
});
This configuration produces the following data structure:
{
"notes": [
{
"title": "First note",
"text": "Text of the first note"
},
{
"title": "Second note",
"text": "Text of the second note"
}
]
}
As you can see, the root of the data is still an object with the notes
property to hold the array of notes. But what we really want is to store the
array of notes as the root element. Until now, the solution was to change
the name of the root value from notes to []:
cms.document({
name: "notes",
storage: "src:notes.json",
fields: [
{
name: "[]",
type: "object-list",
fields: [
"title: text",
"text: textarea",
],
},
],
});
LumeCMS detected the special name "[]" as an instruction to ignore the element and store its content directly. This allows us to store the data as an array:
[
{
"title": "First note",
"text": "Text of the first note"
},
{
"title": "Second note",
"text": "Text of the second note"
}
]
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
type option:
cms.document({
name: "notes",
storage: "src:notes.json",
type: "object-list",
fields: [
"title: text",
"text: textarea",
],
});
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 object, but other available
values are object-list (to store an array of objects) and choose (to allow
the user to choose one structure among a list of options). More types can be
added in future versions.
New previewUrl and sourcePath options
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:
- A way to know the URL generated by a file. For example, if we know that the
file
/posts/hello-world.mdproduces the URL/posts/hello-world/, we can display this URL in the preview panel when the file is being edited. - A way to know the source file of a URL. If we know that the URL
/posts/hello-world/is generated by/posts/hello-world.md, we can create a "Edit this page" link to go directly to the edit form.
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:
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
}
});
The previewUrl 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 _data file):
cms.document({
name: "Common data",
storage: "src:_data.yml",
previewUrl: () => "/", // preview the homepage
fields: [
"title: text",
"description: textarea",
],
});
User-level permissions
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:
cms.collection({
name: "countries",
storage: "src:countries/*.json",
fields: [
"name: text",
"description: textarea",
],
create: false,
delete: false,
rename: false,
});
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:
cms.auth({
user1: {
password: "password1",
name: "Admin",
permissions: {
"countries": {
create: true,
delete: true,
rename: true,
},
},
},
user2: "password2",
});
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 "user1" has a password, the name "Admin" (which is used to show it in the interface instead of "user1"), 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 "user2".
Improved documentLabel
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 title or date properties). The option documentLabel allows
customization of how this document is shown in the interface. For example, it
can transform the name my-first-post.md to a more human My First Post. In
fact, LumeCMS comes with this especific behavior by default, but you can
customize it with your own tranformer:
cms.collection({
documentLabel: (filename) => filename.replace(".json", ""),
// More options...
});
As of version 0.13, this function can return an object with the properties
label, icon, and flags to extract and show more info in the list view.
Let's say our collection of countries is a folder with the following files:
/en-spain.json
/pt-portugal.json
/fr-france.json
/it-italy.json
We can configure the collection to show only the country name, and use the flag icon (from Phosphor) instead of the default document icon. The country code is the code is saved in the flags object:
cms.collection({
documentLabel: (filename) => {
const [code, name] = filename.replace(".json", "").split("-");
return {
label: name,
icon: "flag"
flags: { code }
}
},
// ...more options
});
New relation and relation-list fields
This feature is related to the improvements on documentLabel 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 "people"
collection and we want to assign a country to each person:
cms.collection({
name: "people",
storage: "src:people/*.md",
fields: [
"name: text",
{
name: "country",
type: "relation",
// The collection name that we want to relate
collection: "countries",
// A function to return a label and value for each option
option: ({ label, flags }) => { label, value: flags.code }
}
]
});
Now, this field shows a selector to pick one of the countries and use the code
flag as the value (en for Spain, pt for Portugal, etc). The relation-list
field is similar but allows for storing an array of values.
Better git commits
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 email property
to the user settings:
cms.auth({
user1: {
password: "password1",
// name & email are included in the commits created by this user.
name: "Admin",
email: "user@example.com",
},
});
Allow to edit raw files
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:
cms.document({
name: "Custom styles",
storage: "src:style.css",
});
Removed timezone from Date and Datetime fields
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.
# before
date: 2025-01-11T00:00:00.000Z
# after
date: 2025-01-11 00:00:00
And more changes
There are more changes in this version, like UI improvements, ability to show
EXIF data from uploaded images, the new cssSelector option to highlight an
element in the previewer related with a field, etc.
Take a look at the CHANGELOG.md file to see the complete list of changes.
Lume compability
This version isn't compatible with Lume 3.0.x, but it will be in Lume 3.1.
If you want to try it, upgrade Lume to the latest development version with:
deno task lume upgrade --dev
Then, run deno task serve and the CMS is automatically created if a _cms.ts
file is detected. You won't need to run deno task cms anymore!