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 initSi 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.