Skip to main content
Version: 3.0.0-beta

Building a blog in FireCMS Cloud

blog_example

note

In this tutorial we assume you have set up a Firebase project and a FireCMS instance with custom code. If you don't, check the Quickstart section.

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
Frontend

When you are done implementing the steps in this tutorial, you may want to check how to build a simple next.js frontend app (or any other frontend framework). We suggest next.js since it uses React, and it will make things easier. We are already providing many of the React components you could need in your frontend app, so you can focus on the business logic.

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 content array:
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

tip

You can create this same collection using a template in the FireCMS dashboard.

Let's start by initialising our collection, without any properties:

export const blogCollection = buildCollection<BlogEntry>({
name: "Blog entry",
path: "blog",
properties: {}
});

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"
})
note

You could also add a date field that gets updated whenever a document is saved, with this configuration:

buildProperty( {
name: "Updated on",
dataType: "date",
autoValue: "on_update"
})

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

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 images
  • text: a Markdown text field
  • products: an array of references to another collection, products in 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

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

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

If you are defining your collection in code, you can also register it in the collection itself:

import {buildCollection} from "@firecms/cloud";
export const blogCollection = buildCollection<BlogEntry>({
name: "Blog entry",
path: "blog",
entityViews: [
{
key: "blog_preview",
name: "Preview",
Builder: BlogEntryPreview
}
],
properties: {
// ...
}
});

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"
})
}
})
Sign up to our newsletter to get the latest news and updates. No spam!