[{"data":1,"prerenderedAt":342},["ShallowReactive",2],{"\u002Fprojects\u002Fdualroute\u002F":3,"posts":203},{"id":4,"title":5,"body":6,"description":143,"extension":144,"featured":145,"images":146,"links":163,"meta":170,"navigation":145,"order":171,"path":172,"seo":173,"stack":174,"stats":186,"stem":199,"tag":200,"url":201,"__hash__":202},"projects\u002Fprojects\u002Fdualroute.md","DualRoute",{"type":7,"value":8,"toc":131},"minimark",[9,14,70,74,79,83,87,93,97,103,107],[10,11,13],"h2",{"id":12},"qué-hace","Qué hace",[15,16,17,25,35,41,47,53,59],"ul",{},[18,19,20,24],"li",{},[21,22,23],"strong",{},"Rutas con su modo."," Cada página guardada (una \"ruta\") se abre en modo normal, en incógnito o en los dos a la vez. El modo va con la ruta, no hay que elegirlo cada vez.",[18,26,27,30,31,34],{},[21,28,29],{},"Abrir en bloque."," Filtras por categoría o buscas, marcas las rutas y ",[21,32,33],{},"Abrir seleccionadas"," las abre todas, cada una en su modo. Las de incógnito van juntas en una sola ventana.",[18,36,37,40],{},[21,38,39],{},"Cuándo la abriste por última vez."," Cada apertura desde la extensión queda registrada con la fecha, el modo y si fue suelta o en bloque. Nada más: no lee el historial del navegador ni mira qué pestañas tienes.",[18,42,43,46],{},[21,44,45],{},"Recordatorios por horas."," Si una ruta lleva más de X horas sin abrirse, se marca como pendiente y, si lo activas, llega una notificación. Se limpia sola al volver a abrirla.",[18,48,49,52],{},[21,50,51],{},"Notas con historial."," Notas por ruta o generales, marcadas como importantes o pendientes. Al editarlas se guarda la versión anterior.",[18,54,55,58],{},[21,56,57],{},"Tus datos, en un JSON."," Exportas las secciones que quieras e importas un JSON validado antes de reemplazar nada.",[18,60,61,64,65,69],{},[21,62,63],{},"Local y sin servidor."," Todo se guarda en ",[66,67,68],"code",{},"chrome.storage.local",". Sin cuentas, sin sincronización y sin content scripts. En español e inglés, con tema claro u oscuro.",[10,71,73],{"id":72},"cómo-está-hecho","Cómo está hecho",[75,76,78],"h3",{"id":77},"el-popup-pide-el-service-worker-abre","El popup pide, el service worker abre",[80,81,82],"p",{},"El popup y el panel no abren pestañas. Mandan un mensaje al service worker con los ids de las rutas, y es él quien planifica, pide confirmación si son muchas pestañas, las abre y apunta las estadísticas. Así cerrar el popup a mitad de un lanzamiento no lo corta.",[75,84,86],{"id":85},"un-planificador-puro-y-un-ejecutor-con-fallback","Un planificador puro y un ejecutor con fallback",[80,88,89,92],{},[66,90,91],{},"planLaunch"," reparte las URLs en dos lotes, normal e incógnito (una ruta en \"los dos\" va a ambos), sin tocar ninguna API del navegador, así que se prueba con Vitest sin más. El ejecutor abre las normales como pestañas en segundo plano, con un retardo configurable entre una y otra, y las de incógnito en una única ventana. Si la extensión no tiene permiso en incógnito, las abre en normal y lo avisa.",[75,94,96],{"id":95},"un-solo-estado-escrito-con-candado","Un solo estado, escrito con candado",[80,98,99,100,102],{},"Todo el estado es un objeto validado con Zod bajo una clave de ",[66,101,68],{},". Lo escriben el popup, el panel y el service worker, así que cada escritura pasa por un lock de la Web Locks API que lee, transforma y guarda sin que otro contexto se cuele en medio.",[10,104,106],{"id":105},"problemas-que-resolví","Problemas que resolví",[15,108,109,119,125],{},[18,110,111,114,115,118],{},[21,112,113],{},"Recordatorios que solo existían con el popup abierto."," Al principio se evaluaban cuando abrías la extensión, y la notificación llegaba justo cuando ya no hacía falta. Ahora una alarma de ",[66,116,117],{},"chrome.alarms"," los revisa cada minuto aunque no abras nada.",[18,120,121,124],{},[21,122,123],{},"Escrituras que se pisaban."," El popup y el service worker hacían su propio leer-modificar-guardar, y la última en guardar borraba los cambios de la otra. Lo arreglé serializando todas las escrituras con un lock compartido.",[18,126,127,130],{},[21,128,129],{},"Datos que solo crecían."," El historial de lanzamientos y las revisiones de notas no tenían límite, y todo el estado se lee entero en cada operación. Ahora se quedan en 1.000 lanzamientos y 20 revisiones por nota.",{"title":132,"searchDepth":133,"depth":133,"links":134},"",2,[135,136,142],{"id":12,"depth":133,"text":13},{"id":72,"depth":133,"text":73,"children":137},[138,140,141],{"id":77,"depth":139,"text":78},3,{"id":85,"depth":139,"text":86},{"id":95,"depth":139,"text":96},{"id":105,"depth":133,"text":106},"Extensión de Chrome para abrir con un clic las páginas que revisas cada día, cada una en modo normal, incógnito o en los dos. Lleva la cuenta de cuándo abriste cada una por última vez, avisa cuando toca volver a mirarla, guarda notas con historial de cambios y exporta todo a JSON. Sin historial del navegador, sin content scripts y sin servidor.","md",true,[147,152,157,160],{"src":148,"alt":149,"width":150,"height":151},"\u002Fimages\u002Fposts\u002Fdualroute\u002Fpopup.webp","El popup de DualRoute con la lista de rutas, sus modos de apertura, categorías y un recordatorio pendiente",720,1120,{"src":153,"alt":154,"width":155,"height":156},"\u002Fimages\u002Fposts\u002Fdualroute\u002Fpanel-rutas.webp","El panel de rutas con búsqueda, filtros por categoría, dominio, modo y recordatorio, y la lista de rutas con sus acciones",1600,1075,{"src":158,"alt":159,"width":155,"height":156},"\u002Fimages\u002Fposts\u002Fdualroute\u002Frecordatorios.webp","La sección de recordatorios, con rutas vencidas hace dos horas y otras que vencen dentro de cinco",{"src":161,"alt":162,"width":155,"height":156},"\u002Fimages\u002Fposts\u002Fdualroute\u002Fhistorial.webp","El historial de lanzamientos con la fecha, la ruta, el modo normal o incógnito y si se abrió sola o por lotes",[164,167],{"label":165,"url":166},"Cómo se usa","\u002Fposts\u002Fdualroute-tus-paginas-de-cada-dia",{"label":168,"url":169},"Por dentro","\u002Fposts\u002Fdualroute-por-dentro",{},13,"\u002Fprojects\u002Fdualroute",{"title":5,"description":143},[175,176,177,178,179,180,181,182,183,184,185],"WXT","Manifest V3","React 19","TypeScript","Tailwind CSS 4","Zod","Chrome Storage","Chrome Alarms","Web Locks","Vitest","Playwright",[187,190,193,196],{"value":188,"label":189},"3","modos de apertura",{"value":191,"label":192},"0","content scripts",{"value":194,"label":195},"2","idiomas",{"value":197,"label":198},"75","tests (Vitest y Playwright)","projects\u002Fdualroute","dualroute","https:\u002F\u002Fchromewebstore.google.com\u002Fdetail\u002Fdualroute\u002Fdncknellidppghfbonbigngaidncjigg","aAXfGqm9nPqxEqr7PlsJF0jZk7gUSy0XWCnyeCBPuQU",[204,215,224,231,237,244,250,257,262,270,275,284,292,297,304,309,317,323,329,335],{"title":205,"description":206,"date":207,"path":208,"tags":209,"type":214},"Video Accessibility: los atajos de YouTube en cualquier vídeo de la web","Una extensión de Chrome que añade los atajos de teclado de YouTube a cualquier página con un vídeo HTML. Pulsas 3 y saltas al 30 %, cambias la velocidad, haces un bucle o pulsas el botón de \"me gusta\" con una tecla. Solo en las webs que eliges y sin enviar ningún dato. Qué hace y cómo se usa.","2026-10-26","\u002Fposts\u002Fvideo-accessibility-atajos-de-youtube-en-cualquier-video",[210,211,212,213],"video-accessibility","productividad","accesibilidad","privacidad","guia",{"title":216,"description":217,"date":207,"path":218,"tags":219,"type":223},"Video Accessibility por dentro: una capa de teclado encima de reproductores que no son tuyos","Cómo funciona una extensión de Chrome que añade atajos de teclado a cualquier vídeo HTML. Un permiso amplio con una inyección mínima, encontrar el vídeo correcto entre shadow DOM e iframes, no quedarse con teclas que no son suyas, pulsar botones ajenos con un selector CSS y un aviso en pantalla que pasó de 4,6 MB a 1 KB.","\u002Fposts\u002Fvideo-accessibility-por-dentro",[210,220,221,222],"arquitectura","typescript","vue","tecnico",{"title":225,"description":226,"date":227,"path":228,"tags":229,"type":223},"DevExtractor por dentro: lo que hay detrás de una extensión que parece simple","Cómo funciona una extensión de Chrome que extrae imágenes, SVG inline, enlaces y fuentes de cualquier web y sustituye imágenes en vivo. Manifest V3 con WXT, descargas desde un documento offscreen, reglas de red que no se salen de su sitio, un motor de reemplazo que se puede deshacer y botones de Instagram que no dependen del idioma.","2026-10-25","\u002Fposts\u002Fdevextractor-por-dentro",[230,220,221,222],"devextractor",{"title":232,"description":233,"date":227,"path":234,"tags":235,"type":214},"DevExtractor: descarga SVG inline, imágenes y fuentes de cualquier web, y cambia imágenes en vivo","Una extensión de Chrome para quien hace webs. Saca todos los SVG incrustados, las imágenes, los fondos CSS, los enlaces y las fuentes de una página, los descarga en un ZIP y te deja sustituir imágenes en vivo para enseñar alternativas a un cliente. Qué hace y cómo se usa.","\u002Fposts\u002Fdevextractor-svg-imagenes-y-fuentes-en-un-clic",[230,211,213,236],"proyectos",{"title":238,"description":239,"date":240,"path":241,"tags":242,"type":214},"ABR Offline Converter: convierte imágenes, vídeo, PDF y datos sin subir nada a ningún servidor","Un conversor de archivos que funciona entero en tu navegador. Imágenes, vídeo a GIF, vídeo a audio, CSV a JSON, PDF, fuentes y subtítulos, y un Taller del libro que convierte tus capítulos en Markdown en un EPUB con portada e índice. Qué hace y cómo se usa.","2026-10-24","\u002Fposts\u002Foffline-converter-convierte-sin-subir-nada",[243,213,211,236],"offline-converter",{"title":245,"description":246,"date":240,"path":247,"tags":248,"type":223},"ABR Offline Converter por dentro: un conversor sin servidor, de ffmpeg.wasm a un EPUB hecho a mano","Cómo funciona un conversor de imágenes, vídeo, PDF, fuentes, subtítulos, texto y datos que no tiene backend. Workers, ffmpeg compilado a WebAssembly, codificadores BMP, ICO y TIFF escritos a mano, 811 páginas prerenderizadas y un Taller del libro que genera EPUB3 sin pandoc.","\u002Fposts\u002Foffline-converter-por-dentro",[243,220,221,249],"nuxt",{"title":251,"description":252,"date":253,"path":254,"tags":255,"type":214},"Simple TTS: escucha cualquier artículo de la web en voz alta, sin salir de tu navegador","Una extensión gratuita para Chrome y Edge que lee en voz alta lo que selecciones o el artículo entero con las voces del propio navegador. Sin cuentas, sin servidores y sin APIs externas. Qué hace y cómo se usa.","2026-10-23","\u002Fposts\u002Fsimple-tts-escucha-cualquier-web",[256,213,211,236],"simple-tts",{"title":258,"description":259,"date":253,"path":260,"tags":261,"type":223},"Simple TTS por dentro: mi primera extensión, de JavaScript a WXT con Vue","Cómo pasó Simple TTS de tres archivos de JavaScript a una extensión Manifest V3 con WXT, Vue 3 y TypeScript. Qué me hizo cambiar en cada etapa, cómo se mantiene vivo un service worker durante una lectura larga, por qué leía la frase del día anterior y cómo se consigue que el resaltado vaya a la par de la voz.","\u002Fposts\u002Fsimple-tts-por-dentro",[256,220,221,222],{"title":263,"description":264,"date":265,"path":266,"tags":267,"type":223},"TranscribeLocal por dentro: Whisper en el navegador y cómo dejó de inventarse frases","Cómo ejecuta TranscribeLocal Whisper, la separación de hablantes y un modelo de lenguaje dentro del navegador con Transformers.js, por qué Whisper se inventaba frases en los silencios, qué hace falta para transcribir audios largos y por qué un service worker cortaba las descargas a los cinco minutos.","2026-10-22","\u002Fposts\u002Ftranscribe-local-por-dentro",[268,269,220,221,249],"transcribe-local","ia",{"title":271,"description":272,"date":265,"path":273,"tags":274,"type":214},"TranscribeLocal: transcribe y saca subtítulos de cualquier vídeo, sin salir de tu navegador","Una app web que convierte audio y vídeo en texto y subtítulos con Whisper, distingue quién habla y hace resúmenes, todo en tu ordenador. Sin subir nada a ningún servidor, sin límites de duración y gratis. Qué hace y cómo se usa.","\u002Fposts\u002Ftranscribe-local-subtitulos-en-tu-navegador",[268,269,213,236],{"title":276,"description":277,"date":278,"path":279,"tags":280,"type":214},"PokeStore: un tema de WordPress a medida para tiendas de cartas coleccionables","Un tema de WordPress y WooCommerce hecho desde cero para tiendas de Pokémon y One Piece TCG. Un recorrido por la tienda como la ve quien compra, de la portada a la lista de deseos, y por lo que puede ajustar quien la lleva.","2026-10-21","\u002Fposts\u002Fpokestore-tema-wordpress-para-tiendas-tcg",[281,282,283,236],"pokestore","wordpress","woocommerce",{"title":285,"description":286,"date":287,"path":288,"tags":289,"type":214},"YouTube Like Shortcut: da like con una tecla, como en TikTok","Una extensión de Chrome que da like al vídeo o Short de YouTube que estás viendo al pulsar la L. Cómo se instala, qué opciones tiene y por qué no se dispara mientras escribes.","2026-10-20","\u002Fposts\u002Fyoutube-like-shortcut-like-con-una-tecla",[290,291,211,236],"youtube-like-shortcut","youtube",{"title":293,"description":294,"date":287,"path":295,"tags":296,"type":223},"YouTube Like Shortcut por dentro: pulsar un botón en una web que cambia sin avisar","Cómo una extensión de una sola función llega antes que el reproductor de YouTube, encuentra el botón de like entre variantes ocultas y layouts que cambian con el ancho de la ventana, y se asegura de que el like ha entrado aunque YouTube esté reconstruyendo la página.","\u002Fposts\u002Fyoutube-like-shortcut-por-dentro",[290,291,220,221],{"title":298,"description":299,"date":300,"path":301,"tags":302,"type":214},"AI Upscaler: amplía imágenes y quita fondos con IA, sin salir de tu navegador","Una extensión de Chrome que amplía imágenes ×2 o ×4 y elimina fondos con inteligencia artificial, todo en tu equipo. Sin subir nada a ningún servidor, sin cuentas y gratis. Qué hace y cómo se usa.","2026-10-19","\u002Fposts\u002Fai-upscaler-imagenes-con-ia-en-tu-navegador",[303,269,213,236],"ai-upscaler",{"title":305,"description":306,"date":300,"path":307,"tags":308,"type":223},"AI Upscaler por dentro: dos redes neuronales en una extensión de Chrome","Cómo ejecuta AI Upscaler Real-ESRGAN y U²-Net dentro del navegador con ONNX Runtime Web, por qué la imagen se procesa en mosaicos con margen, qué hubo que parchear en el modelo de quitar fondos y qué obstáculos pone una extensión Manifest V3 a los workers y al WebAssembly.","\u002Fposts\u002Fai-upscaler-por-dentro",[303,269,220,221,222],{"title":310,"description":311,"date":312,"path":313,"tags":314,"type":214},"ABR Cipher: cifrado seguro, completamente en tu navegador","Una suite de cifrado que no envía nada a ningún servidor. Cifrar textos y archivos, compartir secretos con un enlace que caduca, guardar contraseñas y destruir archivos. Qué hace y cómo se usa.","2026-10-18","\u002Fposts\u002Fabr-cipher-cifrado-en-tu-navegador",[315,316,213,236],"cipher","seguridad",{"title":318,"description":319,"date":312,"path":320,"tags":321,"type":223},"ABR Cipher por dentro: criptografía sin servidor, de la URL al disco","Por qué ABR Cipher solo usa la Web Crypto API, cómo viaja un secreto en el fragmento de una URL, cómo se cifra un archivo de varios GB en el navegador y qué hace falta para destruir un archivo de verdad, con un compañero en Rust que lee NTFS.","\u002Fposts\u002Fabr-cipher-por-dentro",[315,316,220,221,322],"rust",{"title":324,"description":325,"date":312,"path":326,"tags":327,"type":223},"ABReader por dentro: el lector que no encontré, construido a mi medida","Voces neuronales que se ejecutan en el navegador, sincronización entre dispositivos sin servidor y once formatos detrás de un mismo contrato. Las decisiones técnicas de ABReader y por qué las tomé.","\u002Fposts\u002Fabreader-por-dentro",[328,220,221,249],"abreader",{"title":330,"description":331,"date":312,"path":332,"tags":333,"type":214},"ABReader: tu biblioteca de libros electrónicos en el navegador","Un lector de libros electrónicos que funciona en el navegador, sin cuentas ni servidores. Qué hace, cómo se usa y cómo probarlo en un minuto.","\u002Fposts\u002Fabreader-tu-biblioteca-en-el-navegador",[328,334,236],"lectura",{"title":336,"description":337,"date":312,"path":338,"tags":339,"type":223},"Cómo construí este blog: estático, sin servidor y sin coste de hosting","Por qué elegí Nuxt y Markdown en lugar de WordPress, cómo separo el código privado del sitio publicado y qué se rompió en el primer despliegue.","\u002Fposts\u002Farquitectura-de-este-blog",[249,222,340,341],"devops","meta",1793026879246]