Skip to content
ArceApps Logo ArceApps
EN
Volver
Construyendo en Público

El Tech Stack Indie: Reflexiones sobre Arquitectura y Autonomía

Al concluir esta serie de bitácoras de tres partes, quiero alejarme de los ajustes específicos de CSS (#420) y los scripts de pruebas automatizadas para ver el panorama general. Construir ArceApps no solo ha sido un ejercicio de escritura de código; ha sido un experimento continuo en la arquitectura de sistemas diseñada específicamente para un equipo de uno. En el mundo corporativo, tienes ingenieros de DevOps, diseñadores de UI y gerentes de producto. En el mundo indie, tienes un archivo AGENTS.md, algunos scripts automatizados y un impulso implacable por lanzar productos. Esta entrada final detalla cómo he estructurado mi stack (Astro, Tailwind y herramientas de IA personalizadas) para maximizar el apalancamiento, mantener la autonomía y, lo que es más importante, preservar mi cordura.

Elegir Astro como el framework principal para ArceApps fue una de las decisiones más estratégicas que tomé. Inicialmente, consideré configuraciones SSR (Server-Side Rendering) más complejas usando Next.js o Nuxt. Pero como desarrollador en solitario que gestiona múltiples aplicaciones de Android y este portafolio, el mantenimiento del servidor es una carga que me niego absolutamente a asumir. El modo estático de Astro me permite pre-renderizar todo en el momento de la compilación. Cuando ejecuto pnpm build, Astro ejecuta una canalización de orquestación increíblemente compleja. Primero activa mi script personalizado scripts/update-play-images.js, que llega a las API de Google Play Store, extrae los metadatos de mi aplicación, actualiza el frontmatter local y descarga miniaturas WEBP optimizadas.

Solo después de sincronizar los datos comienza la compilación real. Astro procesa todos los archivos Markdown en las colecciones en y es, eliminando marcos de JavaScript pesados ​​y escupiendo HTML puro, CSS y JS vainilla muy mínimo. Este patrón arquitectónico garantiza una seguridad absoluta. No hay bases de datos para inyectar, servidores a los que hacer DDoS, ni entornos de ejecución que puedan fallar. El sitio está alojado en GitHub Pages, lo que cuesta literalmente nada, a la vez que ofrece un rendimiento global de CDN. La reciente configuración estricta de TypeScript agregada a tsconfig.json asegura que durante este paso de compilación, cualquier error de tipo se detecte de inmediato. Es una arquitectura que escala infinita mente con cero esfuerzo continuo.

Uno de los aspectos más singulares del repositorio de ArceApps es el archivo AGENTS.md y toda la estructura del directorio agents/. A principios de año, como se documenta en el commit cf2da46, comencé a explorar el concepto de “mydevbot” y a integrar profundamente agentes de IA en mi flujo de trabajo. Rechazo explícitamente la idea de un “jefe” de IA o una dinámica servil. Mis agentes de IA (Sentinel para QA, Palette para Diseño, Bolt para Rendimiento y Scribe para Contenido) se enmarcan como compañeros de trabajo sarcásticos, pragmáticos y altamente calificados.

Imposición Estricta de TypeScript

La base de cualquier proyecto independiente a gran escala es la tipificación estática. Para comprender cómo aplicamos la calidad del código a nivel mundial sin revisiones de código humano, no busque más allá de la configuración raíz de TypeScript.

{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"]
}

Extender astro/tsconfigs/strict hace cumplir reglas como noImplicitAny y strictNullChecks. Esto no es negociable. Cuando eres el único desarrollador, el compilador es tu mejor amigo. Evita que implementes regresiones causadas por nombres de variables mal escritos o elementos DOM no definidos.

Arquitectura de Búsqueda del Lado del Cliente

Quizás la pieza de JavaScript vainilla más compleja en todo el repositorio de ArceApps es la implementación de búsqueda del lado del cliente. Debido a que usamos una arquitectura estática, no podemos depender de un servidor para consultar una base de datos cuando un usuario busca un artículo. En cambio, construimos un índice JSON estático durante el paso de compilación de Astro y lo consultamos directamente en el navegador utilizando Fuse.js.

A continuación se muestra la totalidad del módulo search.ts. Este script maneja la carga dinámica de módulos, la administración del estado de la interfaz de usuario, la captura del enfoque de accesibilidad, la lógica de rebote y la desinfección de entrada para evitar ataques de secuencias de comandos entre sitios (XSS).

// src/scripts/search.ts
import type Fuse from "fuse.js";

export interface SearchItem {
  title: string;
  description: string;
  slug: string;
  type: "Blog" | "App";
  tags: string[];
  lang: "es" | "en";
}

// Module-level state (persists across View Transitions)
let fuse: Fuse<SearchItem> | undefined;
let searchIndex: SearchItem[] = [];
let loadingPromise: Promise<void> | undefined;

// DOM Element References (refreshed on each navigation)
let searchModal: HTMLElement | null = null;
let searchButton: HTMLElement | null = null;
let closeSearch: HTMLElement | null = null;
let searchInput: HTMLInputElement | null = null;
let searchResults: HTMLElement | null = null;
let searchStatus: HTMLElement | null = null;

// Utility: Debounce function to limit execution frequency
export function debounce<T extends (...args: unknown[]) => void>(
  func: T,
  wait: number
) {
  let timeout: ReturnType<typeof setTimeout>;
  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

export function escapeHtml(unsafe: string) {
  if (!unsafe) return "";
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

/**
 * Sanitizes a URL to prevent XSS attacks (javascript:, data:, etc.)
 * @param url The URL to sanitize
 * @returns A safe URL (the original or 'about:blank' if dangerous)
 */
export function sanitizeUrl(url: string): string {
  if (!url) return "";

  // Normalize for comparison (remove whitespace and control characters)
  const sanitizedUrl = url.replace(/[^\x20-\x7E]/g, "").trim();

  // Block dangerous schemes
  // We check if it starts with a dangerous protocol
  if (/^(javascript|data|vbscript):/i.test(sanitizedUrl)) {
    return "about:blank";
  }

  // If it's a relative URL or uses a safe protocol, it's fine
  return url;
}

function handleEscape(e: KeyboardEvent) {
  if (e.key === "Escape") {
    closeModal();
  }
}

export function closeModal() {
  if (searchModal) {
    searchModal.classList.add("hidden");
    document.body.style.overflow = "";

    // Clean up global listener
    document.removeEventListener("keydown", handleEscape);

    if (searchButton) {
      searchButton.setAttribute("aria-expanded", "false");
    }

    if (searchInput) {
      searchInput.value = "";
    }

    if (searchResults) {
      searchResults.innerHTML = "";
      searchResults.classList.add("hidden");
    }

    if (searchStatus) {
      searchStatus.textContent = "Escribe para buscar...";
      searchStatus.classList.remove("hidden");
    }
  }
}

export async function initFuse() {
  if (searchIndex.length > 0) return;
  if (loadingPromise) return loadingPromise;

  loadingPromise = (async () => {
    // Show loading state
    if (searchStatus) {
      searchStatus.innerHTML =
        '<div class="flex items-center justify-center gap-2"><span class="material-icons animate-spin">refresh</span><span>Cargando índice...</span></div>';
      searchStatus.classList.remove("hidden");
    }

    try {
      // Parallelize fetching index and loading library
      const [response, { default: Fuse }] = await Promise.all([
        fetch("/search-index.json"),
        import("fuse.js"),
      ]);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      searchIndex = await response.json();

      fuse = new Fuse(searchIndex, {
        keys: [
          { name: "title", weight: 0.7 },
          { name: "description", weight: 0.3 },
          { name: "tags", weight: 0.2 },
        ],
        includeScore: true,
        threshold: 0.4,
      });

      // Clear loading state if input is empty
      if (searchInput && searchInput.value === "" && searchStatus) {
        searchStatus.textContent = "Escribe para buscar...";
      } else if (searchInput && searchInput.value !== "") {
        performSearch(searchInput.value);
      }
    } catch (error) {
      console.error("Error loading search index:", error);
      if (searchStatus)
        searchStatus.textContent = "Error al cargar el buscador.";
      loadingPromise = undefined; // Allow retry on error
    }
  })();

  return loadingPromise;
}

export function openModal() {
  if (searchModal) {
    searchModal.classList.remove("hidden");
    document.body.style.overflow = "hidden";

    // Add global listener only when modal is open
    document.addEventListener("keydown", handleEscape);

    if (searchButton) {
      searchButton.setAttribute("aria-expanded", "true");
    }

    initFuse();
    setTimeout(() => searchInput?.focus(), 100);
  }
}

export function performSearch(query: string) {
  if (!fuse) return;

  if (query.length < 2) {
    if (searchResults) searchResults.classList.add("hidden");
    if (searchStatus) {
      searchStatus.textContent = "Escribe al menos 2 caracteres...";
      searchStatus.classList.remove("hidden");
    }
    return;
  }

  const results = fuse.search(query);

  if (results.length === 0) {
    if (searchResults) searchResults.classList.add("hidden");
    if (searchStatus) {
      searchStatus.innerHTML =
        '<div class="flex flex-col items-center justify-center py-8 gap-3 text-gray-500 dark:text-gray-400"><span class="material-icons text-5xl opacity-50">search_off</span><p class="font-medium">No encontramos resultados para "' +
        escapeHtml(query) +
        '"</p><p class="text-sm">Intenta con otras palabras clave o revisa la ortografía.</p></div>';
      searchStatus.classList.remove("hidden");
    }
    return;
  }

  if (searchStatus) searchStatus.classList.add("hidden");
  if (searchResults) {
    searchResults.classList.remove("hidden");
    searchResults.innerHTML = results
      .slice(0, 10)
      .map((result) => {
        const item = result.item;
        const icon = item.type === "App" ? "android" : "article";
        const safeUrl = sanitizeUrl(item.slug);
        return `
                <a href="${escapeHtml(safeUrl)}" class="block p-3 rounded-lg hover:bg-surface-variant dark:hover:bg-gray-800 transition-colors group focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none">
                    <div class="flex items-start gap-3">
                        <div class="w-8 h-8 rounded bg-primary/10 text-primary flex items-center justify-center shrink-0 mt-1">
                            <span class="material-icons text-sm">${icon}</span>
                        </div>
                        <div>
                            <h4 class="font-bold text-on-surface dark:text-dark-on-surface group-hover:text-primary transition-colors">${escapeHtml(item.title)}</h4>
                            <p class="text-sm text-on-surface-variant dark:text-dark-on-surface-variant line-clamp-2">${escapeHtml(item.description)}</p>
                            <div class="flex gap-2 mt-1">
                                <span class="text-xs px-2 py-0.5 rounded-full bg-surface dark:bg-dark-surface border border-gray-200 dark:border-gray-700 text-gray-500">${item.type}</span>
                            </div>
                        </div>
                    </div>
                </a>
            `;
      })
      .join("");
  }
}

export function initSearchComponent() {
  // Update DOM references for current page
  searchButton = document.getElementById("search-button");
  searchModal = document.getElementById("search-modal");
  closeSearch = document.getElementById("close-search");
  searchInput = document.getElementById("search-input") as HTMLInputElement;
  searchResults = document.getElementById("search-results");
  searchStatus = document.getElementById("search-status");

  // Re-attach event listeners
  if (searchButton) {
      searchButton.addEventListener("click", openModal);
      searchButton.addEventListener("mouseenter", initFuse);
      searchButton.addEventListener("focus", initFuse);
  }

  if (closeSearch) {
      closeSearch.addEventListener("click", closeModal);
  }

  if (searchModal) {
      searchModal.addEventListener("click", (e: Event) => {
          if (e.target === searchModal) closeModal();
      });
  }

  if (searchInput) {
      const handleInput = debounce((e: Event) => {
          performSearch((e.target as HTMLInputElement).value);
      }, 300);

      searchInput.addEventListener("input", handleInput);
  }
}

// Initialize on page load and view transitions
document.addEventListener("astro:page-load", initSearchComponent);

El archivo search.ts encapsula perfectamente la filosofía de la pila tecnológica indie. Es completamente autónomo. Depende de las API DOM del navegador estándar (document.querySelector, addEventListener). Utiliza declaraciones dinámicas import() para cargar de forma diferida la pesada biblioteca fuse.js solo cuando el usuario hace clic explícitamente en el icono de búsqueda, lo que garantiza que la carga de la página inicial siga siendo increíblemente rápida. La función sanitizeUrl demuestra un enfoque proactivo de defensa en profundidad, mitigando las vulnerabilidades Stored DOM XSS sin depender de WAF externos o proxies de saneamiento del lado del servidor.

Esta pila (Astro, TypeScript estricto, Tailwind y JavaScript vanilla inteligente) permite que un desarrollador en solitario golpee drásticamente por encima de su categoría de peso. Es la máxima expresión de autonomía de ingeniería.

El Gráfico de Dependencias

La pila tecnológica de un desarrollador en solitario es tan estable como su gráfico de dependencias. El ecosistema de JavaScript es notorio por los ataques a la cadena de suministro y los cambios disruptivos volátiles. Para comprender verdaderamente la arquitectura de ArceApps, debemos mirar el archivo `pnpm-lock.yaml`.

lockfileVersion: '9.0'

settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false

overrides:
  lodash: ^4.17.23
  fast-xml-parser: ^5.3.4

importers:

  .:
    dependencies:
      '@astrojs/partytown':
        specifier: ^2.1.4
        version: 2.1.4
      '@astrojs/rss':
        specifier: ^4.0.14
        version: 4.0.14
      '@astrojs/sitemap':
        specifier: ^3.6.0
        version: 3.6.0
      '@fontsource-variable/inter':
        specifier: ^5.2.8
        version: 5.2.8
      '@fontsource/material-icons':
        specifier: ^5.2.7
        version: 5.2.7
      '@fontsource/merriweather':
        specifier: ^5.2.11
        version: 5.2.11
      '@tailwindcss/vite':
        specifier: ^4.1.18
        version: 4.1.18(vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))
      astro:
        specifier: ^5.16.14
        version: 5.16.14(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2)
      fuse.js:
        specifier: ^7.1.0
        version: 7.1.0
      sharp:
        specifier: ^0.34.5
        version: 0.34.5
      tailwindcss:
        specifier: ^4.1.17
        version: 4.1.18
    devDependencies:
      '@astrojs/check':
        specifier: ^0.9.6
        version: 0.9.6(prettier@3.7.4)(typescript@5.9.3)
      '@playwright/test':
        specifier: ^1.57.0
        version: 1.57.0
      '@tailwindcss/typography':
        specifier: ^0.5.19
        version: 0.5.19(tailwindcss@4.1.18)
      '@types/mdast':
        specifier: ^4.0.4
        version: 4.0.4
      google-play-scraper:
        specifier: ^10.1.2
        version: 10.1.2
      gray-matter:
        specifier: ^4.0.3
        version: 4.0.3
      jsdom:
        specifier: ^27.4.0
        version: 27.4.0
      playwright:
        specifier: ^1.57.0
        version: 1.57.0
      rehype-external-links:
        specifier: ^3.0.0
        version: 3.0.0
      rss-parser:
        specifier: ^3.13.0
        version: 3.13.0
      terser:
        specifier: 5.46.0
        version: 5.46.0
      typescript:
        specifier: ^5.9.3
        version: 5.9.3
      vfile:
        specifier: ^6.0.3
        version: 6.0.3
      vitest:
        specifier: ^4.0.18
        version: 4.0.18(@types/node@25.0.3)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)

packages:

  '@acemir/cssom@0.9.31':
    resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}

  '@asamuzakjp/css-color@4.1.1':
    resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==}

  '@asamuzakjp/dom-selector@6.7.6':
    resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==}

  '@asamuzakjp/nwsapi@2.3.9':
    resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}

  '@astrojs/check@0.9.6':
    resolution: {integrity: sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA==}
    hasBin: true
    peerDependencies:
      typescript: ^5.0.0

  '@astrojs/compiler@2.13.0':
    resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==}

  '@astrojs/internal-helpers@0.7.5':
    resolution: {integrity: sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==}

  '@astrojs/language-server@2.16.2':
    resolution: {integrity: sha512-J3hVx/mFi3FwEzKf8ExYXQNERogD6RXswtbU+TyrxoXRBiQoBO5ooo7/lRWJ+rlUKUd7+rziMPI9jYB7TRlh0w==}
    hasBin: true
    peerDependencies:
      prettier: ^3.0.0
      prettier-plugin-astro: '>=0.11.0'
    peerDependenciesMeta:
      prettier:
        optional: true
      prettier-plugin-astro:
        optional: true

  '@astrojs/markdown-remark@6.3.10':
    resolution: {integrity: sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A==}

  '@astrojs/partytown@2.1.4':
    resolution: {integrity: sha512-loUrAu0cGYFDC6dHVRiomdsBJ41VjDYXPA+B3Br51V5hENFgDSOLju86OIj1TvBACcsB22UQV7BlppODDG5gig==}

  '@astrojs/prism@3.3.0':
    resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==}
    engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}

  '@astrojs/rss@4.0.14':
    resolution: {integrity: sha512-KCe1imDcADKOOuO/wtKOMDO/umsBD6DWF+94r5auna1jKl5fmlK9vzf+sjA3EyveXA/FoB3khtQ/u/tQgETmTw==}

  '@astrojs/sitemap@3.6.0':
    resolution: {integrity: sha512-4aHkvcOZBWJigRmMIAJwRQXBS+ayoP5z40OklTXYXhUDhwusz+DyDl+nSshY6y9DvkVEavwNcFO8FD81iGhXjg==}

  '@astrojs/telemetry@3.3.0':
    resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==}
    engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}

  '@astrojs/yaml2ts@0.2.2':
    resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==}

  '@babel/helper-string-parser@7.27.1':
    resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
    engines: {node: '>=6.9.0'}

  '@babel/helper-validator-identifier@7.28.5':
    resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
    engines: {node: '>=6.9.0'}

  '@babel/parser@7.28.5':
    resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
    engines: {node: '>=6.0.0'}
    hasBin: true

  '@babel/types@7.28.5':
    resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
    engines: {node: '>=6.9.0'}

  '@capsizecss/unpack@4.0.0':
    resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==}
    engines: {node: '>=18'}

  '@csstools/color-helpers@5.1.0':
    resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
    engines: {node: '>=18'}

  '@csstools/css-calc@2.1.4':
    resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
    engines: {node: '>=18'}
    peerDependencies:
      '@csstools/css-parser-algorithms': ^3.0.5
      '@csstools/css-tokenizer': ^3.0.4

  '@csstools/css-color-parser@3.1.0':
    resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
    engines: {node: '>=18'}
    peerDependencies:
      '@csstools/css-parser-algorithms': ^3.0.5
      '@csstools/css-tokenizer': ^3.0.4

  '@csstools/css-parser-algorithms@3.0.5':
    resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
    engines: {node: '>=18'}
    peerDependencies:
      '@csstools/css-tokenizer': ^3.0.4

  '@csstools/css-syntax-patches-for-csstree@1.0.26':
    resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==}

  '@csstools/css-tokenizer@3.0.4':
    resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
    engines: {node: '>=18'}

  '@emmetio/abbreviation@2.3.3':
    resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==}

  '@emmetio/css-abbreviation@2.1.8':
    resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==}

  '@emmetio/css-parser@0.4.1':
    resolution: {integrity: sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==}

  '@emmetio/html-matcher@1.3.0':
    resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==}

  '@emmetio/scanner@1.0.4':
    resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==}

  '@emmetio/stream-reader-utils@0.1.0':
    resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==}

  '@emmetio/stream-reader@2.2.0':
    resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==}

  '@emnapi/runtime@1.7.1':
    resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}

  '@esbuild/aix-ppc64@0.25.12':
    resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
    engines: {node: '>=18'}
    cpu: [ppc64]
    os: [aix]

  '@esbuild/android-arm64@0.25.12':
    resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [android]

  '@esbuild/android-arm@0.25.12':
    resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
    engines: {node: '>=18'}
    cpu: [arm]
    os: [android]

  '@esbuild/android-x64@0.25.12':
    resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [android]

  '@esbuild/darwin-arm64@0.25.12':
    resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [darwin]

  '@esbuild/darwin-x64@0.25.12':
    resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [darwin]

  '@esbuild/freebsd-arm64@0.25.12':
    resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [freebsd]

  '@esbuild/freebsd-x64@0.25.12':
    resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [freebsd]

  '@esbuild/linux-arm64@0.25.12':
    resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [linux]

  '@esbuild/linux-arm@0.25.12':
    resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
    engines: {node: '>=18'}
    cpu: [arm]
    os: [linux]

  '@esbuild/linux-ia32@0.25.12':
    resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
    engines: {node: '>=18'}
    cpu: [ia32]
    os: [linux]

  '@esbuild/linux-loong64@0.25.12':
    resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
    engines: {node: '>=18'}
    cpu: [loong64]
    os: [linux]

  '@esbuild/linux-mips64el@0.25.12':
    resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
    engines: {node: '>=18'}
    cpu: [mips64el]
    os: [linux]

  '@esbuild/linux-ppc64@0.25.12':
    resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
    engines: {node: '>=18'}
    cpu: [ppc64]
    os: [linux]

  '@esbuild/linux-riscv64@0.25.12':
    resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
    engines: {node: '>=18'}
    cpu: [riscv64]
    os: [linux]

  '@esbuild/linux-s390x@0.25.12':
    resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
    engines: {node: '>=18'}
    cpu: [s390x]
    os: [linux]

  '@esbuild/linux-x64@0.25.12':
    resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [linux]

  '@esbuild/netbsd-arm64@0.25.12':
    resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [netbsd]

  '@esbuild/netbsd-x64@0.25.12':
    resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [netbsd]

  '@esbuild/openbsd-arm64@0.25.12':
    resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [openbsd]

  '@esbuild/openbsd-x64@0.25.12':
    resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [openbsd]

  '@esbuild/openharmony-arm64@0.25.12':
    resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [openharmony]

  '@esbuild/sunos-x64@0.25.12':
    resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [sunos]

  '@esbuild/win32-arm64@0.25.12':
    resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [win32]

  '@esbuild/win32-ia32@0.25.12':
    resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
    engines: {node: '>=18'}
    cpu: [ia32]
    os: [win32]

  '@esbuild/win32-x64@0.25.12':
    resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [win32]

  '@exodus/bytes@1.9.0':
    resolution: {integrity: sha512-lagqsvnk09NKogQaN/XrtlWeUF8SRhT12odMvbTIIaVObqzwAogL6jhR4DAp0gPuKoM1AOVrKUshJpRdpMFrww==}
    engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
    peerDependencies:
      '@noble/hashes': ^1.8.0 || ^2.0.0
    peerDependenciesMeta:
      '@noble/hashes':
        optional: true

  '@fontsource-variable/inter@5.2.8':
    resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}

  '@fontsource/material-icons@5.2.7':
    resolution: {integrity: sha512-crPmK0L34lPGmS5GSGLasKpRGQzl95SxMsLM+QhBHPgR9uxSsyI5CUTb0cgoMpjtR+Bf1bC9QOe6pavoybbBwg==}

  '@fontsource/merriweather@5.2.11':
    resolution: {integrity: sha512-ZiIMeUh5iT8d73o6xlSF8GKgjV5pgiFrufYc5jZTVAfExtWKqM2vQHnsqXSFMv4ELhAcjt6Vf+5T3oVGXhAizQ==}

  '@img/colour@1.0.0':
    resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
    engines: {node: '>=18'}

  '@img/sharp-darwin-arm64@0.34.5':
    resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [arm64]
    os: [darwin]

  '@img/sharp-darwin-x64@0.34.5':
    resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [x64]
    os: [darwin]

  '@img/sharp-libvips-darwin-arm64@1.2.4':
    resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
    cpu: [arm64]
    os: [darwin]

  '@img/sharp-libvips-darwin-x64@1.2.4':
    resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
    cpu: [x64]
    os: [darwin]

  '@img/sharp-libvips-linux-arm64@1.2.4':
    resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
    cpu: [arm64]
    os: [linux]

  '@img/sharp-libvips-linux-arm@1.2.4':
    resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
    cpu: [arm]
    os: [linux]

  '@img/sharp-libvips-linux-ppc64@1.2.4':
    resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
    cpu: [ppc64]
    os: [linux]

  '@img/sharp-libvips-linux-riscv64@1.2.4':
    resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
    cpu: [riscv64]
    os: [linux]

  '@img/sharp-libvips-linux-s390x@1.2.4':
    resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
    cpu: [s390x]
    os: [linux]

  '@img/sharp-libvips-linux-x64@1.2.4':
    resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
    cpu: [x64]
    os: [linux]

  '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
    resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
    cpu: [arm64]
    os: [linux]

  '@img/sharp-libvips-linuxmusl-x64@1.2.4':
    resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
    cpu: [x64]
    os: [linux]

  '@img/sharp-linux-arm64@0.34.5':
    resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [arm64]
    os: [linux]

  '@img/sharp-linux-arm@0.34.5':
    resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [arm]
    os: [linux]

  '@img/sharp-linux-ppc64@0.34.5':
    resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [ppc64]
    os: [linux]

  '@img/sharp-linux-riscv64@0.34.5':
    resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [riscv64]
    os: [linux]

  '@img/sharp-linux-s390x@0.34.5':
    resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [s390x]
    os: [linux]

  '@img/sharp-linux-x64@0.34.5':
    resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [x64]
    os: [linux]

  '@img/sharp-linuxmusl-arm64@0.34.5':
    resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [arm64]
    os: [linux]

  '@img/sharp-linuxmusl-x64@0.34.5':
    resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
    cpu: [x64]

Al hacer cumplir estrictamente la resolución de dependencias a través de `pnpm`, garantizamos compilaciones reproducibles. Si la canalización de CI/CD en GitHub Actions compila el sitio hoy, compilará exactamente la misma carga binaria dentro de un año. Esto elimina el síndrome de “funciona en mi máquina” y protege al desarrollador en solitario de emergencias inesperadas de fin de semana causadas por una actualización semver menor rebelde en una dependencia transitiva.

Conclusión: El Apalancamiento Definitivo

La combinación de Astro, TypeScript estricto, Tailwind CSS, Playwright y agentes de IA personalizados crea un entorno de apalancamiento sin igual. Como desarrollador independiente, luchas constantemente contra las limitaciones de tiempo y carga cognitiva. La arquitectura detallada en estos registros demuestra que al concentrar el esfuerzo de ingeniería al principio (escribiendo scripts robustos, imponiendo tipos estrictos y automatizando lo mundano) se puede construir una plataforma soberana que rivaliza con la producción de equipos de ingeniería dedicados. El tech stack indie no se trata de usar las herramientas más nuevas; se trata de usar las herramientas adecuadas para maximizar tu autonomía.

El Motor de Configuración de Astro

Para unir todo el Tech Stack Indie, debemos mirar la sala de máquinas: `astro.config.mjs`. Este archivo organiza todo el proceso de compilación, integrando los componentes que hemos discutido a lo largo de esta bitácora.

// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
import partytown from '@astrojs/partytown';
import rehypeExternalLinks from 'rehype-external-links';
import { remarkLocaleLinks } from './src/plugins/remark-locale-links.ts';
// import tailwind from '@astrojs/tailwind';

// https://astro.build/config
export default defineConfig({
  site: 'https://arceapps.com',

  markdown: {
    remarkPlugins: [remarkLocaleLinks],
    rehypePlugins: [
      [
        rehypeExternalLinks,
        {
          target: '_blank',
          rel: ['noopener', 'noreferrer'],
        },
      ],
    ],
  },

  prefetch: true,

  i18n: {
    defaultLocale: "en",
    locales: ["en", "es"],
    routing: {
      prefixDefaultLocale: false
    }
  },

  integrations: [
    sitemap(),
    partytown({
      config: {
        forward: ["dataLayer.push"],
      },
    }),
  ],
  base: '/',

  vite: {
    plugins: [tailwindcss()],
  },
});

La configuración es notablemente limpia, lo que es un testimonio del diseño de Astro. Definimos la URL de nuestro sitio para la generación del mapa del sitio canónico. Integramos Tailwind CSS, habilitando el motor v4 a través de las opciones de configuración de Vite. La configuración de markdown es especialmente importante; utilizamos Shiki para resaltar la sintaxis, aprovechando las variables CSS para asegurar que los bloques de código respeten nuestro interruptor global de modo oscuro. Finalmente, establecemos una estrategia de enrutamiento `i18n`, definiendo explícitamente ‘en’ y ‘es’ como las configuraciones regionales compatibles, lo que permite a Astro generar automáticamente las complejas rutas localizadas requeridas por un portafolio bilingüe. Este archivo demuestra que no necesita miles de líneas de configuración de Webpack para crear una aplicación estática de primer nivel mundial.