It’s been four years since I moved my blog to this Hugo-based static website. Until now, my workflow for writing new posts involved a lot of manual work:

  1. Creating Markdown files in different directories.
  2. Filling in the YAML front matter.
  3. Creating separate static image folders.
  4. Choosing categories and tags from existing posts.
  5. Publishing through a pull request-based workflow.

Typically, all these are handled by a CMS. However, the closest thing to a CMS I had was a CLI I wrote in Go and Hugo’s archetypes feature, which worked as well as possible.

This imperfect and borderline tedious workflow added a massive hurdle to an already shabby writing plan. This meant a lot less posts and a lot more burden.

This problem wasn’t really new. I have tried to solve it using different CMS solutions that claim to work well with Hugo websites for the past four years. Every one of them failed to wrangle the mess I call my blog.

It reached a point where I gave up trying to use CMS solutions and instead started adding more glue features to my CLI to fix the shortcomings.

All this was until I found Front Matter CMS a few weeks ago.

Front Matter lives inside Visual Studio Code and is a fully-featured, highly customizable CMS. I had tried it years ago when it was first released and couldn’t get it to work. But now, with its out-of-the-box customization options, it can be tailored to my exact use cases.

In this post, I will walk through my Front Matter setup and show how you can easily manage content on your websites in Visual Studio Code.

What’s Front Matter?

Front Matter is a Visual Studio Code extension that works as a CMS for static websites. It can work with any static site generator, including Hugo, Jekyll, Next.js, Docusaurus, and Gatsby.

Front Matter can be installed directly from Visual Studio Code Marketplace. Once the extension is installed and reloaded, you will be greeted by a welcome screen that will take you through the initial setup.

The initial setup entirely depends on how you have structured your website’s content. Even with a relatively large website, my initial setup was quite effortless.

Tip: You can change your Front Matter setup after the initial setup. i.e., the initial setup is not permanent.

Once this is done, you can open the Front Matter Dashboard.

Front Matter Dashboard
Front Matter Dashboard

I like using this dashboard as a homepage where I can easily view and add posts.

The dashboard is neat and offers everything you need to create content in a consolidated view. It’s also customizable through intuitive extension points. You can check the official documentation to learn more about using and configuring the dashboard. I will instead focus more on my specific setup in this post.

Another useful feature of Front Matter is the Editor Panel, which lets you easily manage the actual front matter in your files.

Front Matter Editor Panel
Front Matter Editor Panel

The panel lets you manage front matter easily. Fields are loaded and populated through a predefined configuration.

You can configure different panels for different types of content, and Front Matter will automatically change them. I will defer to the documentation again to explain the specifics of using the panel and focus on my custom configurations in the following sections.

All Front Matter configurations are stored in the frontmatter.json file and the .frontmatter folder. This makes modifying, version controlling, and sharing your configuration easy. See the documentation for all available configurations.

Specifying Content Types

My blog houses different types of content:

  1. Posts: Normal posts like this one. Stored in the /content/posts/ folder.
  2. Dailies: For my daily logs. Stored in the /content/dailies/ folder.
  3. Newsletters: The web page versions of my newsletter issues. Stored in, you guessed it, the /content/newsletters/ folder.

Each content type has different YAML front matter configurations, i.e., different fields in the front matter (NOT Front Matter CMS). For example, the front matter for this post looks like this:

title: There's a CMS in My IDE!
slug: cms-ide
date: 2024-11-09T12:26:23+05:30
draft: true
toc:
  show: true
  open: true
ShowRelatedContent: false
description: Why I started using Front Matter CMS to work with my Hugo-based static website and how you can do it, too.
summary: Front Matter is a CMS that runs as a Visual Studio Code extension. It's feature-rich and highly customizable, making it the perfect tool to manage my poorly organized blog.
tags:
  - blogs
  - setup
categories:
  - Writing/Blogging
cover:
  image: /images/cms-ide/toy-story-banner.jpg
  alt: Woody and Buzz from Toy Story.
  caption: And there's a snake in my boots! How good was Toy Story?
  relative: true
fmContentType: Post (default)

While the front matter for a daily log is more like this:

title: "#291 Redesigning the Blog"
date: 2024-11-03T18:51:12+05:30
draft: false
summary: A week spent well. Sleep-deprived.
fmContentType: Daily

Front Matter CMS can be easily configured to manage these different types. I can start by specifying the location of the daily logs folder in frontmatter.content.pageFolders:

239{
240  "title": "Dailies",
241  "path": "[[workspace]]/content/dailies",
242  "previewPath": "dailies",
243  "contentTypes": ["Daily"]
244}

Here, I specify the contentTypes for files in this folder as Daily. Daily is defined in frontmatter.taxonomy.contentTypes:

147{
148  "name": "Daily",
149  "pageBundle": false,
150  "filePrefix": "{{date|d-M-yy}}",
151  "fields": [
152    {
153      "title": "Title",
154      "name": "title",
155      "type": "string",
156      "single": true,
157      "actions": [
158        {
159          "title": "Add log number",
160          "script": "./scripts/frontmatter/daily-logs-title.js"
161        }
162      ]
163    },
164    {
165      "title": "Publishing date",
166      "name": "date",
167      "type": "datetime",
168      "dateFormat": "yyyy-MM-dd'T'HH:mm:ssXXX",
169      "default": "{{now}}",
170      "isPublishDate": true
171    },
172    {
173      "title": "Draft?",
174      "name": "draft",
175      "type": "draft"
176    },
177    {
178      "title": "Summary",
179      "name": "summary",
180      "type": "string"
181    }
182  ]
183}

Now, every time I open a daily log, Front Matter loads the appropriate editor panel. The only non-trivial parts in this configuration might be:

150: Prefixes file names with the date in the specified format.

157 to 162: Adds a custom action, Add log number, to prefix the title with the log number. More on the script in the next section.

168: Overrides the default date format to ensure consistency with old posts.

My complete frontmatter.json configuration is available on GitHub.

Extending with Scripts

In the above section, I mentioned a custom action, Add log number, which prefixes the title with the log number. The script is a few lines of JavaScript:

 1import { FieldAction } from "@frontmatter/extensibility";
 2import * as fs from "fs";
 3
 4(async () => {
 5  const { frontMatter } = FieldAction.getArguments();
 6
 7  if (!frontMatter.title) {
 8    FieldAction.done();
 9    return;
10  }
11
12  // subtract 1 to offset the index file
13  const logNumber = fs.readdirSync("./content/dailies").length - 1;
14
15  FieldAction.update("#" + logNumber + " " + frontMatter.title);
16})();

This script defines a Field Action, i.e., a script that runs on a particular field; in this case, it is the title field. The code is quite trivial:

1: Create custom scripts using the @frontmatter/extensibility library.

15: Update the title with the #logNumber prefix.

You will find a new field action button next to the title field:

“Add log number” field action
"Add log number" field action

Since I have GitHub Copilot enabled, I also have a Copilot field action.

I also have another script that creates folders to store images. It’s specified in my frontmatter.json file:

257{
258  "frontMatter.custom.scripts": [
259    {
260      "id": "create-image-folder",
261      "title": "Create image folder",
262      "script": "./scripts/frontmatter/create-image-folder.js"
263    }
264  ]
265}

The script takes the current file path, strips it to get the file name, and uses it to create an image folder:

import { ContentScript } from "@frontmatter/extensibility";
import * as fs from "fs";

const { command, scriptPath, workspacePath, filePath, frontMatter, answers } =
  ContentScript.getArguments();

(async () => {
  const folderName = filePath.split("/").pop().split(".").shift();

  const folderPath = `./static/images/${folderName}`;
  fs.mkdirSync(folderPath);

  console.log(`Image folder created: ${folderPath}`);
})();

As is evident from the above code, I suck at writing JavaScript.

This script adds a Custom Action to the Editor Panel:

“Create image folder” custom action
"Create image folder" custom action

You could also automate this script to run after creating posts but a button gives me control.

Finally, I have a UI Extension that adds a placeholder image when the preview image is not provided. This isn’t a functional requirement, but my dashboard looks much better without empty spaces.

UI extensions are also specified in the frontmatter.json file, similar to other scripts/extensions:

264{
265  "frontMatter.extensibility.scripts": ["./.frontmatter/ui/external.js"]
266}

Then, the UI extension is defined as shown below:

import {
  enableDevelopmentMode,
  registerCardImage,
  registerCardFooter,
  registerPanelView,
  registerCustomField,
} from "https://cdn.jsdelivr.net/npm/@frontmatter/extensibility/+esm";

// enableDevelopmentMode();

registerCardImage(async (filePath, metadata) => {
  const image = metadata.fmPreviewImage
    ? metadata.fmPreviewImage
    : `${metadata.fmWebviewUrl}/static/images/preview.png`;
  return `<img src="${image}" alt="${metadata.title}" style="width: 100%; height: auto; object-fit: cover;" />`;
});

Looking at these scripts now, it’s obvious that there’s a lot of room for improvement. But since they just execute locally, there isn’t any additional overhead, which is to say I ain’t fixing them anytime soon.

Writing using Snippets

The most powerful feature in Front Matter, in my opinion, is snippets. It lets you predefine code or code templates that can easily inserted into your posts. This isn’t a novel idea; if you are familiar with code snippets in Visual Studio Code, it’s similar but much easier to use.

I migrated most of my existing code snippets to Front Matter, which made my writing much faster. Let me show some examples.

I have created a Quote snippet that lets me convert the selected text to a blockquote shortcode with a title, author, and link. Instead of manually typing the shortcode, I can insert the snippet and add the values through the GUI, as shown below:

Quote snippet in action
Quote snippet in action

I can easily select the text, apply the quote snippet, add the details, and it’s converted to a shortcode. Less boilerplates for me!

Snippets are configured inside frontMatter.content.snippets in your frontmatter.json file:

268{
269  "frontMatter.content.snippets": {
270    "Quote": {
271      "description": "Quote with the author, link, and title.",
272      "body": [
273        "{{< blockquote author=\"[[author]]\" link=\"[[&link]]\" title=\"[[title]]\" >}}",
274        "[[selection]]",
275        "{{< /blockquote >}}"
276      ],
277      "fields": [
278        {
279          "name": "author",
280          "title": "Author",
281          "type": "string",
282          "single": true
283        },
284        {
285          "name": "title",
286          "title": "Title",
287          "type": "string",
288          "single": true
289        },
290        {
291          "name": "link",
292          "title": "Link",
293          "type": "string",
294          "single": true
295        },
296        {
297          "name": "selection",
298          "title": "Quote",
299          "type": "string",
300          "default": "FM_SELECTED_TEXT"
301        }
302      ]
303    }
304  }
305}

The placeholders ([[author]], [[&link]]) in the snippet are replaced by the value added into the corresponding field in the GUI.

I have also created a Figure media snippet to easily add images with a title, caption, and URL:

268{
269  "frontMatter.content.snippets": {
270    "Figure": {
271      "description": "Figure with title, caption, and link.",
272      "body": "{{< figure src=\"[[&mediaUrl]]#center\" title=\"[[title]]\" caption=\"[[caption]]\" link=\"[[&mediaUrl]]\" target=\"_blank\" class=\"align-center\" >}}",
273      "isMediaSnippet": true
274    }
275  }
276}

Now, I can add an image directly through the UI:

Adding an image using the Figure snippet
Adding an image using the `Figure` snippet

Note that I had already configured the title and the caption for the image in its metadata.

I have similarly added snippets for other shortcodes and commonly used templates. This lets me focus on the content rather than the structure.

A Tailored CMS

To wrap up, Front Matter has been great. It fits all my workflows without any annoying gaps in between to prevent me from fully committing.

My current setup took a couple of days of experimentation. There are even more customizations I can think of to squeeze the last ounce of productivity. But now, the marginal costs of adding more customizations outweigh the marginal benefits—sharpening my knife is futile if I don’t cut anything.

So, in the foreseeable future, I will stick with my current setup. I will use it to write more posts faster and easier, focusing on content rather than structure.