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.

Image

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.md produces 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!