<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Con Criterio: Recursos]]></title><description><![CDATA[Recursos es el repositorio práctico de Con Criterio: skills, guías y referencias listas para usar en tus proyectos.]]></description><link>https://www.concriterio.blog/s/recursos</link><image><url>https://substackcdn.com/image/fetch/$s_!_0cB!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7352102a-975a-44e6-8c38-2cce1ed25978_512x512.png</url><title>Con Criterio: Recursos</title><link>https://www.concriterio.blog/s/recursos</link></image><generator>Substack</generator><lastBuildDate>Mon, 25 May 2026 14:09:44 GMT</lastBuildDate><atom:link href="https://www.concriterio.blog/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Pol Marzà]]></copyright><language><![CDATA[es]]></language><webMaster><![CDATA[hazloconcriterio@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[hazloconcriterio@substack.com]]></itunes:email><itunes:name><![CDATA[Pol Marzà]]></itunes:name></itunes:owner><itunes:author><![CDATA[Pol Marzà]]></itunes:author><googleplay:owner><![CDATA[hazloconcriterio@substack.com]]></googleplay:owner><googleplay:email><![CDATA[hazloconcriterio@substack.com]]></googleplay:email><googleplay:author><![CDATA[Pol Marzà]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[SDD Clarifier]]></title><description><![CDATA[Transforma una idea vaga en especificaciones completas antes de escribir una sola l&#237;nea de c&#243;digo.]]></description><link>https://www.concriterio.blog/p/sdd-clarifier</link><guid isPermaLink="false">https://www.concriterio.blog/p/sdd-clarifier</guid><dc:creator><![CDATA[Pol Marzà]]></dc:creator><pubDate>Thu, 02 Apr 2026 21:05:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/575a3829-e42c-4cc5-a628-89e1251ee2d5_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Qu&#233; es el SDD Clarifier</strong></h2><p>El SDD Clarifier es una skill para Claude con el que <strong>te gu&#237;a el proceso de clarificaci&#243;n de un proyecto</strong> digital antes de escribir c&#243;digo. Tomas una idea &#8212;puede ser una frase, un p&#225;rrafo, una nota de voz transcrita&#8212; y al final tienes un conjunto completo de artefactos de especificaci&#243;n: PRD, arquitectura, modelo de datos, design system, roadmap y specs funcionales compatibles con OpenSpec.</p><p>El problema que resuelve no es t&#233;cnico. Es anterior a lo t&#233;cnico.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.concriterio.blog/subscribe?&quot;,&quot;text&quot;:&quot;Suscribirse&quot;,&quot;language&quot;:&quot;es&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">&#161;Gracias por leer Con Criterio! Suscr&#237;bete gratis para recibir nuevos posts y apoyar mi trabajo.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Escribe tu correo electr&#243;nico..." tabindex="-1"><input type="submit" class="button primary" value="Suscribirse"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Frameworks como OpenSpec, GitHub Copilot Workspace o BMAD asumen que ya sabes qu&#233; quieres construir. Entran en la fase de implementaci&#243;n. El SDD Clarifier cubre el tramo previo: las decisiones de negocio, las decisiones de dise&#241;o y la fase exploratoria donde todav&#237;a hay m&#225;s preguntas que respuestas.</p><div><hr></div><p style="text-align: center;"><strong>Desc&#225;rgate la skill desde aqu&#237;:</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://github.com/HombreFeliz/sdd-clarifier/blob/easytalk/sdd-clarifier-skill.md&quot;,&quot;text&quot;:&quot;Descargar SDD Clarifier&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://github.com/HombreFeliz/sdd-clarifier/blob/easytalk/sdd-clarifier-skill.md"><span>Descargar SDD Clarifier</span></a></p><div><hr></div><h2><strong>C&#243;mo funciona</strong></h2><p>El proceso tiene tres fases secuenciales.</p><p><strong>Fase 1 &#8212; Exploraci&#243;n (conversacional).</strong> Claude no genera nada todav&#237;a. Pregunta. Extrae del usuario las decisiones necesarias agrupadas en cuatro &#225;reas: negocio (modelo de monetizaci&#243;n, m&#233;tricas de &#233;xito, restricciones), dise&#241;o (plataforma, estilo visual, tono), t&#233;cnica (stack, integraciones, despliegue) y alcance (MVP vs. visi&#243;n completa). M&#225;ximo tres preguntas por turno. Al final, presenta un resumen de todas las decisiones para validaci&#243;n expl&#237;cita.</p><p><strong>Fase 2 &#8212; Formalizaci&#243;n (generaci&#243;n de artefactos).</strong> Una vez confirmado el alcance, genera la estructura de carpetas completa con todos los documentos. Los specs funcionales siguen el formato OpenSpec (SHALL/MUST/SHOULD seg&#250;n RFC 2119), organizados por dominio. Los docs adicionales cubren lo que OpenSpec no cubre: business.md, design-system.md, architecture.md, data-model.md y roadmap.md. Se incluye un CLAUDE.md con instrucciones para el agente de codificaci&#243;n.</p><p><strong>Fase 3 &#8212; Entrega y siguiente paso.</strong> Presenta la estructura generada y da el paso concreto a seguir: inicializar OpenSpec y continuar con el flujo est&#225;ndar de implementaci&#243;n.</p><h2><strong>Filosof&#237;a</strong></h2><p>La skill implementa un principio simple: 80% planificaci&#243;n, 20% ejecuci&#243;n.</p><p>Claude no decide por el usuario. Pregunta, confronta, propone opciones y espera. El usuario mantiene el control y la comprensi&#243;n total del proyecto en todo momento. Si durante la generaci&#243;n falta alguna decisi&#243;n, Claude para y pregunta antes de asumir.</p><p>La potencia sin control no sirve de nada.</p><h2><strong>C&#243;mo instalarla</strong></h2><p>Una skill es un archivo Markdown que Claude carga como contexto de sistema. No es un plugin ni una extensi&#243;n del navegador. No requiere instalaci&#243;n en el sentido tradicional.</p><p><strong>En Claude.ai (web y m&#243;vil).</strong> Ve a Configuraci&#243;n &#8594; Skills &#8594; A&#241;adir skill. Carga el archivo <code>sdd-clarifier-skill.md</code>. A partir de ese momento, Claude lo usar&#225; autom&#225;ticamente cuando detecte que est&#225;s describiendo un proyecto.</p><p><strong>En Claude Code (terminal).</strong> Copia el archivo en <code>~/.claude/skills/sdd-clarifier/SKILL.md</code>. Claude Code lo cargar&#225; en cada sesi&#243;n.</p><p><strong>En Claude Desktop.</strong> El proceso es equivalente al de Claude Code: el archivo va en la carpeta de skills de tu configuraci&#243;n local.</p><p><strong>&#191;Funciona con la API de Anthropic?</strong> No directamente. Las skills son un mecanismo de Claude Code y Claude Desktop. Si usas la API, puedes incluir el contenido del archivo como system prompt manualmente, pero el sistema de skills no se aplica en llamadas directas a la API.</p><div><hr></div><h2><strong>Compatibilidad con OpenSpec</strong></h2><p>Los artefactos que genera esta skill son directamente compatibles con OpenSpec CLI. Una vez que tienes la carpeta generada:</p><ol><li><p>Instala OpenSpec: <code>npm install -g @fission-ai/openspec@latest</code></p></li><li><p>Ejecuta <code>openspec init</code> en el directorio del proyecto</p></li><li><p>Los specs generados por el SDD Clarifier se integran como fuente de verdad</p></li><li><p>Para nuevas funcionalidades, usa el flujo est&#225;ndar: <code>/opsx:propose</code>, <code>/opsx:apply</code>, <code>/opsx:archive</code></p></li></ol><div><hr></div><h3 style="text-align: center;">&#191;Te ha resultado &#250;til este recurso?</h3><p style="text-align: center;">Todos los recursos y materiales de Con Criterio son gratuitos, pero si me quieres apoyar para que siga construyendo y compartiendo de esta manera, puedes invitarme a un caf&#233; :)</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://buymeacoffee.com/polmarza&quot;,&quot;text&quot;:&quot;Inv&#237;tame a un caf&#233;&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://buymeacoffee.com/polmarza"><span>Inv&#237;tame a un caf&#233;</span></a></p><div><hr></div><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.concriterio.blog/subscribe?&quot;,&quot;text&quot;:&quot;Suscribirse&quot;,&quot;language&quot;:&quot;es&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">&#161;Gracias por leer Con Criterio! Suscr&#237;bete gratis para recibir nuevos posts y apoyar mi trabajo.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Escribe tu correo electr&#243;nico..." tabindex="-1"><input type="submit" class="button primary" value="Suscribirse"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Astro + Sanity + Vercel]]></title><description><![CDATA[Lecciones aprendidas integrando un CMS headless con un generador de sitios est&#225;ticos y despliegue automatizado.]]></description><link>https://www.concriterio.blog/p/astro-sanity-vercel</link><guid isPermaLink="false">https://www.concriterio.blog/p/astro-sanity-vercel</guid><dc:creator><![CDATA[Pol Marzà]]></dc:creator><pubDate>Thu, 02 Apr 2026 20:43:54 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/de6beba9-3a10-4146-9eaa-c4cadbf7fed1_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hace unos d&#237;as tuve que montar una web para un cliente (no t&#233;cnico) el cual quer&#237;a actualizar el contenido de forma regular. </em></p><p><em>Para construir dicha web us&#233; un combo ganador como es <strong><a href="https://astro.build">Astro</a></strong>, <strong><a href="https://www.sanity.io">Sanity</a></strong> y <strong><a href="https://vercel.com">Vercel</a></strong>, pero mientras lo constru&#237;a me encontr&#233; una serie de problemas que no esperaba.</em> </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.concriterio.blog/subscribe?&quot;,&quot;text&quot;:&quot;Suscribirse&quot;,&quot;language&quot;:&quot;es&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">&#161;Gracias por leer Con Criterio! Suscr&#237;bete gratis para recibir nuevos posts y apoyar mi trabajo.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Escribe tu correo electr&#243;nico..." tabindex="-1"><input type="submit" class="button primary" value="Suscribirse"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><em>Los he documentado para que, si tambi&#233;n quieres montar algo con este t&#225;ndem y luego lo despliegas en Vercel, puedas tener una ayuda para no entrar en los bucles con los que me encontr&#233; yo :)</em></p><div><hr></div><h2><strong>1. Arquitectura general</strong></h2><pre><code><code>Sanity Studio (CMS) &#8594; Sanity API &#8594; Astro (SSG) &#8594; Vercel (hosting)
       &#8595;                                              &#8593;
   Webhook &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; Deploy Hook</code></code></pre><ul><li><p><strong>Sanity</strong> es donde se guarda y gestiona todo el contenido: textos, im&#225;genes, documentos.</p></li><li><p><strong>Astro</strong> genera las p&#225;ginas HTML del sitio web en el momento de la construcci&#243;n (build), pidiendo los datos a Sanity.</p></li><li><p><strong>Vercel</strong> aloja el sitio y lo reconstruye autom&#225;ticamente cada vez que Sanity le avisa de que algo ha cambiado.</p></li></ul><h3><strong>Por qu&#233; generar p&#225;ginas est&#225;ticas (SSG) y no renderizar en servidor (SSR)</strong></h3><p>Para webs donde el contenido no cambia constantemente (corporativas, portfolios, blogs, sitios de proyecto), generar p&#225;ginas est&#225;ticas es la mejor opci&#243;n:</p><p><strong>SSG (P&#193;GINAS PRE-GENERADAS)SSR (GENERADAS EN CADA VISITA)Velocidad</strong>Instant&#225;neo, el HTML ya est&#225; listoCada visita hace una consulta a la API<strong>Coste</strong>Gratis o casi gratisCada visita consume recursos de servidor<strong>Resiliencia</strong>Si Sanity se cae, la web sigue funcionandoSi Sanity se cae, la web deja de funcionar<strong>Contenido actualizado</strong>Tarda ~30-40 segundos en reconstruirseInmediato</p><p><strong>Regla general:</strong> si el contenido cambia pocas veces al d&#237;a, las p&#225;ginas est&#225;ticas son suficientes. Solo necesitas renderizado en servidor si tu contenido cambia constantemente (tiendas online, dashboards, feeds en tiempo real).</p><h2><strong>2. Configuraci&#243;n del cliente Sanity</strong></h2><pre><code><code>import { createClient } from '@sanity/client';

export const sanityClient = createClient({
  projectId: 'tu-project-id',
  dataset: 'production',
  apiVersion: '2024-01-01',
  useCdn: false, // &#8592; CR&#205;TICO
});</code></code></pre><h3><strong>La trampa de la cach&#233; (useCdn)</strong></h3><p>Este es probablemente el error m&#225;s dif&#237;cil de detectar en toda la integraci&#243;n.</p><p><strong>El problema:</strong> con <code>useCdn: true</code>, Sanity devuelve datos desde su cach&#233; en lugar de ir a buscar los m&#225;s recientes. Cuando un editor publica contenido y se dispara la reconstrucci&#243;n del sitio, el build pide datos a Sanity, pero la cach&#233; todav&#237;a tiene la versi&#243;n anterior. El resultado: el sitio se reconstruye con datos desactualizados.</p><p><strong>C&#243;mo se manifiesta:</strong></p><ul><li><p>Los cambios hechos en Sanity no aparecen en la web despu&#233;s de reconstruir</p></li><li><p>Los tiempos de construcci&#243;n de las p&#225;ginas son sospechosamente r&#225;pidos (~15ms en vez de ~3s)</p></li><li><p>No hay ning&#250;n error en los registros del sistema: simplemente los datos son viejos</p></li></ul><p><strong>Soluci&#243;n:</strong> usar <code>useCdn: false</code> para los builds. En sitios est&#225;ticos no hay penalizaci&#243;n real, porque las consultas solo se ejecutan una vez durante la construcci&#243;n, no en cada visita del usuario.</p><p><strong>Regla:</strong> <code>useCdn: true</code> es para aplicaciones que renderizan en servidor con mucho tr&#225;fico, donde la velocidad de respuesta importa mucho. Para sitios est&#225;ticos, siempre <code>useCdn: false</code>.</p><h2><strong>3. Esquemas de contenido</strong></h2><h3><strong>Singletons vs. Colecciones</strong></h3><ul><li><p><strong>Singletons:</strong> documentos que solo existen una vez (la secci&#243;n hero de la home, la p&#225;gina &#8220;sobre nosotros&#8221;, el formulario de contacto, la configuraci&#243;n general). Se crean una vez y despu&#233;s solo se editan.</p></li><li><p><strong>Colecciones:</strong> documentos que se repiten (miembros del equipo, entidades, publicaciones del blog).</p></li></ul><pre><code><code>// Singleton: se le asigna un _id fijo para que no se pueda duplicar
defineType({
  name: 'heroSection',
  type: 'document',
  // ...
})

// En sanity.config.ts: se impide que los editores creen o borren singletons
document: {
  actions: (input, context) =&gt;
    singletonTypes.has(context.schemaType)
      ? input.filter(({ action }) =&gt;
          action &amp;&amp; ['publish', 'discardChanges', 'restore'].includes(action))
      : input,
}</code></code></pre><h3><strong>Internacionalizaci&#243;n a nivel de campo</strong></h3><p>En vez de crear un documento separado por cada idioma, se crean campos que contienen ambos idiomas dentro del mismo documento:</p><pre><code><code>// localeString: un campo corto con versi&#243;n en cada idioma
defineType({
  name: 'localeString',
  type: 'object',
  fields: [
    { name: 'es', type: 'string', title: 'Espa&#241;ol' },
    { name: 'en', type: 'string', title: 'English' },
  ],
})</code></code></pre><p>Hay tres variantes seg&#250;n el tipo de texto:</p><ul><li><p><strong>localeString</strong> para textos cortos (t&#237;tulos, etiquetas)</p></li><li><p><strong>localeText</strong> para textos largos (p&#225;rrafos, descripciones)</p></li><li><p><strong>localeBlockContent</strong> para texto enriquecido (con negritas, cursivas, enlaces)</p></li></ul><p><strong>Ventaja:</strong> el editor ve los dos idiomas en el mismo formulario, sin tener que cambiar de documento. Esto reduce errores y hace mucho m&#225;s f&#225;cil mantener ambos idiomas sincronizados.</p><h2><strong>4. Patr&#243;n de respaldo (fallback)</strong></h2><p>Nunca dependas al 100% de una fuente de datos externa. Si Sanity no est&#225; disponible o el contenido todav&#237;a no se ha cargado, la web tiene que seguir mostrando algo.</p><pre><code><code>// En el componente Astro
let hero, about, settings;
try {
  const data = await getHomePageData();
  hero = data.hero;
  about = data.about;
  settings = data.settings;
} catch {
  // Sanity no est&#225; disponible, usamos los textos de respaldo
}

// Funci&#243;n auxiliar que decide de d&#243;nde sacar el texto
function s(
  sanityValue: { es?: string; en?: string } | null,
  fallbackKey: string,
  lang: string
): string {
  if (sanityValue) {
    return sanityValue[lang] ?? sanityValue.es ?? t(fallbackKey, lang);
  }
  return t(fallbackKey, lang);  // textos de respaldo est&#225;ticos
}</code></code></pre><p><strong>Regla:</strong> mant&#233;n un archivo de textos est&#225;ticos (<code>translations.ts</code>) como red de seguridad. Sanity es la fuente principal de datos; los textos est&#225;ticos son el plan B.</p><h2><strong>5. Consultas GROQ</strong></h2><h3><strong>Pide solo lo que necesitas</strong></h3><p>Siempre especifica qu&#233; campos quieres en lugar de traerte el documento entero:</p><pre><code><code>// &#10060; Malo: trae todo, incluidos campos internos de Sanity que no usas
*[_type == "teamMember"]

// &#9989; Bueno: solo los campos que vas a usar
*[_type == "teamMember"] | order(order asc){
  _id,
  name,
  role,
  institution,
  "photoUrl": photo.asset-&gt;url,
  initials,
  order
}</code></code></pre><h3><strong>Resuelve las referencias de im&#225;genes en la propia consulta</strong></h3><p>Sanity guarda las im&#225;genes como referencias internas. Puedes convertirlas en URLs directamente dentro de la consulta:</p><pre><code><code>"logoUrl": logo.asset-&gt;url
"photoUrl": photo.asset-&gt;url</code></code></pre><h3><strong>Carga en paralelo</strong></h3><p>Usa <code>Promise.all</code> para pedir todos los datos a la vez, en lugar de uno detr&#225;s de otro:</p><pre><code><code>const [hero, about, team, entities] = await Promise.all([
  getHeroSection(),
  getAboutSection(),
  getTeamMembers(),
  getEntities(),
]);</code></code></pre><h2><strong>6. Reconstrucci&#243;n autom&#225;tica: Vercel + Sanity Webhooks</strong></h2><h3><strong>Configuraci&#243;n</strong></h3><ol><li><p><strong>En Vercel:</strong> crear un Deploy Hook en Settings &#8594; Git &#8594; Deploy Hooks. Esto genera una URL del tipo <code>https://api.vercel.com/v1/integrations/deploy/...</code></p></li><li><p><strong>En Sanity:</strong> crear un Webhook en manage.sanity.io &#8594; API &#8594; Webhooks</p><ul><li><p>URL: la del Deploy Hook que acabas de crear en Vercel</p></li><li><p>Disparadores: Crear, Actualizar, Eliminar</p></li><li><p>Filtro: vac&#237;o (cualquier cambio dispara la reconstrucci&#243;n)</p></li></ul></li></ol><h3><strong>Flujo completo</strong></h3><pre><code><code>El editor publica algo en Sanity
  &#8594; Sanity env&#237;a una notificaci&#243;n (webhook) a Vercel
    &#8594; Vercel descarga el c&#243;digo y ejecuta la construcci&#243;n del sitio
      &#8594; Astro pide los datos a Sanity (con useCdn: false)
        &#8594; Se genera el HTML con los datos actualizados
          &#8594; El sitio est&#225; online (~30-40 segundos en total)</code></code></pre><h3><strong>Problemas habituales</strong></h3><ol><li><p><strong>La cach&#233; te da datos viejos</strong> (ya explicado arriba): usar <code>useCdn: false</code>.</p></li><li><p><strong>Condici&#243;n de carrera en el webhook:</strong> el webhook se dispara inmediatamente al publicar. Si Sanity tarda unos milisegundos en propagar los datos internamente, la construcci&#243;n puede capturar datos a medio actualizar. En la pr&#225;ctica es raro, pero si ocurre, una segunda reconstrucci&#243;n lo soluciona.</p></li><li><p><strong>Actualizar el panel de edici&#243;n no es lo mismo que actualizar la web:</strong></p><ul><li><p><code>npm run sanity:deploy</code> actualiza el Sanity Studio (la herramienta de edici&#243;n). Solo hace falta cuando cambias la estructura de los datos (esquemas).</p></li><li><p>El webhook de Vercel reconstruye la web p&#250;blica. Se dispara cuando cambia el contenido.</p></li></ul></li></ol><h2><strong>7. Sanity Studio: buenas pr&#225;cticas</strong></h2><h3><strong>Organizar el panel de navegaci&#243;n</strong></h3><p>Agrupa los documentos en el men&#250; lateral para que los editores encuentren todo f&#225;cil:</p><pre><code><code>structureTool({
  structure: (S) =&gt;
    S.list().title('Contenido').items([
      S.listItem().title('Inicio').child(
        S.list().title('Secciones de inicio').items([
          S.listItem().title('Hero').child(/* singleton */),
          S.listItem().title('Sobre el proyecto').child(/* singleton */),
          S.listItem().title('Contacto').child(/* singleton */),
        ])
      ),
      S.divider(),
      S.listItem().title('Equipo').child(/* lista filtrada */),
      S.listItem().title('Entidades').child(/* lista filtrada */),
      // ...
    ]),
})</code></code></pre><h3><strong>Proteger los singletons</strong></h3><p>Impide que los editores puedan crear duplicados o borrar documentos que deber&#237;an ser &#250;nicos:</p><pre><code><code>document: {
  actions: (input, context) =&gt;
    singletonTypes.has(context.schemaType)
      ? input.filter(({ action }) =&gt;
          action &amp;&amp; ['publish', 'discardChanges', 'restore'].includes(action))
      : input,
}</code></code></pre><h3><strong>Acceso y permisos</strong></h3><ul><li><p>Para iniciar sesi&#243;n en local: <code>npx sanity login</code> (no <code>sanity login</code> directamente, puede que no est&#233; en el PATH)</p></li><li><p>Los colaboradores acceden al Studio desde el navegador, a trav&#233;s de la URL donde lo hayas desplegado</p></li><li><p>Los permisos de cada usuario se gestionan en manage.sanity.io &#8594; Members</p></li></ul><h2><strong>8. Im&#225;genes</strong></h2><h3><strong>Proporciones y recorte</strong></h3><p>Define las proporciones en CSS y deja que el navegador se encargue del recorte:</p><pre><code><code>.team-photo {
  aspect-ratio: 3/4;    /* Proporci&#243;n vertical tipo retrato */
  object-fit: cover;     /* Recorta sin deformar la imagen */
  width: 100%;
}</code></code></pre><p><strong>Indicaci&#243;n para editores:</strong> subir fotos de al menos 600&#215;800px para retratos. Si son m&#225;s grandes no pasa nada, el CSS se encarga de ajustarlas.</p><h3><strong>Transformaci&#243;n de im&#225;genes con URL builder</strong></h3><p>Sanity permite redimensionar y recortar im&#225;genes autom&#225;ticamente a trav&#233;s de URLs:</p><pre><code><code>import { createImageUrlBuilder } from '@sanity/image-url';

const builder = createImageUrlBuilder(sanityClient);

export function urlFor(source) {
  return builder.image(source);
}

// Ejemplo: urlFor(photo).width(400).height(533).url()</code></code></pre><p><strong>Nota:</strong> usa <code>createImageUrlBuilder</code> (importaci&#243;n con nombre), no la importaci&#243;n por defecto que est&#225; marcada como obsoleta.</p><h2><strong>9. Carga masiva de contenido</strong></h2><p>Para no tener que introducir todo el contenido inicial a mano en el Studio, crea un script de carga:</p><pre><code><code>import { createClient } from '@sanity/client';

const client = createClient({
  projectId: 'tu-project-id',
  dataset: 'production',
  token: process.env.SANITY_TOKEN, // token con permisos de escritura
  apiVersion: '2024-01-01',
  useCdn: false,
});

// createOrReplace: se puede ejecutar muchas veces sin crear duplicados
await client.createOrReplace({
  _id: 'heroSection',
  _type: 'heroSection',
  tag: { es: 'Proyecto de investigaci&#243;n', en: 'Research project' },
  // ...
});</code></code></pre><p><strong>Importante:</strong> usa <code>createOrReplace</code> en vez de <code>create</code>. As&#237; el script se puede ejecutar varias veces sin duplicar contenido (es idempotente).</p><p><strong>Seguridad:</strong> nunca subas tokens de API al repositorio. Gu&#225;rdalos como variables de entorno y rev&#243;calos cuando ya no los necesites.</p><h2><strong>10. Checklist de integraci&#243;n</strong></h2><ul><li><p><code>useCdn: false</code> en el cliente Sanity para builds est&#225;ticos</p></li><li><p>Textos de respaldo si Sanity no responde</p></li><li><p>Deploy Hook de Vercel configurado</p></li><li><p>Webhook de Sanity apuntando al Deploy Hook</p></li><li><p>Singletons protegidos para que no se puedan duplicar ni borrar</p></li><li><p>Campos biling&#252;es (ES/EN) en todos los textos</p></li><li><p>Consultas GROQ pidiendo solo los campos necesarios</p></li><li><p>Carga de datos en paralelo con <code>Promise.all</code></p></li><li><p>Script de carga inicial para el contenido</p></li><li><p>README con instrucciones de setup, comandos y flujo de despliegue</p></li><li><p>Studio desplegado para que los colaboradores puedan acceder (<code>npm run sanity:deploy</code>)</p></li><li><p>Proporciones de imagen definidas en CSS con <code>aspect-ratio</code> + <code>object-fit: cover</code></p></li></ul><h2><strong>Resumen</strong></h2><p>La combinaci&#243;n Astro + Sanity + Vercel funciona muy bien para sitios donde el contenido lo gestiona un equipo no t&#233;cnico:</p><ol><li><p><strong>Sanity</strong> da a los editores una interfaz c&#243;moda para gestionar contenido.</p></li><li><p><strong>Astro</strong> genera HTML r&#225;pido y barato.</p></li><li><p><strong>Vercel</strong> despliega autom&#225;ticamente con webhooks.</p></li></ol><p>Los puntos cr&#237;ticos son:</p><ul><li><p><code>useCdn: false</code> para que los builds siempre obtengan datos actualizados.</p></li><li><p><strong>Textos de respaldo</strong> para que la web nunca se quede en blanco.</p></li><li><p><strong>Entender que actualizar el Studio y actualizar la web</strong> son procesos distintos con disparadores distintos.</p></li></ul><p>Con estas pr&#225;cticas, el flujo es: el editor cambia contenido en Sanity &#8594; la web se actualiza sola en ~30 segundos &#8594; sin intervenci&#243;n t&#233;cnica.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.concriterio.blog/subscribe?&quot;,&quot;text&quot;:&quot;Suscribirse&quot;,&quot;language&quot;:&quot;es&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">&#161;Gracias por leer Con Criterio! Suscr&#237;bete gratis para recibir nuevos posts y apoyar mi trabajo.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Escribe tu correo electr&#243;nico..." tabindex="-1"><input type="submit" class="button primary" value="Suscribirse"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>