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:
- Set a
themePreference
cookie via client-side JavaScript (theme.js
) upon toggle: - Read this cookie on the server using
cookie-parser
in route handlers (routes/*.js
): - Conditionally apply the
dark-mode
class directly to the<html>
tag during server-side rendering inheader.ejs
:
// Inside theme.js click handler
document.cookie = `themePreference=\${newTheme};path=/;max-age=31536000;SameSite=Lax;Secure`;
// Inside Express route handler
const currentTheme = req.cookies.themePreference || 'light'; // Default to light
<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
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
withstatus: 'pending'
using aReview
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()
- Submissions saved via
- 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
).
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.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>
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 ofDATABASE_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: