The Indie Tech Stack: Reflections on Architecture and Autonomy
As I wrap up this three-part devlog series, I want to zoom out from the specific CSS tweaks (#420) and automated testing scripts to look at the bigger picture. Building ArceApps hasn’t just been an exercise in writing code; it’s been an ongoing experiment in systems architecture designed specifically for a team of one. In the corporate world, you have DevOps engineers, UI designers, and product managers. In the indie world, you have an AGENTS.md file, a few automated scripts, and a relentless drive to ship. This final entry details how I’ve structured my stack—Astro, Tailwind, and custom AI tooling—to maximize leverage, maintain autonomy, and most importantly, preserve my sanity.
Choosing Astro as the core framework for ArceApps was one of the most strategic decisions I made. Initially, I considered more complex SSR (Server-Side Rendering) setups using Next.js or Nuxt. But as a solo developer managing multiple Android apps and this portfolio, server maintenance is a liability I absolutely refuse to take on. Astro’s static mode allows me to pre-render everything at build time. When I run pnpm build, Astro executes an incredibly complex orchestration pipeline. It first triggers my custom scripts/update-play-images.js, which hits the Google Play Store APIs, scrapes my application metadata, updates the local frontmatter, and downloads optimized WEBP thumbnails.
Only after the data is synchronized does the actual build begin. Astro processes all the Markdown files across the en and es content collections, stripping out heavy JavaScript frameworks and spitting out pure HTML, CSS, and highly minimal vanilla JS. This architectural pattern ensures absolute security. There are no databases to inject, no servers to DDoS, and no runtime environments to crash. The site is hosted on GitHub Pages, costing literally nothing, while delivering global CDN edge performance. The recent strict TypeScript configuration added to tsconfig.json ensures that during this build step, any type errors—even in complex generic search functions—are caught immediately. It’s an architecture that scales infinitely with zero ongoing infrastructure effort.
One of the most unique aspects of the ArceApps repository is the AGENTS.md file and the entire agents/ directory structure. Earlier in the year, as documented in commit cf2da46, I began exploring the concept of “mydevbot” and deeply integrating AI agents into my workflow. I explicitly reject the idea of an AI “boss” or subservient dynamic. My AI agents—Sentinel for QA, Palette for Design, Bolt for Performance, and Scribe for Content—are framed as sarcastic, pragmatic, highly-skilled co-workers.
Strict TypeScript Enforcment
The foundation of any large-scale independent project is static typing. To understand how we enforce code quality globally without human code reviews, look no further than the root TypeScript configuration.
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
Extending astro/tsconfigs/strict enforces rules like noImplicitAny and strictNullChecks. This is non-negotiable. When you are the only developer, the compiler is your best friend. It prevents you from deploying regressions caused by misspelled variable names or undefined DOM elements.
Client-Side Search Architecture
Perhaps the most complex piece of vanilla JavaScript in the entire ArceApps repository is the client-side search implementation. Because we use a static architecture, we cannot rely on a server to query a database when a user searches for an article. Instead, we build a static JSON index during the Astro build step and query it directly in the browser using Fuse.js.
Below is the entirety of the search.ts module. This script handles dynamic module loading, UI state management, accessibility focus trapping, debounce logic, and input sanitization to prevent Cross-Site Scripting (XSS) attacks.
// 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* 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);
The search.ts file perfectly encapsulates the indie tech stack philosophy. It is completely self-contained. It relies on standard browser DOM APIs (document.querySelector, addEventListener). It uses dynamic import() statements to lazily load the heavy fuse.js library only when the user explicitly clicks the search icon, ensuring the initial page load remains blazing fast. The sanitizeUrl function demonstrates a proactive defense-in-depth approach, mitigating Stored DOM XSS vulnerabilities without relying on external WAFs or server-side sanitation proxies.
This stack—Astro, strict TypeScript, Tailwind, and intelligent vanilla JavaScript—allows a solo developer to punch drastically above their weight class. It is the ultimate expression of engineering autonomy.
The Dependency Graph
A solo developer’s tech stack is only as stable as its dependency graph. The JavaScript ecosystem is notorious for supply chain attacks and volatile breaking changes. To truly understand the architecture of ArceApps, we must look at the `pnpm-lock.yaml` file.
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]
By strictly enforcing dependency resolution via `pnpm`, we guarantee reproducible builds. If the CI/CD pipeline on GitHub Actions compiles the site today, it will compile the exact same binary payload a year from now. This eliminates the “works on my machine” syndrome and protects the solo developer from unexpected weekend emergencies caused by a rogue minor semver update in a transitive dependency.
Conclusion: The Ultimate Leverage
The combination of Astro, strict TypeScript, Tailwind CSS, Playwright, and custom AI agents creates an environment of unparalleled leverage. As an indie developer, you are constantly fighting against the constraints of time and cognitive load. The architecture detailed in these logs proves that by front-loading the engineering effort—by writing robust scripts, enforcing strict types, and automating the mundane—you can build a sovereign platform that rivals the output of dedicated engineering teams. The indie tech stack is not about using the newest tools; it is about using the right tools to maximize your autonomy.
The Astro Configuration Engine
To tie the entire Indie Tech Stack together, we must look at the engine room: `astro.config.mjs`. This file orchestrates the entire build process, integrating the components we’ve discussed throughout this devlog.
// @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()],
},
});
The configuration is remarkably clean, which is a testament to Astro’s design. We define our site URL for canonical sitemap generation. We integrate Tailwind CSS, enabling the v4 engine via the Vite configuration options. The markdown configuration is especially important; we utilize Shiki for syntax highlighting, leveraging CSS variables to ensure code blocks respect our global dark mode toggle. Finally, we establish an `i18n` routing strategy, explicitly defining ‘en’ and ‘es’ as supported locales, allowing Astro to automatically generate the complex localized routes required by a bilingual portfolio. This file proves that you do not need thousands of lines of Webpack configuration to build a world-class static application.