Front Matter CMS: the best partner for Astro among all headless CMS

Table of Content

Intro

This website is built using Astro, a JS (meta)framework. Along the way of building, I suffered a great deal from a certain thing irrelevant to the implementation of features: Choice of CMS.

As of 2024, the headless CMS community, in particular, is so chaotic that CMS services are sprouting like mashrooms.
While having a lot of options is grateful, it's nothing but hell for newbies.
"I just wanted to build my blog but a large amount of time melted away while working on tutorials for CMS before I noticed..." I'm one of them.

Then, I bumped into Front Matter CMS.
"It's obvious this star is shining differently than others...!" I was certain about that so decided to leave my content's fate up to it.

Hereafter you'll see how Front Matter CMS works together with Astro as an example, which is my website.
For more details of it, see the article below.

Reader personas

  • Want to build and run one's own website or blog but overwhelmed by countless options for headless CMS
  • Struggling to find a headless CMS for a website built with Astro
  • Want to see examples of Front Matter CMS in collaboration with Astro

My proficiency level

As of the day when I'm writing this article, my proficiency level in this field is as follows:

  • Have a three-year experience as a software engineer in total (Not in a row)
  • Have never used Front Matter CMS
  • Have built several websites using CMS like WordPress and MicroCMS

Main

Why Front Matter CMS?

There are three reasons:

  • Writing and storing articles in local
  • Markdown/MDX file format
  • Compatible with directory-based internationalization(i18n)

Especially the first one is what makes it different from other headless CMS and makes me decide using it.

And surprisingly, it offers an AI assistance feature where you can ask them whatever via the VSCode command.
So you don't even have to access the internet to look up something, which is pretty cool coupled with the capability of editing articles offline.

 Demo: Front Matter AI
Demo: Front Matter AI

Downsides of Front Matter CMS

Having said that, there are still some downsides:

  • Exclusive to VSCode users
  • Available languages are limited
    • As of Jul 2024, en, de, and ja are supported
    • Of course you can be a contributor by translating the strings and sent a pull request!
  • Hard to edit articles without your PC (e.g., on your smartphone)

My Astro project's overview

Thanks to the Astro Content Collections API, you can easily manage content and data collections under the src/content directory.

Below is my project's structural overview. For brevity, most irrelevant parts are omitted from the directory tree.

younagi.dev/
├── public
├── src/
│   ├── assets/
│   │   ├── font
│   │   └── images
│   └── content/
│       ├── blog/
│       │   ├── en
│       │   └── ja
│       ├── categories/
│       │   └── ...
│       ├── i18n/
│       │   └── ...
│       ├── meta/
│       │   └── ...
│       ├── news/
│       │   └── ...
│       ├── page/
│       │   └── ...
│       ├── tags/
│       │   └── ...
│       └── config.ts
├── frontmatter.json
└── astro.config.ts

Off-topic, the Content Collections work with the config.ts file, in which all the contents' types & fields are defined.

src/content/config.ts
import { defineCollection, reference, z } from "astro:content";
import { colors } from '@/components/models/Taxonomy';

const blog = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    description: z.string().optional(),
    publishedAt: z.coerce.date(),
    modifiedAt: z.coerce.date().optional(),
    category: z.object({
      metadata: reference("categories"),
      slug: z.string(),
    }),
    tags: z.object({
      metadata: reference("tags"),
      slugList: z.array(z.string()).optional(),
    }),
    draft: z.enum(["draft", "in progress", "published"]),
    level: z
      .union([
        z.literal(1),
        z.literal(2),
        z.literal(3),
        z.literal(4),
        z.literal(5),
      ])
      .optional(),
  }),
});

const taxonomySchema = z.object({
  title: z.string(),
  slug: z.string(),
  ruby: z.string(),
  color: z.enum(colors),
});

const categories = defineCollection({
  type: "data",
  schema: z.array(taxonomySchema),
});

const tags = defineCollection({
  type: "data",
  schema: z.array(taxonomySchema),
});

// ...

export const collections = { blog, categories, tags /* ... */ };

The astro.config.ts is a config file for the Astro project overall. As far as it's concerned with this article, I added the i18n feature here.

You're going to have to make Front Matter CMS settings consistent with this later.

astro.config.ts
import { defineConfig /* ... */ } from "astro/config";
// ...

export default defineConfig({
  // ...
  i18n: {
    defaultLocale: "en",
    locales: ["en", "ja"],
  },
  // ...
});

Configurations

When it comes to the setup, consult the Front Matter official doc. I suppose you cannot get lost with the doc and support of the AI assistance.

After the setup, you're supposed to tweak the frontmatter.json to make it consistent with your contents and data configurations.

The framework is Astro this time, but you'll probably be able to do this with other frameworks too as long as you have Markdown/MDX contents in it.
The thing is how correctly you specify your content's locations in the config file.

frontmatter.json
{
  "$schema": "https://frontmatter.codes/frontmatter.schema.json",
  "frontMatter.framework.id": "astro",
  "frontMatter.preview.host": "http://localhost:4321",
  "frontMatter.content.publicFolder": "./src/assets/images/",
  "frontMatter.content.pageFolders": [
    {
      "title": "blog",
      "path": "[[workspace]]/src/content/blog",
      "contentTypes": ["blog"],
      "defaultLocale": "en"
    },
    {
      "title": "news",
      "path": "[[workspace]]/src/content/news",
      "contentTypes": ["news"],
      "defaultLocale": "en"
    },
    {
      "title": "page",
      "path": "[[workspace]]/src/content/page",
      "contentTypes": ["page"],
      "defaultLocale": "en"
    }
  ],
  "frontMatter.content.i18n": [
    {
      "title": "English",
      "locale": "en",
      "path": "en"
    },
    {
      "title": "Japanese",
      "locale": "ja",
      "path": "ja"
    }
  ],
  "frontMatter.content.draftField": {
    "name": "draft",
    "type": "choice",
    "choices": ["draft", "in progress", "published"]
  },
  "frontMatter.taxonomy.seoTitleLength": 90,
  "frontMatter.taxonomy.contentTypes": [
    {
      "name": "blog",
      "pageBundle": false,
      "previewPath": "'blog'",
      "filePrefix": null,
      "clearEmpty": true,
      "fileType": "mdx",
      "fields": [
        {
          "title": "Title",
          "name": "title",
          "type": "string",
          "single": true,
          "required": true
        },
        {
          "title": "Description",
          "name": "description",
          "type": "string"
        },
        {
          "title": "Category",
          "name": "category",
          "type": "fields",
          "fields": [
            {
              "title": "Category ID",
              "name": "metadata",
              "type": "categories",
              "required": true,
              "taxonomyLimit": 1,
              "singleValueAsString": true
            },
            {
              "title": "Category Slug",
              "name": "slug",
              "type": "dataFile",
              "dataFileId": "categories",
              "dataFileKey": "slug",
              "dataFileValue": "title",
              "required": true,
              "singleValueAsString": true
            }
          ]
        },
        {
          "title": "Tags",
          "name": "tags",
          "type": "fields",
          "fields": [
            {
              "title": "Tags ID",
              "name": "metadata",
              "type": "tags",
              "required": true,
              "taxonomyLimit": 1,
              "singleValueAsString": true
            },
            {
              "title": "Tag Slugs",
              "name": "slugList",
              "type": "dataFile",
              "dataFileId": "tags",
              "dataFileKey": "slug",
              "dataFileValue": "title",
              "multiple": true
            }
          ]
        },
        {
          "title": "Draft Status",
          "name": "draft",
          "type": "draft",
          "required": true,
          "default": "draft"
        },
        {
          "title": "Published At",
          "name": "publishedAt",
          "type": "datetime",
          "default": "{{now}}",
          "isPublishDate": true
        },
        {
          "title": "Modified At",
          "name": "modifiedAt",
          "type": "datetime",
          "isModifiedDate": true
        },
        {
          "title": "Level",
          "name": "level",
          "type": "number",
          "numberOptions": {
            "min": 1,
            "max": 5,
            "step": 1
          }
        },
        {
          "title": "type",
          "name": "type",
          "type": "string"
        }
      ]
    },
    {
      "name": "news",
      "pageBundle": false,
      "previewPath": "'news'",
      "filePrefix": null,
      "clearEmpty": true,
      "fileType": "md",
      "fields": [
        {
          "title": "Title",
          "name": "title",
          "type": "string",
          "single": true,
          "required": true
        },
        {
          "title": "Published At",
          "name": "publishedAt",
          "type": "datetime",
          "default": "{{now}}",
          "isPublishDate": true
        },
        {
          "title": "Modified At",
          "name": "modifiedAt",
          "type": "datetime",
          "isModifiedDate": true
        },
        {
          "title": "type",
          "name": "type",
          "type": "string"
        },
        {
          "title": "draft",
          "name": "draft",
          "type": "draft"
        }
      ]
    },
    {
      "name": "page",
      "previewPath": "'page'",
      "pageBundle": false,
      "clearEmpty": true,
      "filePrefix": null,
      "fileType": "mdx",
      "fields": [
        {
          "title": "Title",
          "name": "title",
          "type": "string",
          "single": true,
          "required": true
        },
        {
          "title": "Description",
          "name": "description",
          "type": "string"
        },
        {
          "title": "Modified At",
          "name": "modifiedAt",
          "type": "datetime",
          "isModifiedDate": true
        }
      ]
    }
  ],
  "frontMatter.data.types": [
    {
      "id": "categories",
      "schema": {
        "title": "Categories",
        "type": "object",
        "required": ["title", "slug", "color"],
        "properties": {
          "title": {
            "type": "string",
            "title": "Title"
          },
          "slug": {
            "type": "string",
            "title": "Slug"
          },
          "color": {
            "type": "string",
            "title": "Color"
          }
        }
      }
    },
    {
      "id": "tags",
      "schema": {
        "title": "Tags",
        "type": "object",
        "required": ["title", "slug", "color"],
        "properties": {
          "title": {
            "type": "string",
            "title": "Title"
          },
          "slug": {
            "type": "string",
            "title": "Slug"
          },
          "color": {
            "type": "string",
            "title": "Color"
          }
        }
      }
    }
  ],
  "frontMatter.data.files": [
    {
      "title": "En Categories",
      "id": "categories",
      "type": "categories",
      "file": "[[workspace]]/src/content/categories/en/categories.yml",
      "fileType": "yaml"
    },
    {
      "title": "Ja Categories",
      "id": "categories",
      "type": "categories",
      "file": "[[workspace]]/src/content/categories/ja/categories.yml",
      "fileType": "yaml"
    },
    {
      "title": "En Tags",
      "id": "tags",
      "type": "tags",
      "file": "[[workspace]]/src/content/tags/en/tags.yml",
      "fileType": "yaml"
    },
    {
      "title": "Ja Tags",
      "id": "tags",
      "type": "tags",
      "file": "[[workspace]]/src/content/tags/ja/tags.yml",
      "fileType": "yaml"
    }
  ],
  "frontMatter.snippets.wrapper.enabled": false,
  "frontMatter.content.snippets": {
    "Image with caption": {
      "description": "Insert image with caption",
      "body": "![ [[alt]] ](../../../assets/images/[[filename]])",
      "isMediaSnippet": true
    },
    "Code block with specified language": {
      "description": "Insert code block snippet",
      "body": ["```[[language]] title=\"[[filename]]\"", "", "```"],
      "fields": [
        {
          "name": "language",
          "title": "Language",
          "type": "choice",
          "choices": [
            "html",
            "css",
            "javascript",
            "typescript",
            "rust",
            "yml",
            "astro",
            "jsx",
            "tsx",
            "json",
            "markdown",
            "mdx",
            "bash",
            "txt"
          ],
          "single": true,
          "default": "markdown"
        },
        {
          "name": "filename",
          "title": "filename",
          "type": "string",
          "single": true,
          "default": ""
        }
      ]
    },
    "Callout": {
      "description": "Insert a callout",
      "body": ["> [![[type]] ][[symbol]][[title]]", "> [[content]]"],
      "fields": [
        {
          "name": "type",
          "title": "Type",
          "type": "choice",
          "choices": [
            "info",
            "note",
            "warning",
            "quote",
            "question",
            "failure",
            "check"
          ],
          "single": true,
          "default": "info"
        },
        {
          "name": "symbol",
          "title": "Symbol",
          "type": "choice",
          "choices": ["+", "-", " "],
          "single": true,
          "default": " "
        },
        {
          "name": "title",
          "title": "Title",
          "type": "string",
          "single": true,
          "default": ""
        },
        {
          "name": "content",
          "title": "Content",
          "type": "string",
          "single": true,
          "default": ""
        }
      ]
    }
  },
  "frontMatter.website.host": "https://younagi.dev/",
  "frontMatter.framework.startCommand": "bun dev",
  "frontMatter.dashboard.openOnStart": true
}

In the above options, some are automatically added during the setup process and some are not. Here are some important items that I manually added:

  • frontMatter.content.publicFolder: Specify your assets directory if needed
  • frontMatter.content.pageFolders: Specify your content collections here
    • In my case, there are three content types: blog, news, and page
  • frontMatter.content.i18n: Specify as many languages as you use
    • This is fully compatible with every directory-based internationalization(i18n)
  • frontMatter.content.draftField: Not necessary
    • I just wanted to have three article statuses rather than the default true or false flag.
  • frontMatter.data.types: Specify your data collections here
    • In my case, there are two content types: categories and tags
  • frontMatter.data.files: Associate your data collections with specific paths and file formats
    • In my case, each data is aggregated in one file. (e.g., "src/content/tags/en/tags.yml") Here, each one of file path is specified. By the same token as contents, there should be as many directories as languages you use
  • frontMatter.content.snippets: Customize your snippets here
    • In my case, I had to customize the media snippet since I modify the assets directory.
Why did I have to create the media snippet on my own?
The path specified in the publicFolder is automatically reflected on that of the media snippet.
That said, the original media snippet has the image path like /src/assets/images/image.png, which is an invalid path. (Correctly, ../../../assets/images/image.png)

Usability

With all the configurations in mind, my dashboard looks like this.

Create a new article

Only a little time and effort needed till you start writing!

  1. Open the command palette
  2. Select the "Front Matter: Create new content"
  3. Select a type and language for your content
  4. Fill in the title
  5. Now it's ready!
Demo: Create a new article

Create a new data content

You no longer have to directly add them in the file. Just fill in the customized input fields from your dashboard.

Demo: Create a new data content

Edit an article

Notable points are:

  • Frontmatter values can be inserted from the visual editor
    • on the left in the image
  • The Markdown editor palette available
    • on the top right in the image

In editing mode
In editing mode

It feels comfortable that they are accessible from several ways. (e.g., the snippet feature callable from the palette as well as the VSCode command)

Manage assets

You can view and manage all assets from your dashboard. Advantages are:

  • Easily distinguishable at a glance
  • Able to drag & drop it onto the page to add a new asset
  • Searchable from the search window
  • Assets' metadata is editable directly from the menu

Media view in my dashboard
Media view in my dashboard

Outro

Front Matter CMS is a VSCode extension. Even if it stops developing, migrating to other CMS is easy.
That would be one of the most important factors considering the pace as which modern technology is advancing.

For the same reason, I've been using Obsidian as my knowledge base and Astro as a meta framework of this website. The common ground is "portability".

  • Obsidian: All contents are written in Markdown and managed in local unlike Notion
    • All you need to do for migration is move all the .md files
  • Astro: In .astro files, HTML, CSS, and vanilla JS can be used. Content is written in Markdown/MDX.
    • Even if Astro reaches its end of life, still you'll be able to reuse them(HTML, CSS, etc..) since those basic technologies aren't supposed to be deprecated that rapidly

Thanks👏

◀ Back to Top Scroll to Top ▲