Building a blog in FireCMS Cloud

We don’t need to explain the benefits of using a headless CMS instead of a traditional blogging approach like WordPress, but here are a few:
- it is easier and faster to develop on, since there is no coupling between frontend and backend.
- since your frontend is independent, you are free to change it in any way you want, same goes for the backend.
- it is suited for omnichannel applications, you can use the same backend and CMS with multiple apps and websites
- smaller, specialised teams
- scalability
- less costs
- flexibility and simplicity
Let’s build a blog with FireCMS
Section titled “Let’s build a blog with FireCMS”We will be building a collection that hosts blog posts. Each of the blog posts will include a dynamic array of elements.
FireCMS has a built-in feature that allows you to build dynamic arrays. The
array property can be configured with the prop oneOf, to contain objects
that hold a specific type of value. This is perfect for building our blog entry
data structure!
These are the types that we will use:
- We define the type of the blog collection:
type BlogEntry = { name: string, header_image: string, created_on: Date, status: string, content: (BlogEntryImages | BlogEntryText | BlogEntryProducts)[];}- And each one of the types of the
contentarray:
type BlogEntryImages = { type: "images"; value: string[];}
type BlogEntryText = { type: "text"; value: string;}
type BlogEntryProducts = { type: "products"; value: object[]; // We use a generic object here, but feel free to define a type for your products}Create the collection
Section titled “Create the collection”Let’s start by initialising our collection, without any properties:
export const blogCollection = buildCollection<BlogEntry>({ name: "Blog entry", path: "blog", properties: {}});Basic properties
Section titled “Basic properties”Then we are going to add some simple properties for our entries.
- We want to have title, that must be always set, so we set the required prop in validation to true:
buildProperty({ name: "Title", validation: { required: true }, dataType: "string"})- An image that will be on the top of the blog post:
buildProperty({ name: "Header image", dataType: "string", storage: { storagePath: "images", acceptedFiles: ["image/*"], metadata: { cacheControl: "max-age=1000000" } }})- and a “created on” date that gets autogenerated when the document is created.
buildProperty( { name: "Created on", dataType: "date", autoValue: "on_create"})Conditional status field
Section titled “Conditional status field”Now we want to add a status string property that will have two possible values:
published and draft. We only want to allow the published
state when the rest of the fields are correct.
In this case we will keep it simple, and we will just check if the header image is set:
buildProperty(({ values }) => ({ name: "Status", validation: { required: true }, dataType: "string", columnWidth: 140, enumValues: { published: { id: "published", label: "Published", disabled: !values.header_image, }, draft: "Draft" }}))Content of the blog entry
Section titled “Content of the blog entry”The content of our blog entries needs to be dynamic, so that content managers are able to create complex entries with different components.
The content will be an array of objects, that will have a type attribute (that
will work as a discriminator), and a value attribute.
We will define 3 types:
images: an array of imagestext: a Markdown text fieldproducts: an array of references to another collection,productsin this case.
We use the oneOf prop in array properties which is designed exactly for this
use case. You just need to define
buildProperty({ name: "Content", description: "Example of a complex array with multiple properties as children", validation: { required: true }, dataType: "array", columnWidth: 400, oneOf: { typeField: "type", // you can ommit these `typeField` and `valueField` props to use the defaults valueField: "value", properties: { images: buildProperty({ name: "Images", dataType: "array", of: buildProperty({ dataType: "string", storage: { storagePath: "images", acceptedFiles: ["image/*"], metadata: { cacheControl: "max-age=1000000" } } }), description: "This fields allows uploading multiple images at once and reordering" }), text: buildProperty({ dataType: "string", name: "Text", markdown: true }), products: buildProperty({ name: "Products", dataType: "array", of: { dataType: "reference", path: "products" // you need to define a valid collection in this path } }) } }})This array configuration will create objects in the datasource with the format:
{ // ... content: [ { "type": "text", "value": "Sunglasses or sun glasses (informally called shades or sunnies; more names below) are a form of protective eyewear designed primarily to prevent bright sunlight and high-energy visible light from damaging or discomforting the eyes. They can sometimes function as a visual aid, as variously employed for conditions such as light sensitivity, displays, and ultraviolet protection, and to improve visual clarity. In the early 20th century, they were also known as sun cheaters (or sun glasses) and were made from cardboard, tin, and other metals, with lenses made from crudely ground glass. They were also available in the form of goggles, with improvements in technology, sunglasses have become more Earth-friendly, and the use of plastics and other modern materials is now commonplace. " }, { "type": "images", "value": [ "images/photo-1511499767150-a48a237f0083.jpeg", "images/photo-1577803645773-f96470509666.jpeg" ] }, { "type": "text", "value": "Sunglasses have long been associated with celebrities and film actors primarily from a desire to mask their identity. Since the 1940s, sunglasses have been popular as a fashion accessory, especially on the beach. " }, { "type": "products", "value": [ { "id": "B001UQ71F0", "path": "products", }, { "id": "B001UQ71F0", "path": "products", } ] } ], // ...}Creating a preview view
Section titled “Creating a preview view”Let’s make use of another feature of FireCMS: custom views for entities!
FireCMS allows you to add additional views to your entity views, that are defined as React components. The props you receive for building this component are the entity collection, the original entity and the modified values.
In this case, we will create some React components to represent our blog entry
like the frontend app would do. This is the same code that you could
use in any SSR framework using React, such as next.js
You could also have more complex setup that sends your data to your SSR app through an API and render the result.
You can find the code for BlogEntryPreview in
BlogEntryPreview.
When you have created your component, there are 2 ways you can add it to your component:
Register it in the CMS
Section titled “Register it in the CMS”The preferred way is to register it in the CMS, so that it is available in the view selector of the collection.
In order to do so, you need to add it to the entityViews prop of the FireCMS main app config export:
import { FireCMSAppConfig } from "@firecms/cloud";const appConfig: FireCMSAppConfig = { version: "1", collections: [], entityViews: [ { key: "blog_preview", name: "Preview", Builder: BlogEntryPreview } ],}Register it in the collection
Section titled “Register it in the collection”If you are defining your collection in code, you can also register it in the collection itself:
import {buildCollection} from "@firecms/core";export const blogCollection = buildCollection<BlogEntry>({ name: "Blog entry", path: "blog", entityViews: [ { key: "blog_preview", name: "Preview", Builder: BlogEntryPreview } ], properties: { // ... }});Complete code:
Section titled “Complete code:”If we put together all the parts that we have build in this tutorial, we get the following code for the blog collection:
import { buildCollection, buildProperty } from "@firecms/core";import { BlogEntryPreview } from "./BlogEntryPreview";import { BlogEntry } from "./types";
export const blogCollection = buildCollection<BlogEntry>({ name: "Blog entry", id: "blog", path: "blog", entityViews: [{ key: "preview", name: "Preview", Builder: BlogEntryPreview }], properties: { name: buildProperty({ name: "Name", validation: { required: true }, dataType: "string" }), header_image: buildProperty({ name: "Header image", dataType: "string", storage: { mediaType: "image", storagePath: "images", acceptedFiles: ["image/*"], metadata: { cacheControl: "max-age=1000000" } } }), content: buildProperty({ name: "Content", description: "Example of a complex array with multiple properties as children", validation: { required: true }, dataType: "array", columnWidth: 400, oneOf: { typeField: "type", // you can ommit these `typeField` and `valueField` props to use the defaults valueField: "value", properties: { images: buildProperty({ name: "Images", dataType: "array", of: buildProperty({ dataType: "string", storage: { mediaType: "image", storagePath: "images", acceptedFiles: ["image/*"], metadata: { cacheControl: "max-age=1000000" } } }), description: "This fields allows uploading multiple images at once and reordering" }), text: buildProperty({ dataType: "string", name: "Text", markdown: true }), products: buildProperty({ name: "Products", dataType: "array", of: { dataType: "reference", path: "products" // you need to define a valid collection in this path } }) } } }), status: buildProperty(({ values }) => ({ name: "Status", validation: { required: true }, dataType: "string", columnWidth: 140, enumValues: { published: { id: "published", label: "Published", disabled: !values.header_image }, draft: "Draft" }, defaultValue: "draft" })), created_on: buildProperty({ name: "Created on", dataType: "date", autoValue: "on_create" }) }})