In 2025 I focused on building small, focused projects to sharpen specific skills. Large applications often hide the root cause of a problem behind layers of abstraction. By isolating issues in smaller codebases, I could confront and fix the actual misunderstanding instead of patching around it.

This post walks through three of those projects: AnimalSounds, CheeseMath, and EthicsFrontEndDemo. For each one I will explain the real failure, what I changed, and why the lesson stuck.

Project 1: AnimalSounds

Repository: AnimalSounds
Live deployment: View demo

The challenge: browser autoplay policies and static subpaths

The initial scope was simple: a DOM manipulation exercise that triggers HTML5 <audio> elements via JavaScript event listeners. I used a standard Next.js App Router structure.

Failure point 1: the iOS audio context lock

After deployment, the app worked on desktop Chrome but failed silently on iOS Safari. It tried to instantiate the AudioContext and preload assets on window.onload.

Mobile browsers enforce strict autoplay policies. The AudioContext starts in a suspended state on WebKit browsers. It cannot transition to running until a distinct user interaction event, usually touchend or click, occurs. My code was trying to play audio before that handshake happened, so the browser rejected the request.

Resolution

I refactored the audio engine to use a lazy-load pattern. I removed the automatic initialization and replaced it with a singleton that checks the context state. The first user interaction on the page now triggers a zero-volume buffer playback, which effectively unlocks the audio subsystem for the rest of the session.

Failure point 2: GitHub Pages subpath routing

The second failure was asset resolution. Locally, at localhost:3000, an image reference to /images/cow.png resolves to localhost:3000/images/cow.png.

GitHub Pages hosts project repositories on a subpath: username.github.io/repo-name/. When deployed, the app kept requesting assets from the domain root, so every static media file 404ed.

Resolution

Hardcoding relative paths (./) proved brittle. The robust fix was Next.js environment configuration. I added a basePath in next.config.js that applies the repository name only during production builds.

// next.config.mjs
const isProd = process.env.NODE_ENV === 'production';
const repoName = 'AnimalSounds';
const nextConfig = {
basePath: isProd ? `/${repoName}` : '',
assetPrefix: isProd ? `/${repoName}/` : '',
output: 'export',
images: {
unoptimized: true, // Required for static export
},
};
export default nextConfig;

This keeps asset routing logic identical across environments while the build pipeline rewrites paths dynamically.

Execution process checkpoint illustration for this section.

Project 2: CheeseMath (Jest tests)

Repository: CheeseMath-Jest-Tests
Live deployment: View demo

The challenge: ESM vs. CommonJS in test environments

The goal was a robust unit testing suite using Jest. The app logic involved geometric calculations written in modern ES6+ JavaScript.

Failure point: the “Cannot use import statement” error

The project used ES modules (import/export) to stay compatible with modern bundlers. But Jest runs in a Node.js environment, which historically defaults to CommonJS (require).

Running the test suite crashed immediately with SyntaxError: Cannot use import statement outside a module. Node.js was executing the source files directly without transpilation, so it did not recognize import in .js files unless package.json specified "type": "module" or the file used .mjs.

Simply changing the package type broke other tooling. I discovered the testing environment and the production environment were effectively two runtimes with different build pipelines.

Resolution

I integrated Babel as a bridge between source code and the test runner. That required installing babel-jest, @babel/core, and @babel/preset-env.

I configured babel.config.js to target the current Node version used by Jest, rather than the browser targets used for the build.

// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current', // Transforms ES6 import to CJS require for Jest
},
},
],
],
};

This lets the source code stay clean, modern ES6 while Jest receives the CommonJS it expects at runtime. The lesson was that “testable code” often needs a dedicated compilation infrastructure.

Project 3: EthicsFrontEndDemo

Repository: EthicsFrontEndDemo
Live deployment: View demo

The challenge: CSS Grid vs. Flexbox for two-dimensional layouts

This project built a responsive corporate landing page without CSS frameworks. The main challenge was structuring the layout grid.

Failure point: the Flexbox trap

I initially laid out the entire page using Flexbox. Flexbox is great for one-dimensional alignment — distributing items in a single row or column — but it struggles when elements must align on both the X and Y axes at the same time.

To force a grid with Flexbox, I had to use negative margins, complex calc() width percentages like width: calc(33.33% - 20px), and extra container wrappers to handle wrapping. The result was layout thrashing on mobile, where content overflowed the viewport horizontally because the padding calculations did not account for the scrollbar width.

Resolution: CSS Grid

I scrapped the Flexbox approach for the main container and implemented CSS Grid. The difference in code complexity was substantial.

The failed Flexbox approach:

.container {
display: flex;
flex-wrap: wrap;
margin: -10px;
}
.card {
flex: 0 0 calc(33.33% - 20px);
margin: 10px;
}
/* Media queries required to manually resize percentages for tablet/mobile */

The successful Grid approach:

.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}

Using grid-template-columns with auto-fit and minmax lets the browser handle space distribution. It automatically decides how many columns fit and wraps to the next row when the minimum width is violated. That eliminated multiple media queries and manual margin calculations, proving Grid is the better engine for macro layouts.

Delivery workflow checkpoint illustration for this section.

What all three taught me

  1. AnimalSounds proved that static hosting environments need path configuration strategies (basePath) that differ from local development.
  2. CheeseMath proved that modern JavaScript needs a transpilation pipeline (Babel) to bridge the gap between ESM source code and CommonJS test runners.
  3. EthicsFrontEndDemo proved that choosing the right layout engine (Grid over Flexbox) reduces code volume and technical debt.

Technical growth comes from encountering and resolving these specific environment-level conflicts, not from reading documentation alone.

Closing

Small projects are not practice dummies. They are the fastest way to find the real boundary between “I read about this” and “I can actually make this work.” These three demos were tiny, but each one fixed a misunderstanding I would have carried into larger projects.

Keep reading