2025-01-25

Cómo añadir TinaCMS a un proyecto de Next.js y React paso a paso.

Suponiendo que ya tenes un proyecto con Next.js, React, Astro o lo que sea, mi primera recomendación es que crees una rama nueva en git, así tenés la implementación de TinaCMS por separado hasta que esté lista.

Tambien tener a mano la documentación oficial para casos particulares o actualizaciones que hayan salido después de este post: Documentación oficial

Instalación y configuración inicial de TinaCMS en Next.js.

npx @tinacms/cli@latest init

Si no supiste qué poner con lo que te iba preguntando, acá te dejo las respuestas:

  • ¿Qúe framework? Si estás usando Next.js o React, la opción a elegir es Next.js. Si estás usando Astro, entonces la opción a elegir es Other.
  • ¿Typescript? Eso queda a tu elección, pero los ejemplos son con Typescript.
  • pages\demo\blog\[filename].tsx. Cuidado, si te aparece esto quiere decir que ya tenés cosas en esa ubicación y las va a sobrescribir.

Corrección de errores comunes en la configuración de package.json con TinaCMS

Parece ser que hay veces que la instalación falla y no configura correctamente el package.json. Lo que hay que hacer es modificar los scripts de esta forma:

"scripts": {
  "dev": "tinacms dev -c \"next dev\"",
  "build": "tinacms build && next build",
  "start": "tinacms build && next start"
}

Cómo iniciar TinaCMS por primera vez en tu proyecto

Si tienes dudas sobre cómo configurar o entender el routing en Next.js, podes consultar mi guía específica en la página: Routing en Next.js.

Para si hicimos todo bien ponemos como siempre npm run dev, le va a costar arrancar más de lo normal porque TinaCMS levanta un servidor. Deberíamos ver nuestro proyecto normal y si accedemos a localhost:<tu-puerto>/admin/index.html, Deberíamos ver el panel de TinaCMS.

Vista del panel TinaCMS en un proyecto Next.js para editar contenido visualmente

Si llegado este punto tenés algún error, la documentación oficial tiene un apartado de errores comunes:

Errores comunes

Primeros pasos con el editor visual de TinaCMS

Tomando de partida la imagen anterior, si vamos al menú (arriba a la izquierda), vamos a ver una colección de posts y al entrar en el post lo vamos a poder editar.

Interfaz de TinaCMS mostrando una colección de posts en Next.jsEjemplo de edición de un post usando el editor visual de TinaCMS

Cómo configurar el archivo config.ts y organizar contenido con TinaCMS

Por si todavía no te diste cuenta, cuando instalamos TinaCMS nos creó dos carpetas en la raíz del proyecto, content/posts/hello-world.md que es lo que estuvimos viendo en la última imagen y tina, en content vamos a poner todo el contenido y en tina está la configuración de TinaCMS.

Estructura de config.ts: Personalización de colecciones en TinaCMS

Primero analicemos el ejemplo que nos dan por default para después empezar con lo nuestro.

schema: {
  // `collections` es un array de objetos. Cada objeto representa una colección
  // dentro de TinaCMS, y define cómo se estructuran y gestionan los datos.
  collections: [
    {
      name: "post",
      // `name` es el identificador interno de la colección y es lo que vamos a usar en el código.
      label: "Posts",
      // `label` es el nombre que ve el usuario en la interfaz.
      path: "content/posts",
      // `path` indica la ubicación en el sistema de archivos donde se almacenan
      // los datos de esta colección.
      fields: [
        // `fields` define los campos (o propiedades) de cada entrada en esta colección.
        {
          type: "string",
          name: "title",
          label: "Title",
          isTitle: true,
          // `isTitle` indica que este campo será el título principal
          // y el nombre del archivo asociado.
          required: true,
          // Si es isTitle siempre tiene que ser required:true
          // en otros campos es optativo
        },
        {
          type: "rich-text",
          // Existen múltiples tipos de campos (`type`), como `string`, `number`,
          // `image`, `rich-text`, etc.
          name: "body",
          label: "Body",
          isBody: true,
        },
      ],
      // En la interfaz (`ui`), podes personalizar el comportamiento y la experiencia
      // del usuario al editar esta colección.
      ui: {
        // La función `router` define la ruta a la que se dirigirá el usuario
        // al editar un documento específico en esta colección.
        router: ({ document }) => `/demo/blog/${document._sys.filename}`,
      },
    },
  ],
},

Ahora que ya entendemos un poco la estructura de cómo se organizan los datos, vamos a pasar a crear una colección.

Guía práctica para crear una colección en TinaCMS

Primero vamos a identificar qué queremos que el usuario cambie. En este caso, vamos a querer que cambie tres cosas: la imagen, la lista y el texto de los enlaces.

Vista de los campos configurados en una colección dentro de TinaCMS

Ya tenemos identificado como queremos separar nuestros fields y de qué tipo van a ser.

Así debería quedar nuestra collection:

	schema: {
		collections: [
			{
				name: 'home',
				label: 'Home Page',
				path: 'content/Home',
				// Tipo de archivo donde se guarda la información
				format: 'json',
				ui: {
					router: () => {
						return '/'
						// Cuando editamos esta colección, nos redirige a la ruta principal (`/`).
					},
				},
				fields: [
					{
						type: 'image',
						// Este type nos permite mandar solo images
						name: 'urlImage',
						label: 'Image',
						required: true,
					},
					{
						name: 'list',
						label: 'List',
						// Como vimos antes esto va a ser una lista, por lo tanto
						// la cantidad de items puede variar, esto lo logramos
						// con un type object y list:true.
						type: 'object',
						list: true,
						required: true,
						ui: {
							itemProps: (item) => {
								// Este label es el que vamos a ver que tiene cada item
								return { label: item.text }
							},
						},
						fields: [
							// Abrimos otro field que sería la información que necesita cada item
							// en este caso solo el text
							{ label: 'Text', name: 'text', type: 'string', required: true },
						],
					},
					// Y estos son string comunes con nombres comunes
					{
						type: 'string',
						name: 'firstLink',
						label: 'First Link',
						required: true,
					},
					{
						type: 'string',
						name: 'secondLink',
						label: 'Second Link',
						required: true,
					},
				],
			},
		],
	},

Ahora podríamos ir a nuestra interfaz de tinaCMS y crear la home desde ahí, pero yo prefiero crearla directamente desde un archivo.

Vamos a ir al path que le dimos a nuestra collection, es este caso content/Home y crear un archivo con el nombre que quieras, yo le voy a poner home.json.

Y ahora, siguiendo la estructura y los names que le dimos antes, vamos a crear nuestro home

{
	"urlImage": "/next.svg",
	"list": [
		{
			"text": "Get started by editing app/page.tsx."
		},
		{
			"text": "Save and see your changes instantly."
		}
	],
	"firstLink": "Deploy now",
	"secondLink": "Read our docs"
}

Ya que tenemos nuestro home armado, deberíamos poder verlo disponible en nuestra interfaz (f5 si no aparece). Al seleccionarlo, te redirigirá a la home o URL que hayas configurado, pero aún no aparecerán cambios editables, bueno ese es el siguiente paso.

Edición visual en TinaCMS: Cómo vincular datos dinámicos con componentes React

Ahora empieza lo curioso, tal vez, o capaz que soy solo yo, vamos a necesitar dos componentes, uno que sea asincrono el cual va a solicitar la información al archivo y el otro que sea "use client", el cual va a utilizar esa información y va a ir cambiando.

Con ejemplos es todo más fácil, así que ahí vamos.

Primero vamos a empezar con el asincrono que va a ser el padre:


export default async function Home() {
	// El queries.home es por el name que le dimos a nuestra colección
	// En el result vamos a tener toda la información necesaria
	const result = await client.queries.home({
		// El relativePath es el nombre del archivo
		relativePath: 'home.json',
	})
	return (
		<div className={styles.page}>
			<main className={styles.main}>
				{/* Le pasamos todo el result */}
				<CustomHome {...result} />
			</main>
		</div>
	)
}

Ya tenemos el asíncrono, ahora vamos por el que consume y actualiza la información.

TinaCMS tiene un sistema de types automáticos que va generándolos según lo que vayamos haciendo, en este punto ya nos debería haber generado el HomeQuery, otra vez es home porque es el name que le pusimos arriba, pero puede ser cualquier otro

'use client'
// Importante el use client para más adelante
import { HomeQuery } from '@/tina/__generated__/types'
// Este type lo genero automáticamente

// Vamos a setear el type de las props con este formato
// Data va a tener la información que necesitamos
export default function CustomHome(props: {
	data: HomeQuery
	variables: {
		relativePath: string
	}
	query: string
})

// Este hook es por el que usamos el use client
// Desde esta data vamos a consumir para rellenar el componente
const { data } = useTina(props)

return (
	<>
		<Image
			className={styles.logo}
			// Aca usamos la imagen
			src={data.home.urlImage}
			alt='Next.js logo'
			width={180}
			height={38}
			priority
		/>
		<ol>
			{/* Y aca la list con cada item */}
			{data.home.list.map((item, index) => (
				<li key={index}>{item.text}</li>
			))}
		</ol>
	</>
)

Si hicimos todo bien deberíamos tener algo asi:

Vista previa de un componente React consumiendo datos dinámicos de TinaCMS

Si modificamos algo en la interfaz se ve modificado instantáneamente en el home.

Ahora solo queda el último detalle que es opcional, que el usuario pueda hacer clic en un elemento y que este se abra en la interfaz.

Interactividad con TinaCMS: Cómo usar data-tina-field y el helper tinaField()

A este atributo hay que pasarle data y de qué componente se trata, según el caso también como vemos en el map se pasa directamente el item

Esto va a hacer que se cree un recuadro de color azul al rededor de los elementos que son clickeables.

<>
	<Image src={data.home.urlImage} data-tina-field={tinaField(data.home, 'urlImage')} />
	<ol>
		{data.home.list.map((item, index) => (
			<li key={index} data-tina-field={tinaField(item)}>
				{item.text}
			</li>
		))}
	</ol>
</>

Y listo. Si hay algún error o se queda desactualizado, por favor háganmelo llegar.

Espero que haya sido de ayuda.