Building a personal website is a common developer milestone. My goal was to create more than a static page – a dynamic portfolio, blog, and connection point using Node.js, Express, EJS, and Sequelize, with a PostgreSQL database via Supabase. This post outlines the iterative process, including tackling common web development challenges, often with input from an AI programming partner.

Foundation: Node.js and EJS

The project started with a standard Express server structure (server.js), EJS for server-side templating with partials (header.ejs, footer.ejs), and Sequelize defining the initial Post model. This provided a clean base.

Challenge #1: Eliminating the Theme Flash (FOUC)

Implementing a light/dark theme toggle using localStorage initially caused a "Flash of Unstyled Content" (FOUC). Client-side fixes proved unreliable. The robust solution involved server-side rendering coordinated with cookies:

  1. Set a themePreference cookie via client-side JavaScript (theme.js) upon toggle:
  2. 
    // Inside theme.js click handler
    document.cookie = `themePreference=\${newTheme};path=/;max-age=31536000;SameSite=Lax;Secure`;
        
  3. Read this cookie on the server using cookie-parser in route handlers (routes/*.js):
  4. 
    // Inside Express route handler
    const currentTheme = req.cookies.themePreference || 'light'; // Default to light
        
  5. Conditionally apply the dark-mode class directly to the <html> tag during server-side rendering in header.ejs:
  6. 
    <html lang="en" class="<%= locals.currentTheme === 'dark' ? 'dark-mode' : '' %>">
        

This ensured the initial HTML arrived correctly themed, finally eliminating the flash.

Insight: Effectively preventing FOUC often requires coordinating server-side rendering with client-side preferences via cookies.

Challenge #2: Performance vs. Aesthetics

vanta vs particles comparasion

An initial background using Vanta.js (WebGL) caused performance issues. Prioritizing usability led to switching to the lighter particles.js (Canvas 2D). This then required making its initialization script (particles-init.js) theme-aware to prevent its own color flash:


// Inside particles-init.js (Simplified)
const isDarkMode = document.documentElement.classList.contains('dark-mode');
const particleColor = isDarkMode ? '#888888' : '#555555'; // Example colors
const lineColor = isDarkMode ? '#555555' : '#cccccc';

particlesJS('particles-js', {
  particles: {
    color: { value: particleColor },
    line_linked: { color: lineColor },
    // ... other config ...
  }
});

Insight: Balance visual appeal with performance. Optimize or find alternatives if an effect hinders user experience.

Feature Implementation: Blog, Recommendations, and Polish

With the core stable, features were added:

  • Blog: Implemented list (/blog) and detail (/blog/:postId) pages using Sequelize (findAll, findByPk) and EJS templates. Snippets are HTML-stripped server-side.
  • Recommendations: Added a /reviews page with a form.
    • Submissions saved via POST /reviews/submit with status: 'pending' using a Review model.
    • Backend uses express-validator for validation, express-rate-limit, and a honeypot field.
    • 
      // Example validation rule in routes/reviews.js
      body('name', 'Name is required').trim().notEmpty(),
      body('linkedinUrl', 'Please enter a valid URL')
        .optional({ checkFalsy: true }).trim().isURL()
                  
    • Validation errors re-render the form with messages and old data.
    • Approved reviews (status updated manually in DB) are displayed via GET /reviews.
    • Includes a dedicated "Thank You" page (/reviews/thank-you).
  • Syntax Highlighting: Integrated Prism.js. Added light/dark themes (prism.css, prism-okaidia.css) to header.ejs and used theme.js to dynamically enable/disable the correct theme CSS based on the site's current theme. Code blocks require <pre><code class="language-xxx"> structure.
  • Styling: Added specific CSS rules in style.css for blog content elements (code blocks, quotes, images).

Beyond Features: SEO & Security

Non-functional requirements were also addressed:

  • SEO: Implemented dynamic meta descriptions, JSON-LD Schema, robots.txt, and a dynamic /sitemap.xml route.
  • 
    <!-- Example entry in generated sitemap.xml -->
    <url>
      <loc>https://www.alexandrustoica.dev/blog/1</loc>
      <lastmod>2025-04-24</lastmod>
      <changefreq>monthly</changefreq>
      <priority>0.9</priority>
    </url>
         
  • Security: Added helmet, configured Content Security Policy, secured the theme cookie, used environment variables for secrets, implemented form validation/rate limiting/honeypot, and recommended npm audit.
  • 
    // Example: Using Helmet in server.js
    app.use(
      helmet({
        contentSecurityPolicy: {
          directives: {
            ...helmet.contentSecurityPolicy.getDefaultDirectives(),
            "script-src": ["'self'", /* CDNs... */]
          }
        }
      })
    );
         

Debugging Notes

The process involved resolving various issues:

  • Database connection errors (ENETUNREACH, ENOTFOUND) requiring verification of DATABASE_URL and hostname choice (IPv4 vs IPv6).
  • Content Security Policy (CSP) errors blocking CDN scripts, fixed by configuring helmet.
  • Subresource Integrity (SRI) check failures, fixed by removing potentially outdated `integrity` hashes.
  • The persistent theme flashing requiring the server-side cookie solution.
  • Mobile layout bugs fixed via CSS media queries.

Insight: Debugging is inevitable. Systematic checking (logs, DevTools, isolating changes) is key.

Conclusion & Source Code

This website is an ongoing project, reflecting an iterative development process. Building it involved addressing structure, UX, performance, features, SEO, and security. It highlights the blend of planning, coding, and debugging required in software development.

The complete source code is available on GitHub:

https://github.com/retixz/personal-website/