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
npx @tinacms/cli@latest init
Si no supiste qué poner con lo que te iba preguntando, acá te dejo las respuestas:
Next.js
. Si estás usando Astro, entonces la opción a elegir es Other
.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.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"
}
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.
Si llegado este punto tenés algún error, la documentación oficial tiene un apartado de errores comunes:
Errores comunes
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.
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.
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.
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.
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.
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:
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.
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.