<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged vite</title>
<link>https://coderlegion.com/tag/vite</link>
<description>Powered by Question2Answer</description>
<item>
<title>Project LifeOps (Part 2): React Frontend with Glassmorphism, 360° Dashboard, and Dark Mode Design Sy</title>
<link>https://coderlegion.com/27058/project-lifeops-part-react-frontend-with-glassmorphism-360-dashboard-dark-mode-design</link>
<description>&lt;p&gt;In &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Part 1 of this series&lt;/a&gt;, we established the foundational cloud infrastructure: a zero-cost ($0/month) deployment topology, a polymorphic PostgreSQL schema hosted on Supabase, and a modular, high-performance REST API built with FastAPI.&lt;/p&gt;
&lt;p&gt;Having a robust database and resilient backend is critical, but a &lt;strong&gt;Personal Operating System (LifeOps)&lt;/strong&gt; only succeeds if using it every single day is an engaging, friction-free, and visually stimulating experience. If an interface feels clunky, sluggish, or resembles an uninspired, boilerplate admin template, the habit of tracking workouts, reading milestones, and professional tasks dissolves within days.&lt;/p&gt;
&lt;p&gt;To solve this, I set out to engineer a Single Page Application (SPA) pairing a &lt;strong&gt;modern Glassmorphism Dark Mode aesthetic&lt;/strong&gt;, &lt;strong&gt;instantaneous sub-millisecond interaction speeds&lt;/strong&gt;, &lt;strong&gt;360° data telemetry&lt;/strong&gt;, and &lt;strong&gt;seamless device responsiveness&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;Test the live app&lt;/strong&gt;: You can explore the production build of LifeOps directly at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;https://datalaria.com/apps/lifeops/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-lifeops-series-roadmap&quot;&gt;🗺️ LifeOps Series Roadmap&lt;/h4&gt;
&lt;p&gt;To understand how every architectural layer fits together, this series spans 5 structured installments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Personal Operating System Architecture and FastAPI + Supabase Backend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 2 (This article)&lt;/strong&gt;: React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 3&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;In-Memory Word Dossiers (.docx) &amp;amp; Multi-Sheet Excel Engine (.xlsx)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Part 5&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part5_deploy_mobile_pwa/&quot;&gt;24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-frontend-tooling-blazing-speeds-with-vite-amp-react-&quot;&gt;1. Frontend Tooling: Blazing Speeds with Vite &amp;amp; React ⚡&lt;/h4&gt;
&lt;p&gt;When configuring the client workspace (&lt;code&gt;lifeops-app&lt;/code&gt;), the primary objective was maintaining a modular, lean codebase free from the dependency bloat that plagues modern web development.&lt;/p&gt;
&lt;p&gt;I chose &lt;strong&gt;React 18/19&lt;/strong&gt; paired with &lt;strong&gt;Vite&lt;/strong&gt; as our next-generation build tool:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;lifeops-app&quot;,
  &quot;private&quot;: true,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;lint&quot;: &quot;oxlint&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;
  },
  &quot;dependencies&quot;: {
    &quot;@supabase/supabase-js&quot;: &quot;^2.112.2&quot;,
    &quot;lucide-react&quot;: &quot;^1.31.0&quot;,
    &quot;react&quot;: &quot;^19.2.8&quot;,
    &quot;react-dom&quot;: &quot;^19.2.8&quot;,
    &quot;react-router-dom&quot;: &quot;^7.18.2&quot;,
    &quot;recharts&quot;: &quot;^3.10.1&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@vitejs/plugin-react&quot;: &quot;^6.0.4&quot;,
    &quot;vite&quot;: &quot;^8.2.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;why-this-technical-combination-&quot;&gt;Why this technical combination?&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instant Server Start &amp;amp; Hot Module Replacement (HMR)&lt;/strong&gt;: Vite serves source code over native browser &lt;em&gt;ES Modules&lt;/em&gt; during development, completely eliminating the sluggish bundling delays of legacy setups like Webpack or Create React App.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistent Iconography with &lt;code&gt;lucide-react&lt;/code&gt;&lt;/strong&gt;: Featherweight SVG icons with unified stroke weight, fully configurable via native React props.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Declarative Data Visualization with &lt;code&gt;recharts&lt;/code&gt;&lt;/strong&gt;: Composable SVG-based chart primitives engineered specifically for React state, avoiding clunky imperative DOM manipulations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero Bulky UI Frameworks&lt;/strong&gt;: We avoid heavy CSS component libraries. Every component is styled using pure modern CSS, native variables, and GPU-accelerated backdrop blur filters.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-design-system-pure-glassmorphism-dark-mode-&quot;&gt;2. Design System: Pure Glassmorphism Dark Mode 🎨&lt;/h4&gt;
&lt;p&gt;The visual identity of LifeOps revolves around &lt;strong&gt;translucent frosted glassmorphism&lt;/strong&gt;: sleek cards floating above a deep slate backdrop (&lt;code&gt;#0b0f17&lt;/code&gt;), accented by subtle emerald, cyan, and purple radial glows.&lt;/p&gt;
&lt;p&gt;Rather than loading bloated CSS utilities, we defined our design tokens directly in &lt;code&gt;:root&lt;/code&gt; inside &lt;code&gt;src/index.css&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root {
  /* Premium Dark Mode Base Palette */
  --bg-main: #0b0f17;
  --bg-sidebar: #111827;
  --bg-card: rgba(17, 24, 39, 0.7);
  --bg-card-hover: rgba(31, 41, 55, 0.8);
  --bg-glass: rgba(15, 23, 42, 0.65);
  
  /* Borders &amp;amp; Frosted Glass Blur */
  --border-color: rgba(255, 255, 255, 0.08);
  --border-glow: rgba(59, 130, 246, 0.3);
  --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
  --backdrop-blur: blur(16px);

  /* Semantic Color Accents */
  --accent-emerald: #10b981;
  --accent-emerald-glow: rgba(16, 185, 129, 0.25);
  --accent-cyan: #06b6d4;
  --accent-cyan-glow: rgba(6, 182, 212, 0.25);
  --accent-purple: #8b5cf6;
  --accent-purple-glow: rgba(139, 92, 246, 0.25);
  --accent-amber: #f59e0b;
  --accent-rose: #f43f5e;

  /* Typography */
  --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
  --font-heading: 'Outfit', 'Inter', sans-serif;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;reusable-utility-classes-glass-panel-amp-glass-card&quot;&gt;Reusable Utility Classes: &lt;code&gt;.glass-panel&lt;/code&gt; &amp;amp; &lt;code&gt;.glass-card&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;With these tokens in place, we encapsulated the frosted glass effect into two reusable CSS classes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.glass-panel {
  background: var(--bg-card);
  backdrop-filter: var(--backdrop-blur);
  -webkit-backdrop-filter: var(--backdrop-blur);
  border: 1px solid var(--border-color);
  border-radius: 16px;
  box-shadow: var(--glass-shadow);
}

.glass-card {
  background: linear-gradient(145deg, rgba(30, 41, 59, 0.5) 0%, rgba(15, 23, 42, 0.6) 100%);
  backdrop-filter: var(--backdrop-blur);
  -webkit-backdrop-filter: var(--backdrop-blur);
  border: 1px solid var(--border-color);
  border-radius: 16px;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

.glass-card:hover {
  border-color: rgba(255, 255, 255, 0.18);
  transform: translateY(-2px);
  box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.45);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key trick lies in layering &lt;code&gt;backdrop-filter: blur(16px)&lt;/code&gt; with an angled linear gradient (&lt;code&gt;145deg&lt;/code&gt;) and a hairline semi-transparent border (&lt;code&gt;rgba(255, 255, 255, 0.08)&lt;/code&gt;). This produces an authentic specular edge highlight on the top-left corner, mimicking physical bevelled glass.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-reactive-authentication-amp-fastapi-integration-&quot;&gt;3. Reactive Authentication &amp;amp; FastAPI Integration 🔐&lt;/h4&gt;
&lt;p&gt;The frontend communicates with two core cloud services:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Supabase Auth&lt;/strong&gt;: User identity management, session tokens, and cryptographic verification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FastAPI Backend on Render&lt;/strong&gt;: Business domain logic, aggregated analytics, and streaming report generation.&lt;/li&gt;
&lt;/ol&gt;
&lt;h5 id=&quot;authcontext-jsx-real-time-session-lifecycle&quot;&gt;&lt;code&gt;AuthContext.jsx&lt;/code&gt;: Real-Time Session Lifecycle&lt;/h5&gt;
&lt;p&gt;We built a centralized context provider subscribing to auth state transitions via &lt;code&gt;onAuthStateChange&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React, { createContext, useContext, useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [session, setSession] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    // 1. Fetch initial session on application mount
    supabase.auth.getSession().then(({ data: { session } }) =&amp;gt; {
      setSession(session);
      setUser(session?.user ?? null);
      setLoading(false);
    });

    // 2. Listen to active auth transitions (login, logout, token refresh)
    const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) =&amp;gt; {
      setSession(session);
      setUser(session?.user ?? null);
      setLoading(false);
    });

    return () =&amp;gt; subscription.unsubscribe();
  }, []);

  const signIn = (email, password) =&amp;gt; supabase.auth.signInWithPassword({ email, password });
  const signOut = () =&amp;gt; supabase.auth.signOut();

  return (
    &amp;lt;AuthContext.Provider loading, session, signIn, signOut user, value=&quot;{{&quot; }}&amp;gt;
      {children}
    &amp;lt;/AuthContext.Provider&amp;gt;
  );
}

export const useAuth = () =&amp;gt; useContext(AuthContext);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;apifetch-seamless-jwt-injection&quot;&gt;&lt;code&gt;apiFetch&lt;/code&gt;: Seamless JWT Injection&lt;/h5&gt;
&lt;p&gt;To ensure every API call to FastAPI is authenticated without manual boilerplate, we crafted a lightweight wrapper in &lt;code&gt;src/lib/api.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { supabase } from './supabase';

export const API_BASE_URL = import.meta.env.VITE_API_URL || '[https://lifeops-api.onrender.com](https://lifeops-api.onrender.com)';

export async function apiFetch(endpoint, options = {}) {
  // Extract active JWT access token from Supabase session
  const { data: { session } } = await supabase.auth.getSession();
  const token = session?.access_token;

  const headers = {
    'Content-Type': 'application/json',
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
    ...(options.headers || {}),
  };

  const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers });

  if (!response.ok) {
    const errorData = await response.json().catch(() =&amp;gt; ({ detail: response.statusText }));
    throw new Error(errorData.detail || `HTTP Error ${response.status}`);
  }

  return response.status === 204 ? null : response.json();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Through this abstraction, individual components never touch raw authorization headers: whenever a signed-in user triggers an action, their JWT is transparently forwarded to FastAPI.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-engineering-the-360-dashboard-&quot;&gt;4. Engineering the 360° Dashboard 📊&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;Main Dashboard&lt;/strong&gt; (&lt;code&gt;DashboardPage.jsx&lt;/code&gt;) is the command center that greets the user upon login, designed to communicate a holistic view of life and work metrics in under 5 seconds.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/lifeops_dashboard_360_en.png&quot; alt=&quot;LifeOps 360° Dashboard overview with Glassmorphism and Dark Mode&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│  👋 Welcome back, Diego!                                               │
│  Daily overview of personal telemetry and professional project flow.   │
├─────────────────┬─────────────────┬─────────────────┬──────────────────┤
│ 🏃 ACTIVITIES   │ 💼 PROJECTS     │ 📋 TASKS        │ 🔥 HABITS        │
│      28         │        5        │      14         │     6 / 7        │
│ 142 total       │ Of 8 active     │ 2 overdue       │ Active streaks   │
├─────────────────┴─────────────────┴─────────────────┴──────────────────┤
│  ⚡ QUICK ACTIONS: [+ Log Workout] [+ New Book] [+ Task] [+ Report]     │
├──────────────────────────────────────┬─────────────────────────────────┤
│  📊 ACTIVITY BREAKDOWN (30 DAYS)     │  📡 BACKEND HEALTH MONITOR      │
│  [Recharts Bar Chart with Neon Bars] │  🟢 FastAPI Online (Render.com) │
│  ■ Fitness  ■ Books  ■ Cinema        │  🗄️ Supabase DB (lifeops schema)│
└──────────────────────────────────────┴─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;4-1-navigation-architecture-sidebar-and-shortcuts-menu-sidebar-jsx-&quot;&gt;4.1. Navigation Architecture: Sidebar and Shortcuts Menu (&lt;code&gt;Sidebar.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;To keep navigation intuitive without cluttering the screen, we organized the sidebar into two functional tiers: primary top-level areas (Dashboard, Personal Area, Professional Area, Reports, Settings) and quick thematic shortcuts linking directly to everyday logging flows (Fitness, Books, Cinema, Kanban Board):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/lifeops_sidebar_menu_en.png&quot; alt=&quot;Sidebar navigation menu with primary sections and quick access shortcuts&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;4-2-kpi-stat-cards-statwidget-jsx-&quot;&gt;4.2. KPI Stat Cards (&lt;code&gt;StatWidget.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;The top metrics grid features semantic accent variants designed for quick cognitive parsing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export function StatWidget({ title, value, subtitle, icon: Icon, color = 'emerald' }) {
  return (
    &amp;lt;div className={`stat-widget glass-card color-${color}`}&amp;gt;
      &amp;lt;div className=&quot;stat-widget-header&quot;&amp;gt;
        &amp;lt;span className=&quot;stat-title&quot;&amp;gt;{title}&amp;lt;/span&amp;gt;
        {Icon &amp;amp;&amp;amp; (
          &amp;lt;div className=&quot;stat-icon-wrapper&quot;&amp;gt;
            &amp;lt;Icon size=&quot;{20}&quot;/&amp;gt;
          &amp;lt;/div&amp;gt;
        )}
      &amp;lt;/div&amp;gt;
      &amp;lt;div className=&quot;stat-widget-body&quot;&amp;gt;
        &amp;lt;span className=&quot;stat-value&quot;&amp;gt;{value ?? 0}&amp;lt;/span&amp;gt;
        {subtitle &amp;amp;&amp;amp; &amp;lt;span className=&quot;stat-subtitle&quot;&amp;gt;{subtitle}&amp;lt;/span&amp;gt;}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each color variant (&lt;code&gt;color-emerald&lt;/code&gt;, &lt;code&gt;color-cyan&lt;/code&gt;, &lt;code&gt;color-purple&lt;/code&gt;, &lt;code&gt;color-amber&lt;/code&gt;) washes the icon container in a 20% opacity translucent glow (&lt;code&gt;--accent-emerald-glow&lt;/code&gt;), establishing instant visual categorization without visual noise.&lt;/p&gt;
&lt;h5 id=&quot;4-3-activity-telemetry-with-recharts&quot;&gt;4.3. Activity Telemetry with Recharts&lt;/h5&gt;
&lt;p&gt;To visualize monthly activity volume, we fetch data from &lt;code&gt;/api/v1/stats/breakdown&lt;/code&gt; and render a responsive bar chart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';

const chartData = [
  { name: 'Sport', count: breakdown?.sport || 0, color: '#10b981' },
  { name: 'Books', count: breakdown?.book || 0, color: '#06b6d4' },
  { name: 'Cinema/TV', count: breakdown?.film || 0, color: '#8b5cf6' },
  { name: 'Learning', count: breakdown?.learning || 0, color: '#f59e0b' },
  { name: 'Journal', count: breakdown?.journal || 0, color: '#ec4899' },
];

&amp;lt;div style={{ width: '100%', height: 220 }}&amp;gt;
  &amp;lt;ResponsiveContainer height=&quot;100%&quot; width=&quot;100%&quot;&amp;gt;
    &amp;lt;BarChart -20, 0 10, bottom: data=&quot;{chartData}&quot; left: margin=&quot;{{&quot; right: top: }}&amp;gt;
      &amp;lt;XAxis dataKey=&quot;name&quot; fontSize=&quot;{12}&quot; stroke=&quot;#6b7280&quot; tickLine=&quot;{false}&quot;/&amp;gt;
      &amp;lt;YAxis allowDecimals=&quot;{false}&quot; fontSize=&quot;{12}&quot; stroke=&quot;#6b7280&quot; tickLine=&quot;{false}&quot;/&amp;gt;
      &amp;lt;Tooltip '#fff', '8px', 'rgba(17, 'rgba(255, 0.1)', 0.95)', 24, 255, 39, backgroundColor: borderColor: borderRadius: color: contentStyle=&quot;{{&quot; }}/&amp;gt;
      &amp;lt;Bar 0, 0]} 6, dataKey=&quot;count&quot; radius=&quot;{[6,&quot;&amp;gt;
        {chartData.map((entry, index) =&amp;gt; (
          &amp;lt;Cell fill=&quot;{entry.color}&quot; key=&quot;{`cell-${index}`}&quot;/&amp;gt;
        ))}
      &amp;lt;/Bar&amp;gt;
    &amp;lt;/BarChart&amp;gt;
  &amp;lt;/ResponsiveContainer&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;4-4-quick-actions-menu-quickactions-jsx-&quot;&gt;4.4. Quick Actions Menu (&lt;code&gt;QuickActions.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Friction is the number one reason personal productivity dashboards fall out of use. Logging a workout or finishing a book chapter must never require navigating through multiple nested views. From the central hub, the &lt;code&gt;QuickActions&lt;/code&gt; panel exposes direct action buttons that launch focused data entry modals instantly:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/lifeops_quick_actions_menu_en.png&quot; alt=&quot;Quick Actions panel for rapid logging of activities, reading sessions, and executive reports&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-production-resilience-handling-render-cold-starts-systemhealth-&quot;&gt;5. Production Resilience: Handling Render Cold-Starts (&lt;code&gt;SystemHealth&lt;/code&gt;) 🛡️&lt;/h4&gt;
&lt;p&gt;As noted in Part 1, our backend runs on the free tier of &lt;strong&gt;Render.com&lt;/strong&gt;, which automatically puts containers to sleep after 15 minutes of inactivity.&lt;/p&gt;
&lt;p&gt;When an end-user visits LifeOps after several hours of downtime, the initial HTTP handshake takes 20 to 40 seconds while the container spins up. Without proactive frontend handling, users would encounter frozen loaders or abrupt network errors.&lt;/p&gt;
&lt;p&gt;To solve this, I designed the &lt;strong&gt;&lt;code&gt;SystemHealth.jsx&lt;/code&gt;&lt;/strong&gt; component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export function SystemHealth() {
  const [health, setHealth] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [retryCount, setRetryCount] = useState(0);

  const checkHealth = async (isAutoRetry = false) =&amp;gt; {
    setLoading(true);
    if (!isAutoRetry) setError(null);

    try {
      const data = await api.getHealth();
      setHealth(data);
      setError(null);
    } catch (err) {
      setError(err.message || 'API Connection Error');
      
      // Auto-retry up to 3 times with 6-second backoff while Render wakes up
      if (retryCount &amp;lt; 3) {
        setTimeout(() =&amp;gt; setRetryCount((prev) =&amp;gt; prev + 1), 6000);
      }
    } finally {
      setLoading(false);
    }
  };

  useEffect(() =&amp;gt; {
    checkHealth(retryCount &amp;gt; 0);
  }, [retryCount]);

  return (
    &amp;lt;div className=&quot;system-health-widget glass-panel&quot;&amp;gt;
      {error ? (
        &amp;lt;div className=&quot;health-alert error&quot;&amp;gt;
          &amp;lt;AlertCircle size=&quot;{18}&quot;/&amp;gt;
          &amp;lt;span&amp;gt;Render instance waking up (Cold start)... retrying automatically.&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
      ) : (
        &amp;lt;div className=&quot;health-alert success&quot;&amp;gt;
          &amp;lt;CheckCircle2 size=&quot;{18}&quot;/&amp;gt;
          &amp;lt;span&amp;gt;{health?.engine} — Online (Schema: {health?.schema})&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;[!NOTE]&lt;br&gt;
This automatic retry strategy converts what would otherwise look like an outage into an informative, real-time telemetry indicator.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;6-featherweight-native-internationalization-i18n-&quot;&gt;6. Featherweight Native Internationalization (i18n) 🌐&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/lifeops_header_language_en.png&quot; alt=&quot;Header controls bar with bilingual language switcher (ES / EN) and Supabase Auth status&quot;&gt;&lt;/p&gt;
&lt;p&gt;Many single-page apps install heavy internationalization suites that bundle unnecessary complexity. For LifeOps, I built an elegant, zero-dependency translation engine in &lt;strong&gt;under 80 lines&lt;/strong&gt; within &lt;code&gt;LanguageContext.jsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React, { createContext, useContext, useState, useEffect } from 'react';
import { es } from '../i18n/locales/es';
import { en } from '../i18n/locales/en';

const translations = { es, en };
const LanguageContext = createContext();

export function LanguageProvider({ children }) {
  const [language, setLanguageState] = useState(() =&amp;gt; {
    const saved = localStorage.getItem('lifeops_language');
    if (saved === 'es' || saved === 'en') return saved;
    return navigator.language?.toLowerCase().startsWith('es') ? 'es' : 'en';
  });

  const toggleLanguage = () =&amp;gt; {
    const next = language === 'es' ? 'en' : 'es';
    setLanguageState(next);
    localStorage.setItem('lifeops_language', next);
    document.documentElement.lang = next;
  };

  // Dot-notation resolver with dynamic token interpolation
  const t = (path, params = {}) =&amp;gt; {
    const dict = translations[language] || translations.es;
    let val = path.split('.').reduce((prev, curr) =&amp;gt; prev?.[curr], dict) || path;
    if (typeof val === 'string') {
      return Object.keys(params).reduce((str, key) =&amp;gt; str.replace(new RegExp(`\\{${key}\\}`, 'g'), params[key]), val);
    }
    return val;
  };

  return (
    &amp;lt;LanguageContext.Provider language, t toggleLanguage, value=&quot;{{&quot; }}&amp;gt;
      {children}
    &amp;lt;/LanguageContext.Provider&amp;gt;
  );
}

export const useLanguage = () =&amp;gt; useContext(LanguageContext);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;key-architectural-benefits-&quot;&gt;Key Architectural Benefits:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Zero External Dependencies&lt;/strong&gt;: Shaves over 100 KB from production bundle footprints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intuitive API&lt;/strong&gt;: Translates seamlessly via &lt;code&gt;t('dashboard.welcome')&lt;/code&gt; or interpolates counts with &lt;code&gt;t('sport.stats.distance', { count: 10 })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart Persistence &amp;amp; Auto-Detection&lt;/strong&gt;: Stores preference in &lt;code&gt;localStorage&lt;/code&gt; and detects the browser's preferred locale on first visit.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;conclusion-amp-what-s-next-&quot;&gt;Conclusion &amp;amp; What's Next 🎯&lt;/h4&gt;
&lt;p&gt;With our React frontend fully configured, the Glassmorphism Dark Mode design system operational, reactive authentication connected to Supabase, and the 360° Dashboard displaying real-time metrics, LifeOps now has an engaging, modern client ready for daily use.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Part 3&lt;/strong&gt; of this series, we dive directly into the functional core:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fitness &amp;amp; Sport Module&lt;/strong&gt;: Logging runs, gym workouts, and cycling sessions with pace calculations, Personal Bests (PB), and calorie tracking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading &amp;amp; Cinema Tracking&lt;/strong&gt;: Dynamic page completion bars, streaming platform catalogs, and ratings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Professional Kanban Board&lt;/strong&gt;: 4-column project workflow, task prioritization, and &lt;strong&gt;chronological progress logging with timestamps stored in PostgreSQL JSONB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;references-amp-useful-links-&quot;&gt;References &amp;amp; Useful Links 🔗&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Production Application&lt;/strong&gt;: Try the live app at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;datalaria.com/apps/lifeops&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Backend API&lt;/strong&gt;: Interactive OpenAPI Swagger documentation at &lt;a rel=&quot;nofollow&quot; href=&quot;https://lifeops-api.onrender.com/docs&quot;&gt;lifeops-api.onrender.com/docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;⚛️ &lt;strong&gt;React Documentation&lt;/strong&gt;: Official React 19 documentation at &lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/&quot;&gt;react.dev&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Vite&lt;/strong&gt;: Next-generation frontend tooling at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vite.dev/&quot;&gt;vite.dev&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Recharts&lt;/strong&gt;: Declarative charting library for React at &lt;a rel=&quot;nofollow&quot; href=&quot;https://recharts.org/&quot;&gt;recharts.org&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Lucide Icons&lt;/strong&gt;: Clean, open-source SVG icon library at &lt;a rel=&quot;nofollow&quot; href=&quot;https://lucide.dev/&quot;&gt;lucide.dev&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See you in Part 3! Feel free to share your thoughts or frontend architectural tips in the comments below. 👇&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27058/project-lifeops-part-react-frontend-with-glassmorphism-360-dashboard-dark-mode-design</guid>
<pubDate>Sun, 13 Sep 2026 12:12:17 +0000</pubDate>
</item>
<item>
<title>Vite+ Beta Explained: The Future of JavaScript Tooling?</title>
<link>https://coderlegion.com/22117/vite-beta-explained-the-future-of-javascript-tooling</link>
<description>&lt;p&gt;The team behind Vite has introduced &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://voidzero.dev/posts/announcing-vite-plus-beta&quot;&gt;Vite+&lt;/a&gt;&lt;/strong&gt;, a new unified frontend toolchain currently available in beta. At first glance, it might look like just another CLI on top of Vite. But after reading the announcement, it's clear that the vision is much bigger.&lt;/p&gt;
&lt;p&gt;Vite+ isn't a replacement for Vite. Instead, it's an attempt to bring together the tools we commonly use throughout the frontend development lifecycle under one consistent developer experience.&lt;/p&gt;
&lt;p&gt;If you've ever found yourself juggling multiple CLIs, configuration files, linters, formatters, test runners, and build tools, Vite+ is trying to simplify that entire workflow.&lt;/p&gt;
&lt;p&gt;Let's take a closer look.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-is-vite-&quot;&gt;What is Vite+?&lt;/h2&gt;
&lt;p&gt;Vite+ is a unified frontend toolchain from VoidZero that combines project creation, development, building, testing, linting, formatting, type checking, runtime management, package management, and task execution through a single CLI called &lt;strong&gt;&lt;code&gt;vp&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of treating these as separate tools that developers have to install, configure, and maintain individually, Vite+ bundles them into one cohesive ecosystem.&lt;/p&gt;
&lt;p&gt;The idea is simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;One toolchain. One CLI. One consistent developer experience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you've worked on multiple React or Vite projects, you've probably seen each repository using a slightly different combination of tools. One project uses ESLint, another uses Oxlint. One uses Prettier, another uses Biome. Some rely on npm scripts, while others use custom task runners.&lt;/p&gt;
&lt;p&gt;None of these choices are wrong, but they do increase the amount of project-specific knowledge every developer has to learn.&lt;/p&gt;
&lt;p&gt;Vite+ aims to reduce that friction.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-included-&quot;&gt;What's Included?&lt;/h2&gt;
&lt;p&gt;Rather than introducing brand-new tools, Vite+ brings together some of the best modern tooling into a single ecosystem.&lt;/p&gt;
&lt;h3 id=&quot;vite&quot;&gt;Vite&lt;/h3&gt;
&lt;p&gt;Vite continues to power the development server and production builds with its fast startup time and excellent developer experience.&lt;/p&gt;
&lt;p&gt;If you're already using Vite, this part will feel familiar.&lt;/p&gt;
&lt;h3 id=&quot;vitest&quot;&gt;Vitest&lt;/h3&gt;
&lt;p&gt;Testing is handled by Vitest, which has become the preferred testing framework for many Vite projects thanks to its speed and compatibility with the Vite ecosystem.&lt;/p&gt;
&lt;p&gt;Instead of deciding between multiple testing solutions, Vite+ embraces a single, well-integrated choice.&lt;/p&gt;
&lt;h3 id=&quot;rolldown&quot;&gt;Rolldown&lt;/h3&gt;
&lt;p&gt;One of the most interesting additions is &lt;strong&gt;Rolldown&lt;/strong&gt;, the Rust-based bundler that's designed to deliver Rollup compatibility with significantly better performance.&lt;/p&gt;
&lt;p&gt;As Rolldown continues to mature, it has the potential to reduce build times without requiring developers to rewrite their existing build configurations.&lt;/p&gt;
&lt;h3 id=&quot;tsdown&quot;&gt;tsdown&lt;/h3&gt;
&lt;p&gt;Vite+ also includes &lt;strong&gt;tsdown&lt;/strong&gt;, a tool focused on building TypeScript libraries.&lt;/p&gt;
&lt;p&gt;If you've published TypeScript packages before, you'll know that creating optimized builds and declaration files often requires additional setup. tsdown aims to make that workflow much simpler.&lt;/p&gt;
&lt;h3 id=&quot;oxlint-and-oxfmt&quot;&gt;Oxlint and Oxfmt&lt;/h3&gt;
&lt;p&gt;Linting and formatting are handled by &lt;strong&gt;Oxlint&lt;/strong&gt; and &lt;strong&gt;Oxfmt&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;These Rust-powered tools focus on performance while maintaining compatibility with modern JavaScript and TypeScript development.&lt;/p&gt;
&lt;p&gt;For larger repositories, even small improvements in linting and formatting speed can noticeably improve the development experience.&lt;/p&gt;
&lt;h3 id=&quot;runtime-and-package-management&quot;&gt;Runtime and Package Management&lt;/h3&gt;
&lt;p&gt;Vite+ also manages JavaScript runtimes and package managers through the same interface.&lt;/p&gt;
&lt;p&gt;Instead of expecting developers to install and configure everything manually, the toolchain attempts to provide a more consistent environment across machines and teams.&lt;/p&gt;
&lt;h3 id=&quot;task-runner&quot;&gt;Task Runner&lt;/h3&gt;
&lt;p&gt;The new task runner allows common development tasks to be executed through the same CLI instead of scattering them across multiple npm scripts.&lt;/p&gt;
&lt;p&gt;This makes projects easier to understand and helps establish consistent conventions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-this-is-more-than-just-another-cli&quot;&gt;Why This Is More Than Just Another CLI&lt;/h2&gt;
&lt;p&gt;The biggest takeaway from Vite+ isn't the new commands.&lt;/p&gt;
&lt;p&gt;It's the shift toward &lt;strong&gt;opinionated consistency&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Modern frontend projects often rely on a collection of excellent tools, but they're usually assembled manually. Every project makes different decisions, leading to different commands, different configurations, and different maintenance requirements.&lt;/p&gt;
&lt;p&gt;Vite+ doesn't claim that existing tools are inadequate.&lt;/p&gt;
&lt;p&gt;Instead, it asks a different question:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What if all these tools worked together as one product instead of separate projects?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That approach offers several practical benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster project onboarding.&lt;/li&gt;
&lt;li&gt;Fewer configuration files to maintain.&lt;/li&gt;
&lt;li&gt;More consistent workflows across repositories.&lt;/li&gt;
&lt;li&gt;Easier upgrades.&lt;/li&gt;
&lt;li&gt;Reduced decision fatigue when starting new projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Experienced developers can certainly assemble this stack themselves.&lt;/p&gt;
&lt;p&gt;The value of Vite+ lies in providing these integrations out of the box.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-vite-feels-built-for-the-ai-era&quot;&gt;Why Vite+ Feels Built for the AI Era&lt;/h2&gt;
&lt;p&gt;One aspect of the announcement that stood out to me wasn't a feature, it was the direction.&lt;/p&gt;
&lt;p&gt;We're no longer writing code alone.&lt;/p&gt;
&lt;p&gt;Whether you use GitHub Copilot, Cursor, Claude Code, or other AI coding assistants, these tools work best when projects follow predictable structures and conventions.&lt;/p&gt;
&lt;p&gt;AI assistants have to infer how your project is organized.&lt;/p&gt;
&lt;p&gt;If every repository uses different commands, different build tools, different scripts, and different configuration patterns, that inference becomes harder.&lt;/p&gt;
&lt;p&gt;A unified toolchain changes that.&lt;/p&gt;
&lt;p&gt;When creating, building, testing, linting, formatting, and running tasks all follow consistent conventions, AI agents spend less time guessing and more time helping.&lt;/p&gt;
&lt;p&gt;That's a subtle but important advantage.&lt;/p&gt;
&lt;p&gt;I don't think Vite+ was created solely for AI-assisted development, but it certainly feels aligned with where software development is heading.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;should-you-adopt-it-today-&quot;&gt;Should You Adopt It Today?&lt;/h2&gt;
&lt;p&gt;Since Vite+ is currently in beta, I'd approach it differently depending on the project.&lt;/p&gt;
&lt;p&gt;If you're starting a personal project or exploring new tooling, it's absolutely worth trying. It offers a glimpse into what the future of frontend tooling might look like.&lt;/p&gt;
&lt;p&gt;For production applications that are already stable, I'd wait until the ecosystem matures further before migrating.&lt;/p&gt;
&lt;p&gt;The good news is that Vite+ builds upon technologies many developers are already using, so adopting it later shouldn't feel like learning an entirely new ecosystem.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Vite+ isn't trying to replace every frontend tool.&lt;/p&gt;
&lt;p&gt;It's trying to replace the experience of stitching those tools together yourself.&lt;/p&gt;
&lt;p&gt;That's an important distinction.&lt;/p&gt;
&lt;p&gt;Frontend development has reached a point where most teams use the same categories of tools, bundlers, test runners, linters, formatters, package managers, and task runners. The challenge is no longer finding good tools; it's making them work well together.&lt;/p&gt;
&lt;p&gt;Vite+ is VoidZero's answer to that challenge.&lt;/p&gt;
&lt;p&gt;Will it become the standard frontend toolchain? It's too early to say.&lt;/p&gt;
&lt;p&gt;But one thing is clear: the conversation is shifting from &lt;strong&gt;building faster tools&lt;/strong&gt; to &lt;strong&gt;building more cohesive developer experiences&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And if Vite+ delivers on that vision, it could influence how frontend projects are built for years to come.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22117/vite-beta-explained-the-future-of-javascript-tooling</guid>
<pubDate>Wed, 08 Jul 2026 13:54:52 +0000</pubDate>
</item>
<item>
<title>Rebuilding Defrag98: Getting the Details Right</title>
<link>https://coderlegion.com/21630/rebuilding-defrag98-getting-the-details-right</link>
<description>&lt;p&gt;There's a specific kind of satisfaction that comes from watching the Windows 98 defrag screen do its thing. Clusters shuffling around. That progress bar crawling. The sense that your computer was doing something &lt;em&gt;important&lt;/em&gt;, even if you had no idea what.&lt;/p&gt;
&lt;p&gt;That's the feeling I wanted to bottle when I built &lt;a rel=&quot;nofollow&quot; href=&quot;/blog/windows-98-defrag-simulator&quot;&gt;the first version of defrag98.com&lt;/a&gt; back in 2024. The project took off beyond anything I'd expected. It got picked up by &lt;a rel=&quot;nofollow&quot; href=&quot;https://news.ycombinator.com/item?id=40962195&quot;&gt;Hacker News&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.theverge.com/2024/7/14/24198206/take-a-moment-to-reflect&quot;&gt;The Verge&lt;/a&gt;, and people from all over the world spent time staring at a browser tab the way their younger selves once stared at a CRT monitor. Mission accomplished.&lt;/p&gt;
&lt;p&gt;But the more I looked at it, the more I noticed the cracks.&lt;/p&gt;
&lt;h3 id=&quot;it-wasn-t-quite-right&quot;&gt;It wasn't quite right&lt;/h3&gt;
&lt;p&gt;The most significant problem was the simulation logic. The original Windows 98 defragmenter didn't march through clusters sequentially. It read blocks, wrote them, and shuffled them around in a messier, more organic process that made watching it genuinely hypnotic. My first implementation was too orderly. It &lt;em&gt;looked&lt;/em&gt; like a simulation. The real one looked like a machine thinking.&lt;/p&gt;
&lt;p&gt;The legend was missing too. The real Windows 98 defragmenter had a dedicated button that opened a dialog explaining what each block color meant: optimized, reading, writing, bad cluster. Without it, the simulation was just a pretty light show. Now it has one.&lt;/p&gt;
&lt;p&gt;On the visual side, the cluster grid also had a black background, when the real defragmenter used white. A small detail, but exactly the kind of thing that breaks the spell for anyone who actually remembers the original.&lt;/p&gt;
&lt;p&gt;Then there was the progress bar. The real utility didn't draw a smooth, continuous fill. It revealed a row of chunky segmented blocks one at a time, with a &quot;% Complete&quot; caption underneath. Mine was too modern, too clean. Now it fills block by block the way it should, and the window is sized to give it room to breathe.&lt;/p&gt;
&lt;p&gt;Even the controls were missing some manners. The original asked you to confirm before stopping a defrag, and it told you plainly when the job was &lt;em&gt;Paused&lt;/em&gt; or &lt;em&gt;Stopped&lt;/em&gt;. Small things, but they're the difference between a screensaver and a tool you can actually operate.&lt;/p&gt;
&lt;h3 id=&quot;the-rebuild-next-js-to-vite-and-a-faithful-simulation&quot;&gt;The rebuild: Next.js to Vite, and a faithful simulation&lt;/h3&gt;
&lt;p&gt;I moved the project from Next.js to Vite + React. This might sound like a lateral move, but it was the right call for what &lt;a rel=&quot;nofollow&quot; href=&quot;https://defrag98.com&quot;&gt;defrag98.com&lt;/a&gt; actually is: a single-page, entirely client-side app with no routing, no server-side data, and no need for SSR. Next.js is excellent at what it does, but fighting its SSR model to build something that runs entirely in the browser felt like wearing a suit to go swimming.&lt;/p&gt;
&lt;p&gt;Vite gave back the dev experience that SSR had been quietly taxing. Faster builds, instant HMR, and a mental model that actually matches the app. While I was in there, the rest of the stack got a refresh too: Tailwind CSS v4 through its native Vite plugin (no more PostCSS pipeline), Base UI for the accessible dialogs, selects and checkboxes, Zustand for state, TypeScript 6, and the Rust-based oxc toolchain (oxlint and oxfmt) in place of ESLint and Prettier for near-instant linting and formatting.&lt;/p&gt;
&lt;p&gt;The bigger change was moving cluster rendering to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element. Instead of creating and updating hundreds of DOM nodes, I'm now drawing directly to a 2D canvas context. The number of clusters rendered adapts to whichever disk size you select, so a larger virtual drive produces a denser, more satisfying grid. The difference is easy to feel: the animation is smoother, the browser is happier, and it scales without complaint. I also size the canvas before the first paint, which killed the layout shift that used to jump around on mobile.&lt;/p&gt;
&lt;p&gt;On the simulation side, I rewrote the defrag algorithm to properly model read and write operations on individual clusters, with non-sequential processing that mirrors how the original utility actually behaved. If you remember the way blocks would jump around seemingly at random before gradually settling into order, that's what it does now.&lt;/p&gt;
&lt;p&gt;The sound got some attention too. The hard disk loop and the completion chime were re-encoded to a fraction of their old size (the HDD loop went from over a megabyte to around 350KB), shipped as Opus/WebM with an AAC fallback, and the Win98 fonts are preloaded now so nothing flashes on first paint. There are also toggles in Settings to mute the hard disk sound or the mouse clicks, and your choice is remembered the next time you visit.&lt;/p&gt;
&lt;p&gt;When a defrag finishes, you now get the authentic Windows 98 &quot;Disk Defragmenter&quot; message box telling you it's done, chime and all, instead of the run just quietly ending.&lt;/p&gt;
&lt;p&gt;A couple of things changed off-screen that I care about as much as the visible ones. I dropped Google Analytics and Tag Manager in favor of Cloudflare's cookieless Web Analytics, so there's no tracking cookie and no consent banner getting between you and the nostalgia. The site is hosted on Cloudflare Workers as static assets, and it ships an &lt;code&gt;llms.txt&lt;/code&gt; so the AI crawlers that come poking around get a clean description of what this thing is.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://defrag98.com&quot;&gt;defrag98.com&lt;/a&gt; also ships a web app manifest, so on supported browsers you can install it to your device and launch it like a native app. And it now runs fully offline: a service worker built with vite-plugin-pwa and Workbox precaches the app shell and every runtime asset, the fonts, cursors, audio, and icons, so once you've opened it once you can defragment a virtual drive on a plane with no signal. New builds activate quietly on the next launch, with no update prompt to nag you. Feels right for something built to feel like software from another era.&lt;/p&gt;
&lt;p&gt;There's also a hidden Easter egg somewhere in the app. I'll leave it at that. If you grew up on Windows 98, you'll know it when you see it.&lt;/p&gt;
&lt;h3 id=&quot;a-quieter-ask&quot;&gt;A quieter ask&lt;/h3&gt;
&lt;p&gt;The original version had a donation banner styled as an authentic Windows ME system notification: yellow background, full viewport width, sliding down from the top of the screen five seconds into your session. It was accurate to the era. It was also an interruption, landing right in the middle of the nostalgic moment it was supposed to be protecting.&lt;/p&gt;
&lt;p&gt;The data confirmed it. Despite solid traffic, very few visitors converted to donors.&lt;/p&gt;
&lt;p&gt;The new approach takes a cue from pre-release Windows software, the small line of unobtrusive text in the corner of the screen that says something like &lt;em&gt;&quot;This is a pre-release version&quot;&lt;/em&gt;. I added a quiet line in the bottom-right of the desktop area, linking to my &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/morellodev&quot;&gt;Buy Me a Coffee page&lt;/a&gt;. It's there if you want it. It doesn't demand your attention. The only other time donating comes up is in that completion message box, once the defrag has actually finished and you've had your moment: a gentle Yes or No, never an interruption. The minimum donation has also dropped from $5 to $3, because the goal was never to extract money. It's to help cover the domain and hosting costs that keep &lt;a rel=&quot;nofollow&quot; href=&quot;https://defrag98.com&quot;&gt;defrag98.com&lt;/a&gt; running for anyone who gets something out of it.&lt;/p&gt;
&lt;h3 id=&quot;why-this-still-matters-to-me&quot;&gt;Why this still matters to me&lt;/h3&gt;
&lt;p&gt;I've received $148 in total donations since launching. That's not a business. But it's proof that people feel something when they use it, enough to voluntarily send a few dollars to a stranger on the internet for a free toy.&lt;/p&gt;
&lt;p&gt;That's the part that keeps me iterating on this instead of moving on. It's not a portfolio piece. It's a tiny time machine, and apparently enough people want to visit 1998 for a few minutes that keeping the lights on feels worth caring about.&lt;/p&gt;
&lt;p&gt;If you haven't tried it yet, &lt;a rel=&quot;nofollow&quot; href=&quot;https://defrag98.com&quot;&gt;go defragment something&lt;/a&gt;. If you used to stare at this screen as a kid, I hope it hits the way I intended. And if you find the Easter egg, well, you'll know what to do.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21630/rebuilding-defrag98-getting-the-details-right</guid>
<pubDate>Tue, 30 Jun 2026 18:36:59 +0000</pubDate>
</item>
<item>
<title>How I Migrated From Astro 5 to 6 With All My React Islands</title>
<link>https://coderlegion.com/18883/how-i-migrated-from-astro-5-to-6-with-all-my-react-islands</link>
<description>&lt;p&gt;I run EdgeKits on the full Cloudflare stack, and the marketing site is mostly Astro with a handful of React islands - a newsletter form, a toaster, a desktop nav. Astro 6 moved the dev server into workerd so local development finally mirrors production. I expected a version bump. I got an afternoon of debugging.&lt;/p&gt;
&lt;p&gt;Here is every error I hit, in the order I met it, with the log that produced it, why it happened, and the exact fix. If you are doing an Astro 5 to 6 migration with React islands on Cloudflare Workers, this is the map I wish I'd had.&lt;/p&gt;
&lt;p&gt;My stack: Astro 6.3.6, &lt;code&gt;@astrojs/cloudflare&lt;/code&gt; 13.5.x, &lt;code&gt;@astrojs/react&lt;/code&gt; 5.0.5, React 19, Vite 7, Tailwind 4, &lt;code&gt;output: 'server'&lt;/code&gt; on Workers.&lt;/p&gt;
&lt;h3 id=&quot;first-wall-the-vite-dependency-scan-crashes&quot;&gt;First wall: the Vite dependency scan crashes&lt;/h3&gt;
&lt;p&gt;The very first &lt;code&gt;npm run dev&lt;/code&gt; printed this before the server was even ready:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ERROR] [vite] (!) Failed to run dependency scan. Skipping dependency pre-bundling.
X [ERROR] Unexpected newline after &quot;throw&quot;
    src/domain/analytics/components/ConsentBanner.astro:7:24
      7 | if (consentStatus) throw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The confusing part: that line in my source says &lt;code&gt;return&lt;/code&gt;, not &lt;code&gt;throw&lt;/code&gt;. I never wrote a &lt;code&gt;throw&lt;/code&gt; there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The cause.&lt;/strong&gt; Astro compiles a top-level early &lt;code&gt;return&lt;/code&gt; in component frontmatter into a &lt;code&gt;throw&lt;/code&gt;. esbuild's dependency scanner then tries to parse the compiled output and trips on JavaScript's automatic-semicolon-insertion rule: &lt;code&gt;throw&lt;/code&gt; cannot be followed by a newline. A single offending file aborts the scan for the entire project.&lt;/p&gt;
&lt;p&gt;The knock-on effect is the dangerous part. With the scan dead, Vite stops pre-bundling and starts discovering dependencies lazily - one at a time, on each request. Remember that; it is the root of the next two crashes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; Move the condition out of the frontmatter and into the template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// before: if (consentStatus) return
const showBanner = !consentStatus
---

{showBanner &amp;amp;&amp;amp; &amp;lt;ConsentUI &amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do the same anywhere you have a bare early &lt;code&gt;return&lt;/code&gt; in frontmatter. (You can also lean on the dependency pre-declaration from the next section, which makes the failed scan harmless - but cleaning up the returns is the tidy fix.)&lt;/p&gt;
&lt;h3 id=&quot;the-boss-fight-invalid-hook-call-in-astro-6-on-cloudflare&quot;&gt;The boss fight: Invalid hook call in Astro 6 on Cloudflare&lt;/h3&gt;
&lt;p&gt;With the site loading, the first page render exploded:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ERROR] [vite] Invalid hook call. Hooks can only be called inside
of the body of a function component.
[ERROR] [vite] TypeError: Cannot read properties of null (reading 'useState')
    at useState (.../.vite/deps_ssr/chunk-EMAOOZFV.js)
    at useSubscribeNewsletter (src/hooks/useSubscribeNewsletter.ts)
    at NewsletterFlow (src/components/layout/islands/NewsletterFlow.tsx)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The React message lists three suspects: mismatched versions, broken rules of hooks, or more than one copy of React. None of them was true in the usual sense. Two clues told me this was a dev-server problem, not my code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It only failed on the &lt;strong&gt;first&lt;/strong&gt; load. A browser refresh rendered the page fine.&lt;/li&gt;
&lt;li&gt;Switching to the Node adapter made it vanish completely.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The cause.&lt;/strong&gt; In Astro 6 the Cloudflare adapter runs SSR inside workerd via &lt;code&gt;@cloudflare/vite-plugin&lt;/code&gt;. Vite optimizes server dependencies into a &lt;code&gt;deps_ssr&lt;/code&gt; folder. Because my dependency scan had failed, deps were being discovered lazily; every discovery re-ran optimization and reloaded the worker. React and &lt;code&gt;react-dom/server&lt;/code&gt; landed in different optimize passes, and after a reload &lt;code&gt;react-dom/server&lt;/code&gt; held a reference to a React instance whose hook dispatcher was now &lt;code&gt;null&lt;/code&gt;. Calling &lt;code&gt;useState&lt;/code&gt; read that &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That is the &quot;more than one copy of React&quot; warning - except the split was in the optimizer, not on disk. &lt;code&gt;npm ls react&lt;/code&gt; showed a single, deduped &lt;code&gt;react@19&lt;/code&gt;. The duplication was a timing artifact, not a packaging one.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-that-did-not-work-vite-ssr-optimizedeps&quot;&gt;The fix that did not work: vite.ssr.optimizeDeps&lt;/h3&gt;
&lt;p&gt;My first instinct was to list the deps in &lt;code&gt;vite.ssr.optimizeDeps.include&lt;/code&gt;. Nothing changed. That cost me a while, so learn from it:&lt;/p&gt;
&lt;p&gt;With the Cloudflare plugin, &lt;strong&gt;SSR is its own Vite environment, and &lt;code&gt;vite.ssr.optimizeDeps&lt;/code&gt; never reaches it.&lt;/strong&gt; The documented way to configure that environment's optimizer is a small Vite plugin using the &lt;code&gt;configEnvironment&lt;/code&gt; hook.&lt;/p&gt;
&lt;h3 id=&quot;the-config-that-worked-a-configenvironment-optimizer-plugin&quot;&gt;The config that worked: a configEnvironment optimizer plugin&lt;/h3&gt;
&lt;p&gt;A tiny plugin that pre-bundles the server graph for every non-client environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const SERVER_OPTIMIZE_DEPS = [
  'react',
  'react-dom',
  'react-dom/server.edge',
  'react/jsx-runtime',
  'react-hook-form',
  '@hookform/resolvers/zod',
  'sonner',
  'cmdk',
  'radix-ui',
  'class-variance-authority',
  'tailwind-merge',
  'zod',
  'drizzle-orm',
  'drizzle-orm/d1',
  '@astrojs/rss',
]

function optimizeServerDeps() {
  return {
    name: 'optimize-server-deps',
    configEnvironment(name) {
      if (name !== 'client') {
        return { optimizeDeps: { include: SERVER_OPTIMIZE_DEPS } }
      }
    },
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then two lines in &lt;code&gt;vite.resolve&lt;/code&gt; that matter as much as the include list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;resolve: {
  dedupe: ['react', 'react-dom'],
  alias: { 'react-dom/server': 'react-dom/server.edge' },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dedupe&lt;/code&gt; keeps a single React instance across the client and worker graphs. The alias forces the Web-Streams (&quot;edge&quot;) build of &lt;code&gt;react-dom/server&lt;/code&gt; everywhere. In Astro 5 I only needed that build in production; now that dev runs in workerd, it has to be the edge build in dev too.&lt;/p&gt;
&lt;p&gt;The whole idea: get every dependency optimized in &lt;strong&gt;one pass at startup&lt;/strong&gt;, so there is no lazy discovery, no reload mid-render, and nothing to desync. Add the same list to the client-side &lt;code&gt;vite.optimizeDeps.include&lt;/code&gt; so the browser graph doesn't churn either.&lt;/p&gt;
&lt;h3 id=&quot;the-i18n-ghost-the-astro-i18n-virtual-module-forces-one-last-reload&quot;&gt;The i18n ghost: the astro:i18n virtual module forces one last reload&lt;/h3&gt;
&lt;p&gt;After that, the reload cascade shrank to a single line - and a new crash:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[vite] new dependencies optimized: astro/virtual-modules/i18n.js
[vite] reloading
[ERROR] Cannot read properties of undefined (reading 'i18n')
    at getComponentByRoute (.../deps_ssr/astro_app_entrypoint_dev.js)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same disease, different mask. The one remaining late reload came from Astro's own &lt;code&gt;astro:i18n&lt;/code&gt; virtual module, pulled in by the &lt;code&gt;i18n: {}&lt;/code&gt; block in my config. The reload re-instantiated the worker mid-request and left the app config object undefined - so reading &lt;code&gt;.i18n&lt;/code&gt; off it threw.&lt;/p&gt;
&lt;p&gt;The catch: it is a &lt;strong&gt;virtual&lt;/strong&gt; module, so I can't pre-bundle it through &lt;code&gt;include&lt;/code&gt;. But I can tell the optimizer to leave it alone:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;optimizeDeps: {
  include: SERVER_OPTIMIZE_DEPS,
  exclude: ['astro:i18n', 'astro/virtual-modules/i18n.js'],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No optimization for that module means no reload, which means the React chunks stay put. Both crashes gone. This was also the moment I confirmed &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/17022/stop-shipping-translations-to-the-client-edge-native-i18n-with-astro-and-cloudflare-part&quot;&gt;my custom KV-based i18n engine&lt;/a&gt; was never the culprit - it was Astro's built-in virtual module all along.&lt;/p&gt;
&lt;h3 id=&quot;react-icons-brought-it-back-phosphor-icons-react&quot;&gt;React icons brought it back: @phosphor-icons/react&lt;/h3&gt;
&lt;p&gt;Adding &lt;code&gt;@phosphor-icons/react&lt;/code&gt; reintroduced the cascade risk, because it is one more island dependency the SSR pass meets on first render. The fix is identical: add it to both the client and server include lists.&lt;/p&gt;
&lt;p&gt;One trap I walked straight into: the docs-friendly &lt;code&gt;@phosphor-icons/react/dist/ssr&lt;/code&gt; subpath throws type and resolution errors under &lt;code&gt;moduleResolution: &quot;bundler&quot;&lt;/code&gt;, which Vite and Astro use by default. The plain &lt;code&gt;@phosphor-icons/react&lt;/code&gt; import works in both &lt;code&gt;.astro&lt;/code&gt; files and React islands, so I dropped the &lt;code&gt;dist/ssr&lt;/code&gt; subpath entirely. In &lt;code&gt;.astro&lt;/code&gt; files the icon renders server-side and ships only HTML; in islands, named imports tree-shake. No bundle-size penalty either way.&lt;/p&gt;
&lt;h3 id=&quot;the-whole-astro-config-mjs-in-one-place&quot;&gt;The whole astro.config.mjs, in one place&lt;/h3&gt;
&lt;p&gt;Here is the migration-relevant shape of my config - the two helper constants, the optimizer plugin, and where they plug into &lt;code&gt;defineConfig&lt;/code&gt;. Everything unrelated (fonts, image, i18n locales, markdown) is unchanged from Astro 5 and trimmed out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// astro.config.mjs
import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
import tailwindcss from '@tailwindcss/vite'
import react from '@astrojs/react'
import mdx from '@astrojs/mdx'

// Pre-bundle these for the SSR/workerd environment in ONE pass at startup, so
// Vite never discovers them lazily and reloads the worker mid-render.
const SERVER_OPTIMIZE_DEPS = [
  'react',
  'react-dom',
  'react-dom/server.edge',
  'react-dom/client',
  'react/jsx-runtime',
  'react-hook-form',
  '@hookform/resolvers/zod',
  'sonner',
  'cmdk',
  'radix-ui',
  '@phosphor-icons/react',
  'class-variance-authority',
  'tailwind-merge',
  'clsx',
  'zod',
  'drizzle-orm',
  'drizzle-orm/d1',
  'drizzle-orm/sqlite-core',
  'drizzle-zod',
  '@astrojs/rss',
  // Astro internals that were optimized lazily right before the i18n crash.
  // Real package exports, safe to pre-bundle.
  'astro/zod',
  'astro/assets/services/noop',
  'astro/actions/runtime/entrypoints/server.js',
]

// Virtual modules the optimizer must NOT touch. Excluding astro:i18n kills the
// last late reload - the one that desynced React and threw &quot;Invalid hook call&quot;.
const SERVER_OPTIMIZE_EXCLUDE = ['astro:i18n', 'astro/virtual-modules/i18n.js']

// SSR is its own Vite environment under @cloudflare/vite-plugin. The only way to
// reach its optimizer is configEnvironment - vite.ssr.optimizeDeps is ignored.
function optimizeServerDeps() {
  return {
    name: 'optimize-server-deps',
    /** @param {string} name */
    configEnvironment(name) {
      if (name !== 'client') {
        return {
          optimizeDeps: {
            include: SERVER_OPTIMIZE_DEPS,
            exclude: SERVER_OPTIMIZE_EXCLUDE,
          },
        }
      }
    },
  }
}

export default defineConfig({
  adapter: cloudflare({
    configPath: 'wrangler.jsonc',
    imageService: 'cloudflare',
  }),
  integrations: [react(), mdx()],
  // site, image, i18n, markdown - unchanged from Astro 5, omitted here.

  vite: {
    plugins: [tailwindcss(), optimizeServerDeps()],
    resolve: {
      dedupe: ['react', 'react-dom'],
      // dev now runs in workerd, so use the edge build of react-dom/server everywhere
      alias: { 'react-dom/server': 'react-dom/server.edge' },
    },
    // same list for the client graph so the browser doesn't churn either
    optimizeDeps: {
      include: [
        'react',
        'react-dom',
        'react-dom/client',
        'react/jsx-runtime',
        'react-hook-form',
        '@hookform/resolvers/zod',
        'sonner',
        'cmdk',
        'radix-ui',
        '@phosphor-icons/react',
        'class-variance-authority',
        'tailwind-merge',
      ],
    },
    ssr: {
      // cookie is CommonJS and chokes workerd's optimizer - keep it external
      external: [
        'async_hooks',
        'node:fs',
        'node:path',
        'node:url',
        'node:crypto',
        'cookie',
      ],
    },
  },

  output: 'server',
  build: { inlineStylesheets: 'always' }, // kills the render-blocking CSS request
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-production-only-twist-a-cold-hit-served-the-homepage&quot;&gt;The production-only twist: a cold hit served the homepage&lt;/h3&gt;
&lt;p&gt;Local dev was clean, so I deployed. Then a direct hit on an article URL did something I could never reproduce on my machine: the &lt;strong&gt;first&lt;/strong&gt; request to &lt;code&gt;/en/blog/&amp;lt;slug&amp;gt;/&lt;/code&gt; returned the homepage, and only a second request showed the article. An external link landing cold on the page did the same. Refresh, and it behaved. The classic &quot;works on the second try.&quot;&lt;/p&gt;
&lt;p&gt;The i18n middleware was the obvious suspect, since it does locale redirects, so I instrumented every &lt;code&gt;context.redirect&lt;/code&gt; with a log line and watched &lt;code&gt;wrangler tail&lt;/code&gt;. On a valid deep URL: &lt;strong&gt;nothing&lt;/strong&gt;. The middleware never ran. That was the tell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The cause.&lt;/strong&gt; My &lt;code&gt;wrangler.jsonc&lt;/code&gt; had &lt;code&gt;run_worker_first: false&lt;/code&gt;. With that, Cloudflare's static-asset layer answers &lt;em&gt;before&lt;/em&gt; the Worker. On a cold hit to an SSR route with no matching static file, that layer short-circuited and served the landing page - my middleware and SSR never executed. The warm second request reached the Worker and rendered the article. It is also why dev never showed it: there is no Cloudflare asset layer in &lt;code&gt;astro dev&lt;/code&gt;, so the middleware always runs locally.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; One line, so the Worker - and the middleware it carries - runs first on every route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsonc&quot;&gt;// wrangler.jsonc
&quot;assets&quot;: { &quot;directory&quot;: &quot;./dist&quot;, &quot;binding&quot;: &quot;ASSETS&quot;, &quot;run_worker_first&quot;: true }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redeployed, hit the URL cold: article on the first try. But &lt;code&gt;true&lt;/code&gt; has a cost - now &lt;em&gt;every&lt;/em&gt; request, including each &lt;code&gt;/_astro/*&lt;/code&gt; chunk, font, and image, travels through the Worker instead of being served straight from the asset layer. On a mostly-static site that is wasted latency.&lt;/p&gt;
&lt;p&gt;So the setting I actually shipped is the array form, which runs the Worker first only for page routes and lets static assets bypass it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsonc&quot;&gt;// wrangler.jsonc
&quot;assets&quot;: { &quot;directory&quot;: &quot;./dist&quot;, &quot;binding&quot;: &quot;ASSETS&quot;, &quot;run_worker_first&quot;: [&quot;/*&quot;, &quot;!/_astro/*&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pages still hit the Worker first - middleware runs, no homepage fallback - while &lt;code&gt;/_astro/*&lt;/code&gt; goes straight to the asset layer. I confirmed it with &lt;code&gt;wrangler tail&lt;/code&gt;: under &lt;code&gt;true&lt;/code&gt; every asset request showed up in the Worker logs; with the array, the &lt;code&gt;/_astro/*&lt;/code&gt; lines vanish and only page routes remain. Same fix for the bug, asset latency back to normal.&lt;/p&gt;
&lt;h3 id=&quot;the-short-checklist-astro-5-6-with-react-islands-on-cloudflare&quot;&gt;The short checklist: Astro 5 → 6 with React islands on Cloudflare&lt;/h3&gt;
&lt;p&gt;If you are migrating Astro 5 → 6 with React islands on Cloudflare, this is the whole thing on one page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Move frontmatter early &lt;code&gt;return&lt;/code&gt; into template conditionals (or accept the harmless scan warning).&lt;/li&gt;
&lt;li&gt;Configure the SSR optimizer through a &lt;code&gt;configEnvironment&lt;/code&gt; plugin, &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;ssr.optimizeDeps&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolve.dedupe&lt;/code&gt; React, and alias &lt;code&gt;react-dom/server&lt;/code&gt; to &lt;code&gt;react-dom/server.edge&lt;/code&gt; in dev too.&lt;/li&gt;
&lt;li&gt;Pre-include every island dependency so the optimizer runs once at startup.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exclude&lt;/code&gt; the &lt;code&gt;astro:i18n&lt;/code&gt; virtual module to kill the last reload.&lt;/li&gt;
&lt;li&gt;Import Phosphor icons from the main entry, never &lt;code&gt;/dist/ssr&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inlineStylesheets: 'always'&lt;/code&gt; clears the CSS render-block.&lt;/li&gt;
&lt;li&gt;Make the Worker run first on page routes via &lt;code&gt;run_worker_first&lt;/code&gt; in &lt;code&gt;wrangler.jsonc&lt;/code&gt; - use the array &lt;code&gt;[&quot;/*&quot;, &quot;!/_astro/*&quot;]&lt;/code&gt; so pages get the middleware but &lt;code&gt;/_astro/*&lt;/code&gt; still serves straight from the asset layer. Otherwise the SSR middleware silently won't run on a cold hit and Cloudflare serves the landing page instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clear the optimizer cache (&lt;code&gt;node_modules/.vite&lt;/code&gt;) between attempts - stale chunks will lie to you. After all of it: the dev server boots clean, every island hydrates on the first load, and PageSpeed lands in the high 90s on mobile and around 90 on desktop.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18883/how-i-migrated-from-astro-5-to-6-with-all-my-react-islands</guid>
<pubDate>Mon, 25 May 2026 12:18:52 +0000</pubDate>
</item>
<item>
<title>Rethinking the Frontend: My Journey from Static HTML to React's Modular World</title>
<link>https://coderlegion.com/10827/rethinking-the-frontend-my-journey-from-static-html-to-reacts-modular-world</link>
<description>&lt;h2 id=&quot;react-is-a-mindset-not-just-syntax&quot;&gt;React is a Mindset, Not Just Syntax&lt;/h2&gt;
&lt;p&gt;When I first started moving from plain HTML to React, I thought I just needed to learn some new syntax. I was wrong. The real challenge wasn't the code; it was unlearning how I viewed a website.&lt;br&gt;
I remember working on my first portfolio project right after finishing my CSS course. My instructor had a strict rule: No pre-made templates. I had to build everything from scratch. It was a nightmare. Even though I was using Bootstrap to speed things up, I found myself lost in a sea of thousands of utility classes. My HTML files became &quot;div-soups,&quot; with lines like class=&quot;d-flex justify-content-center align-items-center col-lg-4 col-md-6 mb-4&quot;. Debugging a simple layout shift took me hours, and by the time I finished, it had taken nearly two full weeks of frustrating trial and error. Finding an original idea that wasn't a clone of a template while managing all those classes was the hardest part.&lt;br&gt;
That struggle was my turning point. In the old days, I’d look at a page as a single, overwhelming document. React changed that. It taught me to stop seeing &quot;pages&quot; and start seeing &quot;systems.&quot; Instead of micromanaging the browser, I now describe what I want the UI to look like, and React handles the heavy lifting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Think of React components like Lego blocks. You build small, solid pieces first, and then you snap them together to create something complex.&lt;/p&gt;
&lt;h2 id=&quot;step-1-ditching-the-slow-lane---why-i-use-vite&quot;&gt;Step 1: Ditching the Slow Lane - Why I Use Vite&lt;/h2&gt;
&lt;p&gt;Before you can build anything, you need an environment that doesn't get in your way. For a while, Create React App (CRA) was the default, but it’s become slow for today’s standards.&lt;/p&gt;
&lt;h4 id=&quot;why-vite-feels-like-a-superpower&quot;&gt;Why Vite Feels Like a Superpower&lt;/h4&gt;
&lt;p&gt;I switched to Vite (which means &quot;fast&quot; in French), and the difference was night and day. Unlike the old days of waiting for my portfolio to refresh every time I changed a Bootstrap class, Vite uses Native ESM. This means it doesn't make you wait for the whole project to bundle before you can see your changes. Your edits show up instantly.&lt;/p&gt;
&lt;h4 id=&quot;setting-up-the-project-scaffold&quot;&gt;Setting up the project scaffold&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;npm create vite@latest my-react-app -- --template react
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9022146003414479344&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;strong&gt;Figure 1:&lt;/strong&gt; Initializing the project using Vite and installing necessary dependencies through the terminal.&lt;/p&gt;
&lt;h4 id=&quot;don-t-forget-this-part-i-ve-missed-it-more-than-once-&quot;&gt;Don't forget this part! (I've missed it more than once)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;cd my-react-app
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Quick Note:&lt;/strong&gt; If you ever run npm run dev and see a blank page or a &quot;command not found&quot; error, 99% of the time it's because you skipped npm install or you are not in the correct project folder. Make sure your terminal is opened inside the main directory where package.json lives. We've all been there!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Feature &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;The Old Way (HTML + Bootstrap) &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;The New Way (React + Bootstrap)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Organization&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;One massive, confusing HTML file &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Multiple small, clean files&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Logic&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Manual jQuery or JS updates &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Automatic updates via Props/State&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Debugging&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Hard to find where tags end &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Easy to isolate the component&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;what-makes-a-good-component-&quot;&gt;What Makes a Good Component?&lt;/h3&gt;
&lt;p&gt;A clean component should do one thing well. It takes in some info (Props) and spits out the UI (JSX). Here’s a quick example of a StatusCard I built using Bootstrap classes within React:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * A simple card to show service health using Bootstrap classes.
 */
function StatusCard({ title, status }) {
  // Logic to change the Bootstrap badge color based on status
  const badgeClass = status === 'online' ? 'bg-success' : 'bg-danger';

  return (
    &amp;lt;div className=&quot;card shadow-sm m-2&quot; style={{ width: '18rem' }}&amp;gt;
      &amp;lt;div className=&quot;card-body&quot;&amp;gt;
        &amp;lt;h5 className=&quot;card-title&quot;&amp;gt;{title}&amp;lt;/h5&amp;gt;
        &amp;lt;span className={`badge ${badgeClass}`}&amp;gt;
          {status === 'online' ? 'Service Up' : 'Service Down'}
        &amp;lt;/span&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default StatusCard;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10469180796008730946&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;strong&gt;Figure 2:&lt;/strong&gt; Implementing modular logic within the StatusCard component and managing the local file structure in VS Code.&lt;/p&gt;
&lt;h4 id=&quot;what-s-happening-here-&quot;&gt;What’s happening here?&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Clean Logic:&lt;/strong&gt; I use a simple variable (&lt;code&gt;badgeClass&lt;/code&gt;) to toggle between &lt;code&gt;bg-success&lt;/code&gt; and &lt;code&gt;bg-danger&lt;/code&gt;. No more manual DOM manipulation.&lt;br&gt;
&lt;strong&gt;Reusability:&lt;/strong&gt; Instead of copying and pasting the card's HTML 10 times, I just call &lt;code&gt;&amp;lt;StatusCard &amp;gt;&lt;/code&gt; with different props.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4120442806822832876&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;strong&gt;Figure 3:&lt;/strong&gt; The final rendered UI in the browser, demonstrating dynamic styling and component reusability on localhost:5173.&lt;/p&gt;
&lt;h3 id=&quot;solving-the-bootstrap-mess-logic-over-layout&quot;&gt;Solving the Bootstrap &quot;Mess&quot;: Logic over Layout&lt;/h3&gt;
&lt;p&gt;A huge part of my two-week portfolio frustration was keeping track of where one Bootstrap row ended and another began. In a giant HTML file, one missing &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt; can break your entire layout.&lt;/p&gt;
&lt;h4 id=&quot;how-react-components-tamed-bootstrap&quot;&gt;How React Components Tamed Bootstrap&lt;/h4&gt;
&lt;p&gt;In React, I don't have to look at 500 lines of HTML at once. I can put my Navbar in one file, my Hero section in another, and my Footer in a third.&lt;br&gt;
No more &quot;Div-Soup&quot;: Each component only contains the code it needs.&lt;br&gt;
Easier Debugging: If the Grid layout is broken in the Header, I know exactly which small file to check.&lt;br&gt;
Component-Level Styling: I can still use Bootstrap for the heavy lifting, but React handles the structure.&lt;/p&gt;
&lt;h3 id=&quot;jsx-it-s-like-html-but-with-brains&quot;&gt;JSX: It’s Like HTML, But With Brains&lt;/h3&gt;
&lt;p&gt;JSX looks like HTML, but it’s actually JavaScript. This is where most of my early &quot;bugs&quot; came from because I kept trying to use standard HTML attributes.&lt;/p&gt;
&lt;h4 id=&quot;three-rules-i-learned-the-hard-way&quot;&gt;Three Rules I Learned the Hard Way&lt;/h4&gt;
&lt;p&gt;The Parent Rule: You can't return two things side-by-side. Wrap them in a tag or a Fragment &lt;code&gt;&amp;lt;&amp;gt; ... &amp;lt;&amp;gt;&lt;/code&gt;.&lt;br&gt;
CamelCase Everything: Since it’s JS, you can’t use class. You must use className.&lt;br&gt;
Close Your Tags: Every tag must be closed, even &lt;code&gt;&amp;lt;br &amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;img &amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;technical-inquiry-amp-retrospective-resolving-common-development-challenges&quot;&gt;Technical Inquiry &amp;amp; Retrospective: Resolving Common Development Challenges&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q1: Integration of CSS Frameworks (Bootstrap)&lt;/strong&gt;&lt;br&gt;
•&lt;strong&gt;Inquiry:&lt;/strong&gt; Is it feasible to maintain Bootstrap within a React-based ecosystem?&lt;br&gt;
•&lt;strong&gt;Insight:&lt;/strong&gt; Absolutely. However, the transition from class to className is crucial. Beyond syntax, the real shift is moving from global styling to &lt;strong&gt;Component-Level Styling&lt;/strong&gt;, ensuring that Bootstrap elements remain modular and don't create &quot;Div-Soup.&quot;&lt;br&gt;
&lt;strong&gt;Q2: Performance and Rendering Optimization&lt;/strong&gt;&lt;br&gt;
•&lt;strong&gt;Inquiry:&lt;/strong&gt; Why do initial React applications sometimes experience performance lag?&lt;br&gt;
•&lt;strong&gt;Insight:&lt;/strong&gt; This is often a result of inefficient component structure. During my &lt;strong&gt;Movie Watchlist&lt;/strong&gt; project, I learned that a monolithic file causes unnecessary re-renders. By refactoring into specialized components, we leverage the &lt;strong&gt;Virtual DOM&lt;/strong&gt; more effectively, ensuring high-speed UI updates.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-focus-on-the-logic-not-just-the-look&quot;&gt;Conclusion: Focus on the Logic, Not Just the Look&lt;/h3&gt;
&lt;p&gt;Moving to React is a journey of unlearning. To anyone stuck in the &quot;Bootstrap Spaghetti&quot; phase: focus on making your components small.&lt;/p&gt;
&lt;p&gt;Clean code isn't about how it looks today—it's about how easily you can change it next week or later.&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/learn/thinking-in-react&quot;&gt;React Docs: Thinking in React&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://vitejs.dev/guide/&quot;&gt;Vite: Getting Started Guide&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10827/rethinking-the-frontend-my-journey-from-static-html-to-reacts-modular-world</guid>
<pubDate>Sat, 31 Jan 2026 13:34:56 +0000</pubDate>
</item>
<item>
<title>Guide to Containerizing a Modern JavaScript SPA (Vue/Vite/React) with a Multi-Stage Nginx Build </title>
<link>https://coderlegion.com/6171/guide-to-containerizing-modern-javascript-spa-vue-vite-react-with-multi-stage-nginx-build</link>
<description>&lt;p&gt;This guide outlines the professional, multi-stage Docker strategy required to package a built Single Page Application (SPA), ensuring the final image is secure, minimal, and correctly configured to serve the application and handle remote API calls. This architecture is valid for &lt;strong&gt;Vue, Vite, React, Angular, or Svelte&lt;/strong&gt; applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-multi-stage-dockerfile-build-vs-serve&quot;&gt;1. Multi-Stage Dockerfile: Build vs. Serve&lt;/h3&gt;
&lt;p&gt;This file uses a two-stage build to drastically reduce the final image size (eliminating Node.js and build tools) and enhance security.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# Dockerfile

# Stage 1: Build the Application (AS build)
FROM docker.io/node:20-alpine AS build

# 1. Inject API URL at build time 
ARG VITE_API_URL=http://localhost:3000/api
ENV VITE_API_URL=$VITE_API_URL

WORKDIR /app
COPY package*.json ./
RUN npm ci 
COPY . .
RUN npm run build:deploy 

# ---

# Stage 2: Serve the Application (AS final)
FROM docker.io/library/nginx:stable-alpine AS final

# 1. Custom Nginx Configuration Setup
RUN rm /etc/nginx/nginx.conf
COPY nginx-main.conf /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/conf.d/

# 2. Grant Non-Root User Permissions (CRITICAL for security &amp;amp; stability)
# Fixes 'mkdir() &quot;/var/cache/nginx/client_temp&quot; failed (13: Permission denied)'
RUN mkdir -p /var/run/nginx \
    &amp;amp;&amp;amp; chown -R nginx:nginx /var/cache/nginx \
    &amp;amp;&amp;amp; chown -R nginx:nginx /var/run/nginx
    
# 3. Copy only the required static files from the build stage
COPY --from=build /app/dist /usr/share/nginx/html

EXPOSE 8080

# 4. Run as Non-Root User (Security Best Practice)
USER nginx

CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-nginx-configuration-files-the-missing-pieces-&quot;&gt;2. Nginx Configuration Files (The Missing Pieces)&lt;/h3&gt;
&lt;p&gt;These custom files are necessary to resolve critical permission and port errors when running Nginx as a non-root user.&lt;/p&gt;
&lt;h4 id=&quot;a-main-configuration-nginx-main-conf-&quot;&gt;A. Main Configuration (&lt;code&gt;nginx-main.conf&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;This file replaces the default &lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;. It controls the Master Process directives.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;# nginx-main.conf (Copied to /etc/nginx/nginx.conf)

# Fixes 'open() &quot;/run/nginx.pid&quot; failed (13: Permission denied)'
# This MUST be in the main context.
pid /var/run/nginx/nginx.pid; 

# Sets the user for worker processes (ignored by Dockerfile's USER, but good practice)
user  nginx; 
worker_processes  auto;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    # Include our custom server block
    include /etc/nginx/conf.d/*.conf; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;b-server-block-nginx-conf-&quot;&gt;B. Server Block (&lt;code&gt;nginx.conf&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;This file defines how your application is served and is placed in the &lt;code&gt;conf.d&lt;/code&gt; directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;# nginx.conf (Copied to /etc/nginx/conf.d/nginx.conf)

server {
    # Fixes 'bind() to 0.0.0.0:80 failed (13: Permission denied)'
    # Nginx listens on a high port (&amp;gt;1024) which the non-root user can bind to.
    listen 8080; 
    root /usr/share/nginx/html;
    index index.html;

    # Security: Hides the Nginx version
    server_tokens off;

    # SPA Routing: Directs all unmatched requests to index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Security: Block access to hidden files
    location ~ /\. {
        deny all;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-local-development-with-podman-compose&quot;&gt;3. Local Development with Podman Compose&lt;/h3&gt;
&lt;p&gt;Using a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file automates the entire process.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: '3.8'

services:
  frontend:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        VITE_API_URL: https:// http://localhost:3000/api/v1
    
    container_name: frontend-container
    
    # Maps host's 8080 port to the container's 8080 listening port
    ports:
      - &quot;8080:8080&quot;
    
    image: my-frontend-app:v1.0.0
    restart: always 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;essential-podman-compose-commands&quot;&gt;Essential Podman Compose Commands&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Command &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Action&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;podman compose build&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Executes the &lt;code&gt;Dockerfile&lt;/code&gt; with the specified &lt;code&gt;args&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;podman compose up -d&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Builds (if needed) and starts the container in the background.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;podman compose ps&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Checks the running status of the service(s).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;podman compose down&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Stops and removes the container(s) and network.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-guide-reusability-and-troubleshooting&quot;&gt;4. Guide Reusability and Troubleshooting&lt;/h3&gt;
&lt;p&gt;This architecture is reusable for nearly any SPA. Just update the &lt;strong&gt;&lt;code&gt;VITE_API_URL&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;docker-compose.yaml&lt;/code&gt; and adjust the build script (&lt;code&gt;npm run build:deploy&lt;/code&gt;) and output folder (&lt;code&gt;/app/dist&lt;/code&gt;) in the &lt;code&gt;Dockerfile&lt;/code&gt; if needed.&lt;/p&gt;
&lt;p&gt;The most critical errors encountered and resolved are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Error Logged &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Cause &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Solution in this Guide&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;bind() to 0.0.0.0:80 failed&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Non-root user attempting to use privileged port 80. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Change &lt;code&gt;listen&lt;/code&gt; to &lt;strong&gt;&lt;code&gt;8080&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;nginx.conf&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;open() &quot;/run/nginx.pid&quot; failed&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Non-root user lacking write permission to the default PID file location. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Add &lt;strong&gt;&lt;code&gt;pid /var/run/nginx/nginx.pid;&lt;/code&gt;&lt;/strong&gt; to &lt;code&gt;nginx-main.conf&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;mkdir() &quot;/var/cache/...&quot; failed&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Non-root user lacking permission to Nginx cache directory. &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Add &lt;strong&gt;&lt;code&gt;chown -R nginx:nginx /var/cache/nginx&lt;/code&gt;&lt;/strong&gt; to &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;You can see the files there with a lot more remarks i made &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/IT-WIBRC/coding-remarks/blob/main/remarks/containerization/multi-stage-nginx-spa-with-online-api/readme.md&quot;&gt;coding-remarks&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-conclusion&quot;&gt;5. Conclusion&lt;/h3&gt;
&lt;p&gt;By implementing this Multi-Stage Build and carefully configuring Nginx for a non-root user, you've achieved the trifecta of modern container deployment: security, stability, and speed. Every step—from moving the PID file to changing the listen port—is a direct defense against common production pitfalls and security vulnerabilities. This standardized approach is the most reliable way to ship any modern JavaScript SPA, ensuring your application moves seamlessly from local development to production. You are now equipped with the knowledge to troubleshoot the most common containerization challenges and build a robust, production-ready frontend environment. &lt;/p&gt;
</description>
<category>Containerization</category>
<guid isPermaLink="true">https://coderlegion.com/6171/guide-to-containerizing-modern-javascript-spa-vue-vite-react-with-multi-stage-nginx-build</guid>
<pubDate>Tue, 07 Oct 2025 17:15:59 +0000</pubDate>
</item>
</channel>
</rss>