Pular para o conteúdo

Construindo um blog no FireCMS Cloud

blog_example

Não precisamos explicar os benefícios de usar um headless CMS em vez de uma abordagem de blog tradicional como o WordPress, mas aqui estão alguns:

  • é mais fácil e rápido de desenvolver, pois não há acoplamento entre frontend e backend.
  • como seu frontend é independente, você é livre para alterá-lo da forma que quiser, o mesmo vale para o backend.
  • é adequado para aplicações omnicanal, você pode usar o mesmo backend e CMS com múltiplos apps e sites
  • equipes menores e especializadas
  • escalabilidade
  • menos custos
  • flexibilidade e simplicidade

Vamos construir uma coleção que hospeda posts de blog. Cada um dos posts do blog incluirá um array dinâmico de elementos.

O FireCMS tem um recurso integrado que permite construir arrays dinâmicos. A propriedade de array pode ser configurada com a prop oneOf, para conter objetos que contêm um tipo específico de valor. Isso é perfeito para construir nossa estrutura de dados de entrada de blog!

Esses são os tipos que usaremos:

  • Definimos o tipo da coleção de blog:
type BlogEntry = {
name: string,
header_image: string,
created_on: Date,
status: string,
content: (BlogEntryImages | BlogEntryText | BlogEntryProducts)[];
}
  • E cada um dos tipos do array content:
type BlogEntryImages = {
type: "images";
value: string[];
}
type BlogEntryText = {
type: "text";
value: string;
}
type BlogEntryProducts = {
type: "products";
value: object[]; // Usamos um objeto genérico aqui, mas sinta-se à vontade para definir um tipo para seus produtos
}

Vamos começar inicializando nossa coleção, sem nenhuma propriedade:

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

Em seguida vamos adicionar algumas propriedades simples para nossas entradas.

  • Queremos ter um título, que deve sempre estar definido, então definimos a prop required em validation como true:
buildProperty({
name: "Title",
validation: { required: true },
dataType: "string"
})
  • Uma imagem que ficará no topo do post do blog:
buildProperty({
name: "Header image",
dataType: "string",
storage: {
storagePath: "images",
acceptedFiles: ["image/*"],
metadata: {
cacheControl: "max-age=1000000"
}
}
})
  • e uma data “created on” que é autogerada quando o documento é criado.
buildProperty( {
name: "Created on",
dataType: "date",
autoValue: "on_create"
})

Agora queremos adicionar uma propriedade string status que terá dois valores possíveis: published e draft. Só queremos permitir o estado published quando o restante dos campos estiver correto.

Neste caso vamos manter simples, e apenas verificaremos se a imagem de cabeçalho está definida:

buildProperty(({ values }) => ({
name: "Status",
validation: { required: true },
dataType: "string",
columnWidth: 140,
enumValues: {
published: {
id: "published",
label: "Published",
disabled: !values.header_image,
},
draft: "Draft"
}
}))

O conteúdo das nossas entradas de blog precisa ser dinâmico, para que os gestores de conteúdo possam criar entradas complexas com diferentes componentes.

O conteúdo será um array de objetos, que terá um atributo type (que funciona como discriminador) e um atributo value.

Definiremos 3 tipos:

  • images: um array de imagens
  • text: um campo de texto Markdown
  • products: um array de referências para outra coleção, products neste caso.

Usamos a prop oneOf em propriedades de array, que é projetada exatamente para este caso de uso. Você só precisa definir:

buildProperty({
name: "Content",
description: "Exemplo de um array complexo com múltiplas propriedades como filhos",
validation: { required: true },
dataType: "array",
columnWidth: 400,
oneOf: {
typeField: "type", // você pode omitir as props `typeField` e `valueField` para usar os padrões
valueField: "value",
properties: {
images: buildProperty({
name: "Images",
dataType: "array",
of: buildProperty({
dataType: "string",
storage: {
storagePath: "images",
acceptedFiles: ["image/*"],
metadata: {
cacheControl: "max-age=1000000"
}
}
}),
description: "Este campo permite fazer upload de múltiplas imagens de uma vez e reordenar"
}),
text: buildProperty({
dataType: "string",
name: "Text",
markdown: true
}),
products: buildProperty({
name: "Products",
dataType: "array",
of: {
dataType: "reference",
path: "products" // você precisa definir uma coleção válida neste caminho
}
})
}
}
})

Esta configuração de array criará objetos no datasource com o formato:

{
// ...
content: [
{
"type": "text",
"value": "Óculos escuros ou óculos de sol são uma forma de proteção ocular..."
},
{
"type": "images",
"value": [
"images/photo-1511499767150-a48a237f0083.jpeg",
"images/photo-1577803645773-f96470509666.jpeg"
]
}
],
// ...
}

Vamos usar outro recurso do FireCMS: views personalizadas para entidades!

O FireCMS permite adicionar views adicionais às suas views de entidade, que são definidas como componentes React. As props que você recebe para construir este componente são a coleção da entidade, a entidade original e os valores modificados.

Neste caso, vamos criar alguns componentes React para representar nossa entrada de blog como o app frontend faria. Este é o mesmo código que você poderia usar em qualquer framework SSR usando React, como next.js.

Você também poderia ter uma configuração mais complexa que envia seus dados para seu app SSR através de uma API e renderiza o resultado.

Você pode encontrar o código para BlogEntryPreview em BlogEntryPreview.

Quando você tiver criado seu componente, há 2 formas de adicioná-lo à sua coleção:

A forma preferida é registrá-lo no CMS, para que esteja disponível no seletor de view da coleção.

Para isso, você precisa adicioná-lo à prop entityViews da exportação do config principal do app FireCMS:

import { FireCMSAppConfig } from "@firecms/cloud";
const appConfig: FireCMSAppConfig = {
version: "1",
collections: [],
entityViews: [
{
key: "blog_preview",
name: "Preview",
Builder: BlogEntryPreview
}
],
}

Se você estiver definindo sua coleção em código, também pode registrá-lo na própria coleção:

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

Se juntarmos todas as partes que construímos neste tutorial, obtemos o seguinte código para a coleção de blog:

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