Saltearse al contenido

Construir un blog en FireCMS Cloud

blog_example

No es necesario explicar los beneficios de usar un CMS headless en lugar de un enfoque de blog tradicional como WordPress, pero aquí hay algunos:

  • es más fácil y rápido de desarrollar, ya que no hay acoplamiento entre frontend y backend.
  • dado que tu frontend es independiente, puedes cambiarlo de cualquier forma que desees, lo mismo ocurre con el backend.
  • está diseñado para aplicaciones multicanal, puedes usar el mismo backend y CMS con múltiples apps y sitios web
  • equipos más pequeños y especializados
  • escalabilidad
  • menores costes
  • flexibilidad y simplicidad

Vamos a construir una colección que aloje entradas de blog. Cada una de las entradas incluirá un array dinámico de elementos.

FireCMS tiene una funcionalidad integrada que te permite construir arrays dinámicos. La propiedad de tipo array puede configurarse con la prop oneOf, para contener objetos que almacenan un tipo específico de valor. ¡Esto es perfecto para construir la estructura de datos de nuestras entradas de blog!

Estos son los tipos que usaremos:

  • Definimos el tipo de la colección de blogs:
type BlogEntry = {
name: string,
header_image: string,
created_on: Date,
status: string,
content: (BlogEntryImages | BlogEntryText | BlogEntryProducts)[];
}
  • Y cada uno de los tipos del array content:
type BlogEntryImages = {
type: "images";
value: string[];
}
type BlogEntryText = {
type: "text";
value: string;
}
type BlogEntryProducts = {
type: "products";
value: object[]; // Usamos un objeto genérico aquí, siéntete libre de definir un tipo para tus productos
}

Empecemos inicializando nuestra colección, sin ninguna propiedad:

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

Luego vamos a añadir algunas propiedades simples para nuestras entradas.

  • Queremos tener un título, que debe estar siempre establecido, así que ponemos la validación required a true:
buildProperty({
name: "Título",
validation: { required: true },
dataType: "string"
})
  • Una imagen que estará en la parte superior de la entrada del blog:
buildProperty({
name: "Imagen de cabecera",
dataType: "string",
storage: {
storagePath: "images",
acceptedFiles: ["image/*"],
metadata: {
cacheControl: "max-age=1000000"
}
}
})
  • y una fecha “created on” que se genera automáticamente cuando se crea el documento.
buildProperty( {
name: "Creado el",
dataType: "date",
autoValue: "on_create"
})

Ahora queremos añadir una propiedad string status que tendrá dos valores posibles: published y draft. Solo queremos permitir el estado published cuando el resto de los campos son correctos.

En este caso lo mantendremos simple, y solo comprobaremos si la imagen de cabecera está establecida:

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

El contenido de nuestras entradas de blog necesita ser dinámico, para que los gestores de contenido puedan crear entradas complejas con diferentes componentes.

El contenido será un array de objetos, que tendrán un atributo type (que funcionará como discriminador), y un atributo value.

Definiremos 3 tipos:

  • images: un array de imágenes
  • text: un campo de texto Markdown
  • products: un array de referencias a otra colección, products en este caso

Usamos la prop oneOf en las propiedades de array, que está diseñada exactamente para este caso de uso. Solo necesitas definir:

buildProperty({
name: "Contenido",
description: "Ejemplo de array complejo con múltiples propiedades como hijos",
validation: { required: true },
dataType: "array",
columnWidth: 400,
oneOf: {
typeField: "type", // puedes omitir estas props `typeField` y `valueField` para usar los valores predeterminados
valueField: "value",
properties: {
images: buildProperty({
name: "Imágenes",
dataType: "array",
of: buildProperty({
dataType: "string",
storage: {
storagePath: "images",
acceptedFiles: ["image/*"],
metadata: {
cacheControl: "max-age=1000000"
}
}
}),
description: "Este campo permite subir múltiples imágenes a la vez y reordenarlas"
}),
text: buildProperty({
dataType: "string",
name: "Texto",
markdown: true
}),
products: buildProperty({
name: "Productos",
dataType: "array",
of: {
dataType: "reference",
path: "products" // necesitas definir una colección válida en esta ruta
}
})
}
}
})

¡Aprovechemos otra funcionalidad de FireCMS: vistas personalizadas para entidades!

FireCMS te permite añadir vistas adicionales a tus vistas de entidades, que están definidas como componentes React. Las props que recibes para construir este componente son la colección de entidades, la entidad original y los valores modificados.

En este caso, crearemos algunos componentes React para representar nuestra entrada de blog tal como lo haría la aplicación frontend. Este es el mismo código que podrías usar en cualquier framework SSR que use React, como next.js.

También podrías tener una configuración más compleja que envíe tus datos a tu app SSR a través de una API y renderice el resultado.

Puedes encontrar el código de BlogEntryPreview en BlogEntryPreview.

Cuando hayas creado tu componente, hay 2 formas de añadirlo:

La forma preferida es registrarlo en el CMS, para que esté disponible en el selector de vistas de la colección.

Para hacerlo, necesitas añadirlo a la prop entityViews de la configuración principal de la app FireCMS:

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

Si estás definiendo tu colección en código, también puedes registrarlo directamente en la colección:

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

Si juntamos todas las partes que hemos construido en este tutorial, obtenemos el siguiente código para la colección 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"
})
}
})