[{"data":1,"prerenderedAt":8316},["ShallowReactive",2],{"blog-post-es-\u002Fblog\u002Fintellisense-de-tailwindcss-en-nuxt-3":3,"blog-list-es":195},{"id":4,"title":5,"body":6,"category":178,"date":179,"description":180,"draft":181,"extension":182,"meta":183,"navigation":184,"path":185,"seo":186,"stem":187,"tags":188,"translationKey":192,"videoId":193,"__hash__":194},"blog_es\u002Fblog\u002Fintellisense-de-tailwindcss-en-nuxt-3.md","Cómo arreglar el Intellisense de TailwindCSS en un proyecto Nuxt 3",{"type":7,"value":8,"toc":173},"minimark",[9,14,26,30,45,49,55,79,166,169],[10,11,13],"h2",{"id":12},"el-problema","El problema",[15,16,17,18,25],"p",{},"Tenía un proyecto con Nuxt 3 y TailwindCSS abierto en VS Code, y el Intellisense de Tailwind no funcionaba bien. Lo primero que probé fue reinstalar la extensión, sin resultado. Después de buscar un rato encontré ",[19,20,24],"a",{"href":21,"rel":22},"https:\u002F\u002Fgithub.com\u002Ftailwindlabs\u002Ftailwindcss-intellisense\u002Fissues\u002F663#issuecomment-1316788128",[23],"nofollow","esta solución",", que es la que comparto hoy.",[10,27,29],{"id":28},"por-qué-no-funciona","Por qué no funciona",[15,31,32,33,37,38,41,42,44],{},"En un proyecto Nuxt existe el directorio ",[34,35,36],"code",{},".nuxt",". Nuxt lo usa durante el desarrollo para generar tu aplicación Vue. Si miras dentro, verás un archivo llamado ",[34,39,40],{},".nuxt\u002Ftailwind.config.cjs",". Es decir, Tailwind encuentra dos archivos de configuración en el mismo proyecto: uno en la raíz y otro dentro de ",[34,43,36],{},".",[10,46,48],{"id":47},"la-solución","La solución",[15,50,51,52,54],{},"Puedes decirle a la extensión que ignore el directorio ",[34,53,36],{},". Para excluirlo de tu espacio de trabajo:",[56,57,58,66,76],"ul",{},[59,60,61,62,65],"li",{},"Crea una carpeta ",[34,63,64],{},"\u002F.vscode"," en la raíz del proyecto.",[59,67,68,69,72,73,44],{},"Dentro de ",[34,70,71],{},".vscode",", añade un archivo ",[34,74,75],{},"settings.json",[59,77,78],{},"Copia este código en ese archivo.",[80,81,86],"pre",{"className":82,"code":83,"language":84,"meta":85,"style":85},"language-json shiki shiki-themes github-dark-default","\u002F\u002F \u002F.vscode\u002Fsettings.json\n{\n  \"tailwindCSS.files.exclude\": [\n    \"**\u002F.git\u002F**\",\n    \"**\u002Fnode_modules\u002F**\",\n    \"**\u002F.hg\u002F**\",\n    \"**\u002F.svn\u002F**\",\n    \"**\u002F.nuxt\u002F**\"\n  ]\n}\n","json","",[34,87,88,97,104,114,124,132,140,148,154,160],{"__ignoreMap":85},[89,90,93],"span",{"class":91,"line":92},"line",1,[89,94,96],{"class":95},"sH3jZ","\u002F\u002F \u002F.vscode\u002Fsettings.json\n",[89,98,100],{"class":91,"line":99},2,[89,101,103],{"class":102},"sZEs4","{\n",[89,105,107,111],{"class":91,"line":106},3,[89,108,110],{"class":109},"sPWt5","  \"tailwindCSS.files.exclude\"",[89,112,113],{"class":102},": [\n",[89,115,117,121],{"class":91,"line":116},4,[89,118,120],{"class":119},"s9uIt","    \"**\u002F.git\u002F**\"",[89,122,123],{"class":102},",\n",[89,125,127,130],{"class":91,"line":126},5,[89,128,129],{"class":119},"    \"**\u002Fnode_modules\u002F**\"",[89,131,123],{"class":102},[89,133,135,138],{"class":91,"line":134},6,[89,136,137],{"class":119},"    \"**\u002F.hg\u002F**\"",[89,139,123],{"class":102},[89,141,143,146],{"class":91,"line":142},7,[89,144,145],{"class":119},"    \"**\u002F.svn\u002F**\"",[89,147,123],{"class":102},[89,149,151],{"class":91,"line":150},8,[89,152,153],{"class":119},"    \"**\u002F.nuxt\u002F**\"\n",[89,155,157],{"class":91,"line":156},9,[89,158,159],{"class":102},"  ]\n",[89,161,163],{"class":91,"line":162},10,[89,164,165],{"class":102},"}\n",[15,167,168],{},"A partir de ahí el Intellisense de Tailwind debería volver a funcionar.",[170,171,172],"style",{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":85,"searchDepth":99,"depth":99,"links":174},[175,176,177],{"id":12,"depth":99,"text":13},{"id":28,"depth":99,"text":29},{"id":47,"depth":99,"text":48},"FRONTEND","2024-11-12","El Intellisense de TailwindCSS deja de funcionar en proyectos Nuxt 3. Aquí está la causa y una solución sencilla.",false,"md",{},true,"\u002Fblog\u002Fintellisense-de-tailwindcss-en-nuxt-3",{"title":5,"description":180},"blog\u002Fintellisense-de-tailwindcss-en-nuxt-3",[189,190,191],"nuxt","tailwindcss","vscode","fix-tailwindcss-intellisense-in-nuxt-3",null,"OlLEZeg5B704tLAfHJkJkmoRlGi2anUsXufv4DOQ8cg",[196,307,422,503,942,1078,1182,1351,1528,1634,1748,3287,4978],{"id":197,"title":198,"body":199,"category":293,"date":294,"description":295,"draft":181,"extension":182,"meta":296,"navigation":184,"path":297,"seo":298,"stem":299,"tags":300,"translationKey":305,"videoId":193,"__hash__":306},"blog_es\u002Fblog\u002Faprendizaje-asistido-por-ia.md","El auge del aprendizaje asistido por IA y las herramientas que cambian cómo estudiamos",{"type":7,"value":200,"toc":282},[201,205,208,211,215,218,226,231,234,238,245,249,252,256,259,263,266,269,272,276,279],[10,202,204],{"id":203},"te-acuerdas-de-cómo-estudiábamos","¿Te acuerdas de cómo estudiábamos?",[15,206,207],{},"Todos hemos pasado por ahí: mirando fijamente un libro de texto, subrayador en mano, intentando meter información en la cabeza antes de un examen. O tratando de aprender algo nuevo y perdiéndote entre tutoriales interminables. Solía ser un camino solitario, con esa pregunta de fondo: \"¿lo estaré haciendo bien?\".",[15,209,210],{},"Ahora hay una forma de aprender mucho más personalizada.",[10,212,214],{"id":213},"qué-significa-la-ia-aplicada-al-aprendizaje","Qué significa la IA aplicada al aprendizaje",[15,216,217],{},"Es normal preguntarse cómo encaja la IA en una rutina de estudio. Durante años pareció cosa de películas de ciencia ficción, pero ya forma parte del día a día, también de cómo aprendemos.",[15,219,220,221,225],{},"El aprendizaje asistido por IA consiste en adaptar la experiencia educativa a ",[222,223,224],"em",{},"ti",". Piensa en un tutor disponible a cualquier hora que conoce tus puntos fuertes, tus lagunas y tu manera de aprender, y arma un recorrido a tu medida. Nada de clases iguales para todos ni ejercicios genéricos.",[227,228,230],"h3",{"id":229},"rutas-de-aprendizaje-personalizadas","Rutas de aprendizaje personalizadas",[15,232,233],{},"Imagina que estás aprendiendo un lenguaje de programación nuevo. En lugar de seguir un temario rígido, la herramienta evalúa lo que ya sabes, detecta dónde necesitas más práctica y te propone recursos y ejercicios acordes. Si te cuestan los bucles, te dará más problemas de bucles. Si te manejas bien con las variables, pasarás al siguiente reto.",[227,235,237],{"id":236},"corrección-inmediata-y-práctica-que-se-adapta","Corrección inmediata y práctica que se adapta",[15,239,240,241,244],{},"Acuérdate de esperar días a que te devolvieran los deberes corregidos. Estas herramientas responden al instante, explican ",[222,242,243],{},"por qué"," algo está mal y te guían hacia la solución correcta. Esa corrección inmediata asienta los conceptos mucho más rápido. Además, la práctica se ajusta: si repites el mismo error, te presentará problemas parecidos de formas distintas hasta que encaje.",[227,246,248],{"id":247},"selección-y-recomendación-de-contenido","Selección y recomendación de contenido",[15,250,251],{},"Internet es un océano de información y encontrar material fiable, pertinente y comprensible puede ser un trabajo en sí mismo. La IA hace de bibliotecaria personal: filtra artículos, vídeos y cursos y te recomienda lo que necesitas en cada momento. Aprende tus preferencias y va afinando sus sugerencias.",[227,253,255],{"id":254},"repetición-espaciada-y-retención","Repetición espaciada y retención",[15,257,258],{},"Una de las mayores dificultades del aprendizaje es recordar lo aprendido. Muchas de estas herramientas incorporan algoritmos de repetición espaciada. El término suena técnico, pero significa algo sencillo: la IA te recuerda repasar un concepto justo antes de que se te olvide. Un sistema de recordatorios para tu memoria, para que lo aprendido dure.",[10,260,262],{"id":261},"un-ejemplo-skillmate-ai","Un ejemplo: SkillMate AI",[15,264,265],{},"SkillMate AI, una de las herramientas que he usado, encaja justo en esa descripción: un entrenador de aprendizaje que te acompaña durante el proceso.",[15,267,268],{},"En lugar de volcarte información encima, interactúa contigo, entiende cómo aprendes y orienta en consecuencia. Te ayuda a practicar conceptos, explica los temas difíciles en términos más simples y genera cuestionarios a tu medida para comprobar si lo has entendido.",[15,270,271],{},"Si te atascas con un concepto, puedes preguntarle y te lo explicará de otra manera, quizá con una analogía que te resulte cercana. Si necesitas más práctica en una habilidad concreta, genera ejercicios centrados en esa área. Y si quieres repasar antes de un examen, te ayuda a recuperar lo importante y te hace preguntas sobre ello.",[10,273,275],{"id":274},"hacia-dónde-va-esto","Hacia dónde va esto",[15,277,278],{},"La entrada de la IA en el aprendizaje no es una moda pasajera, sino un cambio real en la manera de adquirir conocimientos y habilidades. Permite a cada persona llevar las riendas de su formación, avanzar a su ritmo y recibir el apoyo concreto que necesita.",[15,280,281],{},"Seas estudiante preparando exámenes, profesional ampliando competencias o simplemente alguien con curiosidad, la próxima vez que abras un libro puedes probar a apoyarte en una herramienta de este tipo.",{"title":85,"searchDepth":99,"depth":99,"links":283},[284,285,291,292],{"id":203,"depth":99,"text":204},{"id":213,"depth":99,"text":214,"children":286},[287,288,289,290],{"id":229,"depth":106,"text":230},{"id":236,"depth":106,"text":237},{"id":247,"depth":106,"text":248},{"id":254,"depth":106,"text":255},{"id":261,"depth":99,"text":262},{"id":274,"depth":99,"text":275},"CAREER","2025-09-16","Cómo los tutores personalizados con IA están cambiando la educación, el desarrollo de habilidades y la retención de lo aprendido.",{},"\u002Fblog\u002Faprendizaje-asistido-por-ia",{"title":198,"description":295},"blog\u002Faprendizaje-asistido-por-ia",[301,302,303,304],"AI","Education","Learning","SkillMate AI","ai-powered-learning","8_4LzH3asOiqJ2gfvDO_R42NUOC1LwtJ1LzHvqkTQAw",{"id":308,"title":309,"body":310,"category":409,"date":410,"description":411,"draft":181,"extension":182,"meta":412,"navigation":184,"path":413,"seo":414,"stem":415,"tags":416,"translationKey":420,"videoId":193,"__hash__":421},"blog_es\u002Fblog\u002Fcomo-la-ia-esta-cambiando-la-correccion-de-errores.md","Cómo la IA está cambiando la corrección de errores",{"type":7,"value":311,"toc":399},[312,316,323,326,329,333,336,339,343,346,353,357,360,364,379,382,386,389,393,396],[10,313,315],{"id":314},"la-batalla-que-nunca-termina","La batalla que nunca termina",[15,317,318,319,322],{},"Cualquiera que haya escrito algo de código conoce la secuencia: escribes unas líneas que se ven bien, ejecutas el programa y aparece un error. O peor, el programa funciona pero no hace lo que ",[222,320,321],{},"creías"," haberle pedido.",[15,324,325],{},"Encontrar y arreglar esos bichos digitales ha sido siempre una de las partes que más tiempo consume del desarrollo, y de las más frustrantes. Es un trabajo detectivesco en el que las pistas son mensajes de error crípticos y los sospechosos, millones de líneas de código.",[15,327,328],{},"Hay un ayudante nuevo que facilita bastante esa batalla: la inteligencia artificial.",[10,330,332],{"id":331},"cómo-entra-la-ia-en-la-depuración","Cómo entra la IA en la depuración",[15,334,335],{},"Puede que te preguntes qué puede hacer una IA con algo tan fino como un punto y coma mal puesto o un fallo de lógica. La IA no va solo de robots y coches autónomos; también está cambiando la manera en que escribimos, probamos y corregimos el código.",[15,337,338],{},"Piensa en ella como un asistente incansable capaz de recorrer cantidades enormes de código y datos mucho más rápido que una persona. Un revisor disponible a cualquier hora.",[227,340,342],{"id":341},"detección-anticipada-de-errores","Detección anticipada de errores",[15,344,345],{},"Imagina que tu editor te dijera que la línea que acabas de escribir puede dar problemas más adelante. Eso es, a grandes rasgos, la detección predictiva. Los modelos se entrenan con conjuntos enormes de código que incluyen millones de errores junto con sus correcciones, así que aprenden los patrones que acompañan a los fallos habituales.",[15,347,348,349,352],{},"Mientras escribes, el modelo analiza el código y puede señalar problemas ",[222,350,351],{},"antes"," de que ejecutes el programa. Se parece a un corrector ortográfico, pero para errores de lógica y cuellos de botella de rendimiento.",[227,354,356],{"id":355},"clasificación-automática-de-los-reportes","Clasificación automática de los reportes",[15,358,359],{},"En proyectos grandes, un reporte de error no consiste solo en arreglar código: hay que entender su impacto, asignarlo a la persona adecuada y decidir cuán crítico es. Aquí la IA puede leer los reportes, compararlos con casos anteriores, clasificarlos, proponer un nivel de gravedad e incluso sugerir qué miembro del equipo encaja mejor según el trabajo que ha hecho antes. Eso ahorra mucho tiempo y ayuda a que los fallos críticos se atiendan primero.",[227,361,363],{"id":362},"sugerencias-de-arreglo-y-a-veces-el-arreglo-entero","Sugerencias de arreglo, y a veces el arreglo entero",[15,365,366,367,370,371,374,375,378],{},"Aquí es donde la cosa se pone interesante. Algunas herramientas no se limitan a ",[222,368,369],{},"encontrar"," el error: proponen cómo corregirlo. Piensa en un aviso del tipo \"hay un error en esta línea, ¿querías usar ",[34,372,373],{},"==="," en lugar de ",[34,376,377],{},"=","?\".",[15,380,381],{},"Los modelos más avanzados generan directamente fragmentos de código que resuelven el fallo. Lo hacen entendiendo el contexto del error y apoyándose en patrones de correcciones que funcionaron en situaciones parecidas. No siempre aciertan y hace falta que una persona revise y apruebe, pero aceleran mucho la investigación inicial y la búsqueda de soluciones.",[227,383,385],{"id":384},"mejores-casos-de-prueba","Mejores casos de prueba",[15,387,388],{},"Muchos errores se cuelan porque las pruebas no cubren lo suficiente. La IA puede analizar tu código y tu suite de tests y proponer casos nuevos que expongan situaciones límite en las que no habías pensado. También puede generar datos o escenarios de prueba nuevos.",[10,390,392],{"id":391},"sustituye-esto-a-los-programadores","¿Sustituye esto a los programadores?",[15,394,395],{},"No. Diseñar y entender de verdad un sistema complejo sigue necesitando intuición, creatividad y criterio humanos. Lo que hace la IA es quedarse con la parte repetitiva y lenta de la depuración.",[15,397,398],{},"Eso deja más tiempo para diseñar funcionalidades nuevas y resolver problemas más grandes, en lugar de perseguir un punto y coma perdido. La próxima vez que estés cazando un fallo, notarás cuánto trabajo hacen ya estas herramientas de fondo.",{"title":85,"searchDepth":99,"depth":99,"links":400},[401,402,408],{"id":314,"depth":99,"text":315},{"id":331,"depth":99,"text":332,"children":403},[404,405,406,407],{"id":341,"depth":106,"text":342},{"id":355,"depth":106,"text":356},{"id":362,"depth":106,"text":363},{"id":384,"depth":106,"text":385},{"id":391,"depth":99,"text":392},"AI_LAB","2025-09-18","Las herramientas con IA detectan fallos antes, acortan el tiempo de depuración y mejoran la calidad del código.",{},"\u002Fblog\u002Fcomo-la-ia-esta-cambiando-la-correccion-de-errores",{"title":309,"description":411},"blog\u002Fcomo-la-ia-esta-cambiando-la-correccion-de-errores",[301,417,418,419],"bug fixing","software development","code quality","how-ai-is-transforming-bug-fixing","WFnlWOn5fkkNMFOWoy-6E3liRdon2uTEh8XskZPqZxU",{"id":423,"title":424,"body":425,"category":490,"date":491,"description":492,"draft":181,"extension":182,"meta":493,"navigation":184,"path":494,"seo":495,"stem":496,"tags":497,"translationKey":501,"videoId":193,"__hash__":502},"blog_es\u002Fblog\u002Fconectar-dominio-namecheap-con-vercel.md","Cómo conectar tu dominio de Namecheap con una app desplegada en Vercel",{"type":7,"value":426,"toc":484},[427,430,434,437,441,444,447,451,454,461,468,474,478,481],[15,428,429],{},"Si has comprado un dominio en Namecheap y quieres conectarlo con tu aplicación en Vercel, hay unos cuantos pasos que seguir.",[10,431,433],{"id":432},"_1-añade-el-dominio-en-vercel","1. Añade el dominio en Vercel",[15,435,436],{},"Entra en tu cuenta de Vercel y abre el panel de la aplicación. Ve a \"Settings\" y luego a \"Domains\". Pulsa \"Add Domain\", escribe tu dominio y confirma con \"Add\".",[10,438,440],{"id":439},"_2-copia-los-registros-dns-de-vercel","2. Copia los registros DNS de Vercel",[15,442,443],{},"Una vez añadido el dominio, necesitas los registros DNS que Vercel te indica. Vuelve a la sección \"Domains\", pulsa sobre el dominio que acabas de añadir y entra en \"DNS Records\".",[15,445,446],{},"Verás la lista de registros que tendrás que crear en Namecheap: un registro A, uno CNAME y uno TXT.",[10,448,450],{"id":449},"_3-crea-los-registros-en-namecheap","3. Crea los registros en Namecheap",[15,452,453],{},"Entra en tu cuenta de Namecheap y abre el panel del dominio. Ve a \"Advanced DNS\" y pulsa \"Add New Record\".",[15,455,456,457,460],{},"Empieza por el registro A. En el desplegable \"Type\" elige \"A (Address)\". En el campo \"Host\" escribe ",[34,458,459],{},"@",", sin comillas. En \"Value\" pon la dirección IP que te da Vercel.",[15,462,463,464,467],{},"Sigue con el CNAME. En \"Type\" elige \"CNAME (Alias)\", en \"Host\" escribe ",[34,465,466],{},"www"," y en \"Value\" pon el valor que aparece en Vercel.",[15,469,470,471,473],{},"Termina con el TXT. En \"Type\" elige \"TXT (Text)\", en \"Host\" escribe otra vez ",[34,472,459],{}," y en \"Value\" pon el valor de Vercel.",[10,475,477],{"id":476},"_4-verifica-los-registros","4. Verifica los registros",[15,479,480],{},"Cuando los tengas todos creados, comprueba que son correctos. Vuelve al panel de tu aplicación en Vercel, pulsa sobre el dominio y luego en \"Verify DNS Configuration\". Vercel revisará si la configuración es la esperada.",[15,482,483],{},"La propagación de los registros DNS puede tardar, así que ten paciencia. Cuando termine, Vercel los verificará y tu dominio quedará conectado con la aplicación. Si algo se atasca, escribe al soporte de Vercel.",{"title":85,"searchDepth":99,"depth":99,"links":485},[486,487,488,489],{"id":432,"depth":99,"text":433},{"id":439,"depth":99,"text":440},{"id":449,"depth":99,"text":450},{"id":476,"depth":99,"text":477},"PRODUCT_ENG","2024-09-13","Los pasos para apuntar un dominio comprado en Namecheap hacia tu aplicación desplegada en Vercel.",{},"\u002Fblog\u002Fconectar-dominio-namecheap-con-vercel",{"title":424,"description":492},"blog\u002Fconectar-dominio-namecheap-con-vercel",[498,499,500],"namecheap","vercel","deployment","connect-namecheap-domain-to-vercel","tBipFB6K4cVZLOzliTN-Fm1S04cnlVAF7rk0YpkFyEk",{"id":504,"title":505,"body":506,"category":409,"date":294,"description":931,"draft":181,"extension":182,"meta":932,"navigation":184,"path":933,"seo":934,"stem":935,"tags":936,"translationKey":940,"videoId":193,"__hash__":941},"blog_es\u002Fblog\u002Fel-futuro-de-los-asistentes-de-codigo-con-ia.md","El futuro de los asistentes de código con IA y GPT-5",{"type":7,"value":507,"toc":917},[508,512,515,518,522,525,528,532,536,539,558,811,814,818,821,824,828,831,834,838,845,852,856,862,869,873,880,883,886,889,893,896,907,911,914],[10,509,511],{"id":510},"cómo-ha-cambiado-escribir-código","Cómo ha cambiado escribir código",[15,513,514],{},"¿Te acuerdas de cuando escribíamos cada línea a mano, buscábamos puntos y coma que desaparecían sin explicación y pasábamos horas detrás de un solo error?",[15,516,517],{},"El desarrollo de software siempre ha consistido en resolver problemas, pero las herramientas con las que los resolvemos cambian sin parar. De las tarjetas perforadas a los IDE, y de ahí a los asistentes de código con IA. Si estás pensando \"¿otra herramienta más?\", la respuesta es que sí merece la pena cuando quieres escribir código más rápido, más limpio y más fácil de escalar.",[10,519,521],{"id":520},"qué-aporta-gpt-5","Qué aporta GPT-5",[15,523,524],{},"Hemos visto crecer la IA en muchos campos y ahora está influyendo de forma clara en cómo escribimos software. Herramientas como GitHub Copilot, construidas sobre modelos de la familia GPT de OpenAI, ya empezaron a cambiar las reglas. ¿Y qué pasa cuando hablamos de algo más avanzado, como GPT-5?",[15,526,527],{},"Imagina un asistente que ha leído casi todo el código escrito, que conoce a fondo distintos lenguajes de programación y que puede anticipar lo que estás intentando hacer. Esa es la promesa de estos modelos. Ya no se trata solo de autocompletar: hay sugerencias con criterio, detección de errores e incluso funciones o módulos enteros generados a partir de una instrucción breve.",[10,529,531],{"id":530},"dónde-acelera-tu-trabajo","Dónde acelera tu trabajo",[227,533,535],{"id":534},"generación-de-código","Generación de código",[15,537,538],{},"Es la ventaja más evidente. En lugar de escribir a mano el código repetitivo o los patrones habituales, describes lo que quieres conseguir y el modelo lo genera.",[15,540,541,542,545,546,549,550,553,554,557],{},"Supón que necesitas una función que consulte una API, gestione el estado de carga y muestre los errores. Lo que tienes en la cabeza es algo así: \"necesito una función ",[34,543,544],{},"fetchData"," con ",[34,547,548],{},"async\u002Fawait"," y manejar los estados ",[34,551,552],{},"isLoading"," y ",[34,555,556],{},"error","\". A partir del contexto del proyecto o de una instrucción corta, el modelo puede producir este esqueleto:",[80,559,563],{"className":560,"code":561,"language":562,"meta":85,"style":85},"language-javascript shiki shiki-themes github-dark-default","async function fetchData(url) {\n  const isLoading = ref(true);\n  const error = ref(null);\n  const data = ref(null);\n\n  try {\n    const response = await fetch(url);\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    data.value = await response.json();\n  } catch (e) {\n    error.value = e.message;\n  } finally {\n    isLoading.value = false;\n  }\n  return { data, isLoading, error };\n}\n","javascript",[34,564,565,588,611,629,646,651,659,678,692,721,726,744,756,767,777,791,797,806],{"__ignoreMap":85},[89,566,567,571,574,578,581,585],{"class":91,"line":92},[89,568,570],{"class":569},"suJrU","async",[89,572,573],{"class":569}," function",[89,575,577],{"class":576},"sc3cj"," fetchData",[89,579,580],{"class":102},"(",[89,582,584],{"class":583},"sQhOw","url",[89,586,587],{"class":102},") {\n",[89,589,590,593,597,600,603,605,608],{"class":91,"line":99},[89,591,592],{"class":569},"  const",[89,594,596],{"class":595},"sFSAA"," isLoading",[89,598,599],{"class":569}," =",[89,601,602],{"class":576}," ref",[89,604,580],{"class":102},[89,606,607],{"class":595},"true",[89,609,610],{"class":102},");\n",[89,612,613,615,618,620,622,624,627],{"class":91,"line":106},[89,614,592],{"class":569},[89,616,617],{"class":595}," error",[89,619,599],{"class":569},[89,621,602],{"class":576},[89,623,580],{"class":102},[89,625,626],{"class":595},"null",[89,628,610],{"class":102},[89,630,631,633,636,638,640,642,644],{"class":91,"line":116},[89,632,592],{"class":569},[89,634,635],{"class":595}," data",[89,637,599],{"class":569},[89,639,602],{"class":576},[89,641,580],{"class":102},[89,643,626],{"class":595},[89,645,610],{"class":102},[89,647,648],{"class":91,"line":126},[89,649,650],{"emptyLinePlaceholder":184},"\n",[89,652,653,656],{"class":91,"line":134},[89,654,655],{"class":569},"  try",[89,657,658],{"class":102}," {\n",[89,660,661,664,667,669,672,675],{"class":91,"line":142},[89,662,663],{"class":569},"    const",[89,665,666],{"class":595}," response",[89,668,599],{"class":569},[89,670,671],{"class":569}," await",[89,673,674],{"class":576}," fetch",[89,676,677],{"class":102},"(url);\n",[89,679,680,683,686,689],{"class":91,"line":150},[89,681,682],{"class":569},"    if",[89,684,685],{"class":102}," (",[89,687,688],{"class":569},"!",[89,690,691],{"class":102},"response.ok) {\n",[89,693,694,697,700,703,705,708,711,713,716,719],{"class":91,"line":156},[89,695,696],{"class":569},"      throw",[89,698,699],{"class":569}," new",[89,701,702],{"class":576}," Error",[89,704,580],{"class":102},[89,706,707],{"class":119},"`HTTP error! status: ${",[89,709,710],{"class":102},"response",[89,712,44],{"class":119},[89,714,715],{"class":102},"status",[89,717,718],{"class":119},"}`",[89,720,610],{"class":102},[89,722,723],{"class":91,"line":162},[89,724,725],{"class":102},"    }\n",[89,727,729,732,734,736,739,741],{"class":91,"line":728},11,[89,730,731],{"class":102},"    data.value ",[89,733,377],{"class":569},[89,735,671],{"class":569},[89,737,738],{"class":102}," response.",[89,740,84],{"class":576},[89,742,743],{"class":102},"();\n",[89,745,747,750,753],{"class":91,"line":746},12,[89,748,749],{"class":102},"  } ",[89,751,752],{"class":569},"catch",[89,754,755],{"class":102}," (e) {\n",[89,757,759,762,764],{"class":91,"line":758},13,[89,760,761],{"class":102},"    error.value ",[89,763,377],{"class":569},[89,765,766],{"class":102}," e.message;\n",[89,768,770,772,775],{"class":91,"line":769},14,[89,771,749],{"class":102},[89,773,774],{"class":569},"finally",[89,776,658],{"class":102},[89,778,780,783,785,788],{"class":91,"line":779},15,[89,781,782],{"class":102},"    isLoading.value ",[89,784,377],{"class":569},[89,786,787],{"class":595}," false",[89,789,790],{"class":102},";\n",[89,792,794],{"class":91,"line":793},16,[89,795,796],{"class":102},"  }\n",[89,798,800,803],{"class":91,"line":799},17,[89,801,802],{"class":569},"  return",[89,804,805],{"class":102}," { data, isLoading, error };\n",[89,807,809],{"class":91,"line":808},18,[89,810,165],{"class":102},[15,812,813],{},"Ahí se ahorran los minutos, a veces horas, que se van en teclear estructuras repetidas.",[227,815,817],{"id":816},"entender-y-documentar-el-código","Entender y documentar el código",[15,819,820],{},"¿Te has quedado mirando un bloque escrito por otra persona (o por ti hace seis meses) preguntándote qué hace exactamente? El modelo puede explicar la lógica complicada y hasta generar la documentación.",[15,822,823],{},"Si tienes una expresión regular enrevesada o un algoritmo difícil, seleccionas el código y le pides que explique el regex o que describa el propósito del algoritmo. Traduce la lógica a frases legibles, lo que facilita que alguien nuevo se incorpore al equipo o que entiendas código heredado. También puede proponer documentación en formatos como JSDoc.",[227,825,827],{"id":826},"detectar-errores-antes-de-ejecutar","Detectar errores antes de ejecutar",[15,829,830],{},"Aquí es donde la IA va más allá del autocompletado. Puede revisar tu código en busca de errores, cuellos de botella de rendimiento y vulnerabilidades antes incluso de que lo ejecutes.",[15,832,833],{},"Si se te cuela un error de índice en un bucle o se te olvida sanear la entrada del usuario, puede marcarlo en el editor y proponer una corrección, o reescribir directamente el fragmento problemático. Piensa en un linter que entiende el contexto mucho mejor que las herramientas tradicionales.",[227,835,837],{"id":836},"sugerencias-de-refactorización","Sugerencias de refactorización",[15,839,840,841,844],{},"Una cosa es escribir código y otra escribir código ",[222,842,843],{},"bueno",". El modelo puede actuar como una persona con experiencia que revisa tu trabajo y propone mejoras de legibilidad, eficiencia y buenas prácticas.",[15,846,847,848,851],{},"Si has escrito una cadena larga de condicionales y existe una forma más ordenada, puede sugerirte un ",[34,849,850],{},"switch",", un objeto de búsqueda o un patrón de diseño que simplifique la lógica. También señala bucles ineficientes y estructuras de datos mejorables.",[227,853,855],{"id":854},"aprender-por-el-camino","Aprender por el camino",[15,857,858,859,861],{},"Quizá te preguntes si esto te convertirá en peor programador. Puedes usarlo justo al revés, como un mentor disponible a cualquier hora. Cuando el modelo propone una solución o un patrón que no conoces, puedes preguntarle ",[222,860,243],{}," ese enfoque es mejor.",[15,863,864,865,868],{},"Por ejemplo, si sugiere usar un ",[34,866,867],{},"Map"," en lugar de un objeto plano para cierta estructura de datos y le preguntas por qué, te explicará las diferencias de rendimiento, el orden de las claves y cómo trata claves de distintos tipos.",[10,870,872],{"id":871},"la-parte-humana-sigue-contando","La parte humana sigue contando",[15,874,875,876,879],{},"Antes de pensar que la IA va a sustituir a todo el mundo, conviene frenar un poco. GPT-5 es muy potente, pero es una ",[222,877,878],{},"herramienta","; no reemplaza la creatividad, la resolución de problemas ni el criterio.",[15,881,882],{},"No conoce el contexto como tú. Los modelos son buenos con los patrones, pero no entienden la lógica de negocio concreta de tu proyecto, su visión a largo plazo ni los requisitos con matices. La dirección general la sigues marcando tú, y su salida hay que revisarla con ojo crítico.",[15,884,885],{},"También está la parte ética: confiar solo en la IA puede arrastrar los sesgos presentes en sus datos de entrenamiento o producir soluciones mediocres para problemas realmente nuevos.",[15,887,888],{},"Y luego están los errores de la propia IA. A veces genera código que parece correcto pero esconde un fallo sutil. La responsabilidad de probar y garantizar la calidad del resultado sigue siendo tuya.",[10,890,892],{"id":891},"cómo-encaja-en-tu-flujo","Cómo encaja en tu flujo",[15,894,895],{},"Lo bueno de estas herramientas es que se integran en los editores que ya usas. Trabajes con VS Code, IntelliJ u otro, hay extensiones que ponen estas capacidades a tu alcance.",[15,897,898,899,902,903,906],{},"Imagina escribir un comentario del tipo ",[34,900,901],{},"\u002F\u002F Función para validar el email del usuario"," y ver aparecer la función justo debajo. O teclear ",[34,904,905],{},"const user = "," y recibir sugerencias de propiedades basadas en el esquema de tu proyecto. La idea es reducir fricción y mantenerte concentrado.",[10,908,910],{"id":909},"hacia-dónde-va","Hacia dónde va",[15,912,913],{},"El futuro de la programación es un trabajo compartido entre el ingenio humano y la inteligencia artificial. Estas herramientas no están para quitarte el trabajo, sino para quedarse con la parte repetitiva y dejarte la difícil y la nueva.",[170,915,916],{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":85,"searchDepth":99,"depth":99,"links":918},[919,920,921,928,929,930],{"id":510,"depth":99,"text":511},{"id":520,"depth":99,"text":521},{"id":530,"depth":99,"text":531,"children":922},[923,924,925,926,927],{"id":534,"depth":106,"text":535},{"id":816,"depth":106,"text":817},{"id":826,"depth":106,"text":827},{"id":836,"depth":106,"text":837},{"id":854,"depth":106,"text":855},{"id":871,"depth":99,"text":872},{"id":891,"depth":99,"text":892},{"id":909,"depth":99,"text":910},"Cómo los asistentes de código con IA como GPT-5 cambian el flujo de desarrollo, en qué ayudan de verdad y dónde sigue haciendo falta una persona.",{},"\u002Fblog\u002Fel-futuro-de-los-asistentes-de-codigo-con-ia",{"title":505,"description":931},"blog\u002Fel-futuro-de-los-asistentes-de-codigo-con-ia",[301,937,938,939],"GPT-5","coding","productivity","the-future-of-ai-coding-assistants","T_ncF68h8XtT47NpESL_re0pdrrmM-nardfdRvIMxbA",{"id":943,"title":944,"body":945,"category":293,"date":1065,"description":1066,"draft":181,"extension":182,"meta":1067,"navigation":184,"path":1068,"seo":1069,"stem":1070,"tags":1071,"translationKey":1076,"videoId":193,"__hash__":1077},"blog_es\u002Fblog\u002Festrategias-de-ingresos-pasivos-para-programadores.md","Cinco vías de ingresos pasivos para programadores",{"type":7,"value":946,"toc":1057},[947,950,953,960,963,967,970,977,983,989,992,996,999,1002,1005,1009,1012,1015,1018,1022,1025,1028,1031,1034,1038,1041,1044,1047,1051,1054],[15,948,949],{},"¿Has querido alguna vez que tu código siga trabajando mientras duermes o, sencillamente, mientras vives? Los ingresos pasivos, ese dinero que llega con poco esfuerzo continuo, resultan atractivos para quien se gana la vida construyendo cosas. Si nos pasamos el día construyendo para otros, bien podríamos construir algo que nos siga pagando.",[15,951,952],{},"Nuestras habilidades encajan con esto. Nada de pirámides ni de convertirse en magnate inmobiliario: el material es lo que ya hacemos, es decir, crear productos digitales, automatizar procesos y compartir lo que sabemos.",[15,954,955,956,959],{},"Si te suena a cuento, el escepticismo es razonable, sobre todo con un trabajo que ya se lleva la mayor parte de la semana. Esto no es esfuerzo ",[222,957,958],{},"cero",". Es esfuerzo concentrado al principio que devuelve con el tiempo, y cuando algo está montado, el mantenimiento suele ser mucho menor que la construcción.",[15,961,962],{},"Aquí van cinco vías que los programadores están usando de verdad, y cómo cada una se acerca a algo así como mil dólares al mes.",[10,964,966],{"id":965},"_1-productos-digitales-ebooks-cursos-y-plantillas","1. Productos digitales: ebooks, cursos y plantillas",[15,968,969],{},"Probablemente el camino más directo. Tienes conocimiento y habilidades prácticas que otros quieren, así que empaquétalos.",[15,971,972,976],{},[973,974,975],"strong",{},"Ebooks y guías."," Si sabes explicar cosas difíciles de forma sencilla, o dominas una tecnología concreta con la que otros pelean, escríbelo. Un recorrido a fondo por un framework, una guía sobre rendimiento, el conjunto de prácticas al que has llegado. Lo escribes una vez, le das formato y lo vendes en Gumroad, Leanpub o tu propia web. Alguien especializado en Vue podría escribir un manual sobre rendimiento en Nuxt.",[15,978,979,982],{},[973,980,981],{},"Cursos en línea."," Si prefieres enseñar en vídeo, plataformas como Udemy, Teachable o Skillshare hacen bastante llevadero publicar lecciones con ejercicios y cuestionarios. Grabas una vez y la plataforma se encarga de la venta y la entrega.",[15,984,985,988],{},[973,986,987],{},"Plantillas y bases de proyecto."," A los programadores les gusta empezar sobre algo sólido. Una biblioteca de componentes bien acabada, una base de backend robusta o una plantilla de sitio web se pueden vender en mercados como Creative Market o ThemeForest. Un kit inicial de SaaS con autenticación, base de datos y un panel básico es un ejemplo habitual.",[15,990,991],{},"Lo que decide si esto funciona: resolver un problema concreto o cubrir un hueco real de conocimiento. La documentación reempaquetada no se vende. Lo que se vende es la parte que solo puede escribir alguien con tu experiencia.",[10,993,995],{"id":994},"_2-un-saas-pequeño","2. Un SaaS pequeño",[15,997,998],{},"Esta vía es la más cercana al trabajo diario. Un SaaS es una herramienta alojada en línea por la que la gente paga. No el próximo Facebook: algo pequeño, específico, un solo problema.",[15,1000,1001],{},"Busca una molestia recurrente en un grupo concreto, ya sean programadores, gente de marketing o pequeños negocios, construye una aplicación web sencilla que la resuelva y cobra una suscripción mensual o anual. Un monitor de disponibilidad básico, una herramienta que convierta texto en imágenes para redes, un CRM ligero para autónomos. Una vez construido, el trabajo lo hace el software, y a ti te queda el mantenimiento, el soporte y alguna funcionalidad nueva de vez en cuando.",[15,1003,1004],{},"Empieza de verdad pequeño. El error más común es intentar construir algo grande y lleno de funciones. Elige un problema, resuélvelo especialmente bien, publica pronto y añade el resto si alguien lo pide.",[10,1006,1008],{"id":1007},"_3-vender-una-api","3. Vender una API",[15,1010,1011],{},"Las API son la forma en que un software habla con otro. Si puedes construir un servicio o un conjunto de datos que otros quieran conectar a sus aplicaciones, ahí hay una posible fuente de ingresos.",[15,1013,1014],{},"Construyes una API que ofrece un dato o una función concreta, como información meteorológica, manipulación de imágenes, resumen de textos o conversión de divisas, y cobras según el uso. Algo que convierta markdown en PDF bien maquetado, o que dé cotizaciones de criptomonedas poco conocidas. Cuando es estable y está documentada, funciona en segundo plano atendiendo peticiones, y tu tiempo se va al marketing y al soporte.",[15,1016,1017],{},"Esta opción exige más infraestructura que las demás, además de cuidar la documentación y los límites de uso. Si tienes experiencia agregando datos, o un algoritmo que no tiene nadie más, merece la pena valorarla.",[10,1019,1021],{"id":1020},"_4-código-abierto-con-una-capa-comercial","4. Código abierto con una capa comercial",[15,1023,1024],{},"Mucha gente contribuye a proyectos abiertos de forma gratuita. Hay maneras de obtener ingresos sin cerrar el proyecto.",[15,1026,1027],{},"Si mantienes una biblioteca popular, puedes dejar el núcleo abierto y vender una versión pro con funciones avanzadas, soporte dedicado o escalabilidad para empresas. Una biblioteca de componentes abierta puede vender un generador de temas o soporte prioritario.",[15,1029,1030],{},"La otra vía es el patrocinio. GitHub Sponsors, Open Collective y Patreon existen porque las empresas que dependen de una herramienta a veces están dispuestas a financiarla. Quien mantiene una herramienta de compilación muy usada puede recibir aportaciones mensuales de los negocios que la utilizan.",[15,1032,1033],{},"Esto funciona mejor si ya tienes presencia en la comunidad de código abierto. No va tanto de empezar algo para ganar dinero como de construir sobre lo que ya has regalado.",[10,1035,1037],{"id":1036},"_5-enlaces-de-afiliado-de-lo-que-ya-usas","5. Enlaces de afiliado de lo que ya usas",[15,1039,1040],{},"Aquí no construyes un producto: usas tu credibilidad para recomendar cosas que realmente utilizas. Cuando alguien compra a través de tu enlace, recibes una comisión.",[15,1042,1043],{},"En la práctica son enlaces de afiliado en artículos, vídeos o cursos cuando hablas de herramientas, alojamiento, libros o formación. Un artículo sobre tu entorno de trabajo del año, con enlaces al editor, el monitor, el hosting y los servicios en la nube que usas. Cuando el contenido existe y la gente lo encuentra, los enlaces siguen funcionando.",[15,1045,1046],{},"Aquí lo determinante es la honestidad. Recomienda solo lo que usas. El público detecta enseguida a quien empuja productos que no vienen a cuento para llevarse una comisión.",[10,1048,1050],{"id":1049},"por-dónde-empezar","Por dónde empezar",[15,1052,1053],{},"Mil dólares al mes no llegan de un día para otro. Hace falta constancia y aguantar que las cosas no salgan a la primera. La ventaja del programador es que las habilidades necesarias para montar estas fuentes son las que ya usa.",[15,1055,1056],{},"Elige la que encaje contigo, empieza pequeño y mantente. La primera venta, la primera suscripción, la primera comisión: la acumulación empieza ahí.",{"title":85,"searchDepth":99,"depth":99,"links":1058},[1059,1060,1061,1062,1063,1064],{"id":965,"depth":99,"text":966},{"id":994,"depth":99,"text":995},{"id":1007,"depth":99,"text":1008},{"id":1020,"depth":99,"text":1021},{"id":1036,"depth":99,"text":1037},{"id":1049,"depth":99,"text":1050},"2025-08-28","Formas realistas de crear productos digitales, pequeñas herramientas SaaS o sistemas de trabajo que siguen generando ingresos cuando el trabajo ya está hecho.",{},"\u002Fblog\u002Festrategias-de-ingresos-pasivos-para-programadores",{"title":944,"description":1066},"blog\u002Festrategias-de-ingresos-pasivos-para-programadores",[1072,1073,1074,1075],"passive income","developers","side hustle","financial freedom","passive-income-strategies-for-developers","pD4zqmp6yCv7i36cZX05cn1yiE-hX4mjv4p81EuFBZw",{"id":1079,"title":1080,"body":1081,"category":178,"date":1169,"description":1170,"draft":181,"extension":182,"meta":1171,"navigation":184,"path":1172,"seo":1173,"stem":1174,"tags":1175,"translationKey":1180,"videoId":193,"__hash__":1181},"blog_es\u002Fblog\u002Fguerras-de-frameworks-javascript-vue-react-angular.md","Por qué las guerras de frameworks siguen importando: Vue, React y Angular",{"type":7,"value":1082,"toc":1161},[1083,1087,1090,1093,1097,1100,1103,1106,1109,1113,1116,1119,1122,1125,1129,1132,1135,1138,1141,1145,1148,1151,1155,1158],[10,1084,1086],{"id":1085},"la-discusión-que-no-termina-o-sí","La discusión que no termina, ¿o sí?",[15,1088,1089],{},"Las guerras de frameworks aceleran el pulso de cualquier programador de JavaScript, o al menos lo irritan un poco. Vue contra React contra Angular, cada año un aspirante nuevo y otra ronda de discusiones sobre cuál gana. Y sin embargo siguen en pie los mismos tres. Es legítimo preguntarse si esto importa todavía y si no basta con elegir uno y seguir adelante.",[15,1091,1092],{},"Las ideas de fondo se mantienen, pero el terreno se mueve. El trabajo de rendimiento, las funcionalidades nuevas y hacia dónde va la atención de la comunidad afectan a cómo envejece un proyecto. Elegir framework no va de lo que está de moda, sino de lo que resulta sostenible y agradable de mantener. Esta es la situación de cada uno.",[10,1094,1096],{"id":1095},"react-el-rey-de-los-componentes","React: el rey de los componentes",[15,1098,1099],{},"React empezó esto, o al menos popularizó el enfoque por componentes que casi todos usamos hoy. Salió de Facebook, ahora Meta, y lleva años siendo dominante.",[15,1101,1102],{},"Su mayor ventaja es el ecosistema. Si te topas con un problema, alguien se topó antes y ya hay una respuesta o una biblioteca esperándote. React suele describirse como una biblioteca más que como un framework, lo que significa libertad: eliges el router, la gestión de estado y la configuración de compilación. A los equipos con experiencia que quieren moldear su propio stack eso les encaja. El Virtual DOM mantiene eficientes las actualizaciones de interfaz al reducir la manipulación directa del DOM, y los hooks hicieron los componentes funcionales mucho más fáciles de razonar que los componentes de clase a los que sustituyeron.",[15,1104,1105],{},"El precio de esa libertad es la fatiga de decisión. La cantidad de opciones puede abrumar a quien empieza. React además avanza rápido, y seguir el ritmo de las prácticas actuales, las bibliotecas y lo que queda obsoleto consume tiempo real.",[15,1107,1108],{},"Encaja con equipos de gente con experiencia que valoran la flexibilidad y quieren un ecosistema grande detrás. Para aplicaciones de una sola página complejas y muy interactivas sigue siendo una opción sólida por defecto.",[10,1110,1112],{"id":1111},"vue-accesible-y-progresivo","Vue: accesible y progresivo",[15,1114,1115],{},"Vue es conocido por lo accesible que resulta y por su sintaxis, y ha ido ganando cuota de forma constante. Se sitúa entre la flexibilidad y la estructura sin comprometerse del todo con ninguna.",[15,1117,1118],{},"Donde destaca es en la facilidad de aprendizaje. Si vienes de HTML, CSS y algo de JavaScript, los componentes de archivo único y su API resultan naturales, y suele decirse que tiene la curva más suave de los tres. También se adopta de forma progresiva: puedes meterlo en un proyecto existente por partes o construir una aplicación completa con él. La documentación es un punto fuerte real, clara y bien organizada. Vue es ligero y su sistema de reactividad está bien optimizado. Con Pinia como solución recomendada de gestión de estado, esa parte del stack también quedó resuelta.",[15,1120,1121],{},"Su ecosistema es sólido y crece, pero sigue siendo menor que el de React, así que para casos muy específicos encontrarás menos bibliotecas de terceros. Un detalle práctico relacionado: parte de los recursos y de las conversaciones de la comunidad ocurren fuera del inglés, lo que a veces implica buscar una traducción.",[15,1123,1124],{},"Encaja con quien quiere algo rápido de aprender y cómodo en el día a día, y escala desde proyectos pequeños hasta aplicaciones grandes.",[10,1126,1128],{"id":1127},"angular-estructurado-y-con-opinión","Angular: estructurado y con opinión",[15,1130,1131],{},"Angular, respaldado por Google, es un framework completo. Elegirlo significa adoptar todo un ecosistema junto con opiniones firmes sobre cómo deben construirse las aplicaciones.",[15,1133,1134],{},"Viene todo incluido: enrutado, gestión de estado, manejo de formularios, cliente HTTP. Menos tiempo eligiendo bibliotecas y más escribiendo código. Está construido sobre TypeScript desde la base, así que la comprobación de tipos es obligatoria y no opcional, algo que en aplicaciones grandes se traduce en menos errores en tiempo de ejecución y mejores herramientas. Su CLI es potente de verdad para crear proyectos, generar componentes y servicios, ejecutar pruebas y compilar. El respaldo de Google aporta estabilidad y una hoja de ruta visible. Al ser un framework con opinión, los proyectos Angular se parecen entre sí, lo que ayuda en equipos grandes y en el mantenimiento a largo plazo.",[15,1136,1137],{},"También tiene la curva de aprendizaje más pronunciada de los tres. Módulos, servicios, inyección de dependencias, observables con RxJS y decoradores son mucho que asimilar de golpe. Los proyectos arrastran más código repetitivo que sus equivalentes en React o Vue, y salirse del camino previsto cuesta.",[15,1139,1140],{},"Encaja con aplicaciones grandes y equipos que ponen por delante la estructura, el mantenimiento y la seguridad de tipos.",[10,1142,1144],{"id":1143},"cuál-gana-entonces","¿Cuál gana entonces?",[15,1146,1147],{},"Ninguno, y esa es la respuesta honesta. Cada uno maduró alrededor de sus propias fortalezas mientras tomaba prestado de los demás. El adecuado depende de tu proyecto, de tu equipo y de lo que ya saben.",[15,1149,1150],{},"Elige React si quieres un ecosistema enorme y flexibilidad, con gente dispuesta a tomar decisiones de arquitectura y seguir un ritmo de cambio alto. Elige Vue si buscas un arranque fácil, buena experiencia de desarrollo y equilibrio entre estructura y libertad. Elige Angular para aplicaciones grandes donde una estructura prescrita, la comprobación de tipos y un CLI completo compensan la curva de aprendizaje.",[10,1152,1154],{"id":1153},"lo-que-decide-de-verdad","Lo que decide de verdad",[15,1156,1157],{},"Las guerras de frameworks van menos de ganar que de posicionarse. Los tres son capaces. Lo que decide el resultado de tu proyecto es cuánto rinde tu equipo con la herramienta, si funciona bien para tus usuarios, si el código seguirá siendo comprensible dentro de unos años, si habrá soporte y bibliotecas cuando los necesites y qué saben ya tus programadores. Aprender un framework nuevo tiene un coste, y ese coste forma parte de la decisión.",[15,1159,1160],{},"El ecosistema está más rico que nunca. Elegir la herramienta adecuada rinde más que ganar la discusión.",{"title":85,"searchDepth":99,"depth":99,"links":1162},[1163,1164,1165,1166,1167,1168],{"id":1085,"depth":99,"text":1086},{"id":1095,"depth":99,"text":1096},{"id":1111,"depth":99,"text":1112},{"id":1127,"depth":99,"text":1128},{"id":1143,"depth":99,"text":1144},{"id":1153,"depth":99,"text":1154},"2025-09-17","Dónde está hoy cada uno de los tres grandes frameworks, en qué destaca cada uno y cómo elegir para tu próximo proyecto.",{},"\u002Fblog\u002Fguerras-de-frameworks-javascript-vue-react-angular",{"title":1080,"description":1170},"blog\u002Fguerras-de-frameworks-javascript-vue-react-angular",[562,1176,1177,1178,1179],"frameworks","vue","react","angular","why-javascript-framework-wars-matter-in-2025","EJevEajrlX9tnu3aPmbVS293xhkZSDpno_FgWGbhEog",{"id":1183,"title":1184,"body":1185,"category":293,"date":294,"description":1340,"draft":181,"extension":182,"meta":1341,"navigation":184,"path":1342,"seo":1343,"stem":1344,"tags":1345,"translationKey":1349,"videoId":193,"__hash__":1350},"blog_es\u002Fblog\u002Fhabitos-atomicos-para-programadores.md","Hábitos atómicos para programadores: seis rutinas que merece la pena probar",{"type":7,"value":1186,"toc":1330},[1187,1190,1196,1203,1206,1210,1213,1216,1220,1223,1226,1233,1237,1240,1243,1254,1258,1261,1264,1271,1275,1278,1281,1287,1291,1294,1297,1300,1304,1307,1314,1317,1321,1324,1327],[15,1188,1189],{},"Todos hemos tenido ese día: miras una pantalla en blanco con una entrega encima y la cabeza ejecutando mil procesos, ninguno útil. O empiezas un proyecto con entusiasmo de verdad y ves cómo esa energía se agota más rápido que una prueba gratuita.",[15,1191,1192,1193,44],{},"Mi productividad funcionó durante mucho tiempo como una montaña rusa. Había días en los que lo sacaba todo y días en los que me preguntaba si aún sabía escribir un ",[34,1194,1195],{},"console.log",[15,1197,1198,1199,1202],{},"Eso fue lo que me llevó a los hábitos atómicos. El libro ",[222,1200,1201],{},"Atomic Habits",", de James Clear, se apoya en una sola idea: los cambios diminutos se acumulan hasta producir resultados grandes. No queda lejos de cómo funciona un historial de Git, donde muchos commits pequeños acaban formando una funcionalidad.",[15,1204,1205],{},"Los principios encajan bien con nuestro trabajo. Ya nos movemos entre sistemas y mejoras incrementales, que es justo el material de estos hábitos. Aquí van seis que merece la pena construir.",[10,1207,1209],{"id":1208},"por-qué-funcionan-los-hábitos-pequeños","Por qué funcionan los hábitos pequeños",[15,1211,1212],{},"Puedes preguntarte por qué no simplemente esforzarse más. El problema es que la fuerza de voluntad se agota. Si intentas un cambio enorme de golpe, tu cerebro lo interpreta como una amenaza a su comodidad y se resiste. Los hábitos diminutos pasan por debajo de esa resistencia porque son casi invisibles.",[15,1214,1215],{},"Para un programador eso importa, porque estamos aprendiendo continuamente y sosteniendo problemas complejos en la cabeza. Un conjunto de hábitos pequeños y constantes ayuda a llevar esa carga sin quemarse. Piensa en ello como refactorizar tu propio flujo de trabajo.",[10,1217,1219],{"id":1218},"_1-quince-minutos-de-trabajo-profundo","1. Quince minutos de trabajo profundo",[15,1221,1222],{},"Dedica los primeros quince minutos de la jornada, o el rato justo antes de una tarea grande, a trabajar concentrado y sin interrupciones en lo más importante de tu lista. Abre el editor, cierra las demás pestañas, silencia las notificaciones y pon un temporizador. Sin correo y sin Slack.",[15,1224,1225],{},"Arranca el día con impulso y entrena a tu cerebro para entrar rápido en concentración. Aunque solo consigas esos quince minutos, ya has roto la inercia, y cuando entras en flujo esos quince suelen convertirse en treinta y luego en una hora.",[15,1227,1228,1229,1232],{},"Por ejemplo: la tarea importante de hoy es un flujo de autenticación nuevo. En lugar de dar vueltas alrededor, dedico quince minutos a escribir la interfaz básica de ",[34,1230,1231],{},"AuthService"," e ignoro todo lo demás.",[10,1234,1236],{"id":1235},"_2-revisa-tu-propio-código-al-terminar-el-día","2. Revisa tu propio código al terminar el día",[15,1238,1239],{},"Antes de cerrar, dedica cinco o diez minutos a repasar lo que escribiste ese día. Busca posibles fallos, sitios que convendría refactorizar, nombres que podrían ser más claros. No hace falta arreglar nada en ese momento: la idea es verlo.",[15,1241,1242],{},"Es una retrospectiva pequeña incorporada al día. La calidad del código sube, los errores aparecen antes y ver cómo se acumulan tus propios commits motiva.",[15,1244,1245,1246,1249,1250,1253],{},"Por ejemplo: termino un componente y, antes de subirlo, repaso el archivo y veo una variable llamada ",[34,1247,1248],{},"data"," que quedaría más clara como ",[34,1251,1252],{},"userData",". Anoto el cambio para mañana.",[10,1255,1257],{"id":1256},"_3-aprende-una-cosa-pequeña-al-día","3. Aprende una cosa pequeña al día",[15,1259,1260],{},"Dedica diez minutos diarios a aprender algo pequeño relacionado con tu trabajo. Un artículo corto sobre una función de JavaScript, un vídeo de cinco minutos sobre una técnica de CSS, una extensión nueva del editor, un par de páginas de un libro técnico.",[15,1262,1263],{},"El sector avanza deprisa y mantenerse al día importa, pero intentar hacerlo todo de golpe agobia. Diez minutos lo convierten en algo constante, y las piezas pequeñas se acumulan.",[15,1265,1266,1267,1270],{},"Por ejemplo: veo una mención a algo de la Composition API de Vue 3, abro la documentación y paso diez minutos leyendo sobre ",[34,1268,1269],{},"defineExpose"," y para qué sirve.",[10,1272,1274],{"id":1273},"_4-decide-esta-noche-la-primera-tarea-de-mañana","4. Decide esta noche la primera tarea de mañana",[15,1276,1277],{},"Antes de desconectar, dedica cinco minutos a elegir la única primera tarea de mañana por la mañana. Escríbela. Deja abierto el archivo correspondiente en el editor para que te esté esperando.",[15,1279,1280],{},"Esto elimina la fatiga de decisión y el problema del arranque en frío. Te levantas sabiendo qué hacer, lo que reduce bastante la procrastinación.",[15,1282,1283,1284,44],{},"Por ejemplo: antes de cerrar el portátil veo que la siguiente tarea es integrar un manejador de webhooks, lo apunto en una nota adhesiva y dejo abierto ",[34,1285,1286],{},"stripe-webhook.ts",[10,1288,1290],{"id":1289},"_5-dos-minutos-de-orden-digital","5. Dos minutos de orden digital",[15,1292,1293],{},"Una vez al día, dedica dos minutos a cerrar pestañas que no necesitas, ordenar el escritorio o vaciar la carpeta de descargas. Elige un desorden pequeño, no todos.",[15,1295,1296],{},"Un entorno digital desordenado suele producir una cabeza desordenada. Incluso un orden mínimo reduce el esfuerzo de encontrar las cosas.",[15,1298,1299],{},"Por ejemplo: el navegador tiene treinta pestañas abiertas, así que dedico dos minutos a cerrar cinco que seguro no necesito.",[10,1301,1303],{"id":1302},"_6-un-minuto-de-reinicio-físico","6. Un minuto de reinicio físico",[15,1305,1306],{},"Cada hora, levántate, estírate y apártate de la pantalla durante sesenta segundos. Pon un recordatorio suave o usa un temporizador Pomodoro, y cuando suene no lo ignores. Ponte de pie, rota los hombros, mira por la ventana, camina hasta la cocina.",[15,1308,1309,1310,1313],{},"Pasamos horas encorvados sobre el teclado. Un minuto por hora ayuda con la tensión física, la circulación y el cansancio de la vista. Además, buena parte de las ideas para depurar llegan cuando ",[222,1311,1312],{},"no"," estás mirando el código.",[15,1315,1316],{},"Por ejemplo: suena el temporizador, me levanto, roto los hombros y miro los árboles de fuera durante un minuto antes de volver a sentarme.",[10,1318,1320],{"id":1319},"probarlo-durante-un-mes","Probarlo durante un mes",[15,1322,1323],{},"Ninguno de estos cambios es monumental, y ahí está la gracia. Elige uno o dos y mantenlos con constancia durante unos treinta días.",[15,1325,1326],{},"Las cuatro reglas de Clear ayudan aquí. Hazlo evidente, con una nota en el monitor para el bloque de trabajo profundo. Hazlo atractivo, emparejándolo con algo que disfrutes después. Hazlo fácil empezando de verdad pequeño: si quince minutos son demasiado, haz cinco. Y hazlo satisfactorio llevando un registro, aunque sea una casilla en papel, porque ver crecer la racha motiva por sí solo.",[15,1328,1329],{},"Las acciones pequeñas y constantes acaban notándose en la concentración y la regularidad. En mi caso, eso fue casi todo el cambio.",{"title":85,"searchDepth":99,"depth":99,"links":1331},[1332,1333,1334,1335,1336,1337,1338,1339],{"id":1208,"depth":99,"text":1209},{"id":1218,"depth":99,"text":1219},{"id":1235,"depth":99,"text":1236},{"id":1256,"depth":99,"text":1257},{"id":1273,"depth":99,"text":1274},{"id":1289,"depth":99,"text":1290},{"id":1302,"depth":99,"text":1303},{"id":1319,"depth":99,"text":1320},"Seis hábitos pequeños y prácticos que un programador puede construir para mejorar su concentración y su constancia en un mes.",{},"\u002Fblog\u002Fhabitos-atomicos-para-programadores",{"title":1184,"description":1340},"blog\u002Fhabitos-atomicos-para-programadores",[939,1346,1073,1347,1348],"habits","focus","consistency","atomic-habits-for-developers","c_4MZAysi_x_oS9Um08wOYCouHuK_VWpYFp6B5k_mxA",{"id":1352,"title":1353,"body":1354,"category":490,"date":410,"description":1518,"draft":181,"extension":182,"meta":1519,"navigation":184,"path":1520,"seo":1521,"stem":1522,"tags":1523,"translationKey":1526,"videoId":193,"__hash__":1527},"blog_es\u002Fblog\u002Fherramientas-de-productividad-infravaloradas.md","Diez herramientas de productividad infravaloradas para programadores",{"type":7,"value":1355,"toc":1505},[1356,1359,1363,1366,1369,1372,1376,1379,1382,1385,1389,1392,1399,1403,1406,1409,1412,1416,1419,1422,1426,1429,1432,1435,1439,1442,1445,1449,1452,1455,1459,1466,1482,1485,1489,1492,1495,1498,1502],[15,1357,1358],{},"Programar suele significar llevar una docena de cosas a la vez, desde corregir fallos hasta diseñar funcionalidades nuevas. Todos tenemos el equipo habitual: VS Code, Git, quizá Jira o Trello. Esta lista va de las otras, las que no aparecen en cada recopilación pero quitan fricción del día de trabajo.",[10,1360,1362],{"id":1361},"_1-raycast","1. Raycast",[15,1364,1365],{},"Es razonable preguntar para qué quieres otro lanzador si macOS ya trae Spotlight. Raycast es un lanzador rápido, pensado para el teclado, y llega bastante más lejos.",[15,1367,1368],{},"Desde una sola barra de búsqueda puedes crear fragmentos de texto, convertir divisas, controlar Spotify y gestionar el historial del portapapeles. Puedes escribir tus propias extensiones para automatizar lo repetitivo de tu flujo, como el push diario a Git o llamar a un endpoint concreto con unas pocas teclas. También se conecta con Notion, Jira, Linear y GitHub, así que gestionas tareas, abres incidencias y buscas documentos sin abrir el navegador.",[15,1370,1371],{},"Ahorra ese tipo de segundos que en un mes se convierten en horas.",[10,1373,1375],{"id":1374},"_2-typora","2. Typora",[15,1377,1378],{},"Un editor de markdown no suena a gran cosa hasta que usas este. Si escribes documentación, artículos o notas largas, Typora te da un sitio tranquilo para hacerlo.",[15,1380,1381],{},"No hay paneles divididos: escribes markdown y se va formateando mientras tecleas. El modo de concentración oculta todo salvo el párrafo en el que estás. La exportación cubre PDF, Word, HTML y otros formatos.",[15,1383,1384],{},"Es de esas herramientas que dan ganas de escribir más.",[10,1386,1388],{"id":1387},"_3-warp","3. Warp",[15,1390,1391],{},"Una terminal no parece algo infravalorado, pero Warp parte de otras premisas. Usa IA y un enfoque colaborativo para hacer más eficiente la línea de comandos.",[15,1393,1394,1395,1398],{},"Si no recuerdas un comando de ",[34,1396,1397],{},"git"," poco frecuente, te sugiere uno según tu contexto. Cada comando y su salida se agrupan en un bloque visual, lo que hace mucho más cómodo revisar una sesión hacia atrás. Además puedes compartir comandos y flujos con tu equipo desde la propia terminal.",[10,1400,1402],{"id":1401},"_4-obsidian","4. Obsidian",[15,1404,1405],{},"Casi todos hemos probado una docena de aplicaciones de notas. La diferencia de Obsidian es que está hecha para conectar notas, no para archivarlas.",[15,1407,1408],{},"Todo son archivos markdown locales, así que el control de tus datos es tuyo. Los enlaces funcionan en ambas direcciones, de modo que ves qué notas apuntan a la que estás leyendo, y la vista de grafo muestra esas conexiones.",[15,1410,1411],{},"Para quien aprende continuamente, esa estructura aguanta mucho mejor con los años que un árbol de carpetas.",[10,1413,1415],{"id":1414},"_5-rectangle","5. Rectangle",[15,1417,1418],{},"Sencilla y eficaz. Rectangle es un gestor de ventanas gratuito y de código abierto para macOS que coloca ventanas en distintas disposiciones con atajos de teclado o arrastrando: mitades, cuartos, tamaños propios y varias pantallas.",[15,1420,1421],{},"Se acabó redimensionar y arrastrar a mano. Un cambio pequeño con un efecto perceptible.",[10,1423,1425],{"id":1424},"_6-cron","6. Cron",[15,1427,1428],{},"Sí, una aplicación de calendario. Cron está pensada para quien usa el calendario como herramienta de trabajo y no como registro.",[15,1430,1431],{},"La interfaz es limpia y prioriza el teclado, así que creas eventos y saltas entre fechas sin tocar el ratón. Trae bloques de tiempo integrados, lo que hace práctico reservar tiempo de concentración en vez de esperar a que aparezca.",[15,1433,1434],{},"Ayuda a cumplir un horario, no solo a anotarlo.",[10,1436,1438],{"id":1437},"_7-excalidraw","7. Excalidraw",[15,1440,1441],{},"A veces quieres esbozar una idea, un flujo o la arquitectura de un sistema sin abrir una herramienta de diagramas seria. Excalidraw es una pizarra virtual donde todo parece dibujado a mano.",[15,1443,1444],{},"Ese aspecto tosco es intencionado: encaja con las ideas tempranas, donde un diagrama pulido daría a entender más certeza de la que tienes. Los tableros se comparten para trabajar en tiempo real, se incrustan en Notion u Obsidian y se exportan como SVG o PNG.",[10,1446,1448],{"id":1447},"_8-session","8. Session",[15,1450,1451],{},"Todo el mundo conoce la técnica Pomodoro: veinticinco minutos de trabajo, cinco de descanso. Sencillo en teoría y fácil de olvidar en la práctica. Session es una aplicación mínima que ayuda a cumplirlo.",[15,1453,1454],{},"El temporizador es limpio y discreto, te avisa para descansar y empezar sesiones nuevas, y registra cuánto trabajo concentrado estás sacando.",[10,1456,1458],{"id":1457},"_9-fzf","9. fzf",[15,1460,1461,1462,1465],{},"Esta es más técnica, pero ",[34,1463,1464],{},"fzf"," cambia cómo se siente la terminal. Es un buscador difuso de propósito general para la línea de comandos: archivos, historial, procesos, commits de Git.",[15,1467,1468,1469,1472,1473,1472,1476,553,1479,1481],{},"Encuentras casi cualquier cosa en unas pocas teclas, y se integra con ",[34,1470,1471],{},"cd",", ",[34,1474,1475],{},"grep",[34,1477,1478],{},"history",[34,1480,1397],{},". Es muy configurable, así que acaba adaptándose a tus costumbres.",[15,1483,1484],{},"En proyectos grandes ahorra una cantidad de tiempo sorprendente.",[10,1486,1488],{"id":1487},"_10-toucan","10. Toucan",[15,1490,1491],{},"Esta no es una herramienta de programación en el sentido habitual. Toucan es una extensión de navegador que sustituye discretamente algunas palabras de las páginas que ya visitas por sus equivalentes en el idioma que estás aprendiendo.",[15,1493,1494],{},"Adquieres vocabulario en contexto sin reservar tiempo de estudio. Además rompe el día con algo que no es código, y alejarte de un problema con una tarea de otro tipo suele ser justo lo que te permite volver a él con la cabeza despejada.",[15,1496,1497],{},"La relación con el trabajo de desarrollo es menos rara de lo que parece: los programadores viajan, trabajan en equipos internacionales y leen documentación escrita en cualquier parte del mundo.",[10,1499,1501],{"id":1500},"para-terminar","Para terminar",[15,1503,1504],{},"Ninguna de estas aparece en todas las listas de mejores aplicaciones de productividad, y cada una elimina una fricción concreta. La productividad no va tanto de trabajar más; en buena medida va de quitar los pequeños obstáculos entre tú y el trabajo.",{"title":85,"searchDepth":99,"depth":99,"links":1506},[1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517],{"id":1361,"depth":99,"text":1362},{"id":1374,"depth":99,"text":1375},{"id":1387,"depth":99,"text":1388},{"id":1401,"depth":99,"text":1402},{"id":1414,"depth":99,"text":1415},{"id":1424,"depth":99,"text":1425},{"id":1437,"depth":99,"text":1438},{"id":1447,"depth":99,"text":1448},{"id":1457,"depth":99,"text":1458},{"id":1487,"depth":99,"text":1488},{"id":1500,"depth":99,"text":1501},"Herramientas poco conocidas que reducen el trabajo repetitivo, ordenan lo que sabes y facilitan la concentración.",{},"\u002Fblog\u002Fherramientas-de-productividad-infravaloradas",{"title":1353,"description":1518},"blog\u002Fherramientas-de-productividad-infravaloradas",[939,1524,1073,1525],"tools","workflow","underrated-productivity-tools-for-developers","B7gNweMNDW_AwUDdux6RkZsC1Qw04r8or1BSnBmePFw",{"id":4,"title":5,"body":1529,"category":178,"date":179,"description":180,"draft":181,"extension":182,"meta":1631,"navigation":184,"path":185,"seo":1632,"stem":187,"tags":1633,"translationKey":192,"videoId":193,"__hash__":194},{"type":7,"value":1530,"toc":1626},[1531,1533,1538,1540,1548,1550,1554,1568,1622,1624],[10,1532,13],{"id":12},[15,1534,17,1535,25],{},[19,1536,24],{"href":21,"rel":1537},[23],[10,1539,29],{"id":28},[15,1541,32,1542,37,1544,41,1546,44],{},[34,1543,36],{},[34,1545,40],{},[34,1547,36],{},[10,1549,48],{"id":47},[15,1551,51,1552,54],{},[34,1553,36],{},[56,1555,1556,1560,1566],{},[59,1557,61,1558,65],{},[34,1559,64],{},[59,1561,68,1562,72,1564,44],{},[34,1563,71],{},[34,1565,75],{},[59,1567,78],{},[80,1569,1570],{"className":82,"code":83,"language":84,"meta":85,"style":85},[34,1571,1572,1576,1580,1586,1592,1598,1604,1610,1614,1618],{"__ignoreMap":85},[89,1573,1574],{"class":91,"line":92},[89,1575,96],{"class":95},[89,1577,1578],{"class":91,"line":99},[89,1579,103],{"class":102},[89,1581,1582,1584],{"class":91,"line":106},[89,1583,110],{"class":109},[89,1585,113],{"class":102},[89,1587,1588,1590],{"class":91,"line":116},[89,1589,120],{"class":119},[89,1591,123],{"class":102},[89,1593,1594,1596],{"class":91,"line":126},[89,1595,129],{"class":119},[89,1597,123],{"class":102},[89,1599,1600,1602],{"class":91,"line":134},[89,1601,137],{"class":119},[89,1603,123],{"class":102},[89,1605,1606,1608],{"class":91,"line":142},[89,1607,145],{"class":119},[89,1609,123],{"class":102},[89,1611,1612],{"class":91,"line":150},[89,1613,153],{"class":119},[89,1615,1616],{"class":91,"line":156},[89,1617,159],{"class":102},[89,1619,1620],{"class":91,"line":162},[89,1621,165],{"class":102},[15,1623,168],{},[170,1625,172],{},{"title":85,"searchDepth":99,"depth":99,"links":1627},[1628,1629,1630],{"id":12,"depth":99,"text":13},{"id":28,"depth":99,"text":29},{"id":47,"depth":99,"text":48},{},{"title":5,"description":180},[189,190,191],{"id":1635,"title":1636,"body":1637,"category":293,"date":1735,"description":1736,"draft":181,"extension":182,"meta":1737,"navigation":184,"path":1738,"seo":1739,"stem":1740,"tags":1741,"translationKey":1746,"videoId":193,"__hash__":1747},"blog_es\u002Fblog\u002Flas-48-leyes-del-poder-para-programadores.md","Las 48 leyes del poder para programadores",{"type":7,"value":1638,"toc":1727},[1639,1643,1646,1649,1653,1656,1663,1666,1669,1673,1676,1679,1682,1685,1689,1692,1695,1698,1701,1705,1708,1711,1714,1717,1721,1724],[10,1640,1642],{"id":1641},"el-poder-dentro-del-código","El poder dentro del código",[15,1644,1645],{},"Seguramente conoces \"Las 48 leyes del poder\", el polémico libro de Robert Greene. Suele leerse como una guía de dinámicas sociales en entornos donde hay mucho en juego. Buena parte de esas leyes, escritas pensando en cortes y reinos, encaja también en el mundo tecnológico. Incluso en un sector que se considera meritocrático, leer bien las dinámicas de poder forma parte de cómo la gente gana influencia y acaba liderando.",[15,1647,1648],{},"No te estoy proponiendo montar intrigas maquiavélicas en la próxima daily. Se trata de percibir las fuerzas que ya operan en cualquier organización y actuar con intención. Aquí van cuatro de esas leyes, releídas para quienes pasamos el día dentro de un repositorio.",[10,1650,1652],{"id":1651},"ley-1-nunca-eclipses-al-maestro-o-a-tu-tech-lead","Ley 1: nunca eclipses al maestro, o a tu tech lead",[15,1654,1655],{},"En nuestro mundo el \"maestro\" es tu tech lead, tu responsable o alguien del equipo de arquitectura. Querer enseñar una solución ingeniosa es natural, pero hay momentos en los que conviene dejar que la victoria sea de quien está por encima.",[15,1657,1658,1659,1662],{},"Puedes pensar: \"pero si de verdad soy bueno en esto, ¿por qué esconderlo?\". No se trata de esconder tu talento, sino de ",[222,1660,1661],{},"cómo"," lo presentas. Si señalas los fallos de tu lead delante de todo el mundo, o despliegas una solución que se salta su criterio, no estás sumando aliados. Estás creando rivales.",[15,1664,1665],{},"En la práctica eso significa proponer soluciones en vez de limitarte a encontrar defectos. \"Tu código es ineficiente\" suena muy distinto a \"he estado mirando una optimización para esta función que podría mejorar el rendimiento\". También significa reconocer cuándo la idea de otra persona desbloqueó tu trabajo, y apoyar la dirección de tu lead durante un proyecto grande en lugar de intentar taparla.",[15,1667,1668],{},"Alguien seguro de sí mismo valorará lo que aportas. Alguien inseguro puede leerlo como una amenaza. Distinguir entre los dos y ajustar tu forma de actuar es una habilidad útil.",[10,1670,1672],{"id":1671},"ley-7-consigue-que-otros-hagan-el-trabajo-pero-llévate-el-mérito","Ley 7: consigue que otros hagan el trabajo, pero llévate el mérito",[15,1674,1675],{},"Fuera de contexto suena duro. Releída para programadores va de delegar, acompañar a otros y ejercer influencia: sacar más adelante habilitando a la gente en lugar de escribir tú cada línea.",[15,1677,1678],{},"Cuando pasas a roles senior o de liderazgo, ya no escalas echando más horas de código. La palanca está en el acompañamiento. Si guías a alguien junior a través de un problema y esa persona entrega, tú te llevas el mérito de haber construido un buen equipo y ella se lleva la experiencia. Con las revisiones de código pasa igual: en vez de arreglar tú el fallo, lleva a quien lo escribió hasta la corrección. Sus habilidades crecen y el código mejorado refleja el trabajo del equipo.",[15,1680,1681],{},"Lo mismo aplica a la arquitectura. Tú defines la forma de un sistema complejo, el equipo implementa los módulos y, cuando eso llega a producción, la dirección que marcaste se ve en el resultado.",[15,1683,1684],{},"La idea no es holgazanear, sino multiplicar. El mérito que dura viene de proyectos ambiciosos terminados por un equipo bien dirigido.",[10,1686,1688],{"id":1687},"ley-11-haz-que-dependan-de-ti","Ley 11: haz que dependan de ti",[15,1690,1691],{},"Suena posesivo, pero en un contexto profesional significa volverte realmente valioso y difícil de reemplazar: habilidades, conocimiento o relaciones que te convierten en una pieza clave.",[15,1693,1694],{},"Una parte de eso es estabilidad laboral. La otra es influencia. Cuando la gente depende de lo que sabes, tu opinión pesa más.",[15,1696,1697],{},"Ocurre por varias vías. Te conviertes en la persona de referencia para un sistema crítico o poco atractivo que los demás evitan. Entiendes la lógica de negocio que hay detrás del código mejor que nadie, así que cuando alguien necesita saber por qué algo funciona de determinada manera, acude a ti. Acumulas ese conocimiento no escrito sobre cómo se conectan los sistemas heredados, las rarezas de una API antigua o el motivo de una decisión de arquitectura. O te vuelves el puente entre desarrollo, QA, producto y operaciones, y tu capacidad de traducir entre ellos es lo que hace avanzar los proyectos transversales.",[15,1699,1700],{},"La distinción importante: no acapares información. Sé quien resuelve los problemas que otros no pueden, o los resuelve más rápido.",[10,1702,1704],{"id":1703},"ley-18-no-construyas-fortalezas-para-protegerte","Ley 18: no construyas fortalezas para protegerte",[15,1706,1707],{},"Es fácil desaparecer dentro del editor con los cascos puestos. El trabajo concentrado es bueno. El aislamiento total no lo es, al menos para tu influencia y tu crecimiento.",[15,1709,1710],{},"\"Es que yo solo quiero escribir código\" es una postura legítima. Pero si únicamente hablas con tu equipo inmediato, te pierdes oportunidades y puntos de vista, y tu reputación se detiene en el borde de ese equipo.",[15,1712,1713],{},"Ofrecerte para proyectos que cruzan varios equipos amplía tu red y demuestra que sabes trabajar fuera de tu terreno. Los grupos internos, las charlas técnicas y los meetups te permiten compartir lo que sabes y conocer gente. Las revisiones de código también son un espacio social: pregunta, ofrece ayuda, aprovéchalas para ver otras partes del proyecto. Estar disponible cuando alguien necesita una mano, aunque no sea tu tarea, se acumula con el tiempo.",[15,1715,1716],{},"Mantenerte conectado es lo que hace que te enteres de las oportunidades, ganes aliados y tu trabajo se vea más allá de tu círculo inmediato.",[10,1718,1720],{"id":1719},"código-y-contexto","Código y contexto",[15,1722,1723],{},"\"Las 48 leyes del poder\" es una fuente inesperada de consejo profesional para programadores, pero si le quitas el decorado histórico queda una descripción de dinámicas organizativas corrientes. La habilidad técnica es lo primero, y rara vez basta por sí sola para llegar al nivel donde se toman las decisiones.",[15,1725,1726],{},"Entender cómo funciona la influencia en el día a día de tu equipo te hace más efectivo. No es jugar a nada, es ver el tablero.",{"title":85,"searchDepth":99,"depth":99,"links":1728},[1729,1730,1731,1732,1733,1734],{"id":1641,"depth":99,"text":1642},{"id":1651,"depth":99,"text":1652},{"id":1671,"depth":99,"text":1672},{"id":1687,"depth":99,"text":1688},{"id":1703,"depth":99,"text":1704},{"id":1719,"depth":99,"text":1720},"2025-05-27","Lecciones del libro de Robert Greene releídas para ingenieros de software que quieren más influencia y respeto en su trabajo.",{},"\u002Fblog\u002Flas-48-leyes-del-poder-para-programadores",{"title":1636,"description":1736},"blog\u002Flas-48-leyes-del-poder-para-programadores",[1742,418,1743,1744,1745],"career","power","influence","leadership","48-laws-for-developers","ESg5Xq4HL77wA6AaRZS685GSPnRcSFfhc84tOIiWNMI",{"id":1749,"title":1750,"body":1751,"category":490,"date":294,"description":3275,"draft":181,"extension":182,"meta":3276,"navigation":184,"path":3277,"seo":3278,"stem":3279,"tags":3280,"translationKey":3285,"videoId":193,"__hash__":3286},"blog_es\u002Fblog\u002Fpor-que-el-codigo-limpio-sigue-importando.md","Por qué el código limpio sigue importando: ocho principios que conviene mantener",{"type":7,"value":1752,"toc":3263},[1753,1757,1760,1763,1766,1770,1773,1821,1824,1863,1866,1870,1873,1916,1919,2058,2061,2065,2068,2192,2195,2254,2258,2261,2264,2284,2287,2329,2336,2340,2343,2346,2396,2399,2561,2565,2576,2579,2652,2655,2713,2716,2720,2723,2726,2961,2965,2968,3124,3127,3247,3250,3254,3257,3260],[10,1754,1756],{"id":1755},"la-ia-y-el-sitio-que-ocupa-hoy-el-código-limpio","La IA y el sitio que ocupa hoy el código limpio",[15,1758,1759],{},"La IA está por todas partes en este oficio, desde generar fragmentos hasta escribir funciones enteras, y es razonable preguntarse si escribir código limpio sigue teniendo sentido cuando un modelo puede refactorizarlo por ti.",[15,1761,1762],{},"Yo mismo he ido y venido con esa duda. La IA es un asistente competente, pero no lee la mente. Puede optimizar, refactorizar y proponer alternativas más claras, y todo eso funciona mejor cuando parte de algo sólido. Dale a un buen cocinero ingredientes excelentes y una receta caótica: el plato saldrá igual de mal.",[15,1764,1765],{},"El código limpio va de claridad, de mantenimiento y de cómo envejece un proyecto. De paso, hace más eficaces a tus herramientas de IA. Estos son ocho principios que aguantan.",[10,1767,1769],{"id":1768},"_1-nombres-con-significado","1. Nombres con significado",[15,1771,1772],{},"La claridad empieza aquí. Imagina una historia donde todos los personajes se llaman \"a\", \"b\" y \"c\". Con el código pasa lo mismo. Variables, funciones y clases deberían decir qué hacen o qué representan antes de leer una sola línea de su cuerpo.",[80,1774,1776],{"className":560,"code":1775,"language":562,"meta":85,"style":85},"let d; \u002F\u002F What is 'd'? Days? Data?\nfunction p(a, b) {\n  \u002F* ... *\u002F\n} \u002F\u002F What does 'p' do? What are 'a' and 'b'?\n",[34,1777,1778,1789,1808,1813],{"__ignoreMap":85},[89,1779,1780,1783,1786],{"class":91,"line":92},[89,1781,1782],{"class":569},"let",[89,1784,1785],{"class":102}," d; ",[89,1787,1788],{"class":95},"\u002F\u002F What is 'd'? Days? Data?\n",[89,1790,1791,1794,1797,1799,1801,1803,1806],{"class":91,"line":99},[89,1792,1793],{"class":569},"function",[89,1795,1796],{"class":576}," p",[89,1798,580],{"class":102},[89,1800,19],{"class":583},[89,1802,1472],{"class":102},[89,1804,1805],{"class":583},"b",[89,1807,587],{"class":102},[89,1809,1810],{"class":91,"line":106},[89,1811,1812],{"class":95},"  \u002F* ... *\u002F\n",[89,1814,1815,1818],{"class":91,"line":116},[89,1816,1817],{"class":102},"} ",[89,1819,1820],{"class":95},"\u002F\u002F What does 'p' do? What are 'a' and 'b'?\n",[15,1822,1823],{},"Compáralo con esto:",[80,1825,1827],{"className":560,"code":1826,"language":562,"meta":85,"style":85},"let daysSinceLastLogin;\nfunction calculateTotalPrice(quantity, unitPrice) {\n  \u002F* ... *\u002F\n}\n",[34,1828,1829,1836,1855,1859],{"__ignoreMap":85},[89,1830,1831,1833],{"class":91,"line":92},[89,1832,1782],{"class":569},[89,1834,1835],{"class":102}," daysSinceLastLogin;\n",[89,1837,1838,1840,1843,1845,1848,1850,1853],{"class":91,"line":99},[89,1839,1793],{"class":569},[89,1841,1842],{"class":576}," calculateTotalPrice",[89,1844,580],{"class":102},[89,1846,1847],{"class":583},"quantity",[89,1849,1472],{"class":102},[89,1851,1852],{"class":583},"unitPrice",[89,1854,587],{"class":102},[89,1856,1857],{"class":91,"line":106},[89,1858,1812],{"class":95},[89,1860,1861],{"class":91,"line":116},[89,1862,165],{"class":102},[15,1864,1865],{},"Los buenos nombres funcionan como documentación dentro del código. Reducen la carga mental de leerlo y aceleran la depuración. Un modelo que lea tu código se beneficia de la misma claridad que tú.",[10,1867,1869],{"id":1868},"_2-una-función-debería-hacer-una-sola-cosa","2. Una función debería hacer una sola cosa",[15,1871,1872],{},"Cada función debería tener un único motivo para cambiar. Cuando hace demasiado, cuesta más probarla, cuesta más seguirla y es más fácil romperla al tocarla.",[80,1874,1876],{"className":560,"code":1875,"language":562,"meta":85,"style":85},"function processOrder(order) {\n  \u002F\u002F Validate order\n  \u002F\u002F Save to database\n  \u002F\u002F Send confirmation email\n  \u002F\u002F Update inventory\n}\n",[34,1877,1878,1892,1897,1902,1907,1912],{"__ignoreMap":85},[89,1879,1880,1882,1885,1887,1890],{"class":91,"line":92},[89,1881,1793],{"class":569},[89,1883,1884],{"class":576}," processOrder",[89,1886,580],{"class":102},[89,1888,1889],{"class":583},"order",[89,1891,587],{"class":102},[89,1893,1894],{"class":91,"line":99},[89,1895,1896],{"class":95},"  \u002F\u002F Validate order\n",[89,1898,1899],{"class":91,"line":106},[89,1900,1901],{"class":95},"  \u002F\u002F Save to database\n",[89,1903,1904],{"class":91,"line":116},[89,1905,1906],{"class":95},"  \u002F\u002F Send confirmation email\n",[89,1908,1909],{"class":91,"line":126},[89,1910,1911],{"class":95},"  \u002F\u002F Update inventory\n",[89,1913,1914],{"class":91,"line":134},[89,1915,165],{"class":102},[15,1917,1918],{},"Mejor separarlo:",[80,1920,1922],{"className":560,"code":1921,"language":562,"meta":85,"style":85},"function validateOrder(order) {\n  \u002F* ... *\u002F\n}\nfunction saveOrder(order) {\n  \u002F* ... *\u002F\n}\nfunction sendConfirmationEmail(order) {\n  \u002F* ... *\u002F\n}\nfunction updateInventory(order) {\n  \u002F* ... *\u002F\n}\n\nfunction processOrder(order) {\n  validateOrder(order);\n  saveOrder(order);\n  sendConfirmationEmail(order);\n  updateInventory(order);\n}\n",[34,1923,1924,1937,1941,1945,1958,1962,1966,1979,1983,1987,2000,2004,2008,2012,2024,2032,2039,2046,2053],{"__ignoreMap":85},[89,1925,1926,1928,1931,1933,1935],{"class":91,"line":92},[89,1927,1793],{"class":569},[89,1929,1930],{"class":576}," validateOrder",[89,1932,580],{"class":102},[89,1934,1889],{"class":583},[89,1936,587],{"class":102},[89,1938,1939],{"class":91,"line":99},[89,1940,1812],{"class":95},[89,1942,1943],{"class":91,"line":106},[89,1944,165],{"class":102},[89,1946,1947,1949,1952,1954,1956],{"class":91,"line":116},[89,1948,1793],{"class":569},[89,1950,1951],{"class":576}," saveOrder",[89,1953,580],{"class":102},[89,1955,1889],{"class":583},[89,1957,587],{"class":102},[89,1959,1960],{"class":91,"line":126},[89,1961,1812],{"class":95},[89,1963,1964],{"class":91,"line":134},[89,1965,165],{"class":102},[89,1967,1968,1970,1973,1975,1977],{"class":91,"line":142},[89,1969,1793],{"class":569},[89,1971,1972],{"class":576}," sendConfirmationEmail",[89,1974,580],{"class":102},[89,1976,1889],{"class":583},[89,1978,587],{"class":102},[89,1980,1981],{"class":91,"line":150},[89,1982,1812],{"class":95},[89,1984,1985],{"class":91,"line":156},[89,1986,165],{"class":102},[89,1988,1989,1991,1994,1996,1998],{"class":91,"line":162},[89,1990,1793],{"class":569},[89,1992,1993],{"class":576}," updateInventory",[89,1995,580],{"class":102},[89,1997,1889],{"class":583},[89,1999,587],{"class":102},[89,2001,2002],{"class":91,"line":728},[89,2003,1812],{"class":95},[89,2005,2006],{"class":91,"line":746},[89,2007,165],{"class":102},[89,2009,2010],{"class":91,"line":758},[89,2011,650],{"emptyLinePlaceholder":184},[89,2013,2014,2016,2018,2020,2022],{"class":91,"line":769},[89,2015,1793],{"class":569},[89,2017,1884],{"class":576},[89,2019,580],{"class":102},[89,2021,1889],{"class":583},[89,2023,587],{"class":102},[89,2025,2026,2029],{"class":91,"line":779},[89,2027,2028],{"class":576},"  validateOrder",[89,2030,2031],{"class":102},"(order);\n",[89,2033,2034,2037],{"class":91,"line":793},[89,2035,2036],{"class":576},"  saveOrder",[89,2038,2031],{"class":102},[89,2040,2041,2044],{"class":91,"line":799},[89,2042,2043],{"class":576},"  sendConfirmationEmail",[89,2045,2031],{"class":102},[89,2047,2048,2051],{"class":91,"line":808},[89,2049,2050],{"class":576},"  updateInventory",[89,2052,2031],{"class":102},[89,2054,2056],{"class":91,"line":2055},19,[89,2057,165],{"class":102},[15,2059,2060],{},"Partir una tarea compleja en piezas concretas hace el código modular y bastante más fácil de leer después.",[10,2062,2064],{"id":2063},"_3-no-te-repitas","3. No te repitas",[15,2066,2067],{},"Si estás escribiendo la misma lógica por segunda vez, llévala a una función o a un componente reutilizable. La duplicación destruye el mantenimiento en silencio: cuando hay que corregir un fallo, hay que corregirlo en varios sitios, y tarde o temprano se te escapa uno.",[80,2069,2071],{"className":560,"code":2070,"language":562,"meta":85,"style":85},"\u002F\u002F In file A\nfunction calculateDiscountA(price) {\n  if (price > 100) return price * 0.9;\n  return price;\n}\n\n\u002F\u002F In file B (same logic)\nfunction calculateDiscountB(amount) {\n  if (amount > 100) return amount * 0.9;\n  return amount;\n}\n",[34,2072,2073,2078,2092,2123,2130,2134,2138,2143,2157,2181,2188],{"__ignoreMap":85},[89,2074,2075],{"class":91,"line":92},[89,2076,2077],{"class":95},"\u002F\u002F In file A\n",[89,2079,2080,2082,2085,2087,2090],{"class":91,"line":99},[89,2081,1793],{"class":569},[89,2083,2084],{"class":576}," calculateDiscountA",[89,2086,580],{"class":102},[89,2088,2089],{"class":583},"price",[89,2091,587],{"class":102},[89,2093,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121],{"class":91,"line":106},[89,2095,2096],{"class":569},"  if",[89,2098,2099],{"class":102}," (price ",[89,2101,2102],{"class":569},">",[89,2104,2105],{"class":595}," 100",[89,2107,2108],{"class":102},") ",[89,2110,2111],{"class":569},"return",[89,2113,2114],{"class":102}," price ",[89,2116,2117],{"class":569},"*",[89,2119,2120],{"class":595}," 0.9",[89,2122,790],{"class":102},[89,2124,2125,2127],{"class":91,"line":116},[89,2126,802],{"class":569},[89,2128,2129],{"class":102}," price;\n",[89,2131,2132],{"class":91,"line":126},[89,2133,165],{"class":102},[89,2135,2136],{"class":91,"line":134},[89,2137,650],{"emptyLinePlaceholder":184},[89,2139,2140],{"class":91,"line":142},[89,2141,2142],{"class":95},"\u002F\u002F In file B (same logic)\n",[89,2144,2145,2147,2150,2152,2155],{"class":91,"line":150},[89,2146,1793],{"class":569},[89,2148,2149],{"class":576}," calculateDiscountB",[89,2151,580],{"class":102},[89,2153,2154],{"class":583},"amount",[89,2156,587],{"class":102},[89,2158,2159,2161,2164,2166,2168,2170,2172,2175,2177,2179],{"class":91,"line":156},[89,2160,2096],{"class":569},[89,2162,2163],{"class":102}," (amount ",[89,2165,2102],{"class":569},[89,2167,2105],{"class":595},[89,2169,2108],{"class":102},[89,2171,2111],{"class":569},[89,2173,2174],{"class":102}," amount ",[89,2176,2117],{"class":569},[89,2178,2120],{"class":595},[89,2180,790],{"class":102},[89,2182,2183,2185],{"class":91,"line":162},[89,2184,802],{"class":569},[89,2186,2187],{"class":102}," amount;\n",[89,2189,2190],{"class":91,"line":728},[89,2191,165],{"class":102},[15,2193,2194],{},"Con una sola implementación basta:",[80,2196,2198],{"className":560,"code":2197,"language":562,"meta":85,"style":85},"function applyStandardDiscount(price) {\n  if (price > 100) return price * 0.9;\n  return price;\n}\n\n\u002F\u002F Now both A and B can use applyStandardDiscount\n",[34,2199,2200,2213,2235,2241,2245,2249],{"__ignoreMap":85},[89,2201,2202,2204,2207,2209,2211],{"class":91,"line":92},[89,2203,1793],{"class":569},[89,2205,2206],{"class":576}," applyStandardDiscount",[89,2208,580],{"class":102},[89,2210,2089],{"class":583},[89,2212,587],{"class":102},[89,2214,2215,2217,2219,2221,2223,2225,2227,2229,2231,2233],{"class":91,"line":99},[89,2216,2096],{"class":569},[89,2218,2099],{"class":102},[89,2220,2102],{"class":569},[89,2222,2105],{"class":595},[89,2224,2108],{"class":102},[89,2226,2111],{"class":569},[89,2228,2114],{"class":102},[89,2230,2117],{"class":569},[89,2232,2120],{"class":595},[89,2234,790],{"class":102},[89,2236,2237,2239],{"class":91,"line":106},[89,2238,802],{"class":569},[89,2240,2129],{"class":102},[89,2242,2243],{"class":91,"line":116},[89,2244,165],{"class":102},[89,2246,2247],{"class":91,"line":126},[89,2248,650],{"emptyLinePlaceholder":184},[89,2250,2251],{"class":91,"line":134},[89,2252,2253],{"class":95},"\u002F\u002F Now both A and B can use applyStandardDiscount\n",[10,2255,2257],{"id":2256},"_4-comenta-el-porqué-no-el-qué","4. Comenta el porqué, no el qué",[15,2259,2260],{},"Quizá hayas oído que el buen código no necesita comentarios, y en parte es cierto. Un código claro, con nombres que dicen algo, vuelve redundantes muchos comentarios.",[15,2262,2263],{},"Lo que sí conviene dejar escrito es por qué algo se hizo de determinada manera, o por qué existe un apaño. Ese tipo de comentario vale mucho.",[80,2265,2267],{"className":560,"code":2266,"language":562,"meta":85,"style":85},"\u002F\u002F Increment the counter by 1\ncounter++;\n",[34,2268,2269,2274],{"__ignoreMap":85},[89,2270,2271],{"class":91,"line":92},[89,2272,2273],{"class":95},"\u002F\u002F Increment the counter by 1\n",[89,2275,2276,2279,2282],{"class":91,"line":99},[89,2277,2278],{"class":102},"counter",[89,2280,2281],{"class":569},"++",[89,2283,790],{"class":102},[15,2285,2286],{},"Ese no aporta nada. Este sí:",[80,2288,2290],{"className":560,"code":2289,"language":562,"meta":85,"style":85},"\u002F\u002F This specific regex is used to handle legacy user IDs\n\u002F\u002F which sometimes contain leading zeros and special characters.\nconst userIdRegex = \u002F^[0-9a-zA-Z\\-_]+$\u002F;\n",[34,2291,2292,2297,2302],{"__ignoreMap":85},[89,2293,2294],{"class":91,"line":92},[89,2295,2296],{"class":95},"\u002F\u002F This specific regex is used to handle legacy user IDs\n",[89,2298,2299],{"class":91,"line":99},[89,2300,2301],{"class":95},"\u002F\u002F which sometimes contain leading zeros and special characters.\n",[89,2303,2304,2307,2310,2312,2315,2318,2321,2324,2327],{"class":91,"line":106},[89,2305,2306],{"class":569},"const",[89,2308,2309],{"class":595}," userIdRegex",[89,2311,599],{"class":569},[89,2313,2314],{"class":119}," \u002F",[89,2316,2317],{"class":569},"^",[89,2319,2320],{"class":595},"[0-9a-zA-Z\\-_]",[89,2322,2323],{"class":569},"+$",[89,2325,2326],{"class":119},"\u002F",[89,2328,790],{"class":102},[15,2330,2331,2332,2335],{},"Si hace falta un comentario para explicar ",[222,2333,2334],{},"qué"," hace el código, suele ser señal de que el código podría estar más claro.",[10,2337,2339],{"id":2338},"_5-gestiona-los-errores","5. Gestiona los errores",[15,2341,2342],{},"Las redes se caen, los usuarios escriben cualquier cosa y las API devuelven algo inesperado. Ignorarlo produce comportamientos impredecibles y usuarios que no entienden qué ha fallado.",[15,2344,2345],{},"Cuánta gestión de errores es suficiente depende del contexto, pero una regla razonable es tratarlos allí donde puedes recuperarte o decir algo útil al usuario o al sistema.",[80,2347,2349],{"className":560,"code":2348,"language":562,"meta":85,"style":85},"function getUserData(userId) {\n  const data = api.fetch(userId); \u002F\u002F What if api.fetch fails?\n  return data.name;\n}\n",[34,2350,2351,2365,2385,2392],{"__ignoreMap":85},[89,2352,2353,2355,2358,2360,2363],{"class":91,"line":92},[89,2354,1793],{"class":569},[89,2356,2357],{"class":576}," getUserData",[89,2359,580],{"class":102},[89,2361,2362],{"class":583},"userId",[89,2364,587],{"class":102},[89,2366,2367,2369,2371,2373,2376,2379,2382],{"class":91,"line":99},[89,2368,592],{"class":569},[89,2370,635],{"class":595},[89,2372,599],{"class":569},[89,2374,2375],{"class":102}," api.",[89,2377,2378],{"class":576},"fetch",[89,2380,2381],{"class":102},"(userId); ",[89,2383,2384],{"class":95},"\u002F\u002F What if api.fetch fails?\n",[89,2386,2387,2389],{"class":91,"line":106},[89,2388,802],{"class":569},[89,2390,2391],{"class":102}," data.name;\n",[89,2393,2394],{"class":91,"line":116},[89,2395,165],{"class":102},[15,2397,2398],{},"Algo más parecido a esto:",[80,2400,2402],{"className":560,"code":2401,"language":562,"meta":85,"style":85},"async function getUserData(userId) {\n  try {\n    const response = await api.fetch(userId);\n    if (!response.ok) {\n      throw new Error(`Failed to fetch user data: ${response.statusText}`);\n    }\n    const data = await response.json();\n    return data.name;\n  } catch (error) {\n    console.error(`Error fetching user ${userId}:`, error);\n    \u002F\u002F Potentially return a default value, or re-throw a custom error\n    throw new CustomApplicationError(\"User data unavailable\");\n  }\n}\n",[34,2403,2404,2418,2424,2441,2451,2475,2479,2495,2502,2511,2531,2536,2553,2557],{"__ignoreMap":85},[89,2405,2406,2408,2410,2412,2414,2416],{"class":91,"line":92},[89,2407,570],{"class":569},[89,2409,573],{"class":569},[89,2411,2357],{"class":576},[89,2413,580],{"class":102},[89,2415,2362],{"class":583},[89,2417,587],{"class":102},[89,2419,2420,2422],{"class":91,"line":99},[89,2421,655],{"class":569},[89,2423,658],{"class":102},[89,2425,2426,2428,2430,2432,2434,2436,2438],{"class":91,"line":106},[89,2427,663],{"class":569},[89,2429,666],{"class":595},[89,2431,599],{"class":569},[89,2433,671],{"class":569},[89,2435,2375],{"class":102},[89,2437,2378],{"class":576},[89,2439,2440],{"class":102},"(userId);\n",[89,2442,2443,2445,2447,2449],{"class":91,"line":116},[89,2444,682],{"class":569},[89,2446,685],{"class":102},[89,2448,688],{"class":569},[89,2450,691],{"class":102},[89,2452,2453,2455,2457,2459,2461,2464,2466,2468,2471,2473],{"class":91,"line":126},[89,2454,696],{"class":569},[89,2456,699],{"class":569},[89,2458,702],{"class":576},[89,2460,580],{"class":102},[89,2462,2463],{"class":119},"`Failed to fetch user data: ${",[89,2465,710],{"class":102},[89,2467,44],{"class":119},[89,2469,2470],{"class":102},"statusText",[89,2472,718],{"class":119},[89,2474,610],{"class":102},[89,2476,2477],{"class":91,"line":134},[89,2478,725],{"class":102},[89,2480,2481,2483,2485,2487,2489,2491,2493],{"class":91,"line":142},[89,2482,663],{"class":569},[89,2484,635],{"class":595},[89,2486,599],{"class":569},[89,2488,671],{"class":569},[89,2490,738],{"class":102},[89,2492,84],{"class":576},[89,2494,743],{"class":102},[89,2496,2497,2500],{"class":91,"line":150},[89,2498,2499],{"class":569},"    return",[89,2501,2391],{"class":102},[89,2503,2504,2506,2508],{"class":91,"line":156},[89,2505,749],{"class":102},[89,2507,752],{"class":569},[89,2509,2510],{"class":102}," (error) {\n",[89,2512,2513,2516,2518,2520,2523,2525,2528],{"class":91,"line":162},[89,2514,2515],{"class":102},"    console.",[89,2517,556],{"class":576},[89,2519,580],{"class":102},[89,2521,2522],{"class":119},"`Error fetching user ${",[89,2524,2362],{"class":102},[89,2526,2527],{"class":119},"}:`",[89,2529,2530],{"class":102},", error);\n",[89,2532,2533],{"class":91,"line":728},[89,2534,2535],{"class":95},"    \u002F\u002F Potentially return a default value, or re-throw a custom error\n",[89,2537,2538,2541,2543,2546,2548,2551],{"class":91,"line":746},[89,2539,2540],{"class":569},"    throw",[89,2542,699],{"class":569},[89,2544,2545],{"class":576}," CustomApplicationError",[89,2547,580],{"class":102},[89,2549,2550],{"class":119},"\"User data unavailable\"",[89,2552,610],{"class":102},[89,2554,2555],{"class":91,"line":758},[89,2556,796],{"class":102},[89,2558,2559],{"class":91,"line":769},[89,2560,165],{"class":102},[10,2562,2564],{"id":2563},"_6-mantén-la-consistencia","6. Mantén la consistencia",[15,2566,2567,2568,2571,2572,2575],{},"La consistencia importa sobre todo en equipos grandes: las mismas convenciones de nombres, el mismo formato, los mismos patrones de arquitectura y las mismas decisiones de diseño en todo el proyecto. Cuando una parte usa ",[34,2569,2570],{},"camelCase"," y otra ",[34,2573,2574],{},"snake_case",", leer se vuelve más difícil sin ningún motivo.",[15,2577,2578],{},"Linters y formateadores como ESLint y Prettier resuelven casi todo esto de forma automática.",[80,2580,2582],{"className":560,"code":2581,"language":562,"meta":85,"style":85},"let firstName;\nconst user_id = 123;\nfunction getProducts() {\n  \u002F* ... *\u002F\n}\nconst fetchOrders = async () => {\n  \u002F* ... *\u002F\n};\n",[34,2583,2584,2591,2605,2615,2619,2623,2643,2647],{"__ignoreMap":85},[89,2585,2586,2588],{"class":91,"line":92},[89,2587,1782],{"class":569},[89,2589,2590],{"class":102}," firstName;\n",[89,2592,2593,2595,2598,2600,2603],{"class":91,"line":99},[89,2594,2306],{"class":569},[89,2596,2597],{"class":595}," user_id",[89,2599,599],{"class":569},[89,2601,2602],{"class":595}," 123",[89,2604,790],{"class":102},[89,2606,2607,2609,2612],{"class":91,"line":106},[89,2608,1793],{"class":569},[89,2610,2611],{"class":576}," getProducts",[89,2613,2614],{"class":102},"() {\n",[89,2616,2617],{"class":91,"line":116},[89,2618,1812],{"class":95},[89,2620,2621],{"class":91,"line":126},[89,2622,165],{"class":102},[89,2624,2625,2627,2630,2632,2635,2638,2641],{"class":91,"line":134},[89,2626,2306],{"class":569},[89,2628,2629],{"class":576}," fetchOrders",[89,2631,599],{"class":569},[89,2633,2634],{"class":569}," async",[89,2636,2637],{"class":102}," () ",[89,2639,2640],{"class":569},"=>",[89,2642,658],{"class":102},[89,2644,2645],{"class":91,"line":142},[89,2646,1812],{"class":95},[89,2648,2649],{"class":91,"line":150},[89,2650,2651],{"class":102},"};\n",[15,2653,2654],{},"Frente a:",[80,2656,2658],{"className":560,"code":2657,"language":562,"meta":85,"style":85},"let firstName;\nconst userId = 123;\nfunction getProducts() {\n  \u002F* ... *\u002F\n}\nasync function fetchOrders() {\n  \u002F* ... *\u002F\n}\n",[34,2659,2660,2666,2679,2687,2691,2695,2705,2709],{"__ignoreMap":85},[89,2661,2662,2664],{"class":91,"line":92},[89,2663,1782],{"class":569},[89,2665,2590],{"class":102},[89,2667,2668,2670,2673,2675,2677],{"class":91,"line":99},[89,2669,2306],{"class":569},[89,2671,2672],{"class":595}," userId",[89,2674,599],{"class":569},[89,2676,2602],{"class":595},[89,2678,790],{"class":102},[89,2680,2681,2683,2685],{"class":91,"line":106},[89,2682,1793],{"class":569},[89,2684,2611],{"class":576},[89,2686,2614],{"class":102},[89,2688,2689],{"class":91,"line":116},[89,2690,1812],{"class":95},[89,2692,2693],{"class":91,"line":126},[89,2694,165],{"class":102},[89,2696,2697,2699,2701,2703],{"class":91,"line":134},[89,2698,570],{"class":569},[89,2700,573],{"class":569},[89,2702,2629],{"class":576},[89,2704,2614],{"class":102},[89,2706,2707],{"class":91,"line":142},[89,2708,1812],{"class":95},[89,2710,2711],{"class":91,"line":150},[89,2712,165],{"class":102},[15,2714,2715],{},"Un proyecto consistente se lee como si lo hubiera escrito una sola persona, aunque hayan sido cincuenta. Eso reduce el esfuerzo de saltar entre archivos.",[10,2717,2719],{"id":2718},"_7-escribe-pruebas","7. Escribe pruebas",[15,2721,2722],{},"Escribir pruebas puede parecer una tarea pesada. También es lo que te permite refactorizar y desplegar sin contener la respiración. Con la programación asistida por IA esto importa más, no menos: las pruebas son la forma de saber si el código generado se integra bien y no ha roto otra cosa.",[15,2724,2725],{},"Imagina hacer un cambio pequeño y ver que se cae una parte de la aplicación que no tenía nada que ver. Sin pruebas, eso es una sesión larga de depuración. Con pruebas, el caso que falla te señala el problema.",[80,2727,2729],{"className":560,"code":2728,"language":562,"meta":85,"style":85},"\u002F\u002F function to test\nfunction add(a, b) {\n  return a + b;\n}\n\n\u002F\u002F test file\ndescribe(\"add function\", () => {\n  test(\"should add two positive numbers correctly\", () => {\n    expect(add(1, 2)).toBe(3);\n  });\n\n  test(\"should handle negative numbers\", () => {\n    expect(add(-1, 5)).toBe(4);\n  });\n\n  test(\"should return zero when adding opposite numbers\", () => {\n    expect(add(-3, 3)).toBe(0);\n  });\n});\n",[34,2730,2731,2736,2753,2766,2770,2774,2779,2796,2812,2845,2850,2854,2869,2900,2904,2908,2923,2952,2956],{"__ignoreMap":85},[89,2732,2733],{"class":91,"line":92},[89,2734,2735],{"class":95},"\u002F\u002F function to test\n",[89,2737,2738,2740,2743,2745,2747,2749,2751],{"class":91,"line":99},[89,2739,1793],{"class":569},[89,2741,2742],{"class":576}," add",[89,2744,580],{"class":102},[89,2746,19],{"class":583},[89,2748,1472],{"class":102},[89,2750,1805],{"class":583},[89,2752,587],{"class":102},[89,2754,2755,2757,2760,2763],{"class":91,"line":106},[89,2756,802],{"class":569},[89,2758,2759],{"class":102}," a ",[89,2761,2762],{"class":569},"+",[89,2764,2765],{"class":102}," b;\n",[89,2767,2768],{"class":91,"line":116},[89,2769,165],{"class":102},[89,2771,2772],{"class":91,"line":126},[89,2773,650],{"emptyLinePlaceholder":184},[89,2775,2776],{"class":91,"line":134},[89,2777,2778],{"class":95},"\u002F\u002F test file\n",[89,2780,2781,2784,2786,2789,2792,2794],{"class":91,"line":142},[89,2782,2783],{"class":576},"describe",[89,2785,580],{"class":102},[89,2787,2788],{"class":119},"\"add function\"",[89,2790,2791],{"class":102},", () ",[89,2793,2640],{"class":569},[89,2795,658],{"class":102},[89,2797,2798,2801,2803,2806,2808,2810],{"class":91,"line":150},[89,2799,2800],{"class":576},"  test",[89,2802,580],{"class":102},[89,2804,2805],{"class":119},"\"should add two positive numbers correctly\"",[89,2807,2791],{"class":102},[89,2809,2640],{"class":569},[89,2811,658],{"class":102},[89,2813,2814,2817,2819,2822,2824,2827,2829,2832,2835,2838,2840,2843],{"class":91,"line":156},[89,2815,2816],{"class":576},"    expect",[89,2818,580],{"class":102},[89,2820,2821],{"class":576},"add",[89,2823,580],{"class":102},[89,2825,2826],{"class":595},"1",[89,2828,1472],{"class":102},[89,2830,2831],{"class":595},"2",[89,2833,2834],{"class":102},")).",[89,2836,2837],{"class":576},"toBe",[89,2839,580],{"class":102},[89,2841,2842],{"class":595},"3",[89,2844,610],{"class":102},[89,2846,2847],{"class":91,"line":162},[89,2848,2849],{"class":102},"  });\n",[89,2851,2852],{"class":91,"line":728},[89,2853,650],{"emptyLinePlaceholder":184},[89,2855,2856,2858,2860,2863,2865,2867],{"class":91,"line":746},[89,2857,2800],{"class":576},[89,2859,580],{"class":102},[89,2861,2862],{"class":119},"\"should handle negative numbers\"",[89,2864,2791],{"class":102},[89,2866,2640],{"class":569},[89,2868,658],{"class":102},[89,2870,2871,2873,2875,2877,2879,2882,2884,2886,2889,2891,2893,2895,2898],{"class":91,"line":758},[89,2872,2816],{"class":576},[89,2874,580],{"class":102},[89,2876,2821],{"class":576},[89,2878,580],{"class":102},[89,2880,2881],{"class":569},"-",[89,2883,2826],{"class":595},[89,2885,1472],{"class":102},[89,2887,2888],{"class":595},"5",[89,2890,2834],{"class":102},[89,2892,2837],{"class":576},[89,2894,580],{"class":102},[89,2896,2897],{"class":595},"4",[89,2899,610],{"class":102},[89,2901,2902],{"class":91,"line":769},[89,2903,2849],{"class":102},[89,2905,2906],{"class":91,"line":779},[89,2907,650],{"emptyLinePlaceholder":184},[89,2909,2910,2912,2914,2917,2919,2921],{"class":91,"line":793},[89,2911,2800],{"class":576},[89,2913,580],{"class":102},[89,2915,2916],{"class":119},"\"should return zero when adding opposite numbers\"",[89,2918,2791],{"class":102},[89,2920,2640],{"class":569},[89,2922,658],{"class":102},[89,2924,2925,2927,2929,2931,2933,2935,2937,2939,2941,2943,2945,2947,2950],{"class":91,"line":799},[89,2926,2816],{"class":576},[89,2928,580],{"class":102},[89,2930,2821],{"class":576},[89,2932,580],{"class":102},[89,2934,2881],{"class":569},[89,2936,2842],{"class":595},[89,2938,1472],{"class":102},[89,2940,2842],{"class":595},[89,2942,2834],{"class":102},[89,2944,2837],{"class":576},[89,2946,580],{"class":102},[89,2948,2949],{"class":595},"0",[89,2951,610],{"class":102},[89,2953,2954],{"class":91,"line":808},[89,2955,2849],{"class":102},[89,2957,2958],{"class":91,"line":2055},[89,2959,2960],{"class":102},"});\n",[10,2962,2964],{"id":2963},"_8-sencillez","8. Sencillez",[15,2966,2967],{},"Todo lo anterior se reduce a escribir código simple y fácil de leer. El código enrevesado atrae errores y se resiste al mantenimiento. Busca la solución más sencilla que cumpla el requisito.",[80,2969,2971],{"className":560,"code":2970,"language":562,"meta":85,"style":85},"function calculateTotal(items) {\n  let total = 0;\n  for (let i = 0; i \u003C items.length; i++) {\n    let item = items[i];\n    if (item.type === \"premium\") {\n      total += item.price * 1.1; \u002F\u002F 10% premium fee\n    } else {\n      total += item.price;\n    }\n  }\n  return total;\n}\n",[34,2972,2973,2987,3002,3037,3050,3064,3086,3096,3105,3109,3113,3120],{"__ignoreMap":85},[89,2974,2975,2977,2980,2982,2985],{"class":91,"line":92},[89,2976,1793],{"class":569},[89,2978,2979],{"class":576}," calculateTotal",[89,2981,580],{"class":102},[89,2983,2984],{"class":583},"items",[89,2986,587],{"class":102},[89,2988,2989,2992,2995,2997,3000],{"class":91,"line":99},[89,2990,2991],{"class":569},"  let",[89,2993,2994],{"class":102}," total ",[89,2996,377],{"class":569},[89,2998,2999],{"class":595}," 0",[89,3001,790],{"class":102},[89,3003,3004,3007,3009,3011,3014,3016,3018,3021,3024,3027,3030,3033,3035],{"class":91,"line":106},[89,3005,3006],{"class":569},"  for",[89,3008,685],{"class":102},[89,3010,1782],{"class":569},[89,3012,3013],{"class":102}," i ",[89,3015,377],{"class":569},[89,3017,2999],{"class":595},[89,3019,3020],{"class":102},"; i ",[89,3022,3023],{"class":569},"\u003C",[89,3025,3026],{"class":102}," items.",[89,3028,3029],{"class":595},"length",[89,3031,3032],{"class":102},"; i",[89,3034,2281],{"class":569},[89,3036,587],{"class":102},[89,3038,3039,3042,3045,3047],{"class":91,"line":116},[89,3040,3041],{"class":569},"    let",[89,3043,3044],{"class":102}," item ",[89,3046,377],{"class":569},[89,3048,3049],{"class":102}," items[i];\n",[89,3051,3052,3054,3057,3059,3062],{"class":91,"line":126},[89,3053,682],{"class":569},[89,3055,3056],{"class":102}," (item.type ",[89,3058,373],{"class":569},[89,3060,3061],{"class":119}," \"premium\"",[89,3063,587],{"class":102},[89,3065,3066,3069,3072,3075,3077,3080,3083],{"class":91,"line":134},[89,3067,3068],{"class":102},"      total ",[89,3070,3071],{"class":569},"+=",[89,3073,3074],{"class":102}," item.price ",[89,3076,2117],{"class":569},[89,3078,3079],{"class":595}," 1.1",[89,3081,3082],{"class":102},"; ",[89,3084,3085],{"class":95},"\u002F\u002F 10% premium fee\n",[89,3087,3088,3091,3094],{"class":91,"line":142},[89,3089,3090],{"class":102},"    } ",[89,3092,3093],{"class":569},"else",[89,3095,658],{"class":102},[89,3097,3098,3100,3102],{"class":91,"line":150},[89,3099,3068],{"class":102},[89,3101,3071],{"class":569},[89,3103,3104],{"class":102}," item.price;\n",[89,3106,3107],{"class":91,"line":156},[89,3108,725],{"class":102},[89,3110,3111],{"class":91,"line":162},[89,3112,796],{"class":102},[89,3114,3115,3117],{"class":91,"line":728},[89,3116,802],{"class":569},[89,3118,3119],{"class":102}," total;\n",[89,3121,3122],{"class":91,"line":746},[89,3123,165],{"class":102},[15,3125,3126],{},"Lo mismo, más fácil de seguir:",[80,3128,3130],{"className":560,"code":3129,"language":562,"meta":85,"style":85},"const calculateItemPrice = (item) => {\n  if (item.type === \"premium\") {\n    return item.price * 1.1;\n  }\n  return item.price;\n};\n\nfunction calculateTotal(items) {\n  return items.reduce((acc, item) => acc + calculateItemPrice(item), 0);\n}\n",[34,3131,3132,3152,3164,3176,3180,3186,3190,3194,3206,3243],{"__ignoreMap":85},[89,3133,3134,3136,3139,3141,3143,3146,3148,3150],{"class":91,"line":92},[89,3135,2306],{"class":569},[89,3137,3138],{"class":576}," calculateItemPrice",[89,3140,599],{"class":569},[89,3142,685],{"class":102},[89,3144,3145],{"class":583},"item",[89,3147,2108],{"class":102},[89,3149,2640],{"class":569},[89,3151,658],{"class":102},[89,3153,3154,3156,3158,3160,3162],{"class":91,"line":99},[89,3155,2096],{"class":569},[89,3157,3056],{"class":102},[89,3159,373],{"class":569},[89,3161,3061],{"class":119},[89,3163,587],{"class":102},[89,3165,3166,3168,3170,3172,3174],{"class":91,"line":106},[89,3167,2499],{"class":569},[89,3169,3074],{"class":102},[89,3171,2117],{"class":569},[89,3173,3079],{"class":595},[89,3175,790],{"class":102},[89,3177,3178],{"class":91,"line":116},[89,3179,796],{"class":102},[89,3181,3182,3184],{"class":91,"line":126},[89,3183,802],{"class":569},[89,3185,3104],{"class":102},[89,3187,3188],{"class":91,"line":134},[89,3189,2651],{"class":102},[89,3191,3192],{"class":91,"line":142},[89,3193,650],{"emptyLinePlaceholder":184},[89,3195,3196,3198,3200,3202,3204],{"class":91,"line":150},[89,3197,1793],{"class":569},[89,3199,2979],{"class":576},[89,3201,580],{"class":102},[89,3203,2984],{"class":583},[89,3205,587],{"class":102},[89,3207,3208,3210,3212,3215,3218,3221,3223,3225,3227,3229,3232,3234,3236,3239,3241],{"class":91,"line":156},[89,3209,802],{"class":569},[89,3211,3026],{"class":102},[89,3213,3214],{"class":576},"reduce",[89,3216,3217],{"class":102},"((",[89,3219,3220],{"class":583},"acc",[89,3222,1472],{"class":102},[89,3224,3145],{"class":583},[89,3226,2108],{"class":102},[89,3228,2640],{"class":569},[89,3230,3231],{"class":102}," acc ",[89,3233,2762],{"class":569},[89,3235,3138],{"class":576},[89,3237,3238],{"class":102},"(item), ",[89,3240,2949],{"class":595},[89,3242,610],{"class":102},[89,3244,3245],{"class":91,"line":162},[89,3246,165],{"class":102},[15,3248,3249],{},"La sencillez no consiste en rebajar el código, sino en escribir algo que seguirás entendiendo dentro de un año.",[10,3251,3253],{"id":3252},"el-oficio-no-desaparece","El oficio no desaparece",[15,3255,3256],{},"Seguirán llegando herramientas nuevas y la IA ha cambiado bastantes cosas, pero amplifica lo que ya haces en lugar de eliminar la necesidad de hacerlo bien.",[15,3258,3259],{},"El código limpio se parece más a un hábito que a un reglamento. Es consideración hacia tus compañeros, hacia tus usuarios y hacia la persona que abrirá ese archivo dentro de año y medio, que probablemente serás tú.",[170,3261,3262],{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}",{"title":85,"searchDepth":99,"depth":99,"links":3264},[3265,3266,3267,3268,3269,3270,3271,3272,3273,3274],{"id":1755,"depth":99,"text":1756},{"id":1768,"depth":99,"text":1769},{"id":1868,"depth":99,"text":1869},{"id":2063,"depth":99,"text":2064},{"id":2256,"depth":99,"text":2257},{"id":2338,"depth":99,"text":2339},{"id":2563,"depth":99,"text":2564},{"id":2718,"depth":99,"text":2719},{"id":2963,"depth":99,"text":2964},{"id":3252,"depth":99,"text":3253},"La IA puede refactorizar por ti, y eso hace los fundamentos más útiles, no menos. Ocho prácticas que mantienen manejable un proyecto.",{},"\u002Fblog\u002Fpor-que-el-codigo-limpio-sigue-importando",{"title":1750,"description":3275},"blog\u002Fpor-que-el-codigo-limpio-sigue-importando",[3281,3282,3283,3284],"clean code","software engineering","best practices","programming","why-clean-code-still-matters-in-2025","paz-fTPvdwjlYEmCFMH8ZHoV0xDXvgsojSKKKesKIy8",{"id":3288,"title":3289,"body":3290,"category":178,"date":4967,"description":4968,"draft":181,"extension":182,"meta":4969,"navigation":184,"path":4970,"seo":4971,"stem":4972,"tags":4973,"translationKey":4976,"videoId":193,"__hash__":4977},"blog_es\u002Fblog\u002Friesgos-de-seguridad-en-proyectos-vue.md","Riesgos de seguridad en proyectos Vue.js y cómo cerrarlos",{"type":7,"value":3291,"toc":4959},[3292,3295,3298,3301,3305,3308,3312,3315,3318,3339,3342,3400,3407,3413,3528,3531,3540,3737,3741,3744,3747,3789,3792,3795,3798,4008,4011,4015,4022,4033,4699,4702,4706,4709,4716,4719,4730,4946,4950,4956],[15,3293,3294],{},"Desplegar una aplicación Vue nueva suele venir acompañado de cierta inquietud. ¿Se me habrá escapado algo que pueda exponer datos de los usuarios o comprometer la aplicación?",[15,3296,3297],{},"Dedicamos horas a la interfaz, al rendimiento y a las funcionalidades, y la seguridad acaba casi siempre al final de la cola. El razonamiento suele ser \"es solo el frontend, qué puede pasar\". Pues bastante. Vue en sí es seguro, pero cómo lo usamos y qué traemos a su alrededor es de donde salen las vulnerabilidades.",[15,3299,3300],{},"Abajo hay cinco riesgos frecuentes en proyectos Vue y qué hacer con cada uno.",[10,3302,3304],{"id":3303},"por-qué-importa-la-seguridad-en-el-frontend","Por qué importa la seguridad en el frontend",[15,3306,3307],{},"Se puede argumentar que la seguridad es sobre todo cosa del backend, ya que el servidor gestiona la autenticación, el acceso a la base de datos y todo lo sensible. Es cierto que la seguridad del backend es crítica. Pero el frontend es la superficie con la que el usuario trata directamente, y cuando se ve comprometido los atacantes pueden robar credenciales mediante phishing o registradores de teclas, manipular datos como los precios de una tienda, provocar acciones que el usuario nunca quiso si tu API valida mal, o directamente desfigurar el sitio.",[10,3309,3311],{"id":3310},"_1-cross-site-scripting","1. Cross-site scripting",[15,3313,3314],{},"El XSS ocurre cuando un atacante inyecta un script, normalmente JavaScript, que acaba ejecutándose en el navegador de la víctima. Desde ahí puede llevarse cookies y tokens de sesión o reescribir la página.",[15,3316,3317],{},"Suele pasar cuando la aplicación vuelca la entrada del usuario directamente en el DOM sin sanearla. Imagina una sección de comentarios donde alguien envía esto:",[80,3319,3323],{"className":3320,"code":3321,"language":3322,"meta":85,"style":85},"language-html shiki shiki-themes github-dark-default","\u003Cp>This is a great article!\u003C\u002Fp>\n","html",[34,3324,3325],{"__ignoreMap":85},[89,3326,3327,3329,3331,3334,3336],{"class":91,"line":92},[89,3328,3023],{"class":102},[89,3330,15],{"class":109},[89,3332,3333],{"class":102},">This is a great article!\u003C\u002F",[89,3335,15],{"class":109},[89,3337,3338],{"class":102},">\n",[15,3340,3341],{},"Inofensivo. Ahora esto:",[80,3343,3345],{"className":3320,"code":3344,"language":3322,"meta":85,"style":85},"\u003Cp>\n  This is a great article!\n  \u003Cscript>\n    alert(\"You've been XSSed!\");\n  \u003C\u002Fscript>\n\u003C\u002Fp>\n",[34,3346,3347,3355,3360,3370,3382,3391],{"__ignoreMap":85},[89,3348,3349,3351,3353],{"class":91,"line":92},[89,3350,3023],{"class":102},[89,3352,15],{"class":109},[89,3354,3338],{"class":102},[89,3356,3357],{"class":91,"line":99},[89,3358,3359],{"class":102},"  This is a great article!\n",[89,3361,3362,3365,3368],{"class":91,"line":106},[89,3363,3364],{"class":102},"  \u003C",[89,3366,3367],{"class":109},"script",[89,3369,3338],{"class":102},[89,3371,3372,3375,3377,3380],{"class":91,"line":116},[89,3373,3374],{"class":576},"    alert",[89,3376,580],{"class":102},[89,3378,3379],{"class":119},"\"You've been XSSed!\"",[89,3381,610],{"class":102},[89,3383,3384,3387,3389],{"class":91,"line":126},[89,3385,3386],{"class":102},"  \u003C\u002F",[89,3388,3367],{"class":109},[89,3390,3338],{"class":102},[89,3392,3393,3396,3398],{"class":91,"line":134},[89,3394,3395],{"class":102},"\u003C\u002F",[89,3397,15],{"class":109},[89,3399,3338],{"class":102},[15,3401,3402,3403,3406],{},"Si tu componente enlaza esa entrada sin sanear con ",[34,3404,3405],{},"v-html",", el script se ejecuta para todo el que vea el comentario.",[15,3408,3409,3410,3412],{},"La regla a recordar: nunca uses ",[34,3411,3405],{}," con contenido de usuario en el que no confías. Con la interpolación estándar, Vue escapa el HTML por defecto.",[80,3414,3416],{"className":3320,"code":3415,"language":3322,"meta":85,"style":85},"\u003C!-- Safe: Vue escapes the HTML -->\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cp>{{ userComment }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from 'vue';\nconst userComment = ref('\u003Cscript>alert(\"XSS attempt!\");\u003C\u002Fscript>');\n\u003C\u002Fscript>\n",[34,3417,3418,3423,3432,3441,3455,3463,3471,3475,3486,3502,3520],{"__ignoreMap":85},[89,3419,3420],{"class":91,"line":92},[89,3421,3422],{"class":95},"\u003C!-- Safe: Vue escapes the HTML -->\n",[89,3424,3425,3427,3430],{"class":91,"line":99},[89,3426,3023],{"class":102},[89,3428,3429],{"class":109},"template",[89,3431,3338],{"class":102},[89,3433,3434,3436,3439],{"class":91,"line":106},[89,3435,3364],{"class":102},[89,3437,3438],{"class":109},"div",[89,3440,3338],{"class":102},[89,3442,3443,3446,3448,3451,3453],{"class":91,"line":116},[89,3444,3445],{"class":102},"    \u003C",[89,3447,15],{"class":109},[89,3449,3450],{"class":102},">{{ userComment }}\u003C\u002F",[89,3452,15],{"class":109},[89,3454,3338],{"class":102},[89,3456,3457,3459,3461],{"class":91,"line":126},[89,3458,3386],{"class":102},[89,3460,3438],{"class":109},[89,3462,3338],{"class":102},[89,3464,3465,3467,3469],{"class":91,"line":134},[89,3466,3395],{"class":102},[89,3468,3429],{"class":109},[89,3470,3338],{"class":102},[89,3472,3473],{"class":91,"line":142},[89,3474,650],{"emptyLinePlaceholder":184},[89,3476,3477,3479,3481,3484],{"class":91,"line":150},[89,3478,3023],{"class":102},[89,3480,3367],{"class":109},[89,3482,3483],{"class":595}," setup",[89,3485,3338],{"class":102},[89,3487,3488,3491,3494,3497,3500],{"class":91,"line":156},[89,3489,3490],{"class":569},"import",[89,3492,3493],{"class":102}," { ref } ",[89,3495,3496],{"class":569},"from",[89,3498,3499],{"class":119}," 'vue'",[89,3501,790],{"class":102},[89,3503,3504,3506,3509,3511,3513,3515,3518],{"class":91,"line":162},[89,3505,2306],{"class":569},[89,3507,3508],{"class":595}," userComment",[89,3510,599],{"class":569},[89,3512,602],{"class":576},[89,3514,580],{"class":102},[89,3516,3517],{"class":119},"'\u003Cscript>alert(\"XSS attempt!\");\u003C\u002Fscript>'",[89,3519,610],{"class":102},[89,3521,3522,3524,3526],{"class":91,"line":728},[89,3523,3395],{"class":102},[89,3525,3367],{"class":109},[89,3527,3338],{"class":102},[15,3529,3530],{},"Eso muestra la etiqueta script como texto plano en lugar de como código ejecutable.",[15,3532,3533,3534,3537,3538,44],{},"Si de verdad necesitas renderizar HTML dinámico, por ejemplo procedente de un editor de texto enriquecido, sanea la entrada primero en el backend con una biblioteca como ",[34,3535,3536],{},"DOMPurify"," antes de guardarla, y vuelve a sanear en el frontend como segunda capa antes de pasar nada a ",[34,3539,3405],{},[80,3541,3543],{"className":3320,"code":3542,"language":3322,"meta":85,"style":85},"\u003C!-- Use v-html ONLY with trusted, sanitized content -->\n\u003Ctemplate>\n  \u003Cdiv v-html=\"sanitizedUserContent\">\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref, computed } from 'vue';\n\u002F\u002F In a real app, you'd get this from a backend API after sanitization\nconst rawUserContent = ref('\u003Cp>Hello \u003Cb>World\u003C\u002Fb>! \u003Cscript>alert(\"No!\");\u003C\u002Fscript>\u003C\u002Fp>');\n\n\u002F\u002F For demonstration, let's pretend this is sanitized\n\u002F\u002F In a real app, you'd use a proper library like DOMPurify\nconst sanitizedUserContent = computed(() => {\n  \u002F\u002F This is a VERY simplistic and insecure \"sanitization\" for example purposes.\n  \u002F\u002F DO NOT use this in production. Use a library like DOMPurify!\n  return rawUserContent.value.replace(\u002F\u003Cscript.*?>.*?\u003C\\\u002Fscript>\u002Fg, '');\n});\n\u003C\u002Fscript>\n",[34,3544,3545,3550,3558,3579,3587,3591,3601,3614,3619,3637,3641,3646,3651,3670,3675,3680,3725,3729],{"__ignoreMap":85},[89,3546,3547],{"class":91,"line":92},[89,3548,3549],{"class":95},"\u003C!-- Use v-html ONLY with trusted, sanitized content -->\n",[89,3551,3552,3554,3556],{"class":91,"line":99},[89,3553,3023],{"class":102},[89,3555,3429],{"class":109},[89,3557,3338],{"class":102},[89,3559,3560,3562,3564,3567,3569,3572,3575,3577],{"class":91,"line":106},[89,3561,3364],{"class":102},[89,3563,3438],{"class":109},[89,3565,3566],{"class":595}," v-html",[89,3568,377],{"class":102},[89,3570,3571],{"class":119},"\"sanitizedUserContent\"",[89,3573,3574],{"class":102},">\u003C\u002F",[89,3576,3438],{"class":109},[89,3578,3338],{"class":102},[89,3580,3581,3583,3585],{"class":91,"line":116},[89,3582,3395],{"class":102},[89,3584,3429],{"class":109},[89,3586,3338],{"class":102},[89,3588,3589],{"class":91,"line":126},[89,3590,650],{"emptyLinePlaceholder":184},[89,3592,3593,3595,3597,3599],{"class":91,"line":134},[89,3594,3023],{"class":102},[89,3596,3367],{"class":109},[89,3598,3483],{"class":595},[89,3600,3338],{"class":102},[89,3602,3603,3605,3608,3610,3612],{"class":91,"line":142},[89,3604,3490],{"class":569},[89,3606,3607],{"class":102}," { ref, computed } ",[89,3609,3496],{"class":569},[89,3611,3499],{"class":119},[89,3613,790],{"class":102},[89,3615,3616],{"class":91,"line":150},[89,3617,3618],{"class":95},"\u002F\u002F In a real app, you'd get this from a backend API after sanitization\n",[89,3620,3621,3623,3626,3628,3630,3632,3635],{"class":91,"line":156},[89,3622,2306],{"class":569},[89,3624,3625],{"class":595}," rawUserContent",[89,3627,599],{"class":569},[89,3629,602],{"class":576},[89,3631,580],{"class":102},[89,3633,3634],{"class":119},"'\u003Cp>Hello \u003Cb>World\u003C\u002Fb>! \u003Cscript>alert(\"No!\");\u003C\u002Fscript>\u003C\u002Fp>'",[89,3636,610],{"class":102},[89,3638,3639],{"class":91,"line":162},[89,3640,650],{"emptyLinePlaceholder":184},[89,3642,3643],{"class":91,"line":728},[89,3644,3645],{"class":95},"\u002F\u002F For demonstration, let's pretend this is sanitized\n",[89,3647,3648],{"class":91,"line":746},[89,3649,3650],{"class":95},"\u002F\u002F In a real app, you'd use a proper library like DOMPurify\n",[89,3652,3653,3655,3658,3660,3663,3666,3668],{"class":91,"line":758},[89,3654,2306],{"class":569},[89,3656,3657],{"class":595}," sanitizedUserContent",[89,3659,599],{"class":569},[89,3661,3662],{"class":576}," computed",[89,3664,3665],{"class":102},"(() ",[89,3667,2640],{"class":569},[89,3669,658],{"class":102},[89,3671,3672],{"class":91,"line":769},[89,3673,3674],{"class":95},"  \u002F\u002F This is a VERY simplistic and insecure \"sanitization\" for example purposes.\n",[89,3676,3677],{"class":91,"line":779},[89,3678,3679],{"class":95},"  \u002F\u002F DO NOT use this in production. Use a library like DOMPurify!\n",[89,3681,3682,3684,3687,3690,3692,3695,3697,3700,3702,3704,3706,3708,3712,3715,3718,3720,3723],{"class":91,"line":793},[89,3683,802],{"class":569},[89,3685,3686],{"class":102}," rawUserContent.value.",[89,3688,3689],{"class":576},"replace",[89,3691,580],{"class":102},[89,3693,3694],{"class":119},"\u002F\u003Cscript",[89,3696,44],{"class":595},[89,3698,3699],{"class":569},"*?",[89,3701,2102],{"class":119},[89,3703,44],{"class":595},[89,3705,3699],{"class":569},[89,3707,3023],{"class":119},[89,3709,3711],{"class":3710},"sHaLz","\\\u002F",[89,3713,3714],{"class":119},"script>\u002F",[89,3716,3717],{"class":569},"g",[89,3719,1472],{"class":102},[89,3721,3722],{"class":119},"''",[89,3724,610],{"class":102},[89,3726,3727],{"class":91,"line":799},[89,3728,2960],{"class":102},[89,3730,3731,3733,3735],{"class":91,"line":808},[89,3732,3395],{"class":102},[89,3734,3367],{"class":109},[89,3736,3338],{"class":102},[10,3738,3740],{"id":3739},"_2-cross-site-request-forgery","2. Cross-site request forgery",[15,3742,3743],{},"El CSRF engaña a un usuario con la sesión iniciada para que realice una acción que no pretendía. El atacante consigue que el navegador del usuario envíe una petición a tu servidor, aprovechando que ya está autenticado.",[15,3745,3746],{},"Imagina que tienes la sesión abierta en tu banco en una pestaña y abres un sitio malicioso en otra. Ese sitio incluye un formulario oculto o una etiqueta de imagen que apunta al endpoint de transferencias del banco:",[80,3748,3750],{"className":3320,"code":3749,"language":3322,"meta":85,"style":85},"\u003C!-- Malicious website's hidden content -->\n\u003Cimg\n  src=\"https:\u002F\u002Fyourbank.com\u002Ftransfer?to=attacker&amount=1000000\"\n  style=\"display:none;\"\n\u002F>\n",[34,3751,3752,3757,3764,3774,3784],{"__ignoreMap":85},[89,3753,3754],{"class":91,"line":92},[89,3755,3756],{"class":95},"\u003C!-- Malicious website's hidden content -->\n",[89,3758,3759,3761],{"class":91,"line":99},[89,3760,3023],{"class":102},[89,3762,3763],{"class":109},"img\n",[89,3765,3766,3769,3771],{"class":91,"line":106},[89,3767,3768],{"class":595},"  src",[89,3770,377],{"class":102},[89,3772,3773],{"class":119},"\"https:\u002F\u002Fyourbank.com\u002Ftransfer?to=attacker&amount=1000000\"\n",[89,3775,3776,3779,3781],{"class":91,"line":116},[89,3777,3778],{"class":595},"  style",[89,3780,377],{"class":102},[89,3782,3783],{"class":119},"\"display:none;\"\n",[89,3785,3786],{"class":91,"line":126},[89,3787,3788],{"class":102},"\u002F>\n",[15,3790,3791],{},"Cuando el navegador carga esa imagen envía una petición GET junto con tus cookies de sesión. Si la API no tiene protección CSRF, puede tomarla por legítima.",[15,3793,3794],{},"La defensa habitual es un token CSRF. El servidor genera un token impredecible y lo envía al cliente, en una cookie, una etiqueta meta o la carga inicial. Cada petición que cambia estado desde tu aplicación Vue, es decir POST, PUT y DELETE, incluye ese token en las cabeceras o en el cuerpo. El backend rechaza todo aquello cuyo token no coincida.",[15,3796,3797],{},"En la práctica configuras tu cliente HTTP para que lo añada automáticamente.",[80,3799,3801],{"className":560,"code":3800,"language":562,"meta":85,"style":85},"\u002F\u002F Example using Axios (adjust based on how your backend provides the token)\nimport axios from \"axios\";\n\n\u002F\u002F 1. Get the CSRF token (e.g., from a meta tag in your HTML, or a cookie)\nfunction getCsrfToken() {\n  \u002F\u002F This is an example, your backend might provide it differently\n  const tokenElement = document.querySelector('meta[name=\"csrf-token\"]');\n  return tokenElement ? tokenElement.getAttribute(\"content\") : \"\";\n}\n\nconst csrfToken = getCsrfToken();\n\n\u002F\u002F 2. Configure Axios to include the token in requests\nif (csrfToken) {\n  axios.defaults.headers.common[\"X-CSRF-TOKEN\"] = csrfToken;\n}\n\n\u002F\u002F Now, when you make a POST, PUT, or DELETE request, the token will be included\naxios.post(\"\u002Fapi\u002Ftransfer-money\", {\n  recipient: \"myfriend\",\n  amount: 50,\n});\n",[34,3802,3803,3808,3822,3826,3831,3840,3845,3867,3898,3902,3906,3919,3923,3928,3936,3952,3956,3960,3965,3981,3992,4003],{"__ignoreMap":85},[89,3804,3805],{"class":91,"line":92},[89,3806,3807],{"class":95},"\u002F\u002F Example using Axios (adjust based on how your backend provides the token)\n",[89,3809,3810,3812,3815,3817,3820],{"class":91,"line":99},[89,3811,3490],{"class":569},[89,3813,3814],{"class":102}," axios ",[89,3816,3496],{"class":569},[89,3818,3819],{"class":119}," \"axios\"",[89,3821,790],{"class":102},[89,3823,3824],{"class":91,"line":106},[89,3825,650],{"emptyLinePlaceholder":184},[89,3827,3828],{"class":91,"line":116},[89,3829,3830],{"class":95},"\u002F\u002F 1. Get the CSRF token (e.g., from a meta tag in your HTML, or a cookie)\n",[89,3832,3833,3835,3838],{"class":91,"line":126},[89,3834,1793],{"class":569},[89,3836,3837],{"class":576}," getCsrfToken",[89,3839,2614],{"class":102},[89,3841,3842],{"class":91,"line":134},[89,3843,3844],{"class":95},"  \u002F\u002F This is an example, your backend might provide it differently\n",[89,3846,3847,3849,3852,3854,3857,3860,3862,3865],{"class":91,"line":142},[89,3848,592],{"class":569},[89,3850,3851],{"class":595}," tokenElement",[89,3853,599],{"class":569},[89,3855,3856],{"class":102}," document.",[89,3858,3859],{"class":576},"querySelector",[89,3861,580],{"class":102},[89,3863,3864],{"class":119},"'meta[name=\"csrf-token\"]'",[89,3866,610],{"class":102},[89,3868,3869,3871,3874,3877,3880,3883,3885,3888,3890,3893,3896],{"class":91,"line":150},[89,3870,802],{"class":569},[89,3872,3873],{"class":102}," tokenElement ",[89,3875,3876],{"class":569},"?",[89,3878,3879],{"class":102}," tokenElement.",[89,3881,3882],{"class":576},"getAttribute",[89,3884,580],{"class":102},[89,3886,3887],{"class":119},"\"content\"",[89,3889,2108],{"class":102},[89,3891,3892],{"class":569},":",[89,3894,3895],{"class":119}," \"\"",[89,3897,790],{"class":102},[89,3899,3900],{"class":91,"line":156},[89,3901,165],{"class":102},[89,3903,3904],{"class":91,"line":162},[89,3905,650],{"emptyLinePlaceholder":184},[89,3907,3908,3910,3913,3915,3917],{"class":91,"line":728},[89,3909,2306],{"class":569},[89,3911,3912],{"class":595}," csrfToken",[89,3914,599],{"class":569},[89,3916,3837],{"class":576},[89,3918,743],{"class":102},[89,3920,3921],{"class":91,"line":746},[89,3922,650],{"emptyLinePlaceholder":184},[89,3924,3925],{"class":91,"line":758},[89,3926,3927],{"class":95},"\u002F\u002F 2. Configure Axios to include the token in requests\n",[89,3929,3930,3933],{"class":91,"line":769},[89,3931,3932],{"class":569},"if",[89,3934,3935],{"class":102}," (csrfToken) {\n",[89,3937,3938,3941,3944,3947,3949],{"class":91,"line":779},[89,3939,3940],{"class":102},"  axios.defaults.headers.common[",[89,3942,3943],{"class":119},"\"X-CSRF-TOKEN\"",[89,3945,3946],{"class":102},"] ",[89,3948,377],{"class":569},[89,3950,3951],{"class":102}," csrfToken;\n",[89,3953,3954],{"class":91,"line":793},[89,3955,165],{"class":102},[89,3957,3958],{"class":91,"line":799},[89,3959,650],{"emptyLinePlaceholder":184},[89,3961,3962],{"class":91,"line":808},[89,3963,3964],{"class":95},"\u002F\u002F Now, when you make a POST, PUT, or DELETE request, the token will be included\n",[89,3966,3967,3970,3973,3975,3978],{"class":91,"line":2055},[89,3968,3969],{"class":102},"axios.",[89,3971,3972],{"class":576},"post",[89,3974,580],{"class":102},[89,3976,3977],{"class":119},"\"\u002Fapi\u002Ftransfer-money\"",[89,3979,3980],{"class":102},", {\n",[89,3982,3984,3987,3990],{"class":91,"line":3983},20,[89,3985,3986],{"class":102},"  recipient: ",[89,3988,3989],{"class":119},"\"myfriend\"",[89,3991,123],{"class":102},[89,3993,3995,3998,4001],{"class":91,"line":3994},21,[89,3996,3997],{"class":102},"  amount: ",[89,3999,4000],{"class":595},"50",[89,4002,123],{"class":102},[89,4004,4006],{"class":91,"line":4005},22,[89,4007,2960],{"class":102},[15,4009,4010],{},"Las peticiones GET normalmente no necesitan token porque se supone que son idempotentes. Si tu API cambia estado en un GET, eso es un problema aparte que conviene arreglar.",[10,4012,4014],{"id":4013},"_3-dependencias-con-vulnerabilidades","3. Dependencias con vulnerabilidades",[15,4016,4017,4018,4021],{},"Tu carpeta ",[34,4019,4020],{},"node_modules"," es una cantidad considerable de código ajeno. La mayor parte está bien, y basta un paquete vulnerable para abrir una puerta. O bien un paquete del que dependes, o algo en su propio árbol de dependencias, tiene un fallo conocido; o se compromete la cuenta de quien lo mantiene y llega código malicioso en una actualización.",[15,4023,4024,4025,4028,4029,4032],{},"Audita con regularidad. Tanto ",[34,4026,4027],{},"npm audit"," como ",[34,4030,4031],{},"yarn audit"," comprueban los paquetes instalados frente a vulnerabilidades conocidas y suelen proponer soluciones.",[80,4034,4038],{"className":4035,"code":4036,"language":4037,"meta":85,"style":85},"language-bash shiki shiki-themes github-dark-default","    # npm audit report\n\n    lodash  \u003C4.17.21\n    Severity: high\n    Prototype Pollution - https:\u002F\u002Fnpmjs.com\u002Fadvisories\u002F1526\n    No fix available\n\n    axios  \u003C0.21.1\n    Severity: moderate\n    Arbitrary File Write via Decompress - https:\u002F\u002Fnpmjs.com\u002Fadvisories\u002F1608\n    Fix available: `npm install axios@^0.21.1`\n    ```\n\nAntes de añadir una dependencia, mira si tiene mantenimiento activo, cuánta gente la usa, si hay incidencias de seguridad abiertas y cuándo se actualizó por última vez.\n\nSube al repositorio tu `package-lock.json` o `yarn.lock`. Esos archivos fijan las versiones exactas, de modo que tu equipo y tu pipeline de CI compilan sobre el mismo conjunto probado.\n\n## 4. Secretos que acaban en el bundle\n\nEsto suena evidente y sigue ocurriendo con frecuencia. Las claves de API escritas directamente en componentes o archivos JavaScript acaban en el bundle del cliente, lo que las vuelve públicas. Los mensajes de error detallados filtran información sobre el backend. Variables de entorno que nunca debieron llegar al cliente terminan empaquetadas.\n\nNunca escribas secretos en el código del frontend. Si el frontend necesita una API que requiere clave, ponte en medio: tu aplicación Vue llama a tu backend y tu backend llama a la API de terceros con la clave. Es decir, en lugar de hacer `axios.get('https:\u002F\u002Fapi.thirdparty.com\u002Fdata?apiKey=YOUR_SECRET_KEY')` desde el navegador, la petición pasa por un servidor que controlas.\n\nLas variables de entorno piden el mismo cuidado. Vue CLI expone al bundle del cliente todo lo que lleve el prefijo `VUE_APP_`, y Vite hace lo mismo con `VITE_`. Ahí van los endpoints de API, no las claves. Mantén los secretos de producción fuera del control de versiones y gestiónalos desde la configuración de tu proveedor de alojamiento.\n\nTrata los errores para que tampoco filtren nada. Un error del backend en crudo puede revelar el esquema de la base de datos o rutas del servidor, así que muestra al usuario algo genérico y registra el detalle en el servidor.\n\n```html\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cbutton @click=\"fetchData\">Fetch Data\u003C\u002Fbutton>\n    \u003Cp v-if=\"error\">{{ errorMessage }}\u003C\u002Fp>\n    \u003Cdiv v-if=\"data\">{{ data }}\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n  import { ref } from \"vue\";\n  import axios from \"axios\";\n\n  const data = ref(null);\n  const error = ref(false);\n  const errorMessage = ref(\"\");\n\n  const fetchData = async () => {\n    try {\n      const response = await axios.get(\"\u002Fapi\u002Fsensitive-data\");\n      data.value = response.data;\n      error.value = false;\n    } catch (err) {\n      error.value = true;\n      \u002F\u002F Don't show the raw error to the user!\n      \u002F\u002F console.error(err.response.data); \u002F\u002F Bad practice\n      errorMessage.value =\n        \"Oops! Something went wrong. Please try again later.\";\n      \u002F\u002F Log the detailed error to a server-side logging service\n    }\n  };\n\u003C\u002Fscript>\n","bash",[34,4039,4040,4045,4049,4059,4067,4081,4092,4096,4106,4113,4135,4152,4157,4161,4169,4173,4203,4207,4212,4216,4224,4228,4253,4258,4288,4293,4302,4307,4316,4325,4334,4363,4392,4420,4430,4440,4445,4455,4477,4491,4496,4513,4531,4549,4554,4568,4576,4598,4611,4623,4634,4646,4655,4664,4670,4676,4682,4687,4693],{"__ignoreMap":85},[89,4041,4042],{"class":91,"line":92},[89,4043,4044],{"class":95},"    # npm audit report\n",[89,4046,4047],{"class":91,"line":99},[89,4048,650],{"emptyLinePlaceholder":184},[89,4050,4051,4054,4056],{"class":91,"line":106},[89,4052,4053],{"class":583},"    lodash",[89,4055,3364],{"class":569},[89,4057,4058],{"class":119},"4.17.21\n",[89,4060,4061,4064],{"class":91,"line":116},[89,4062,4063],{"class":583},"    Severity:",[89,4065,4066],{"class":119}," high\n",[89,4068,4069,4072,4075,4078],{"class":91,"line":126},[89,4070,4071],{"class":583},"    Prototype",[89,4073,4074],{"class":119}," Pollution",[89,4076,4077],{"class":119}," -",[89,4079,4080],{"class":119}," https:\u002F\u002Fnpmjs.com\u002Fadvisories\u002F1526\n",[89,4082,4083,4086,4089],{"class":91,"line":134},[89,4084,4085],{"class":583},"    No",[89,4087,4088],{"class":119}," fix",[89,4090,4091],{"class":119}," available\n",[89,4093,4094],{"class":91,"line":142},[89,4095,650],{"emptyLinePlaceholder":184},[89,4097,4098,4101,4103],{"class":91,"line":150},[89,4099,4100],{"class":583},"    axios",[89,4102,3364],{"class":569},[89,4104,4105],{"class":119},"0.21.1\n",[89,4107,4108,4110],{"class":91,"line":156},[89,4109,4063],{"class":583},[89,4111,4112],{"class":119}," moderate\n",[89,4114,4115,4118,4121,4124,4127,4130,4132],{"class":91,"line":162},[89,4116,4117],{"class":583},"    Arbitrary",[89,4119,4120],{"class":119}," File",[89,4122,4123],{"class":119}," Write",[89,4125,4126],{"class":119}," via",[89,4128,4129],{"class":119}," Decompress",[89,4131,4077],{"class":119},[89,4133,4134],{"class":119}," https:\u002F\u002Fnpmjs.com\u002Fadvisories\u002F1608\n",[89,4136,4137,4140,4143,4146,4149],{"class":91,"line":728},[89,4138,4139],{"class":583},"    Fix",[89,4141,4142],{"class":119}," available:",[89,4144,4145],{"class":119}," `",[89,4147,4148],{"class":583},"npm",[89,4150,4151],{"class":119}," install axios@^0.21.1`\n",[89,4153,4154],{"class":91,"line":746},[89,4155,4156],{"class":119},"    ```\n",[89,4158,4159],{"class":91,"line":758},[89,4160,650],{"emptyLinePlaceholder":184},[89,4162,4163,4166],{"class":91,"line":769},[89,4164,4165],{"class":583},"Antes",[89,4167,4168],{"class":119}," de añadir una dependencia, mira si tiene mantenimiento activo, cuánta gente la usa, si hay incidencias de seguridad abiertas y cuándo se actualizó por última vez.\n",[89,4170,4171],{"class":91,"line":779},[89,4172,650],{"emptyLinePlaceholder":184},[89,4174,4175,4178,4181,4184,4187,4190,4192,4195,4198,4200],{"class":91,"line":793},[89,4176,4177],{"class":583},"Sube",[89,4179,4180],{"class":119}," al repositorio tu `",[89,4182,4183],{"class":583},"package-lock.json",[89,4185,4186],{"class":119},"` ",[89,4188,4189],{"class":583},"o",[89,4191,4145],{"class":119},[89,4193,4194],{"class":583},"yarn.lock",[89,4196,4197],{"class":119},"`",[89,4199,44],{"class":595},[89,4201,4202],{"class":119}," Esos archivos fijan las versiones exactas, de modo que tu equipo y tu pipeline de CI compilan sobre el mismo conjunto probado.\n",[89,4204,4205],{"class":91,"line":799},[89,4206,650],{"emptyLinePlaceholder":184},[89,4208,4209],{"class":91,"line":808},[89,4210,4211],{"class":95},"## 4. Secretos que acaban en el bundle\n",[89,4213,4214],{"class":91,"line":2055},[89,4215,650],{"emptyLinePlaceholder":184},[89,4217,4218,4221],{"class":91,"line":3983},[89,4219,4220],{"class":583},"Esto",[89,4222,4223],{"class":119}," suena evidente y sigue ocurriendo con frecuencia. Las claves de API escritas directamente en componentes o archivos JavaScript acaban en el bundle del cliente, lo que las vuelve públicas. Los mensajes de error detallados filtran información sobre el backend. Variables de entorno que nunca debieron llegar al cliente terminan empaquetadas.\n",[89,4225,4226],{"class":91,"line":3994},[89,4227,650],{"emptyLinePlaceholder":184},[89,4229,4230,4233,4236,4239,4242,4245,4247,4250],{"class":91,"line":4005},[89,4231,4232],{"class":583},"Nunca",[89,4234,4235],{"class":119}," escribas secretos en el código del frontend. Si el frontend necesita una API que requiere clave, ponte en medio: tu aplicación Vue llama a tu backend y tu backend llama a la API de terceros con la clave. Es decir, en lugar de hacer `",[89,4237,4238],{"class":583},"axios.get(",[89,4240,4241],{"class":583},"'https:\u002F\u002Fapi.thirdparty.com\u002Fdata?apiKey=YOUR_SECRET_KEY'",[89,4243,4244],{"class":102},")",[89,4246,4186],{"class":119},[89,4248,4249],{"class":583},"desde",[89,4251,4252],{"class":119}," el navegador, la petición pasa por un servidor que controlas.\n",[89,4254,4256],{"class":91,"line":4255},23,[89,4257,650],{"emptyLinePlaceholder":184},[89,4259,4261,4264,4267,4270,4272,4275,4278,4281,4283,4285],{"class":91,"line":4260},24,[89,4262,4263],{"class":583},"Las",[89,4265,4266],{"class":119}," variables de entorno piden el mismo cuidado. Vue CLI expone al bundle del cliente todo lo que lleve el prefijo `",[89,4268,4269],{"class":583},"VUE_APP_",[89,4271,4197],{"class":119},[89,4273,4274],{"class":583},",",[89,4276,4277],{"class":119}," y Vite hace lo mismo con `",[89,4279,4280],{"class":583},"VITE_",[89,4282,4197],{"class":119},[89,4284,44],{"class":595},[89,4286,4287],{"class":119}," Ahí van los endpoints de API, no las claves. Mantén los secretos de producción fuera del control de versiones y gestiónalos desde la configuración de tu proveedor de alojamiento.\n",[89,4289,4291],{"class":91,"line":4290},25,[89,4292,650],{"emptyLinePlaceholder":184},[89,4294,4296,4299],{"class":91,"line":4295},26,[89,4297,4298],{"class":583},"Trata",[89,4300,4301],{"class":119}," los errores para que tampoco filtren nada. Un error del backend en crudo puede revelar el esquema de la base de datos o rutas del servidor, así que muestra al usuario algo genérico y registra el detalle en el servidor.\n",[89,4303,4305],{"class":91,"line":4304},27,[89,4306,650],{"emptyLinePlaceholder":184},[89,4308,4310,4313],{"class":91,"line":4309},28,[89,4311,4312],{"class":119},"```",[89,4314,4315],{"class":583},"html\n",[89,4317,4319,4321,4323],{"class":91,"line":4318},29,[89,4320,3023],{"class":569},[89,4322,3429],{"class":102},[89,4324,3338],{"class":569},[89,4326,4328,4330,4332],{"class":91,"line":4327},30,[89,4329,3364],{"class":569},[89,4331,3438],{"class":583},[89,4333,3338],{"class":102},[89,4335,4337,4339,4342,4345,4347,4350,4353,4355,4358,4361],{"class":91,"line":4336},31,[89,4338,3445],{"class":569},[89,4340,4341],{"class":583},"button",[89,4343,4344],{"class":119}," @click=\"fetchData\"",[89,4346,2102],{"class":569},[89,4348,4349],{"class":119},"Fetch",[89,4351,4352],{"class":119}," Data",[89,4354,3023],{"class":569},[89,4356,4357],{"class":119},"\u002Fbutto",[89,4359,4360],{"class":102},"n",[89,4362,3338],{"class":569},[89,4364,4366,4368,4370,4373,4375,4378,4381,4384,4386,4388,4390],{"class":91,"line":4365},32,[89,4367,3445],{"class":569},[89,4369,15],{"class":583},[89,4371,4372],{"class":119}," v-if=\"error\"",[89,4374,2102],{"class":569},[89,4376,4377],{"class":119},"{{",[89,4379,4380],{"class":119}," errorMessage",[89,4382,4383],{"class":119}," }}",[89,4385,3023],{"class":569},[89,4387,2326],{"class":119},[89,4389,15],{"class":102},[89,4391,3338],{"class":569},[89,4393,4395,4397,4399,4402,4404,4406,4408,4410,4412,4415,4418],{"class":91,"line":4394},33,[89,4396,3445],{"class":569},[89,4398,3438],{"class":583},[89,4400,4401],{"class":119}," v-if=\"data\"",[89,4403,2102],{"class":569},[89,4405,4377],{"class":119},[89,4407,635],{"class":119},[89,4409,4383],{"class":119},[89,4411,3023],{"class":569},[89,4413,4414],{"class":119},"\u002Fdi",[89,4416,4417],{"class":102},"v",[89,4419,3338],{"class":569},[89,4421,4423,4425,4428],{"class":91,"line":4422},34,[89,4424,3364],{"class":569},[89,4426,4427],{"class":583},"\u002Fdiv",[89,4429,3338],{"class":102},[89,4431,4433,4435,4438],{"class":91,"line":4432},35,[89,4434,3023],{"class":569},[89,4436,4437],{"class":102},"\u002Ftemplate",[89,4439,3338],{"class":569},[89,4441,4443],{"class":91,"line":4442},36,[89,4444,650],{"emptyLinePlaceholder":184},[89,4446,4448,4450,4453],{"class":91,"line":4447},37,[89,4449,3023],{"class":569},[89,4451,4452],{"class":102},"script setup",[89,4454,3338],{"class":569},[89,4456,4458,4461,4464,4466,4469,4472,4475],{"class":91,"line":4457},38,[89,4459,4460],{"class":583},"  import",[89,4462,4463],{"class":119}," {",[89,4465,602],{"class":119},[89,4467,4468],{"class":119}," }",[89,4470,4471],{"class":119}," from",[89,4473,4474],{"class":119}," \"vue\"",[89,4476,790],{"class":102},[89,4478,4480,4482,4485,4487,4489],{"class":91,"line":4479},39,[89,4481,4460],{"class":583},[89,4483,4484],{"class":119}," axios",[89,4486,4471],{"class":119},[89,4488,3819],{"class":119},[89,4490,790],{"class":102},[89,4492,4494],{"class":91,"line":4493},40,[89,4495,650],{"emptyLinePlaceholder":184},[89,4497,4499,4501,4503,4505,4507,4509,4511],{"class":91,"line":4498},41,[89,4500,592],{"class":583},[89,4502,635],{"class":119},[89,4504,599],{"class":119},[89,4506,602],{"class":119},[89,4508,580],{"class":102},[89,4510,626],{"class":583},[89,4512,610],{"class":102},[89,4514,4516,4518,4520,4522,4524,4526,4529],{"class":91,"line":4515},42,[89,4517,592],{"class":583},[89,4519,617],{"class":119},[89,4521,599],{"class":119},[89,4523,602],{"class":119},[89,4525,580],{"class":102},[89,4527,4528],{"class":595},"false",[89,4530,610],{"class":102},[89,4532,4534,4536,4538,4540,4542,4544,4547],{"class":91,"line":4533},43,[89,4535,592],{"class":583},[89,4537,4380],{"class":119},[89,4539,599],{"class":119},[89,4541,602],{"class":119},[89,4543,580],{"class":102},[89,4545,4546],{"class":583},"\"\"",[89,4548,610],{"class":102},[89,4550,4552],{"class":91,"line":4551},44,[89,4553,650],{"emptyLinePlaceholder":184},[89,4555,4557,4559,4561,4563,4565],{"class":91,"line":4556},45,[89,4558,592],{"class":583},[89,4560,577],{"class":119},[89,4562,599],{"class":119},[89,4564,2634],{"class":119},[89,4566,4567],{"class":102}," () => {\n",[89,4569,4571,4574],{"class":91,"line":4570},46,[89,4572,4573],{"class":583},"    try",[89,4575,658],{"class":119},[89,4577,4579,4582,4584,4586,4588,4591,4593,4596],{"class":91,"line":4578},47,[89,4580,4581],{"class":583},"      const",[89,4583,666],{"class":119},[89,4585,599],{"class":119},[89,4587,671],{"class":119},[89,4589,4590],{"class":119}," axios.get",[89,4592,580],{"class":102},[89,4594,4595],{"class":583},"\"\u002Fapi\u002Fsensitive-data\"",[89,4597,610],{"class":102},[89,4599,4601,4604,4606,4609],{"class":91,"line":4600},48,[89,4602,4603],{"class":583},"      data.value",[89,4605,599],{"class":119},[89,4607,4608],{"class":119}," response.data",[89,4610,790],{"class":102},[89,4612,4614,4617,4619,4621],{"class":91,"line":4613},49,[89,4615,4616],{"class":583},"      error.value",[89,4618,599],{"class":119},[89,4620,787],{"class":595},[89,4622,790],{"class":102},[89,4624,4626,4629,4632],{"class":91,"line":4625},50,[89,4627,4628],{"class":102},"    } catch (",[89,4630,4631],{"class":583},"err",[89,4633,587],{"class":102},[89,4635,4637,4639,4641,4644],{"class":91,"line":4636},51,[89,4638,4616],{"class":583},[89,4640,599],{"class":119},[89,4642,4643],{"class":595}," true",[89,4645,790],{"class":102},[89,4647,4649,4652],{"class":91,"line":4648},52,[89,4650,4651],{"class":583},"      \u002F\u002F",[89,4653,4654],{"class":119}," Don't show the raw error to the user!\n",[89,4656,4658,4661],{"class":91,"line":4657},53,[89,4659,4660],{"class":119},"      \u002F\u002F console.error(err.response.data);",[89,4662,4663],{"class":119}," \u002F\u002F Bad practice\n",[89,4665,4667],{"class":91,"line":4666},54,[89,4668,4669],{"class":119},"      errorMessage.value =\n",[89,4671,4673],{"class":91,"line":4672},55,[89,4674,4675],{"class":119},"        \"Oops! Something went wrong. Please try again later.\";\n",[89,4677,4679],{"class":91,"line":4678},56,[89,4680,4681],{"class":119},"      \u002F\u002F Log the detailed error to a server-side logging service\n",[89,4683,4685],{"class":91,"line":4684},57,[89,4686,725],{"class":119},[89,4688,4690],{"class":91,"line":4689},58,[89,4691,4692],{"class":119},"  };\n",[89,4694,4696],{"class":91,"line":4695},59,[89,4697,4698],{"class":119},"\u003C\u002Fscript>\n",[15,4700,4701],{},"Da por hecho que todo lo que hay en tu frontend compilado es público, porque lo es.",[10,4703,4705],{"id":4704},"_5-autenticación-y-autorización-en-el-cliente","5. Autenticación y autorización en el cliente",[15,4707,4708],{},"Esto no es exclusivo de Vue, pero aparece constantemente. Aplicar la autorización solo en el frontend es poner un portero en la puerta principal y dejar la trasera abierta.",[15,4710,4711,4712,4715],{},"Se manifiesta al ocultar partes de la interfaz según el rol, al asumir que el backend ya comprobará los permisos, o al guardar una bandera ",[34,4713,4714],{},"isAdmin"," en Pinia o Vuex y tratarla como si fuera vinculante.",[15,4717,4718],{},"Todo endpoint que requiera autenticación debe verificar el token o la sesión, y todo endpoint que requiera un permiso debe comprobarlo antes de hacer nada. El frontend está para la experiencia de uso: ocultar un botón que alguien no puede usar es razonable, pero no es seguridad, y cualquiera puede saltarse tu JavaScript.",[15,4720,4721,4722,4725,4726,4729],{},"Guarda los tokens con cuidado. Las cookies ",[34,4723,4724],{},"HttpOnly"," que establece el backend mantienen los tokens fuera del alcance del JavaScript del cliente, lo que limita el daño de un fallo de XSS. Si usas ",[34,4727,4728],{},"localStorage",", ten claro que estás confiando por completo en que no haya XSS en ninguna parte.",[80,4731,4733],{"className":560,"code":4732,"language":562,"meta":85,"style":85},"\u002F\u002F BAD: Relying on client-side state for authorization\n\u002F\u002F In a real app, an attacker could manipulate `isAdmin` in their browser.\nconst user = {\n  name: \"Alice\",\n  isAdmin: false, \u002F\u002F Imagine an attacker changing this to true in the console\n};\n\nif (user.isAdmin) {\n  \u002F\u002F Show admin button - this is for UX, not security!\n}\n\n\u002F\u002F GOOD: Backend always validates permissions\nasync function deleteUser(userId) {\n  try {\n    \u002F\u002F Frontend sends the request\n    await axios.delete(`\u002Fapi\u002Fusers\u002F${userId}`);\n    alert(\"User deleted!\");\n  } catch (error) {\n    \u002F\u002F Backend rejects if user is not authorized\n    if (error.response && error.response.status === 403) {\n      alert(\"You are not authorized to perform this action.\");\n    } else {\n      alert(\"An error occurred.\");\n    }\n  }\n}\n",[34,4734,4735,4740,4745,4756,4766,4778,4782,4786,4793,4798,4802,4806,4811,4826,4832,4837,4859,4870,4878,4883,4903,4915,4923,4934,4938,4942],{"__ignoreMap":85},[89,4736,4737],{"class":91,"line":92},[89,4738,4739],{"class":95},"\u002F\u002F BAD: Relying on client-side state for authorization\n",[89,4741,4742],{"class":91,"line":99},[89,4743,4744],{"class":95},"\u002F\u002F In a real app, an attacker could manipulate `isAdmin` in their browser.\n",[89,4746,4747,4749,4752,4754],{"class":91,"line":106},[89,4748,2306],{"class":569},[89,4750,4751],{"class":595}," user",[89,4753,599],{"class":569},[89,4755,658],{"class":102},[89,4757,4758,4761,4764],{"class":91,"line":116},[89,4759,4760],{"class":102},"  name: ",[89,4762,4763],{"class":119},"\"Alice\"",[89,4765,123],{"class":102},[89,4767,4768,4771,4773,4775],{"class":91,"line":126},[89,4769,4770],{"class":102},"  isAdmin: ",[89,4772,4528],{"class":595},[89,4774,1472],{"class":102},[89,4776,4777],{"class":95},"\u002F\u002F Imagine an attacker changing this to true in the console\n",[89,4779,4780],{"class":91,"line":134},[89,4781,2651],{"class":102},[89,4783,4784],{"class":91,"line":142},[89,4785,650],{"emptyLinePlaceholder":184},[89,4787,4788,4790],{"class":91,"line":150},[89,4789,3932],{"class":569},[89,4791,4792],{"class":102}," (user.isAdmin) {\n",[89,4794,4795],{"class":91,"line":156},[89,4796,4797],{"class":95},"  \u002F\u002F Show admin button - this is for UX, not security!\n",[89,4799,4800],{"class":91,"line":162},[89,4801,165],{"class":102},[89,4803,4804],{"class":91,"line":728},[89,4805,650],{"emptyLinePlaceholder":184},[89,4807,4808],{"class":91,"line":746},[89,4809,4810],{"class":95},"\u002F\u002F GOOD: Backend always validates permissions\n",[89,4812,4813,4815,4817,4820,4822,4824],{"class":91,"line":758},[89,4814,570],{"class":569},[89,4816,573],{"class":569},[89,4818,4819],{"class":576}," deleteUser",[89,4821,580],{"class":102},[89,4823,2362],{"class":583},[89,4825,587],{"class":102},[89,4827,4828,4830],{"class":91,"line":769},[89,4829,655],{"class":569},[89,4831,658],{"class":102},[89,4833,4834],{"class":91,"line":779},[89,4835,4836],{"class":95},"    \u002F\u002F Frontend sends the request\n",[89,4838,4839,4842,4845,4848,4850,4853,4855,4857],{"class":91,"line":793},[89,4840,4841],{"class":569},"    await",[89,4843,4844],{"class":102}," axios.",[89,4846,4847],{"class":576},"delete",[89,4849,580],{"class":102},[89,4851,4852],{"class":119},"`\u002Fapi\u002Fusers\u002F${",[89,4854,2362],{"class":102},[89,4856,718],{"class":119},[89,4858,610],{"class":102},[89,4860,4861,4863,4865,4868],{"class":91,"line":799},[89,4862,3374],{"class":576},[89,4864,580],{"class":102},[89,4866,4867],{"class":119},"\"User deleted!\"",[89,4869,610],{"class":102},[89,4871,4872,4874,4876],{"class":91,"line":808},[89,4873,749],{"class":102},[89,4875,752],{"class":569},[89,4877,2510],{"class":102},[89,4879,4880],{"class":91,"line":2055},[89,4881,4882],{"class":95},"    \u002F\u002F Backend rejects if user is not authorized\n",[89,4884,4885,4887,4890,4893,4896,4898,4901],{"class":91,"line":3983},[89,4886,682],{"class":569},[89,4888,4889],{"class":102}," (error.response ",[89,4891,4892],{"class":569},"&&",[89,4894,4895],{"class":102}," error.response.status ",[89,4897,373],{"class":569},[89,4899,4900],{"class":595}," 403",[89,4902,587],{"class":102},[89,4904,4905,4908,4910,4913],{"class":91,"line":3994},[89,4906,4907],{"class":576},"      alert",[89,4909,580],{"class":102},[89,4911,4912],{"class":119},"\"You are not authorized to perform this action.\"",[89,4914,610],{"class":102},[89,4916,4917,4919,4921],{"class":91,"line":4005},[89,4918,3090],{"class":102},[89,4920,3093],{"class":569},[89,4922,658],{"class":102},[89,4924,4925,4927,4929,4932],{"class":91,"line":4255},[89,4926,4907],{"class":576},[89,4928,580],{"class":102},[89,4930,4931],{"class":119},"\"An error occurred.\"",[89,4933,610],{"class":102},[89,4935,4936],{"class":91,"line":4260},[89,4937,725],{"class":102},[89,4939,4940],{"class":91,"line":4290},[89,4941,796],{"class":102},[89,4943,4944],{"class":91,"line":4295},[89,4945,165],{"class":102},[10,4947,4949],{"id":4948},"en-resumen","En resumen",[15,4951,4952,4953,4955],{},"Vue te da una base segura y el resto queda de nuestro lado. Sanea la entrada del usuario, sobre todo cualquier cosa que se acerque a ",[34,4954,3405],{},". Implementa tokens CSRF junto con tu backend. Audita y actualiza las dependencias. Mantén los secretos fuera del bundle. Y valida la autenticación y la autorización en el servidor, siempre.",[170,4957,4958],{},"html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sHaLz, html code.shiki .sHaLz{--shiki-default:#7EE787;--shiki-default-font-weight:bold}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}",{"title":85,"searchDepth":99,"depth":99,"links":4960},[4961,4962,4963,4964,4965,4966],{"id":3303,"depth":99,"text":3304},{"id":3310,"depth":99,"text":3311},{"id":3739,"depth":99,"text":3740},{"id":4013,"depth":99,"text":4014},{"id":4704,"depth":99,"text":4705},{"id":4948,"depth":99,"text":4949},"2023-09-15","Cinco vulnerabilidades habituales en aplicaciones Vue, con la solución de cada una.",{},"\u002Fblog\u002Friesgos-de-seguridad-en-proyectos-vue",{"title":3289,"description":4968},"blog\u002Friesgos-de-seguridad-en-proyectos-vue",[1177,4974,562,4975,418],"security","frontend","security-risks-in-vuejs-applications","Gd45EEA7MqKZWm13AJBctxWNj6XcbXxq_O4tntBI3HA",{"id":4979,"title":4980,"body":4981,"category":178,"date":8305,"description":8306,"draft":181,"extension":182,"meta":8307,"navigation":184,"path":8308,"seo":8309,"stem":8310,"tags":8311,"translationKey":8314,"videoId":193,"__hash__":8315},"blog_es\u002Fblog\u002Ftecnicas-avanzadas-de-vue-3.md","Siete técnicas avanzadas de Vue 3 que conviene conocer",{"type":7,"value":4982,"toc":8295},[4983,4986,4993,4999,5002,5005,5284,5287,5494,5498,5509,5512,5583,6144,6155,6159,6166,6169,6365,6370,6576,6585,6589,6602,6725,6728,6777,6793,6797,6800,6803,6976,6979,7140,7144,7147,7150,7391,7688,7700,7704,7707,7722,7844,7851,7929,7936,8138,8278,8289,8292],[15,4984,4985],{},"Vue 3 lleva tiempo formando parte del trabajo diario de frontend. Es intuitivo, resulta agradable construir con él y lo básico te lleva bastante lejos. Lo que sigue está más allá de ese punto: siete técnicas que me han resultado útiles en mis propios proyectos.",[10,4987,4989,4990],{"id":4988},"_1-la-composition-api-más-allá-de-setup","1. La Composition API más allá de ",[34,4991,4992],{},"setup()",[15,4994,4995,4996,4998],{},"Puedes objetar que la Composition API ya no es algo avanzado, y es justo. Casi todos nos manejamos con ",[34,4997,4992],{}," y las referencias reactivas. La parte que suele quedarse sin usar es extraer lógica a composables.",[15,5000,5001],{},"Piensa en un composable como una función que encapsula lógica con estado. Los componentes adelgazan porque solo se ocupan del render, la lógica se agrupa por funcionalidad en lugar de por opción, y esa misma lógica se comparte sin los problemas que causaban los mixins.",[15,5003,5004],{},"Supón que tienes una funcionalidad que consulta datos y controla los estados de carga y error. En vez de repetirla en cada componente:",[80,5006,5008],{"className":560,"code":5007,"language":562,"meta":85,"style":85},"\u002F\u002F composables\u002FuseFetch.js\nimport { ref, onMounted } from \"vue\";\n\nexport function useFetch(url) {\n  const data = ref(null);\n  const error = ref(null);\n  const loading = ref(true);\n\n  async function fetchData() {\n    loading.value = true;\n    error.value = null;\n    try {\n      const res = await fetch(url);\n      if (!res.ok) {\n        throw new Error(`HTTP error! status: ${res.status}`);\n      }\n      data.value = await res.json();\n    } catch (e) {\n      error.value = e;\n    } finally {\n      loading.value = false;\n    }\n  }\n\n  onMounted(fetchData);\n\n  return { data, error, loading, fetchData };\n}\n",[34,5009,5010,5015,5028,5032,5048,5064,5080,5097,5101,5112,5123,5134,5140,5155,5167,5191,5196,5212,5220,5230,5238,5249,5253,5257,5261,5269,5273,5280],{"__ignoreMap":85},[89,5011,5012],{"class":91,"line":92},[89,5013,5014],{"class":95},"\u002F\u002F composables\u002FuseFetch.js\n",[89,5016,5017,5019,5022,5024,5026],{"class":91,"line":99},[89,5018,3490],{"class":569},[89,5020,5021],{"class":102}," { ref, onMounted } ",[89,5023,3496],{"class":569},[89,5025,4474],{"class":119},[89,5027,790],{"class":102},[89,5029,5030],{"class":91,"line":106},[89,5031,650],{"emptyLinePlaceholder":184},[89,5033,5034,5037,5039,5042,5044,5046],{"class":91,"line":116},[89,5035,5036],{"class":569},"export",[89,5038,573],{"class":569},[89,5040,5041],{"class":576}," useFetch",[89,5043,580],{"class":102},[89,5045,584],{"class":583},[89,5047,587],{"class":102},[89,5049,5050,5052,5054,5056,5058,5060,5062],{"class":91,"line":126},[89,5051,592],{"class":569},[89,5053,635],{"class":595},[89,5055,599],{"class":569},[89,5057,602],{"class":576},[89,5059,580],{"class":102},[89,5061,626],{"class":595},[89,5063,610],{"class":102},[89,5065,5066,5068,5070,5072,5074,5076,5078],{"class":91,"line":134},[89,5067,592],{"class":569},[89,5069,617],{"class":595},[89,5071,599],{"class":569},[89,5073,602],{"class":576},[89,5075,580],{"class":102},[89,5077,626],{"class":595},[89,5079,610],{"class":102},[89,5081,5082,5084,5087,5089,5091,5093,5095],{"class":91,"line":142},[89,5083,592],{"class":569},[89,5085,5086],{"class":595}," loading",[89,5088,599],{"class":569},[89,5090,602],{"class":576},[89,5092,580],{"class":102},[89,5094,607],{"class":595},[89,5096,610],{"class":102},[89,5098,5099],{"class":91,"line":150},[89,5100,650],{"emptyLinePlaceholder":184},[89,5102,5103,5106,5108,5110],{"class":91,"line":156},[89,5104,5105],{"class":569},"  async",[89,5107,573],{"class":569},[89,5109,577],{"class":576},[89,5111,2614],{"class":102},[89,5113,5114,5117,5119,5121],{"class":91,"line":162},[89,5115,5116],{"class":102},"    loading.value ",[89,5118,377],{"class":569},[89,5120,4643],{"class":595},[89,5122,790],{"class":102},[89,5124,5125,5127,5129,5132],{"class":91,"line":728},[89,5126,761],{"class":102},[89,5128,377],{"class":569},[89,5130,5131],{"class":595}," null",[89,5133,790],{"class":102},[89,5135,5136,5138],{"class":91,"line":746},[89,5137,4573],{"class":569},[89,5139,658],{"class":102},[89,5141,5142,5144,5147,5149,5151,5153],{"class":91,"line":758},[89,5143,4581],{"class":569},[89,5145,5146],{"class":595}," res",[89,5148,599],{"class":569},[89,5150,671],{"class":569},[89,5152,674],{"class":576},[89,5154,677],{"class":102},[89,5156,5157,5160,5162,5164],{"class":91,"line":769},[89,5158,5159],{"class":569},"      if",[89,5161,685],{"class":102},[89,5163,688],{"class":569},[89,5165,5166],{"class":102},"res.ok) {\n",[89,5168,5169,5172,5174,5176,5178,5180,5183,5185,5187,5189],{"class":91,"line":779},[89,5170,5171],{"class":569},"        throw",[89,5173,699],{"class":569},[89,5175,702],{"class":576},[89,5177,580],{"class":102},[89,5179,707],{"class":119},[89,5181,5182],{"class":102},"res",[89,5184,44],{"class":119},[89,5186,715],{"class":102},[89,5188,718],{"class":119},[89,5190,610],{"class":102},[89,5192,5193],{"class":91,"line":793},[89,5194,5195],{"class":102},"      }\n",[89,5197,5198,5201,5203,5205,5208,5210],{"class":91,"line":799},[89,5199,5200],{"class":102},"      data.value ",[89,5202,377],{"class":569},[89,5204,671],{"class":569},[89,5206,5207],{"class":102}," res.",[89,5209,84],{"class":576},[89,5211,743],{"class":102},[89,5213,5214,5216,5218],{"class":91,"line":808},[89,5215,3090],{"class":102},[89,5217,752],{"class":569},[89,5219,755],{"class":102},[89,5221,5222,5225,5227],{"class":91,"line":2055},[89,5223,5224],{"class":102},"      error.value ",[89,5226,377],{"class":569},[89,5228,5229],{"class":102}," e;\n",[89,5231,5232,5234,5236],{"class":91,"line":3983},[89,5233,3090],{"class":102},[89,5235,774],{"class":569},[89,5237,658],{"class":102},[89,5239,5240,5243,5245,5247],{"class":91,"line":3994},[89,5241,5242],{"class":102},"      loading.value ",[89,5244,377],{"class":569},[89,5246,787],{"class":595},[89,5248,790],{"class":102},[89,5250,5251],{"class":91,"line":4005},[89,5252,725],{"class":102},[89,5254,5255],{"class":91,"line":4255},[89,5256,796],{"class":102},[89,5258,5259],{"class":91,"line":4260},[89,5260,650],{"emptyLinePlaceholder":184},[89,5262,5263,5266],{"class":91,"line":4290},[89,5264,5265],{"class":576},"  onMounted",[89,5267,5268],{"class":102},"(fetchData);\n",[89,5270,5271],{"class":91,"line":4295},[89,5272,650],{"emptyLinePlaceholder":184},[89,5274,5275,5277],{"class":91,"line":4304},[89,5276,802],{"class":569},[89,5278,5279],{"class":102}," { data, error, loading, fetchData };\n",[89,5281,5282],{"class":91,"line":4309},[89,5283,165],{"class":102},[15,5285,5286],{},"Después cualquier componente puede usarla:",[80,5288,5291],{"className":5289,"code":5290,"language":1177,"meta":85,"style":85},"language-vue shiki shiki-themes github-dark-default","\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cdiv v-if=\"loading\">Loading posts...\u003C\u002Fdiv>\n    \u003Cdiv v-else-if=\"error\">Error: {{ error.message }}\u003C\u002Fdiv>\n    \u003Cul v-else>\n      \u003Cli v-for=\"post in data\" :key=\"post.id\">{{ post.title }}\u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { useFetch } from \"@\u002Fcomposables\u002FuseFetch\";\n\nconst { data, error, loading } = useFetch(\n  \"https:\u002F\u002Fjsonplaceholder.typicode.com\u002Fposts\"\n);\n\u003C\u002Fscript>\n",[34,5292,5293,5301,5309,5330,5351,5362,5392,5401,5409,5417,5421,5431,5445,5449,5477,5482,5486],{"__ignoreMap":85},[89,5294,5295,5297,5299],{"class":91,"line":92},[89,5296,3023],{"class":102},[89,5298,3429],{"class":109},[89,5300,3338],{"class":102},[89,5302,5303,5305,5307],{"class":91,"line":99},[89,5304,3364],{"class":102},[89,5306,3438],{"class":109},[89,5308,3338],{"class":102},[89,5310,5311,5313,5315,5318,5320,5323,5326,5328],{"class":91,"line":106},[89,5312,3445],{"class":102},[89,5314,3438],{"class":109},[89,5316,5317],{"class":595}," v-if",[89,5319,377],{"class":102},[89,5321,5322],{"class":119},"\"loading\"",[89,5324,5325],{"class":102},">Loading posts...\u003C\u002F",[89,5327,3438],{"class":109},[89,5329,3338],{"class":102},[89,5331,5332,5334,5336,5339,5341,5344,5347,5349],{"class":91,"line":116},[89,5333,3445],{"class":102},[89,5335,3438],{"class":109},[89,5337,5338],{"class":595}," v-else-if",[89,5340,377],{"class":102},[89,5342,5343],{"class":119},"\"error\"",[89,5345,5346],{"class":102},">Error: {{ error.message }}\u003C\u002F",[89,5348,3438],{"class":109},[89,5350,3338],{"class":102},[89,5352,5353,5355,5357,5360],{"class":91,"line":126},[89,5354,3445],{"class":102},[89,5356,56],{"class":109},[89,5358,5359],{"class":595}," v-else",[89,5361,3338],{"class":102},[89,5363,5364,5367,5369,5372,5374,5377,5380,5382,5385,5388,5390],{"class":91,"line":134},[89,5365,5366],{"class":102},"      \u003C",[89,5368,59],{"class":109},[89,5370,5371],{"class":595}," v-for",[89,5373,377],{"class":102},[89,5375,5376],{"class":119},"\"post in data\"",[89,5378,5379],{"class":595}," :key",[89,5381,377],{"class":102},[89,5383,5384],{"class":119},"\"post.id\"",[89,5386,5387],{"class":102},">{{ post.title }}\u003C\u002F",[89,5389,59],{"class":109},[89,5391,3338],{"class":102},[89,5393,5394,5397,5399],{"class":91,"line":142},[89,5395,5396],{"class":102},"    \u003C\u002F",[89,5398,56],{"class":109},[89,5400,3338],{"class":102},[89,5402,5403,5405,5407],{"class":91,"line":150},[89,5404,3386],{"class":102},[89,5406,3438],{"class":109},[89,5408,3338],{"class":102},[89,5410,5411,5413,5415],{"class":91,"line":156},[89,5412,3395],{"class":102},[89,5414,3429],{"class":109},[89,5416,3338],{"class":102},[89,5418,5419],{"class":91,"line":162},[89,5420,650],{"emptyLinePlaceholder":184},[89,5422,5423,5425,5427,5429],{"class":91,"line":728},[89,5424,3023],{"class":102},[89,5426,3367],{"class":109},[89,5428,3483],{"class":595},[89,5430,3338],{"class":102},[89,5432,5433,5435,5438,5440,5443],{"class":91,"line":746},[89,5434,3490],{"class":569},[89,5436,5437],{"class":102}," { useFetch } ",[89,5439,3496],{"class":569},[89,5441,5442],{"class":119}," \"@\u002Fcomposables\u002FuseFetch\"",[89,5444,790],{"class":102},[89,5446,5447],{"class":91,"line":758},[89,5448,650],{"emptyLinePlaceholder":184},[89,5450,5451,5453,5456,5458,5460,5462,5464,5467,5470,5472,5474],{"class":91,"line":769},[89,5452,2306],{"class":569},[89,5454,5455],{"class":102}," { ",[89,5457,1248],{"class":595},[89,5459,1472],{"class":102},[89,5461,556],{"class":595},[89,5463,1472],{"class":102},[89,5465,5466],{"class":595},"loading",[89,5468,5469],{"class":102}," } ",[89,5471,377],{"class":569},[89,5473,5041],{"class":576},[89,5475,5476],{"class":102},"(\n",[89,5478,5479],{"class":91,"line":779},[89,5480,5481],{"class":119},"  \"https:\u002F\u002Fjsonplaceholder.typicode.com\u002Fposts\"\n",[89,5483,5484],{"class":91,"line":793},[89,5485,610],{"class":102},[89,5487,5488,5490,5492],{"class":91,"line":799},[89,5489,3395],{"class":102},[89,5491,3367],{"class":109},[89,5493,3338],{"class":102},[10,5495,5497],{"id":5496},"_2-teleport-para-elementos-que-necesitan-escapar","2. Teleport, para elementos que necesitan escapar",[15,5499,5500,5501,5504,5505,5508],{},"Si has peleado con guerras de ",[34,5502,5503],{},"z-index"," al renderizar modales, tooltips o notificaciones desde un componente muy anidado, la respuesta es ",[34,5506,5507],{},"Teleport",". Renderiza una parte de tu plantilla en otro punto del DOM, fuera de la jerarquía del componente, manteniendo la reactividad con su estado.",[15,5510,5511],{},"Encaja con modales y diálogos, avisos y notificaciones, tooltips y desplegables.",[80,5513,5515],{"className":3320,"code":5514,"language":3322,"meta":85,"style":85},"\u003C!-- In your App.vue or main layout file -->\n\u003Cbody>\n  \u003Cdiv id=\"app\">\u003C\u002Fdiv>\n  \u003Cdiv id=\"modals-container\">\u003C\u002Fdiv>\n  \u003C!-- This is our teleport target -->\n\u003C\u002Fbody>\n",[34,5516,5517,5522,5531,5551,5570,5575],{"__ignoreMap":85},[89,5518,5519],{"class":91,"line":92},[89,5520,5521],{"class":95},"\u003C!-- In your App.vue or main layout file -->\n",[89,5523,5524,5526,5529],{"class":91,"line":99},[89,5525,3023],{"class":102},[89,5527,5528],{"class":109},"body",[89,5530,3338],{"class":102},[89,5532,5533,5535,5537,5540,5542,5545,5547,5549],{"class":91,"line":106},[89,5534,3364],{"class":102},[89,5536,3438],{"class":109},[89,5538,5539],{"class":595}," id",[89,5541,377],{"class":102},[89,5543,5544],{"class":119},"\"app\"",[89,5546,3574],{"class":102},[89,5548,3438],{"class":109},[89,5550,3338],{"class":102},[89,5552,5553,5555,5557,5559,5561,5564,5566,5568],{"class":91,"line":116},[89,5554,3364],{"class":102},[89,5556,3438],{"class":109},[89,5558,5539],{"class":595},[89,5560,377],{"class":102},[89,5562,5563],{"class":119},"\"modals-container\"",[89,5565,3574],{"class":102},[89,5567,3438],{"class":109},[89,5569,3338],{"class":102},[89,5571,5572],{"class":91,"line":126},[89,5573,5574],{"class":95},"  \u003C!-- This is our teleport target -->\n",[89,5576,5577,5579,5581],{"class":91,"line":134},[89,5578,3395],{"class":102},[89,5580,5528],{"class":109},[89,5582,3338],{"class":102},[80,5584,5586],{"className":5289,"code":5585,"language":1177,"meta":85,"style":85},"\u003C!-- components\u002FMyModal.vue -->\n\u003Ctemplate>\n  \u003CTeleport to=\"#modals-container\">\n    \u003Cdiv v-if=\"isOpen\" class=\"modal-backdrop\" @click=\"close\">\n      \u003Cdiv class=\"modal-content\" @click.stop>\n        \u003Ch3>My Awesome Modal\u003C\u002Fh3>\n        \u003Cp>This modal content is rendered outside the component!\u003C\u002Fp>\n        \u003Cbutton @click=\"close\">Close\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002FTeleport>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from \"vue\";\n\nconst isOpen = ref(false);\n\nconst open = () => (isOpen.value = true);\nconst close = () => (isOpen.value = false);\n\n\u002F\u002F Expose open\u002Fclose for parent component to use\ndefineExpose({ open, close });\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100vw;\n  height: 100vh;\n  background-color: rgba(0, 0, 0, 0.5);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 1000;\n}\n.modal-content {\n  background: white;\n  padding: 20px;\n  border-radius: 8px;\n  min-width: 300px;\n  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n\u003C\u002Fstyle>\n",[34,5587,5588,5593,5601,5617,5648,5666,5680,5693,5712,5721,5729,5737,5745,5749,5759,5771,5775,5792,5796,5818,5839,5843,5848,5855,5863,5867,5878,5885,5898,5909,5920,5935,5949,5978,5990,6002,6013,6025,6029,6036,6048,6063,6077,6091,6132,6136],{"__ignoreMap":85},[89,5589,5590],{"class":91,"line":92},[89,5591,5592],{"class":95},"\u003C!-- components\u002FMyModal.vue -->\n",[89,5594,5595,5597,5599],{"class":91,"line":99},[89,5596,3023],{"class":102},[89,5598,3429],{"class":109},[89,5600,3338],{"class":102},[89,5602,5603,5605,5607,5610,5612,5615],{"class":91,"line":106},[89,5604,3364],{"class":102},[89,5606,5507],{"class":109},[89,5608,5609],{"class":595}," to",[89,5611,377],{"class":102},[89,5613,5614],{"class":119},"\"#modals-container\"",[89,5616,3338],{"class":102},[89,5618,5619,5621,5623,5625,5627,5630,5633,5635,5638,5641,5643,5646],{"class":91,"line":116},[89,5620,3445],{"class":102},[89,5622,3438],{"class":109},[89,5624,5317],{"class":595},[89,5626,377],{"class":102},[89,5628,5629],{"class":119},"\"isOpen\"",[89,5631,5632],{"class":595}," class",[89,5634,377],{"class":102},[89,5636,5637],{"class":119},"\"modal-backdrop\"",[89,5639,5640],{"class":595}," @click",[89,5642,377],{"class":102},[89,5644,5645],{"class":119},"\"close\"",[89,5647,3338],{"class":102},[89,5649,5650,5652,5654,5656,5658,5661,5664],{"class":91,"line":126},[89,5651,5366],{"class":102},[89,5653,3438],{"class":109},[89,5655,5632],{"class":595},[89,5657,377],{"class":102},[89,5659,5660],{"class":119},"\"modal-content\"",[89,5662,5663],{"class":595}," @click.stop",[89,5665,3338],{"class":102},[89,5667,5668,5671,5673,5676,5678],{"class":91,"line":134},[89,5669,5670],{"class":102},"        \u003C",[89,5672,227],{"class":109},[89,5674,5675],{"class":102},">My Awesome Modal\u003C\u002F",[89,5677,227],{"class":109},[89,5679,3338],{"class":102},[89,5681,5682,5684,5686,5689,5691],{"class":91,"line":142},[89,5683,5670],{"class":102},[89,5685,15],{"class":109},[89,5687,5688],{"class":102},">This modal content is rendered outside the component!\u003C\u002F",[89,5690,15],{"class":109},[89,5692,3338],{"class":102},[89,5694,5695,5697,5699,5701,5703,5705,5708,5710],{"class":91,"line":150},[89,5696,5670],{"class":102},[89,5698,4341],{"class":109},[89,5700,5640],{"class":595},[89,5702,377],{"class":102},[89,5704,5645],{"class":119},[89,5706,5707],{"class":102},">Close\u003C\u002F",[89,5709,4341],{"class":109},[89,5711,3338],{"class":102},[89,5713,5714,5717,5719],{"class":91,"line":156},[89,5715,5716],{"class":102},"      \u003C\u002F",[89,5718,3438],{"class":109},[89,5720,3338],{"class":102},[89,5722,5723,5725,5727],{"class":91,"line":162},[89,5724,5396],{"class":102},[89,5726,3438],{"class":109},[89,5728,3338],{"class":102},[89,5730,5731,5733,5735],{"class":91,"line":728},[89,5732,3386],{"class":102},[89,5734,5507],{"class":109},[89,5736,3338],{"class":102},[89,5738,5739,5741,5743],{"class":91,"line":746},[89,5740,3395],{"class":102},[89,5742,3429],{"class":109},[89,5744,3338],{"class":102},[89,5746,5747],{"class":91,"line":758},[89,5748,650],{"emptyLinePlaceholder":184},[89,5750,5751,5753,5755,5757],{"class":91,"line":769},[89,5752,3023],{"class":102},[89,5754,3367],{"class":109},[89,5756,3483],{"class":595},[89,5758,3338],{"class":102},[89,5760,5761,5763,5765,5767,5769],{"class":91,"line":779},[89,5762,3490],{"class":569},[89,5764,3493],{"class":102},[89,5766,3496],{"class":569},[89,5768,4474],{"class":119},[89,5770,790],{"class":102},[89,5772,5773],{"class":91,"line":793},[89,5774,650],{"emptyLinePlaceholder":184},[89,5776,5777,5779,5782,5784,5786,5788,5790],{"class":91,"line":799},[89,5778,2306],{"class":569},[89,5780,5781],{"class":595}," isOpen",[89,5783,599],{"class":569},[89,5785,602],{"class":576},[89,5787,580],{"class":102},[89,5789,4528],{"class":595},[89,5791,610],{"class":102},[89,5793,5794],{"class":91,"line":808},[89,5795,650],{"emptyLinePlaceholder":184},[89,5797,5798,5800,5803,5805,5807,5809,5812,5814,5816],{"class":91,"line":2055},[89,5799,2306],{"class":569},[89,5801,5802],{"class":576}," open",[89,5804,599],{"class":569},[89,5806,2637],{"class":102},[89,5808,2640],{"class":569},[89,5810,5811],{"class":102}," (isOpen.value ",[89,5813,377],{"class":569},[89,5815,4643],{"class":595},[89,5817,610],{"class":102},[89,5819,5820,5822,5825,5827,5829,5831,5833,5835,5837],{"class":91,"line":3983},[89,5821,2306],{"class":569},[89,5823,5824],{"class":576}," close",[89,5826,599],{"class":569},[89,5828,2637],{"class":102},[89,5830,2640],{"class":569},[89,5832,5811],{"class":102},[89,5834,377],{"class":569},[89,5836,787],{"class":595},[89,5838,610],{"class":102},[89,5840,5841],{"class":91,"line":3994},[89,5842,650],{"emptyLinePlaceholder":184},[89,5844,5845],{"class":91,"line":4005},[89,5846,5847],{"class":95},"\u002F\u002F Expose open\u002Fclose for parent component to use\n",[89,5849,5850,5852],{"class":91,"line":4255},[89,5851,1269],{"class":576},[89,5853,5854],{"class":102},"({ open, close });\n",[89,5856,5857,5859,5861],{"class":91,"line":4260},[89,5858,3395],{"class":102},[89,5860,3367],{"class":109},[89,5862,3338],{"class":102},[89,5864,5865],{"class":91,"line":4290},[89,5866,650],{"emptyLinePlaceholder":184},[89,5868,5869,5871,5873,5876],{"class":91,"line":4295},[89,5870,3023],{"class":102},[89,5872,170],{"class":109},[89,5874,5875],{"class":595}," scoped",[89,5877,3338],{"class":102},[89,5879,5880,5883],{"class":91,"line":4304},[89,5881,5882],{"class":595},".modal-backdrop",[89,5884,658],{"class":102},[89,5886,5887,5890,5893,5896],{"class":91,"line":4309},[89,5888,5889],{"class":595},"  position",[89,5891,5892],{"class":102},": ",[89,5894,5895],{"class":595},"fixed",[89,5897,790],{"class":102},[89,5899,5900,5903,5905,5907],{"class":91,"line":4318},[89,5901,5902],{"class":595},"  top",[89,5904,5892],{"class":102},[89,5906,2949],{"class":595},[89,5908,790],{"class":102},[89,5910,5911,5914,5916,5918],{"class":91,"line":4327},[89,5912,5913],{"class":595},"  left",[89,5915,5892],{"class":102},[89,5917,2949],{"class":595},[89,5919,790],{"class":102},[89,5921,5922,5925,5927,5930,5933],{"class":91,"line":4336},[89,5923,5924],{"class":595},"  width",[89,5926,5892],{"class":102},[89,5928,5929],{"class":595},"100",[89,5931,5932],{"class":569},"vw",[89,5934,790],{"class":102},[89,5936,5937,5940,5942,5944,5947],{"class":91,"line":4365},[89,5938,5939],{"class":595},"  height",[89,5941,5892],{"class":102},[89,5943,5929],{"class":595},[89,5945,5946],{"class":569},"vh",[89,5948,790],{"class":102},[89,5950,5951,5954,5956,5959,5961,5963,5965,5967,5969,5971,5973,5976],{"class":91,"line":4394},[89,5952,5953],{"class":595},"  background-color",[89,5955,5892],{"class":102},[89,5957,5958],{"class":595},"rgba",[89,5960,580],{"class":102},[89,5962,2949],{"class":595},[89,5964,1472],{"class":102},[89,5966,2949],{"class":595},[89,5968,1472],{"class":102},[89,5970,2949],{"class":595},[89,5972,1472],{"class":102},[89,5974,5975],{"class":595},"0.5",[89,5977,610],{"class":102},[89,5979,5980,5983,5985,5988],{"class":91,"line":4422},[89,5981,5982],{"class":595},"  display",[89,5984,5892],{"class":102},[89,5986,5987],{"class":595},"flex",[89,5989,790],{"class":102},[89,5991,5992,5995,5997,6000],{"class":91,"line":4432},[89,5993,5994],{"class":595},"  justify-content",[89,5996,5892],{"class":102},[89,5998,5999],{"class":595},"center",[89,6001,790],{"class":102},[89,6003,6004,6007,6009,6011],{"class":91,"line":4442},[89,6005,6006],{"class":595},"  align-items",[89,6008,5892],{"class":102},[89,6010,5999],{"class":595},[89,6012,790],{"class":102},[89,6014,6015,6018,6020,6023],{"class":91,"line":4447},[89,6016,6017],{"class":595},"  z-index",[89,6019,5892],{"class":102},[89,6021,6022],{"class":595},"1000",[89,6024,790],{"class":102},[89,6026,6027],{"class":91,"line":4457},[89,6028,165],{"class":102},[89,6030,6031,6034],{"class":91,"line":4479},[89,6032,6033],{"class":595},".modal-content",[89,6035,658],{"class":102},[89,6037,6038,6041,6043,6046],{"class":91,"line":4493},[89,6039,6040],{"class":595},"  background",[89,6042,5892],{"class":102},[89,6044,6045],{"class":595},"white",[89,6047,790],{"class":102},[89,6049,6050,6053,6055,6058,6061],{"class":91,"line":4498},[89,6051,6052],{"class":595},"  padding",[89,6054,5892],{"class":102},[89,6056,6057],{"class":595},"20",[89,6059,6060],{"class":569},"px",[89,6062,790],{"class":102},[89,6064,6065,6068,6070,6073,6075],{"class":91,"line":4515},[89,6066,6067],{"class":595},"  border-radius",[89,6069,5892],{"class":102},[89,6071,6072],{"class":595},"8",[89,6074,6060],{"class":569},[89,6076,790],{"class":102},[89,6078,6079,6082,6084,6087,6089],{"class":91,"line":4533},[89,6080,6081],{"class":595},"  min-width",[89,6083,5892],{"class":102},[89,6085,6086],{"class":595},"300",[89,6088,6060],{"class":569},[89,6090,790],{"class":102},[89,6092,6093,6096,6098,6100,6103,6105,6108,6110,6113,6115,6117,6119,6121,6123,6125,6127,6130],{"class":91,"line":4551},[89,6094,6095],{"class":595},"  box-shadow",[89,6097,5892],{"class":102},[89,6099,2949],{"class":595},[89,6101,6102],{"class":595}," 4",[89,6104,6060],{"class":569},[89,6106,6107],{"class":595}," 6",[89,6109,6060],{"class":569},[89,6111,6112],{"class":595}," rgba",[89,6114,580],{"class":102},[89,6116,2949],{"class":595},[89,6118,1472],{"class":102},[89,6120,2949],{"class":595},[89,6122,1472],{"class":102},[89,6124,2949],{"class":595},[89,6126,1472],{"class":102},[89,6128,6129],{"class":595},"0.1",[89,6131,610],{"class":102},[89,6133,6134],{"class":91,"line":4556},[89,6135,165],{"class":102},[89,6137,6138,6140,6142],{"class":91,"line":4570},[89,6139,3395],{"class":102},[89,6141,170],{"class":109},[89,6143,3338],{"class":102},[15,6145,6146,6147,6150,6151,6154],{},"Se use donde se use ",[34,6148,6149],{},"MyModal",", su contenido acaba dentro de ",[34,6152,6153],{},"#modals-container",", lo que saca los conflictos de estilos de la ecuación.",[10,6156,6158],{"id":6157},"_3-suspense-para-estados-de-carga-asíncronos","3. Suspense para estados de carga asíncronos",[15,6160,6161,6162,6165],{},"Esperar datos o componentes cargados de forma asíncrona hace que la interfaz se sienta lenta. ",[34,6163,6164],{},"Suspense"," coordina las dependencias asíncronas de un árbol de componentes y muestra un contenido de respaldo mientras se resuelven, de modo que el usuario ve un estado de carga en lugar de un hueco vacío o un diseño roto.",[15,6167,6168],{},"Un componente que consulta datos al montarse:",[80,6170,6172],{"className":5289,"code":6171,"language":1177,"meta":85,"style":85},"\u003C!-- components\u002FAsyncDataDisplay.vue -->\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch2>Data from API:\u003C\u002Fh2>\n    \u003Cp>{{ data.title }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from \"vue\";\n\nconst data = ref(null);\n\n\u002F\u002F Simulate an async data fetch\nawait new Promise((resolve) => setTimeout(resolve, 2000));\nconst res = await fetch(\"https:\u002F\u002Fjsonplaceholder.typicode.com\u002Ftodos\u002F1\");\ndata.value = await res.json();\n\u003C\u002Fscript>\n",[34,6173,6174,6179,6187,6195,6208,6221,6229,6237,6241,6251,6263,6267,6283,6287,6292,6323,6342,6357],{"__ignoreMap":85},[89,6175,6176],{"class":91,"line":92},[89,6177,6178],{"class":95},"\u003C!-- components\u002FAsyncDataDisplay.vue -->\n",[89,6180,6181,6183,6185],{"class":91,"line":99},[89,6182,3023],{"class":102},[89,6184,3429],{"class":109},[89,6186,3338],{"class":102},[89,6188,6189,6191,6193],{"class":91,"line":106},[89,6190,3364],{"class":102},[89,6192,3438],{"class":109},[89,6194,3338],{"class":102},[89,6196,6197,6199,6201,6204,6206],{"class":91,"line":116},[89,6198,3445],{"class":102},[89,6200,10],{"class":109},[89,6202,6203],{"class":102},">Data from API:\u003C\u002F",[89,6205,10],{"class":109},[89,6207,3338],{"class":102},[89,6209,6210,6212,6214,6217,6219],{"class":91,"line":126},[89,6211,3445],{"class":102},[89,6213,15],{"class":109},[89,6215,6216],{"class":102},">{{ data.title }}\u003C\u002F",[89,6218,15],{"class":109},[89,6220,3338],{"class":102},[89,6222,6223,6225,6227],{"class":91,"line":134},[89,6224,3386],{"class":102},[89,6226,3438],{"class":109},[89,6228,3338],{"class":102},[89,6230,6231,6233,6235],{"class":91,"line":142},[89,6232,3395],{"class":102},[89,6234,3429],{"class":109},[89,6236,3338],{"class":102},[89,6238,6239],{"class":91,"line":150},[89,6240,650],{"emptyLinePlaceholder":184},[89,6242,6243,6245,6247,6249],{"class":91,"line":156},[89,6244,3023],{"class":102},[89,6246,3367],{"class":109},[89,6248,3483],{"class":595},[89,6250,3338],{"class":102},[89,6252,6253,6255,6257,6259,6261],{"class":91,"line":162},[89,6254,3490],{"class":569},[89,6256,3493],{"class":102},[89,6258,3496],{"class":569},[89,6260,4474],{"class":119},[89,6262,790],{"class":102},[89,6264,6265],{"class":91,"line":728},[89,6266,650],{"emptyLinePlaceholder":184},[89,6268,6269,6271,6273,6275,6277,6279,6281],{"class":91,"line":746},[89,6270,2306],{"class":569},[89,6272,635],{"class":595},[89,6274,599],{"class":569},[89,6276,602],{"class":576},[89,6278,580],{"class":102},[89,6280,626],{"class":595},[89,6282,610],{"class":102},[89,6284,6285],{"class":91,"line":758},[89,6286,650],{"emptyLinePlaceholder":184},[89,6288,6289],{"class":91,"line":769},[89,6290,6291],{"class":95},"\u002F\u002F Simulate an async data fetch\n",[89,6293,6294,6297,6299,6302,6304,6307,6309,6311,6314,6317,6320],{"class":91,"line":779},[89,6295,6296],{"class":569},"await",[89,6298,699],{"class":569},[89,6300,6301],{"class":595}," Promise",[89,6303,3217],{"class":102},[89,6305,6306],{"class":583},"resolve",[89,6308,2108],{"class":102},[89,6310,2640],{"class":569},[89,6312,6313],{"class":576}," setTimeout",[89,6315,6316],{"class":102},"(resolve, ",[89,6318,6319],{"class":595},"2000",[89,6321,6322],{"class":102},"));\n",[89,6324,6325,6327,6329,6331,6333,6335,6337,6340],{"class":91,"line":793},[89,6326,2306],{"class":569},[89,6328,5146],{"class":595},[89,6330,599],{"class":569},[89,6332,671],{"class":569},[89,6334,674],{"class":576},[89,6336,580],{"class":102},[89,6338,6339],{"class":119},"\"https:\u002F\u002Fjsonplaceholder.typicode.com\u002Ftodos\u002F1\"",[89,6341,610],{"class":102},[89,6343,6344,6347,6349,6351,6353,6355],{"class":91,"line":799},[89,6345,6346],{"class":102},"data.value ",[89,6348,377],{"class":569},[89,6350,671],{"class":569},[89,6352,5207],{"class":102},[89,6354,84],{"class":576},[89,6356,743],{"class":102},[89,6358,6359,6361,6363],{"class":91,"line":808},[89,6360,3395],{"class":102},[89,6362,3367],{"class":109},[89,6364,3338],{"class":102},[15,6366,6367,6368,3892],{},"Envuelto en ",[34,6369,6164],{},[80,6371,6373],{"className":5289,"code":6372,"language":1177,"meta":85,"style":85},"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>My App\u003C\u002Fh1>\n    \u003CSuspense>\n      \u003C!-- Main content that might be async -->\n      \u003CAsyncDataDisplay \u002F>\n\n      \u003C!-- Fallback content while async components are loading -->\n      \u003Ctemplate #fallback>\n        \u003Cdiv>Loading awesome data...\u003C\u002Fdiv>\n      \u003C\u002Ftemplate>\n    \u003C\u002FSuspense>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { defineAsyncComponent } from \"vue\";\n\n\u002F\u002F Define an async component\nconst AsyncDataDisplay = defineAsyncComponent(\n  () => import(\".\u002Fcomponents\u002FAsyncDataDisplay.vue\")\n);\n\u003C\u002Fscript>\n",[34,6374,6375,6383,6391,6405,6413,6418,6428,6432,6437,6451,6464,6472,6480,6488,6496,6500,6510,6523,6527,6532,6546,6564,6568],{"__ignoreMap":85},[89,6376,6377,6379,6381],{"class":91,"line":92},[89,6378,3023],{"class":102},[89,6380,3429],{"class":109},[89,6382,3338],{"class":102},[89,6384,6385,6387,6389],{"class":91,"line":99},[89,6386,3364],{"class":102},[89,6388,3438],{"class":109},[89,6390,3338],{"class":102},[89,6392,6393,6395,6398,6401,6403],{"class":91,"line":106},[89,6394,3445],{"class":102},[89,6396,6397],{"class":109},"h1",[89,6399,6400],{"class":102},">My App\u003C\u002F",[89,6402,6397],{"class":109},[89,6404,3338],{"class":102},[89,6406,6407,6409,6411],{"class":91,"line":116},[89,6408,3445],{"class":102},[89,6410,6164],{"class":109},[89,6412,3338],{"class":102},[89,6414,6415],{"class":91,"line":126},[89,6416,6417],{"class":95},"      \u003C!-- Main content that might be async -->\n",[89,6419,6420,6422,6425],{"class":91,"line":134},[89,6421,5366],{"class":102},[89,6423,6424],{"class":109},"AsyncDataDisplay",[89,6426,6427],{"class":102}," \u002F>\n",[89,6429,6430],{"class":91,"line":142},[89,6431,650],{"emptyLinePlaceholder":184},[89,6433,6434],{"class":91,"line":150},[89,6435,6436],{"class":95},"      \u003C!-- Fallback content while async components are loading -->\n",[89,6438,6439,6441,6443,6446,6449],{"class":91,"line":156},[89,6440,5366],{"class":102},[89,6442,3429],{"class":109},[89,6444,6445],{"class":102}," #",[89,6447,6448],{"class":595},"fallback",[89,6450,3338],{"class":102},[89,6452,6453,6455,6457,6460,6462],{"class":91,"line":162},[89,6454,5670],{"class":102},[89,6456,3438],{"class":109},[89,6458,6459],{"class":102},">Loading awesome data...\u003C\u002F",[89,6461,3438],{"class":109},[89,6463,3338],{"class":102},[89,6465,6466,6468,6470],{"class":91,"line":728},[89,6467,5716],{"class":102},[89,6469,3429],{"class":109},[89,6471,3338],{"class":102},[89,6473,6474,6476,6478],{"class":91,"line":746},[89,6475,5396],{"class":102},[89,6477,6164],{"class":109},[89,6479,3338],{"class":102},[89,6481,6482,6484,6486],{"class":91,"line":758},[89,6483,3386],{"class":102},[89,6485,3438],{"class":109},[89,6487,3338],{"class":102},[89,6489,6490,6492,6494],{"class":91,"line":769},[89,6491,3395],{"class":102},[89,6493,3429],{"class":109},[89,6495,3338],{"class":102},[89,6497,6498],{"class":91,"line":779},[89,6499,650],{"emptyLinePlaceholder":184},[89,6501,6502,6504,6506,6508],{"class":91,"line":793},[89,6503,3023],{"class":102},[89,6505,3367],{"class":109},[89,6507,3483],{"class":595},[89,6509,3338],{"class":102},[89,6511,6512,6514,6517,6519,6521],{"class":91,"line":799},[89,6513,3490],{"class":569},[89,6515,6516],{"class":102}," { defineAsyncComponent } ",[89,6518,3496],{"class":569},[89,6520,4474],{"class":119},[89,6522,790],{"class":102},[89,6524,6525],{"class":91,"line":808},[89,6526,650],{"emptyLinePlaceholder":184},[89,6528,6529],{"class":91,"line":2055},[89,6530,6531],{"class":95},"\u002F\u002F Define an async component\n",[89,6533,6534,6536,6539,6541,6544],{"class":91,"line":3983},[89,6535,2306],{"class":569},[89,6537,6538],{"class":595}," AsyncDataDisplay",[89,6540,599],{"class":569},[89,6542,6543],{"class":576}," defineAsyncComponent",[89,6545,5476],{"class":102},[89,6547,6548,6551,6553,6556,6558,6561],{"class":91,"line":3994},[89,6549,6550],{"class":102},"  () ",[89,6552,2640],{"class":569},[89,6554,6555],{"class":569}," import",[89,6557,580],{"class":102},[89,6559,6560],{"class":119},"\".\u002Fcomponents\u002FAsyncDataDisplay.vue\"",[89,6562,6563],{"class":102},")\n",[89,6565,6566],{"class":91,"line":4005},[89,6567,610],{"class":102},[89,6569,6570,6572,6574],{"class":91,"line":4255},[89,6571,3395],{"class":102},[89,6573,3367],{"class":109},[89,6575,3338],{"class":102},[15,6577,6578,6579,6581,6582,6584],{},"Mientras ",[34,6580,6424],{}," resuelve el ",[34,6583,6296],{}," de su bloque setup, se ve el contenido de respaldo. Cuando termina, el contenido real ocupa su lugar.",[10,6586,6588],{"id":6587},"_4-directivas-personalizadas-para-trabajar-con-el-dom","4. Directivas personalizadas para trabajar con el DOM",[15,6590,6591,6592,1472,6595,6598,6599,44],{},"A veces la herramienta adecuada no es un componente. Para manipulación de bajo nivel del DOM, o para comportamientos reutilizables que no dependen de un componente concreto, las directivas personalizadas te dan acceso directo al elemento al que se asocian. Cosas como ",[34,6593,6594],{},"v-focus",[34,6596,6597],{},"v-tooltip"," o ",[34,6600,6601],{},"v-lazy-load",[80,6603,6605],{"className":560,"code":6604,"language":562,"meta":85,"style":85},"\u002F\u002F main.js or a dedicated directives file\nimport { createApp } from \"vue\";\nimport App from \".\u002FApp.vue\";\n\nconst app = createApp(App);\n\napp.directive(\"focus\", {\n  mounted(el) {\n    el.focus();\n  },\n});\n\napp.mount(\"#app\");\n",[34,6606,6607,6612,6625,6639,6643,6658,6662,6677,6689,6698,6703,6707,6711],{"__ignoreMap":85},[89,6608,6609],{"class":91,"line":92},[89,6610,6611],{"class":95},"\u002F\u002F main.js or a dedicated directives file\n",[89,6613,6614,6616,6619,6621,6623],{"class":91,"line":99},[89,6615,3490],{"class":569},[89,6617,6618],{"class":102}," { createApp } ",[89,6620,3496],{"class":569},[89,6622,4474],{"class":119},[89,6624,790],{"class":102},[89,6626,6627,6629,6632,6634,6637],{"class":91,"line":106},[89,6628,3490],{"class":569},[89,6630,6631],{"class":102}," App ",[89,6633,3496],{"class":569},[89,6635,6636],{"class":119}," \".\u002FApp.vue\"",[89,6638,790],{"class":102},[89,6640,6641],{"class":91,"line":116},[89,6642,650],{"emptyLinePlaceholder":184},[89,6644,6645,6647,6650,6652,6655],{"class":91,"line":126},[89,6646,2306],{"class":569},[89,6648,6649],{"class":595}," app",[89,6651,599],{"class":569},[89,6653,6654],{"class":576}," createApp",[89,6656,6657],{"class":102},"(App);\n",[89,6659,6660],{"class":91,"line":134},[89,6661,650],{"emptyLinePlaceholder":184},[89,6663,6664,6667,6670,6672,6675],{"class":91,"line":142},[89,6665,6666],{"class":102},"app.",[89,6668,6669],{"class":576},"directive",[89,6671,580],{"class":102},[89,6673,6674],{"class":119},"\"focus\"",[89,6676,3980],{"class":102},[89,6678,6679,6682,6684,6687],{"class":91,"line":150},[89,6680,6681],{"class":576},"  mounted",[89,6683,580],{"class":102},[89,6685,6686],{"class":583},"el",[89,6688,587],{"class":102},[89,6690,6691,6694,6696],{"class":91,"line":156},[89,6692,6693],{"class":102},"    el.",[89,6695,1347],{"class":576},[89,6697,743],{"class":102},[89,6699,6700],{"class":91,"line":162},[89,6701,6702],{"class":102},"  },\n",[89,6704,6705],{"class":91,"line":728},[89,6706,2960],{"class":102},[89,6708,6709],{"class":91,"line":746},[89,6710,650],{"emptyLinePlaceholder":184},[89,6712,6713,6715,6718,6720,6723],{"class":91,"line":758},[89,6714,6666],{"class":102},[89,6716,6717],{"class":576},"mount",[89,6719,580],{"class":102},[89,6721,6722],{"class":119},"\"#app\"",[89,6724,610],{"class":102},[15,6726,6727],{},"Se usa así:",[80,6729,6731],{"className":5289,"code":6730,"language":1177,"meta":85,"style":85},"\u003Ctemplate>\n  \u003Cinput v-focus type=\"text\" placeholder=\"I will be focused on mount\" \u002F>\n\u003C\u002Ftemplate>\n",[34,6732,6733,6741,6769],{"__ignoreMap":85},[89,6734,6735,6737,6739],{"class":91,"line":92},[89,6736,3023],{"class":102},[89,6738,3429],{"class":109},[89,6740,3338],{"class":102},[89,6742,6743,6745,6748,6751,6754,6756,6759,6762,6764,6767],{"class":91,"line":99},[89,6744,3364],{"class":102},[89,6746,6747],{"class":109},"input",[89,6749,6750],{"class":595}," v-focus",[89,6752,6753],{"class":595}," type",[89,6755,377],{"class":102},[89,6757,6758],{"class":119},"\"text\"",[89,6760,6761],{"class":595}," placeholder",[89,6763,377],{"class":102},[89,6765,6766],{"class":119},"\"I will be focused on mount\"",[89,6768,6427],{"class":102},[89,6770,6771,6773,6775],{"class":91,"line":106},[89,6772,3395],{"class":102},[89,6774,3429],{"class":109},[89,6776,3338],{"class":102},[15,6778,6779,6780,1472,6783,1472,6786,553,6789,6792],{},"El input recibe el foco cuando el componente se monta. Las directivas pueden engancharse a ",[34,6781,6782],{},"created",[34,6784,6785],{},"mounted",[34,6787,6788],{},"updated",[34,6790,6791],{},"unmounted"," para controlar el comportamiento durante el ciclo de vida del elemento.",[10,6794,6796],{"id":6795},"_5-funciones-de-render-y-jsx","5. Funciones de render y JSX",[15,6798,6799],{},"Para casi todo, la sintaxis de plantillas de Vue es la opción correcta: declarativa, legible y se compila de forma eficiente. Las funciones de render y JSX son para los casos raros en los que necesitas control programático sobre el render, como un generador de tablas muy dinámico o un componente de biblioteca cuya estructura cambia según props complejas.",[15,6801,6802],{},"Probablemente no recurras a esto salvo que la estructura varíe mucho, estés escribiendo una biblioteca de interfaz que necesita máxima flexibilidad o prefieras escribir los componentes enteramente en JavaScript o TypeScript.",[80,6804,6806],{"className":560,"code":6805,"language":562,"meta":85,"style":85},"\u002F\u002F components\u002FDynamicHeading.vue\nimport { h } from \"vue\";\n\nexport default {\n  props: {\n    level: {\n      type: Number,\n      required: true,\n      validator: (val) => val >= 1 && val \u003C= 6,\n    },\n  },\n  setup(props) {\n    return () => h(`h${props.level}`, `This is a level ${props.level} heading`);\n  },\n};\n",[34,6807,6808,6813,6826,6830,6839,6844,6849,6854,6863,6899,6904,6911,6921,6963,6969],{"__ignoreMap":85},[89,6809,6810],{"class":91,"line":92},[89,6811,6812],{"class":95},"\u002F\u002F components\u002FDynamicHeading.vue\n",[89,6814,6815,6817,6820,6822,6824],{"class":91,"line":99},[89,6816,3490],{"class":569},[89,6818,6819],{"class":102}," { h } ",[89,6821,3496],{"class":569},[89,6823,4474],{"class":119},[89,6825,790],{"class":102},[89,6827,6828],{"class":91,"line":106},[89,6829,650],{"emptyLinePlaceholder":184},[89,6831,6832,6834,6837],{"class":91,"line":116},[89,6833,5036],{"class":569},[89,6835,6836],{"class":569}," default",[89,6838,658],{"class":583},[89,6840,6841],{"class":91,"line":126},[89,6842,6843],{"class":102},"  props: {\n",[89,6845,6846],{"class":91,"line":134},[89,6847,6848],{"class":102},"    level: {\n",[89,6850,6851],{"class":91,"line":142},[89,6852,6853],{"class":102},"      type: Number,\n",[89,6855,6856,6859,6861],{"class":91,"line":150},[89,6857,6858],{"class":102},"      required: ",[89,6860,607],{"class":595},[89,6862,123],{"class":102},[89,6864,6865,6868,6871,6874,6876,6878,6881,6884,6887,6890,6892,6895,6897],{"class":91,"line":156},[89,6866,6867],{"class":576},"      validator",[89,6869,6870],{"class":102},": (",[89,6872,6873],{"class":583},"val",[89,6875,2108],{"class":102},[89,6877,2640],{"class":569},[89,6879,6880],{"class":102}," val ",[89,6882,6883],{"class":569},">=",[89,6885,6886],{"class":595}," 1",[89,6888,6889],{"class":569}," &&",[89,6891,6880],{"class":102},[89,6893,6894],{"class":569},"\u003C=",[89,6896,6107],{"class":595},[89,6898,123],{"class":102},[89,6900,6901],{"class":91,"line":162},[89,6902,6903],{"class":102},"    },\n",[89,6905,6906,6909],{"class":91,"line":728},[89,6907,6908],{"class":102},"  }",[89,6910,123],{"class":583},[89,6912,6913,6916,6919],{"class":91,"line":746},[89,6914,6915],{"class":576},"  setup",[89,6917,6918],{"class":583},"(props) ",[89,6920,103],{"class":102},[89,6922,6923,6925,6927,6929,6932,6934,6937,6940,6942,6945,6947,6949,6952,6954,6956,6958,6961],{"class":91,"line":758},[89,6924,2499],{"class":569},[89,6926,2637],{"class":102},[89,6928,2640],{"class":569},[89,6930,6931],{"class":576}," h",[89,6933,580],{"class":102},[89,6935,6936],{"class":119},"`h${",[89,6938,6939],{"class":102},"props",[89,6941,44],{"class":119},[89,6943,6944],{"class":102},"level",[89,6946,718],{"class":119},[89,6948,1472],{"class":102},[89,6950,6951],{"class":119},"`This is a level ${",[89,6953,6939],{"class":102},[89,6955,44],{"class":119},[89,6957,6944],{"class":102},[89,6959,6960],{"class":119},"} heading`",[89,6962,610],{"class":102},[89,6964,6965,6967],{"class":91,"line":769},[89,6966,6908],{"class":102},[89,6968,123],{"class":583},[89,6970,6971,6974],{"class":91,"line":779},[89,6972,6973],{"class":583},"}",[89,6975,790],{"class":102},[15,6977,6978],{},"Lo mismo con JSX, que requiere la configuración de Babel para JSX en Vue:",[80,6980,6984],{"className":6981,"code":6982,"language":6983,"meta":85,"style":85},"language-jsx shiki shiki-themes github-dark-default","\u002F\u002F components\u002FDynamicHeadingJSX.vue\nexport default {\n  props: {\n    level: {\n      type: Number,\n      required: true,\n      validator: (val) => val >= 1 && val \u003C= 6,\n    },\n  },\n  setup(props) {\n    const HeadingTag = `h${props.level}`;\n    return () => (\n      \u003CHeadingTag>This is a level {props.level} heading (with JSX)\u003C\u002FHeadingTag>\n    );\n  },\n};\n","jsx",[34,6985,6986,6991,6999,7003,7007,7011,7019,7047,7051,7057,7065,7087,7098,7123,7128,7134],{"__ignoreMap":85},[89,6987,6988],{"class":91,"line":92},[89,6989,6990],{"class":95},"\u002F\u002F components\u002FDynamicHeadingJSX.vue\n",[89,6992,6993,6995,6997],{"class":91,"line":99},[89,6994,5036],{"class":569},[89,6996,6836],{"class":569},[89,6998,658],{"class":583},[89,7000,7001],{"class":91,"line":106},[89,7002,6843],{"class":102},[89,7004,7005],{"class":91,"line":116},[89,7006,6848],{"class":102},[89,7008,7009],{"class":91,"line":126},[89,7010,6853],{"class":102},[89,7012,7013,7015,7017],{"class":91,"line":134},[89,7014,6858],{"class":102},[89,7016,607],{"class":595},[89,7018,123],{"class":102},[89,7020,7021,7023,7025,7027,7029,7031,7033,7035,7037,7039,7041,7043,7045],{"class":91,"line":142},[89,7022,6867],{"class":576},[89,7024,6870],{"class":102},[89,7026,6873],{"class":583},[89,7028,2108],{"class":102},[89,7030,2640],{"class":569},[89,7032,6880],{"class":102},[89,7034,6883],{"class":569},[89,7036,6886],{"class":595},[89,7038,6889],{"class":569},[89,7040,6880],{"class":102},[89,7042,6894],{"class":569},[89,7044,6107],{"class":595},[89,7046,123],{"class":102},[89,7048,7049],{"class":91,"line":150},[89,7050,6903],{"class":102},[89,7052,7053,7055],{"class":91,"line":156},[89,7054,6908],{"class":102},[89,7056,123],{"class":583},[89,7058,7059,7061,7063],{"class":91,"line":162},[89,7060,6915],{"class":576},[89,7062,6918],{"class":583},[89,7064,103],{"class":102},[89,7066,7067,7069,7072,7074,7077,7079,7081,7083,7085],{"class":91,"line":728},[89,7068,663],{"class":569},[89,7070,7071],{"class":595}," HeadingTag",[89,7073,599],{"class":569},[89,7075,7076],{"class":119}," `h${",[89,7078,6939],{"class":102},[89,7080,44],{"class":119},[89,7082,6944],{"class":102},[89,7084,718],{"class":119},[89,7086,790],{"class":102},[89,7088,7089,7091,7093,7095],{"class":91,"line":746},[89,7090,2499],{"class":569},[89,7092,2637],{"class":102},[89,7094,2640],{"class":569},[89,7096,7097],{"class":102}," (\n",[89,7099,7100,7102,7105,7108,7111,7114,7116,7119,7121],{"class":91,"line":758},[89,7101,5366],{"class":102},[89,7103,7104],{"class":109},"HeadingTag",[89,7106,7107],{"class":102},">This is a level ",[89,7109,7110],{"class":569},"{",[89,7112,7113],{"class":102},"props.level",[89,7115,6973],{"class":569},[89,7117,7118],{"class":102}," heading (with JSX)\u003C\u002F",[89,7120,7104],{"class":109},[89,7122,3338],{"class":102},[89,7124,7125],{"class":91,"line":769},[89,7126,7127],{"class":102},"    );\n",[89,7129,7130,7132],{"class":91,"line":779},[89,7131,6908],{"class":102},[89,7133,123],{"class":583},[89,7135,7136,7138],{"class":91,"line":793},[89,7137,6973],{"class":583},[89,7139,790],{"class":102},[10,7141,7143],{"id":7142},"_6-provide-e-inject","6. Provide e inject",[15,7145,7146],{},"Para estado realmente global, la respuesta es Pinia. Provide e inject cubren el caso intermedio: datos o utilidades que deben llegar al fondo de un árbol de componentes sin pasar props por cada nivel.",[15,7148,7149],{},"Encajan con la información de tema, las preferencias de usuario, funciones auxiliares y datos que cambian poco pero afectan a todo un subárbol.",[80,7151,7153],{"className":5289,"code":7152,"language":1177,"meta":85,"style":85},"\u003C!-- components\u002FGrandparentComponent.vue -->\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Grandparent\u003C\u002Fh1>\n    \u003Cbutton @click=\"toggleTheme\">Toggle Theme\u003C\u002Fbutton>\n    \u003CChildComponent \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { provide, ref } from \"vue\";\nimport ChildComponent from \".\u002FChildComponent.vue\";\n\nconst theme = ref(\"light\");\n\nconst toggleTheme = () => {\n  theme.value = theme.value === \"light\" ? \"dark\" : \"light\";\n};\n\nprovide(\"appTheme\", theme); \u002F\u002F Provide the reactive theme\nprovide(\"toggleThemeFunction\", toggleTheme); \u002F\u002F Provide a function too!\n\u003C\u002Fscript>\n",[34,7154,7155,7160,7168,7176,7189,7209,7218,7226,7234,7238,7248,7261,7275,7279,7297,7301,7316,7344,7348,7352,7368,7383],{"__ignoreMap":85},[89,7156,7157],{"class":91,"line":92},[89,7158,7159],{"class":95},"\u003C!-- components\u002FGrandparentComponent.vue -->\n",[89,7161,7162,7164,7166],{"class":91,"line":99},[89,7163,3023],{"class":102},[89,7165,3429],{"class":109},[89,7167,3338],{"class":102},[89,7169,7170,7172,7174],{"class":91,"line":106},[89,7171,3364],{"class":102},[89,7173,3438],{"class":109},[89,7175,3338],{"class":102},[89,7177,7178,7180,7182,7185,7187],{"class":91,"line":116},[89,7179,3445],{"class":102},[89,7181,6397],{"class":109},[89,7183,7184],{"class":102},">Grandparent\u003C\u002F",[89,7186,6397],{"class":109},[89,7188,3338],{"class":102},[89,7190,7191,7193,7195,7197,7199,7202,7205,7207],{"class":91,"line":126},[89,7192,3445],{"class":102},[89,7194,4341],{"class":109},[89,7196,5640],{"class":595},[89,7198,377],{"class":102},[89,7200,7201],{"class":119},"\"toggleTheme\"",[89,7203,7204],{"class":102},">Toggle Theme\u003C\u002F",[89,7206,4341],{"class":109},[89,7208,3338],{"class":102},[89,7210,7211,7213,7216],{"class":91,"line":134},[89,7212,3445],{"class":102},[89,7214,7215],{"class":109},"ChildComponent",[89,7217,6427],{"class":102},[89,7219,7220,7222,7224],{"class":91,"line":142},[89,7221,3386],{"class":102},[89,7223,3438],{"class":109},[89,7225,3338],{"class":102},[89,7227,7228,7230,7232],{"class":91,"line":150},[89,7229,3395],{"class":102},[89,7231,3429],{"class":109},[89,7233,3338],{"class":102},[89,7235,7236],{"class":91,"line":156},[89,7237,650],{"emptyLinePlaceholder":184},[89,7239,7240,7242,7244,7246],{"class":91,"line":162},[89,7241,3023],{"class":102},[89,7243,3367],{"class":109},[89,7245,3483],{"class":595},[89,7247,3338],{"class":102},[89,7249,7250,7252,7255,7257,7259],{"class":91,"line":728},[89,7251,3490],{"class":569},[89,7253,7254],{"class":102}," { provide, ref } ",[89,7256,3496],{"class":569},[89,7258,4474],{"class":119},[89,7260,790],{"class":102},[89,7262,7263,7265,7268,7270,7273],{"class":91,"line":746},[89,7264,3490],{"class":569},[89,7266,7267],{"class":102}," ChildComponent ",[89,7269,3496],{"class":569},[89,7271,7272],{"class":119}," \".\u002FChildComponent.vue\"",[89,7274,790],{"class":102},[89,7276,7277],{"class":91,"line":758},[89,7278,650],{"emptyLinePlaceholder":184},[89,7280,7281,7283,7286,7288,7290,7292,7295],{"class":91,"line":769},[89,7282,2306],{"class":569},[89,7284,7285],{"class":595}," theme",[89,7287,599],{"class":569},[89,7289,602],{"class":576},[89,7291,580],{"class":102},[89,7293,7294],{"class":119},"\"light\"",[89,7296,610],{"class":102},[89,7298,7299],{"class":91,"line":779},[89,7300,650],{"emptyLinePlaceholder":184},[89,7302,7303,7305,7308,7310,7312,7314],{"class":91,"line":793},[89,7304,2306],{"class":569},[89,7306,7307],{"class":576}," toggleTheme",[89,7309,599],{"class":569},[89,7311,2637],{"class":102},[89,7313,2640],{"class":569},[89,7315,658],{"class":102},[89,7317,7318,7321,7323,7326,7328,7331,7334,7337,7340,7342],{"class":91,"line":799},[89,7319,7320],{"class":102},"  theme.value ",[89,7322,377],{"class":569},[89,7324,7325],{"class":102}," theme.value ",[89,7327,373],{"class":569},[89,7329,7330],{"class":119}," \"light\"",[89,7332,7333],{"class":569}," ?",[89,7335,7336],{"class":119}," \"dark\"",[89,7338,7339],{"class":569}," :",[89,7341,7330],{"class":119},[89,7343,790],{"class":102},[89,7345,7346],{"class":91,"line":808},[89,7347,2651],{"class":102},[89,7349,7350],{"class":91,"line":2055},[89,7351,650],{"emptyLinePlaceholder":184},[89,7353,7354,7357,7359,7362,7365],{"class":91,"line":3983},[89,7355,7356],{"class":576},"provide",[89,7358,580],{"class":102},[89,7360,7361],{"class":119},"\"appTheme\"",[89,7363,7364],{"class":102},", theme); ",[89,7366,7367],{"class":95},"\u002F\u002F Provide the reactive theme\n",[89,7369,7370,7372,7374,7377,7380],{"class":91,"line":3994},[89,7371,7356],{"class":576},[89,7373,580],{"class":102},[89,7375,7376],{"class":119},"\"toggleThemeFunction\"",[89,7378,7379],{"class":102},", toggleTheme); ",[89,7381,7382],{"class":95},"\u002F\u002F Provide a function too!\n",[89,7384,7385,7387,7389],{"class":91,"line":4005},[89,7386,3395],{"class":102},[89,7388,3367],{"class":109},[89,7390,3338],{"class":102},[80,7392,7394],{"className":5289,"code":7393,"language":1177,"meta":85,"style":85},"\u003C!-- components\u002FDeeplyNestedComponent.vue -->\n\u003Ctemplate>\n  \u003Cdiv :class=\"['card', appTheme === 'dark' ? 'dark-mode' : '']\">\n    \u003Cp>Current theme: {{ appTheme }}\u003C\u002Fp>\n    \u003Cbutton @click=\"toggleThemeFunction\">Toggle Theme (from deep)\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { inject } from \"vue\";\n\nconst appTheme = inject(\"appTheme\");\nconst toggleThemeFunction = inject(\"toggleThemeFunction\"); \u002F\u002F Inject the function\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n.card {\n  border: 1px solid #ccc;\n  padding: 15px;\n  margin-top: 10px;\n  border-radius: 5px;\n}\n.dark-mode {\n  background-color: #333;\n  color: #eee;\n  border-color: #555;\n}\n\u003C\u002Fstyle>\n",[34,7395,7396,7401,7409,7425,7438,7457,7465,7473,7477,7487,7500,7504,7522,7543,7551,7555,7565,7572,7591,7604,7618,7630,7634,7641,7652,7664,7676,7680],{"__ignoreMap":85},[89,7397,7398],{"class":91,"line":92},[89,7399,7400],{"class":95},"\u003C!-- components\u002FDeeplyNestedComponent.vue -->\n",[89,7402,7403,7405,7407],{"class":91,"line":99},[89,7404,3023],{"class":102},[89,7406,3429],{"class":109},[89,7408,3338],{"class":102},[89,7410,7411,7413,7415,7418,7420,7423],{"class":91,"line":106},[89,7412,3364],{"class":102},[89,7414,3438],{"class":109},[89,7416,7417],{"class":595}," :class",[89,7419,377],{"class":102},[89,7421,7422],{"class":119},"\"['card', appTheme === 'dark' ? 'dark-mode' : '']\"",[89,7424,3338],{"class":102},[89,7426,7427,7429,7431,7434,7436],{"class":91,"line":116},[89,7428,3445],{"class":102},[89,7430,15],{"class":109},[89,7432,7433],{"class":102},">Current theme: {{ appTheme }}\u003C\u002F",[89,7435,15],{"class":109},[89,7437,3338],{"class":102},[89,7439,7440,7442,7444,7446,7448,7450,7453,7455],{"class":91,"line":126},[89,7441,3445],{"class":102},[89,7443,4341],{"class":109},[89,7445,5640],{"class":595},[89,7447,377],{"class":102},[89,7449,7376],{"class":119},[89,7451,7452],{"class":102},">Toggle Theme (from deep)\u003C\u002F",[89,7454,4341],{"class":109},[89,7456,3338],{"class":102},[89,7458,7459,7461,7463],{"class":91,"line":134},[89,7460,3386],{"class":102},[89,7462,3438],{"class":109},[89,7464,3338],{"class":102},[89,7466,7467,7469,7471],{"class":91,"line":142},[89,7468,3395],{"class":102},[89,7470,3429],{"class":109},[89,7472,3338],{"class":102},[89,7474,7475],{"class":91,"line":150},[89,7476,650],{"emptyLinePlaceholder":184},[89,7478,7479,7481,7483,7485],{"class":91,"line":156},[89,7480,3023],{"class":102},[89,7482,3367],{"class":109},[89,7484,3483],{"class":595},[89,7486,3338],{"class":102},[89,7488,7489,7491,7494,7496,7498],{"class":91,"line":162},[89,7490,3490],{"class":569},[89,7492,7493],{"class":102}," { inject } ",[89,7495,3496],{"class":569},[89,7497,4474],{"class":119},[89,7499,790],{"class":102},[89,7501,7502],{"class":91,"line":728},[89,7503,650],{"emptyLinePlaceholder":184},[89,7505,7506,7508,7511,7513,7516,7518,7520],{"class":91,"line":746},[89,7507,2306],{"class":569},[89,7509,7510],{"class":595}," appTheme",[89,7512,599],{"class":569},[89,7514,7515],{"class":576}," inject",[89,7517,580],{"class":102},[89,7519,7361],{"class":119},[89,7521,610],{"class":102},[89,7523,7524,7526,7529,7531,7533,7535,7537,7540],{"class":91,"line":758},[89,7525,2306],{"class":569},[89,7527,7528],{"class":595}," toggleThemeFunction",[89,7530,599],{"class":569},[89,7532,7515],{"class":576},[89,7534,580],{"class":102},[89,7536,7376],{"class":119},[89,7538,7539],{"class":102},"); ",[89,7541,7542],{"class":95},"\u002F\u002F Inject the function\n",[89,7544,7545,7547,7549],{"class":91,"line":769},[89,7546,3395],{"class":102},[89,7548,3367],{"class":109},[89,7550,3338],{"class":102},[89,7552,7553],{"class":91,"line":779},[89,7554,650],{"emptyLinePlaceholder":184},[89,7556,7557,7559,7561,7563],{"class":91,"line":793},[89,7558,3023],{"class":102},[89,7560,170],{"class":109},[89,7562,5875],{"class":595},[89,7564,3338],{"class":102},[89,7566,7567,7570],{"class":91,"line":799},[89,7568,7569],{"class":595},".card",[89,7571,658],{"class":102},[89,7573,7574,7577,7579,7581,7583,7586,7589],{"class":91,"line":808},[89,7575,7576],{"class":595},"  border",[89,7578,5892],{"class":102},[89,7580,2826],{"class":595},[89,7582,6060],{"class":569},[89,7584,7585],{"class":595}," solid",[89,7587,7588],{"class":595}," #ccc",[89,7590,790],{"class":102},[89,7592,7593,7595,7597,7600,7602],{"class":91,"line":2055},[89,7594,6052],{"class":595},[89,7596,5892],{"class":102},[89,7598,7599],{"class":595},"15",[89,7601,6060],{"class":569},[89,7603,790],{"class":102},[89,7605,7606,7609,7611,7614,7616],{"class":91,"line":3983},[89,7607,7608],{"class":595},"  margin-top",[89,7610,5892],{"class":102},[89,7612,7613],{"class":595},"10",[89,7615,6060],{"class":569},[89,7617,790],{"class":102},[89,7619,7620,7622,7624,7626,7628],{"class":91,"line":3994},[89,7621,6067],{"class":595},[89,7623,5892],{"class":102},[89,7625,2888],{"class":595},[89,7627,6060],{"class":569},[89,7629,790],{"class":102},[89,7631,7632],{"class":91,"line":4005},[89,7633,165],{"class":102},[89,7635,7636,7639],{"class":91,"line":4255},[89,7637,7638],{"class":595},".dark-mode",[89,7640,658],{"class":102},[89,7642,7643,7645,7647,7650],{"class":91,"line":4260},[89,7644,5953],{"class":595},[89,7646,5892],{"class":102},[89,7648,7649],{"class":595},"#333",[89,7651,790],{"class":102},[89,7653,7654,7657,7659,7662],{"class":91,"line":4290},[89,7655,7656],{"class":595},"  color",[89,7658,5892],{"class":102},[89,7660,7661],{"class":595},"#eee",[89,7663,790],{"class":102},[89,7665,7666,7669,7671,7674],{"class":91,"line":4295},[89,7667,7668],{"class":595},"  border-color",[89,7670,5892],{"class":102},[89,7672,7673],{"class":595},"#555",[89,7675,790],{"class":102},[89,7677,7678],{"class":91,"line":4304},[89,7679,165],{"class":102},[89,7681,7682,7684,7686],{"class":91,"line":4309},[89,7683,3395],{"class":102},[89,7685,170],{"class":109},[89,7687,3338],{"class":102},[15,7689,7690,7693,7694,7697,7698,44],{},[34,7691,7692],{},"DeeplyNestedComponent"," recibe el tema y la función sin que ",[34,7695,7696],{},"GrandparentComponent"," pase nada a través de ",[34,7699,7215],{},[10,7701,7703],{"id":7702},"_7-propiedades-globales-y-plugins","7. Propiedades globales y plugins",[15,7705,7706],{},"A veces algo tiene que estar disponible en toda la aplicación: una instancia HTTP, una función de traducción, una utilidad compartida.",[15,7708,7709,7710,7713,7714,7717,7718,7721],{},"La vía más simple es ",[34,7711,7712],{},"app.config.globalProperties",", que se engancha a ",[34,7715,7716],{},"this"," en la Options API y es accesible con ",[34,7719,7720],{},"getCurrentInstance()"," en la Composition API.",[80,7723,7725],{"className":560,"code":7724,"language":562,"meta":85,"style":85},"\u002F\u002F main.js\nimport { createApp } from \"vue\";\nimport App from \".\u002FApp.vue\";\n\nconst app = createApp(App);\n\n\u002F\u002F Attach a global property\napp.config.globalProperties.$myGlobalUtil = {\n  sayHello: () => console.log(\"Hello from global util!\"),\n  version: \"1.0.0\",\n};\n\napp.mount(\"#app\");\n",[34,7726,7727,7732,7744,7756,7760,7772,7776,7781,7790,7814,7824,7828,7832],{"__ignoreMap":85},[89,7728,7729],{"class":91,"line":92},[89,7730,7731],{"class":95},"\u002F\u002F main.js\n",[89,7733,7734,7736,7738,7740,7742],{"class":91,"line":99},[89,7735,3490],{"class":569},[89,7737,6618],{"class":102},[89,7739,3496],{"class":569},[89,7741,4474],{"class":119},[89,7743,790],{"class":102},[89,7745,7746,7748,7750,7752,7754],{"class":91,"line":106},[89,7747,3490],{"class":569},[89,7749,6631],{"class":102},[89,7751,3496],{"class":569},[89,7753,6636],{"class":119},[89,7755,790],{"class":102},[89,7757,7758],{"class":91,"line":116},[89,7759,650],{"emptyLinePlaceholder":184},[89,7761,7762,7764,7766,7768,7770],{"class":91,"line":126},[89,7763,2306],{"class":569},[89,7765,6649],{"class":595},[89,7767,599],{"class":569},[89,7769,6654],{"class":576},[89,7771,6657],{"class":102},[89,7773,7774],{"class":91,"line":134},[89,7775,650],{"emptyLinePlaceholder":184},[89,7777,7778],{"class":91,"line":142},[89,7779,7780],{"class":95},"\u002F\u002F Attach a global property\n",[89,7782,7783,7786,7788],{"class":91,"line":150},[89,7784,7785],{"class":102},"app.config.globalProperties.$myGlobalUtil ",[89,7787,377],{"class":569},[89,7789,658],{"class":102},[89,7791,7792,7795,7798,7800,7803,7806,7808,7811],{"class":91,"line":156},[89,7793,7794],{"class":576},"  sayHello",[89,7796,7797],{"class":102},": () ",[89,7799,2640],{"class":569},[89,7801,7802],{"class":102}," console.",[89,7804,7805],{"class":576},"log",[89,7807,580],{"class":102},[89,7809,7810],{"class":119},"\"Hello from global util!\"",[89,7812,7813],{"class":102},"),\n",[89,7815,7816,7819,7822],{"class":91,"line":162},[89,7817,7818],{"class":102},"  version: ",[89,7820,7821],{"class":119},"\"1.0.0\"",[89,7823,123],{"class":102},[89,7825,7826],{"class":91,"line":728},[89,7827,2651],{"class":102},[89,7829,7830],{"class":91,"line":746},[89,7831,650],{"emptyLinePlaceholder":184},[89,7833,7834,7836,7838,7840,7842],{"class":91,"line":758},[89,7835,6666],{"class":102},[89,7837,6717],{"class":576},[89,7839,580],{"class":102},[89,7841,6722],{"class":119},[89,7843,610],{"class":102},[15,7845,7846,7847,7850],{},"En una plantilla eso es ",[34,7848,7849],{},"\u003Cbutton @click=\"$myGlobalUtil.sayHello()\">Say Hello\u003C\u002Fbutton>",", y en un bloque script setup:",[80,7852,7854],{"className":5289,"code":7853,"language":1177,"meta":85,"style":85},"\u003Cscript setup>\nimport { getCurrentInstance } from \"vue\";\nconst { proxy } = getCurrentInstance();\n\nproxy.$myGlobalUtil.sayHello();\nconsole.log(proxy.$myGlobalUtil.version);\n\u003C\u002Fscript>\n",[34,7855,7856,7866,7879,7897,7901,7911,7921],{"__ignoreMap":85},[89,7857,7858,7860,7862,7864],{"class":91,"line":92},[89,7859,3023],{"class":102},[89,7861,3367],{"class":109},[89,7863,3483],{"class":595},[89,7865,3338],{"class":102},[89,7867,7868,7870,7873,7875,7877],{"class":91,"line":99},[89,7869,3490],{"class":569},[89,7871,7872],{"class":102}," { getCurrentInstance } ",[89,7874,3496],{"class":569},[89,7876,4474],{"class":119},[89,7878,790],{"class":102},[89,7880,7881,7883,7885,7888,7890,7892,7895],{"class":91,"line":106},[89,7882,2306],{"class":569},[89,7884,5455],{"class":102},[89,7886,7887],{"class":595},"proxy",[89,7889,5469],{"class":102},[89,7891,377],{"class":569},[89,7893,7894],{"class":576}," getCurrentInstance",[89,7896,743],{"class":102},[89,7898,7899],{"class":91,"line":116},[89,7900,650],{"emptyLinePlaceholder":184},[89,7902,7903,7906,7909],{"class":91,"line":126},[89,7904,7905],{"class":102},"proxy.$myGlobalUtil.",[89,7907,7908],{"class":576},"sayHello",[89,7910,743],{"class":102},[89,7912,7913,7916,7918],{"class":91,"line":134},[89,7914,7915],{"class":102},"console.",[89,7917,7805],{"class":576},[89,7919,7920],{"class":102},"(proxy.$myGlobalUtil.version);\n",[89,7922,7923,7925,7927],{"class":91,"line":142},[89,7924,3395],{"class":102},[89,7926,3367],{"class":109},[89,7928,3338],{"class":102},[15,7930,7931,7932,7935],{},"Para algo mayor, que implique varias propiedades globales, directivas, componentes o lógica de enrutado, escribe un plugin. Un plugin es una función que recibe la instancia ",[34,7933,7934],{},"app"," y un objeto opcional de opciones.",[80,7937,7939],{"className":560,"code":7938,"language":562,"meta":85,"style":85},"\u002F\u002F plugins\u002FmyPlugin.js\nexport default {\n  install: (app, options) => {\n    \u002F\u002F Make a global method available\n    app.config.globalProperties.$translate = (key) => {\n      return key\n        .split(\".\")\n        .reduce((o, i) => (o ? o[i] : undefined), options.translations);\n    };\n\n    \u002F\u002F Provide a global composable\n    app.provide(\"globalStore\", {\n      user: \"John Doe\",\n      settings: { theme: \"dark\" },\n    });\n\n    \u002F\u002F Or register a global component\n    \u002F\u002F app.component('MyGlobalComponent', MyGlobalComponent)\n  },\n};\n",[34,7940,7941,7946,7954,7974,7979,8000,8008,8023,8058,8063,8067,8072,8086,8096,8107,8112,8116,8121,8126,8132],{"__ignoreMap":85},[89,7942,7943],{"class":91,"line":92},[89,7944,7945],{"class":95},"\u002F\u002F plugins\u002FmyPlugin.js\n",[89,7947,7948,7950,7952],{"class":91,"line":99},[89,7949,5036],{"class":569},[89,7951,6836],{"class":569},[89,7953,658],{"class":583},[89,7955,7956,7959,7961,7963,7965,7968,7970,7972],{"class":91,"line":106},[89,7957,7958],{"class":576},"  install",[89,7960,6870],{"class":102},[89,7962,7934],{"class":583},[89,7964,1472],{"class":102},[89,7966,7967],{"class":583},"options",[89,7969,2108],{"class":102},[89,7971,2640],{"class":569},[89,7973,658],{"class":102},[89,7975,7976],{"class":91,"line":116},[89,7977,7978],{"class":95},"    \u002F\u002F Make a global method available\n",[89,7980,7981,7984,7987,7989,7991,7994,7996,7998],{"class":91,"line":126},[89,7982,7983],{"class":102},"    app.config.globalProperties.",[89,7985,7986],{"class":576},"$translate",[89,7988,599],{"class":569},[89,7990,685],{"class":102},[89,7992,7993],{"class":583},"key",[89,7995,2108],{"class":102},[89,7997,2640],{"class":569},[89,7999,658],{"class":102},[89,8001,8002,8005],{"class":91,"line":134},[89,8003,8004],{"class":569},"      return",[89,8006,8007],{"class":102}," key\n",[89,8009,8010,8013,8016,8018,8021],{"class":91,"line":142},[89,8011,8012],{"class":102},"        .",[89,8014,8015],{"class":576},"split",[89,8017,580],{"class":102},[89,8019,8020],{"class":119},"\".\"",[89,8022,6563],{"class":102},[89,8024,8025,8027,8029,8031,8033,8035,8038,8040,8042,8045,8047,8050,8052,8055],{"class":91,"line":150},[89,8026,8012],{"class":102},[89,8028,3214],{"class":576},[89,8030,3217],{"class":102},[89,8032,4189],{"class":583},[89,8034,1472],{"class":102},[89,8036,8037],{"class":583},"i",[89,8039,2108],{"class":102},[89,8041,2640],{"class":569},[89,8043,8044],{"class":102}," (o ",[89,8046,3876],{"class":569},[89,8048,8049],{"class":102}," o[i] ",[89,8051,3892],{"class":569},[89,8053,8054],{"class":595}," undefined",[89,8056,8057],{"class":102},"), options.translations);\n",[89,8059,8060],{"class":91,"line":156},[89,8061,8062],{"class":102},"    };\n",[89,8064,8065],{"class":91,"line":162},[89,8066,650],{"emptyLinePlaceholder":184},[89,8068,8069],{"class":91,"line":728},[89,8070,8071],{"class":95},"    \u002F\u002F Provide a global composable\n",[89,8073,8074,8077,8079,8081,8084],{"class":91,"line":746},[89,8075,8076],{"class":102},"    app.",[89,8078,7356],{"class":576},[89,8080,580],{"class":102},[89,8082,8083],{"class":119},"\"globalStore\"",[89,8085,3980],{"class":102},[89,8087,8088,8091,8094],{"class":91,"line":758},[89,8089,8090],{"class":102},"      user: ",[89,8092,8093],{"class":119},"\"John Doe\"",[89,8095,123],{"class":102},[89,8097,8098,8101,8104],{"class":91,"line":769},[89,8099,8100],{"class":102},"      settings: { theme: ",[89,8102,8103],{"class":119},"\"dark\"",[89,8105,8106],{"class":102}," },\n",[89,8108,8109],{"class":91,"line":779},[89,8110,8111],{"class":102},"    });\n",[89,8113,8114],{"class":91,"line":793},[89,8115,650],{"emptyLinePlaceholder":184},[89,8117,8118],{"class":91,"line":799},[89,8119,8120],{"class":95},"    \u002F\u002F Or register a global component\n",[89,8122,8123],{"class":91,"line":808},[89,8124,8125],{"class":95},"    \u002F\u002F app.component('MyGlobalComponent', MyGlobalComponent)\n",[89,8127,8128,8130],{"class":91,"line":2055},[89,8129,6908],{"class":102},[89,8131,123],{"class":583},[89,8133,8134,8136],{"class":91,"line":3983},[89,8135,6973],{"class":583},[89,8137,790],{"class":102},[80,8139,8141],{"className":560,"code":8140,"language":562,"meta":85,"style":85},"\u002F\u002F main.js\nimport { createApp } from \"vue\";\nimport App from \".\u002FApp.vue\";\nimport MyPlugin from \".\u002Fplugins\u002FmyPlugin\";\n\nconst app = createApp(App);\n\nconst translations = {\n  hello: \"Bonjour\",\n  welcome: {\n    message: \"Bienvenue!\",\n  },\n};\n\napp.use(MyPlugin, { translations }); \u002F\u002F Use the plugin with options\napp.mount(\"#app\");\n",[34,8142,8143,8147,8159,8171,8185,8189,8201,8205,8216,8226,8231,8241,8245,8249,8253,8266],{"__ignoreMap":85},[89,8144,8145],{"class":91,"line":92},[89,8146,7731],{"class":95},[89,8148,8149,8151,8153,8155,8157],{"class":91,"line":99},[89,8150,3490],{"class":569},[89,8152,6618],{"class":102},[89,8154,3496],{"class":569},[89,8156,4474],{"class":119},[89,8158,790],{"class":102},[89,8160,8161,8163,8165,8167,8169],{"class":91,"line":106},[89,8162,3490],{"class":569},[89,8164,6631],{"class":102},[89,8166,3496],{"class":569},[89,8168,6636],{"class":119},[89,8170,790],{"class":102},[89,8172,8173,8175,8178,8180,8183],{"class":91,"line":116},[89,8174,3490],{"class":569},[89,8176,8177],{"class":102}," MyPlugin ",[89,8179,3496],{"class":569},[89,8181,8182],{"class":119}," \".\u002Fplugins\u002FmyPlugin\"",[89,8184,790],{"class":102},[89,8186,8187],{"class":91,"line":126},[89,8188,650],{"emptyLinePlaceholder":184},[89,8190,8191,8193,8195,8197,8199],{"class":91,"line":134},[89,8192,2306],{"class":569},[89,8194,6649],{"class":595},[89,8196,599],{"class":569},[89,8198,6654],{"class":576},[89,8200,6657],{"class":102},[89,8202,8203],{"class":91,"line":142},[89,8204,650],{"emptyLinePlaceholder":184},[89,8206,8207,8209,8212,8214],{"class":91,"line":150},[89,8208,2306],{"class":569},[89,8210,8211],{"class":595}," translations",[89,8213,599],{"class":569},[89,8215,658],{"class":102},[89,8217,8218,8221,8224],{"class":91,"line":156},[89,8219,8220],{"class":102},"  hello: ",[89,8222,8223],{"class":119},"\"Bonjour\"",[89,8225,123],{"class":102},[89,8227,8228],{"class":91,"line":162},[89,8229,8230],{"class":102},"  welcome: {\n",[89,8232,8233,8236,8239],{"class":91,"line":728},[89,8234,8235],{"class":102},"    message: ",[89,8237,8238],{"class":119},"\"Bienvenue!\"",[89,8240,123],{"class":102},[89,8242,8243],{"class":91,"line":746},[89,8244,6702],{"class":102},[89,8246,8247],{"class":91,"line":758},[89,8248,2651],{"class":102},[89,8250,8251],{"class":91,"line":769},[89,8252,650],{"emptyLinePlaceholder":184},[89,8254,8255,8257,8260,8263],{"class":91,"line":779},[89,8256,6666],{"class":102},[89,8258,8259],{"class":576},"use",[89,8261,8262],{"class":102},"(MyPlugin, { translations }); ",[89,8264,8265],{"class":95},"\u002F\u002F Use the plugin with options\n",[89,8267,8268,8270,8272,8274,8276],{"class":91,"line":793},[89,8269,6666],{"class":102},[89,8271,6717],{"class":576},[89,8273,580],{"class":102},[89,8275,6722],{"class":119},[89,8277,610],{"class":102},[15,8279,8280,8281,8284,8285,8288],{},"A partir de ahí cualquier componente puede llamar a ",[34,8282,8283],{},"$translate('welcome.message')"," o inyectar ",[34,8286,8287],{},"globalStore",". Es el mismo mecanismo que usan Vue Router y Pinia para integrarse en una aplicación.",[15,8290,8291],{},"Estos siete patrones cubren la mayoría de las situaciones en las que lo básico deja de bastar. Cuanto más recurres a ellos, más naturales resultan.",[170,8293,8294],{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}",{"title":85,"searchDepth":99,"depth":99,"links":8296},[8297,8299,8300,8301,8302,8303,8304],{"id":4988,"depth":99,"text":8298},"1. La Composition API más allá de setup()",{"id":5496,"depth":99,"text":5497},{"id":6157,"depth":99,"text":6158},{"id":6587,"depth":99,"text":6588},{"id":6795,"depth":99,"text":6796},{"id":7142,"depth":99,"text":7143},{"id":7702,"depth":99,"text":7703},"2025-09-15","Composables, Teleport, Suspense, directivas personalizadas, funciones de render, provide\u002Finject y plugins, cada uno con su código.",{},"\u002Fblog\u002Ftecnicas-avanzadas-de-vue-3",{"title":4980,"description":8306},"blog\u002Ftecnicas-avanzadas-de-vue-3",[1177,8312,4975,562,8313],"vue3","webdev","mastering-vue3-in-2025","gsDPwq9dtVh79fiU0wEXKp4hk4J8tWJ2UgL5Sh3tahM",1786529719225]