TinaCMS Cliente: NutriVerde โ€” Startup Nutricion con IA

CMS Visual respaldado por Git

Implementacion completa de TinaCMS para que el equipo no tecnico de NutriVerde edite el blog y las landing pages de forma visual, sin tocar codigo ni la terminal.

TinaCMS 2.x Next.js 14 App Router TypeScript Git-backed content
๐ŸŽจ
CMS
TinaCMS
Edicion visual en tiempo real
๐Ÿ“‚
Colecciones
3
posts, pages, authors
โšก
Componentes MDX
4
Callout, RecipeCard, Nutrientes, CTA
๐ŸŒฟ
Deploy
Vercel
Tina Cloud ยท rama main
Flujo editorial
1
โœ๏ธ
Editar en /admin
CM abre nutriVerde.com/admin, edita en interfaz visual
โ†’
2
๐Ÿ‘๏ธ
Preview live
useTina refleja los cambios en tiempo real al lado derecho
โ†’
3
๐Ÿ’พ
Guardar = Git commit
Tina Cloud hace commit del .mdx al repo en GitHub
โ†’
4
๐Ÿš€
Deploy automatico
Vercel detecta el push y redeploya en ~30s
โ†’
5
๐ŸŒ
Publicado
Contenido live en nutriverde.com con historial Git
Estructura del proyecto
nutriverde-web/
๐Ÿ“ tina/ (gestionado por TinaCMS)
๐Ÿ“„ config.ts
๐Ÿ“ __generated__/ (auto-generado)
๐Ÿ“„ client.ts
๐Ÿ“„ types.ts
๐Ÿ“ content/
๐Ÿ“„ posts/ia-nutricion-personalizada.mdx
๐Ÿ“„ posts/receta-bowl-proteico.mdx
๐Ÿ“„ posts/bienestar-microbioma.mdx
๐Ÿ“„ ... 9 posts mas
๐Ÿ“„ pages/home.mdx
๐Ÿ“„ pages/sobre-nosotros.mdx
๐Ÿ“ app/
๐Ÿ“„ posts/[slug]/page.tsx
๐Ÿ“„ blog/page.tsx
๐Ÿ“ src/lib/
๐Ÿ“„ content.ts
๐Ÿ“„ .env.local (secrets Tina Cloud)
Esquemas de contenido โ€” tina/config.ts
๐Ÿ“ Blog Posts
content/posts/*.mdx
string
title REQUERIDO
Titulo del articulo (isTitle)
string
description
Resumen del post (textarea)
datetime
publishedAt REQUERIDO
Fecha de publicacion
image
heroImage
Imagen destacada del post
string
category
recetas ยท nutricion ยท bienestar ยท ia-alimentacion
object
author
name ยท avatar ยท role (objeto anidado)
rich-text
body (isBody)
Contenido MDX con templates: Callout, RecipeCard, Nutrientes
๐Ÿ  Landing Pages
content/pages/*.mdx
string
title REQUERIDO
Titulo de la pagina
object
seo
metaTitle ยท metaDescription ยท ogImage
list
blocks (Page Builder)
Bloques arrastrables para la pagina
Templates de bloques
๐Ÿฆธ Hero Section heading ยท subheading ยท bgImage ยท cta
โœจ Features Grid heading ยท items[]{title, desc, icon}
๐Ÿ“ฃ CTA Banner text ยท buttonText ยท buttonLink
โญ Testimonials items[]{quote, author, rating}
Configuracion โ€” tina/config.ts
๐Ÿ“„ tina/config.ts TypeScript
// tina/config.ts โ€” Configuracion TinaCMS para NutriVerde
import { defineConfig } from "tinacms";

export default defineConfig({
  branch: process.env.NEXT_PUBLIC_TINA_BRANCH || "main",
  clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID!,
  token: process.env.TINA_TOKEN!,

  build: {
    outputFolder: "admin",        // Panel en nutriverde.com/admin
    publicFolder: "public",
  },

  media: {
    tina: {
      mediaRoot: "uploads",       // Imagenes en /public/uploads
      publicFolder: "public",
    },
  },

  schema: {
    collections: [
      // ==============================
      // COLECCION: Blog Posts (MDX)
      // ==============================
      {
        name: "post",
        label: "Articulos del Blog",
        path: "content/posts",
        format: "mdx",
        fields: [
          { type: "string", name: "title", label: "Titulo", isTitle: true, required: true },
          { type: "string", name: "description", label: "Resumen",
            ui: { component: "textarea" } },
          { type: "datetime", name: "publishedAt", label: "Fecha publicacion", required: true },
          { type: "image", name: "heroImage", label: "Imagen destacada" },
          {
            type: "string", name: "category", label: "Categoria",
            options: ["recetas", "nutricion", "bienestar", "ia-alimentacion"],
          },
          {
            type: "object", name: "author", label: "Autor",
            fields: [
              { type: "string", name: "name", label: "Nombre", required: true },
              { type: "image", name: "avatar", label: "Avatar" },
              { type: "string", name: "role", label: "Rol" },
            ],
          },
          {
            type: "rich-text", name: "body", label: "Contenido", isBody: true,
            templates: [
              {
                name: "Callout", label: "Caja de Aviso",
                fields: [
                  { type: "string", name: "type", label: "Tipo",
                    options: ["info", "warning", "tip", "danger"] },
                  { type: "rich-text", name: "children", label: "Contenido" },
                ],
              },
              {
                name: "RecipeCard", label: "Tarjeta de Receta",
                fields: [
                  { type: "string", name: "name", label: "Nombre receta" },
                  { type: "number", name: "kcal", label: "Calorias (kcal)" },
                  { type: "string", name: "time", label: "Tiempo preparacion" },
                  { type: "string", name: "tags", label: "Etiquetas", list: true },
                ],
              },
            ],
          },
        ],
      },
    ],
  },
});
Edicion visual โ€” app/posts/[slug]/page.tsx
๐Ÿ“„ app/posts/[slug]/page.tsx TSX
// Server component: obtiene datos en build/request time
export default async function PostPage({ params }: { params: { slug: string } }) {
  const { data, query, variables } = await client.queries.post({
    relativePath: `${params.slug}.mdx`,
  });

  return <PostClient data={data} query={query} variables={variables} />;
}

// Client component: habilita edicion visual con useTina
"use client";
function PostClient({ data, query, variables }: PostClientProps) {
  // useTina: conecta con el panel /admin y refleja cambios en tiempo real
  const { data: tinaData } = useTina({ query, variables, data });
  const post = tinaData.post;

  return (
    <article className="nutriverde-post">
      {post.heroImage && <img src={post.heroImage} alt={post.title} />}
      <h1>{post.title}</h1>
      <p className="description">{post.description}</p>
      <div className="author-row">
        {post.author?.avatar && <img src={post.author.avatar} alt="" />}
        <span>{post.author?.name}</span>
        <time>{new Date(post.publishedAt).toLocaleDateString('es-ES')}</time>
      </div>
      {/* TinaMarkdown renderiza MDX con componentes custom */}
      <TinaMarkdown content={post.body} components={components} />
    </article>
  );
}

// Componentes MDX custom para NutriVerde
const components = {
  Callout: ({ type, children }: any) => (
    <div className={`callout callout-${type}`}>
      {type === "tip" && "๐Ÿ’ก"} {type === "warning" && "โš ๏ธ"}
      <TinaMarkdown content={children} />
    </div>
  ),
  RecipeCard: ({ name, kcal, time, tags }: any) => (
    <div className="recipe-card">
      <h3>{name}</h3>
      <p>{kcal} kcal ยท {time}</p>
      {tags?.map((t: string) => <span key={t} className="tag">{t}</span>)}
    </div>
  ),
};
Componentes MDX โ€” preview
๐Ÿงฉ Componentes custom disponibles en el editor visual
๐Ÿ’ก Consejo nutricional: Consume proteina en los primeros 30 minutos tras el ejercicio para optimizar la sintesis muscular. El suero de leche y el huevo son fuentes de rapida absorcion.
โš ๏ธ Atencion: Las personas con hipotiroidismo deben consultar al medico antes de seguir una dieta cetogenica estricta.
โ„น๏ธ NutriVerde IA analiza tu microbioma y ajusta las recomendaciones semanalmente. Conecta tu wearable para mayor precision.
๐Ÿฅ—
Bowl Proteico Verde
420 kcal ยท 25 min preparacion ยท 2 raciones
sin-gluten alto-proteina vegano bajo-IG
Queries tipadas โ€” src/lib/content.ts
๐Ÿ“„ src/lib/content.ts TypeScript
// Obtener todos los posts
async function getAllPosts() {
  const res = await client.queries
    .postConnection({ sort: "publishedAt", last: 100 });

  return res.data.postConnection.edges
    ?.map((edge) => ({
      slug:   edge?.node?._sys.filename,
      title:  edge?.node?.title,
      cat:    edge?.node?.category,
      author: edge?.node?.author,
    })) ?? [];
}

// Filtrar por categoria
async function getPostsByCategory(cat: string) {
  const res = await client.queries
    .postConnection({
      filter: { category: { eq: cat } },
    });
  return res.data.postConnection.edges
    ?.map((e) => e?.node) ?? [];
}

// Post individual
async function getPost(slug: string) {
  const res = await client.queries
    .post({ relativePath: `${slug}.mdx` });
  return res.data.post;
}
Instalacion y arranque
๐Ÿ–ฅ๏ธ Terminal โ€” desde la raiz del proyecto nutriverde-web/
$ npx @tinacms/cli@latest init Instala TinaCMS y genera tina/config.ts inicial
$ npm install tinacms @tinacms/cli Dependencias principales
$ npx tinacms dev -c "next dev" Dev server con edicion visual en localhost:3000/admin
$ npx tinacms build && next build Build de produccion para Vercel
๐Ÿ” .env.local โ€” Variables de entorno (Tina Cloud)
NEXT_PUBLIC_TINA_CLIENT_ID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TINA_TOKEN xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_TINA_BRANCH main
Buenas practicas para NutriVerde
1
Git es tu base de datos
Cada edicion del CM crea un commit en GitHub. El historial de articulos es el historial de Git. Rollback = git revert.
2
MDX para contenido rico
Los componentes Callout y RecipeCard estan disponibles directamente en el editor WYSIWYG. El CM los inserta con un clic.
3
Queries tipadas, nunca raw GraphQL
Usa el client auto-generado en tina/__generated__. TypeScript te avisa si el esquema cambia y rompe algun query.
4
Workflow de ramas para reviews
El CM puede crear articulos en rama draft. Los devs hacen review del MDX como cualquier PR antes de mergear a main.
5
Page builder sin codigo
Los bloques hero, features y CTA son arrastrables. El CM monta nuevas landing pages de NutriVerde sin tocar TSX.
6
Git LFS para imagenes pesadas
Las fotos de recetas e ingredientes pueden ser grandes. Configura Git LFS para mantener el repo liviano en GitHub.