Empezando con TanStack Start
En esta guía vas a crear un pequeño blog con TanStack Start (React) que lee sus posts de Content Island:
- Una página de inicio con el listado de todos los posts.
- Una página de detalle por post, en
/posts/<slug>, que pinta su contenido Markdown.
Por el camino verás lo básico que necesitas en cualquier proyecto TanStack Start: server functions, loaders de ruta y cómo encajan para mantener el token de acceso en el servidor.
Requisitos previos
- Node.js 20 o superior.
- Una cuenta de Content Island.
Prepara tu contenido en Content Island
-
Crea un proyecto nuevo desde la página de Proyectos.
-
En la pestaña Modelo, crea un modelo llamado
postcon estos campos:Nombre del campo Tipo Notas titleTexto Corto Obligatorio. slugTexto Corto Obligatorio y único. contentTexto Largo Cuerpo del post en Markdown. -
En la pestaña Contenido, añade dos o tres posts y publícalos.
-
En la pestaña General, copia el Token del proyecto (el token de acceso al API).
Crea el proyecto TanStack Start
-
Crea un proyecto nuevo:
Ventana de terminal npx @tanstack/cli create my-blogcd my-blog -
Instala el cliente de Content Island y
marked(para convertir el Markdown a HTML):Ventana de terminal npm install @content-island/api-client marked
Configura el token de acceso
El token de acceso da acceso a tu proyecto, así que nunca debe llegar al navegador. Guárdalo en una variable de entorno:
CONTENT_ISLAND_ACCESS_TOKEN=tu-token-de-accesoTanStack Start carga el fichero .env en process.env por ti, así que el token está disponible en tu código de servidor sin paquetes extra. En producción, define la misma variable en tu proveedor de hosting.
Crea el cliente
Crea el cliente una sola vez y reutilízalo desde tus server functions:
import { createClient } from '@content-island/api-client';
export const client = createClient({ accessToken: process.env.CONTENT_ISLAND_ACCESS_TOKEN!,});Y un tipo que refleje el modelo post:
export interface Post { id: string; title: string; slug: string; content: string;}Server functions y loaders
Antes de escribir páginas, conviene entender las dos piezas que vas a usar para cargar datos:
loader de ruta | Server function (createServerFn) | |
|---|---|---|
| ¿Dónde se ejecuta? | En el servidor en la primera petición y en el navegador al navegar en cliente. | Siempre en el servidor. |
| ¿Puede usar el token? | No (su código también se envía al navegador). | Sí. |
| ¿Cómo se llama? | Automáticamente, el router lo ejecuta antes de pintar la ruta. | Como una función async normal, desde cualquier sitio. Desde el navegador se convierte en un fetch a tu servidor. |
Así que el patrón es: lee Content Island dentro de server functions y llama a esas server functions desde tus loaders.
import { createServerFn } from '@tanstack/react-start';import { marked } from 'marked';import { client } from './content-island';import type { Post } from '../models/post';
export const getPosts = createServerFn({ method: 'GET' }).handler(async () => client.getContentList<Post>({ contentType: 'post' }));
export const getPostBySlug = createServerFn({ method: 'GET' }) .validator((slug: string) => slug) .handler(async ({ data: slug }) => { const posts = await client.getContentList<Post>({ contentType: 'post', 'fields.slug': slug }); const post = posts.at(0); // undefined si ningún post tiene ese slug return post ? { ...post, html: await marked.parse(post.content) } : null; });validatorvalida (y tipa) los datos que recibe la función. Todo lo que llega del navegador puede manipularse, así que valídalo. También puedes pasarle un esquema de una librería como Zod.- Lo que devuelve una server function se serializa y se envía al navegador, así que devuelve datos planos. Aquí el Markdown ya se convierte a HTML en el servidor.
Lista los posts
Llama a la server function desde el loader de la ruta y lee el resultado con Route.useLoaderData():
import { createFileRoute, Link } from '@tanstack/react-router';import { getPosts } from '../server/posts';
export const Route = createFileRoute('/')({ loader: () => getPosts(), component: Home,});
function Home() { const posts = Route.useLoaderData();
return ( <main> <h1>Mi blog</h1> <ul> {posts.map(post => ( <li key={post.id}> <Link to="/posts/$slug" params={{ slug: post.slug }}> {post.title} </Link> </li> ))} </ul> </main> );}Ejecuta npm run dev y abre la URL que aparece en el terminal para ver tus posts.
Muestra un post
Crea una ruta dinámica. En TanStack Router, $slug en el nombre del fichero es un parámetro de ruta, disponible en los params del loader. Lanza notFound() cuando el post no exista:
// src/routes/posts.$slug.tsximport { createFileRoute, Link, notFound } from '@tanstack/react-router';import { getPostBySlug } from '../server/posts';
export const Route = createFileRoute('/posts/$slug')({ loader: async ({ params }) => { const post = await getPostBySlug({ data: params.slug }); if (!post) { throw notFound(); } return post; }, component: PostPage,});
function PostPage() { const post = Route.useLoaderData();
return ( <main> <Link to="/">← Volver</Link> <h1>{post.title}</h1> <article dangerouslySetInnerHTML={{ __html: post.html }} /> </main> );}Las server functions reciben sus argumentos en la propiedad data: getPostBySlug({ data: params.slug }).
Lo básico del servidor en TanStack Start
Llamar a una server function desde un componente
Las server functions no se limitan a los loaders. Puedes llamarlas desde un manejador de eventos, por ejemplo para un botón «Cargar más»:
const PAGE_SIZE = 5;
export const getPostsPage = createServerFn({ method: 'GET' }) .validator((page: number) => page) .handler(async ({ data: page }) => client.getContentList<Post>({ contentType: 'post', pagination: { take: PAGE_SIZE, skip: page * PAGE_SIZE }, }) );import { useState } from 'react';import { getPostsPage } from '../server/posts';import type { Post } from '../models/post';
export function LoadMore() { const [posts, setPosts] = useState<Post[]>([]); const [page, setPage] = useState(0);
const handleClick = async () => { const nextPosts = await getPostsPage({ data: page }); setPosts([...posts, ...nextPosts]); setPage(page + 1); };
return ( <> <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> <button onClick={handleClick}>Cargar más</button> </> );}Pinta <LoadMore /> donde quieras el botón, por ejemplo debajo del listado en src/routes/index.tsx.
Renderizado
TanStack Start renderiza cada página en el servidor en cada petición (SSR) y después la hidrata en el navegador; las navegaciones siguientes ocurren en cliente y solo piden datos a través de tus server functions. Los cambios de contenido se ven al momento, sin rebuild.
Si quieres servir todo el contenido desde la memoria del servidor y refrescarlo cuando publicas, consulta la guía de Dynamic Snapshots para TanStack Start.
Previsualiza contenido sin publicar
Mientras desarrollas, añade el prefijo PREVIEW_ al token para leer también el contenido que todavía no está publicado:
CONTENT_ISLAND_ACCESS_TOKEN=PREVIEW_tu-token-de-accesoConsulta Preview Token para más detalles.
Siguientes pasos
- Despliega tu aplicación: Vercel, Netlify o Cloudflare Pages.
- Sirve el contenido desde memoria y refréscalo al publicar con Dynamic Snapshots.
- Sirve varios idiomas con Multilenguaje.
- Explora todos los métodos de la librería cliente API.