Personal Portfolio (V1)Vanilla Webpack/Handlebars static portfolio with ADRs

Problem

A personal engineering presence has to do two jobs at once: show the work and show how you think about systems. For v1 that meant a freeCodeCamp-compliant portfolio (fixed navbar, welcome hero, project tiles, profile link) that still read as deliberate architecture - not a theme dump or a framework demo I could not defend.

I did not yet need a blog, typed collections, or a multi-environment CDN. I did need:

  • A constrained stack I could own end-to-end (vanilla HTML/CSS/JS)
  • A build-time content seam so projects and profile data were not hand-copied into markup
  • A design-system surface that documented tokens and patterns alongside the product
  • Written decisions - ADRs and quality attributes - so future-me (and reviewers) could see why the stack looked the way it did

The risk of skipping that discipline was clear: a pretty single page that could not grow, could not explain itself, and would force a full rewrite the moment content or SEO became real requirements.

Solution

I built prj--personal-portfolio--v1 as a static site with an explicit architecture contract: vanilla frontend, Webpack 5 + Handlebars as a lightweight SSG, JSON content merged at build time, and GitHub Pages as the sole hosting path.

The product is two HTML outputs from one pipeline: the portfolio (index) - hero, about, technical arsenal, projects, experience, contact - and a design system page that documents colors, typography, components, and UI patterns (terminal, skill cloud, timeline, competency matrix). Multi-entry *.entry.js bundles keep each UI area's CSS/JS co-located with its Handlebars partial. Nine ADRs lock the framework, build, content, hosting, navigation, structure, Sass, and Babel choices.

Architecture

src/data/*.json          <- profile, projects, pages, skills, …
src/markup/*.hbs         <- index + design_system + partials/
src/styles/**/*.scss     <- tokens, reset, themes, components
src/scripts/**/*.entry.js
        │
        ▼
 Webpack 5
   HandlebarsPlugin  ->  dist/*.html   (all JSON merged into context)
   multi-entry JS/CSS ->  named bundles
   Sass -> MiniCssExtract -> minified CSS
   Babel + Terser -> production JS
        │
        ▼
 GitHub Actions (push main)
   npm ci -> npm run build -> peaceiris/gh-pages -> dist/
        │
        ▼
 GitHub Pages
   paulalexserban.github.io/prj--personal-portfolio--v1/
LayerResponsibility
src/data/JSON content: page shells, profile, projects, skills, competency matrix, experience (and stubs for coursework / arsenal)
src/markup/Handlebars pages + partials; _template_base layout; section composition
src/scripts/Multi-entry feature bundles (navbar, hero, terminal, about, contact, …)
src/styles/Sass tokens / themes; CSS variables for light/dark
build/Modular Webpack config: paths, loaders, plugins, entry glob
ADRs / ASRsDecision log and quality attributes under docs/arch docs/
CI.github/workflows/deploy.yml -> build -> GitHub Pages

Approach

As architect and lead implementer, I treated a "simple" portfolio as a system with boundaries - content, composition, assets, and delivery - and wrote the decisions down before the stack could calcify by accident.

Platform boundaries first

  • Vanilla by ADR, not by default - ADR-001 rejects React/Vue/Bootstrap so the site demonstrates fundamentals and stays cheap to host; complexity cost is an accepted trade-off, not an oversight.
  • Webpack + Handlebars as SSG - ADR-002/003: pages are templates; content is JSON merged into the Handlebars context at build; output is plain HTML/CSS/JS with no runtime server.
  • Modular build config - webpack.config.cjs stays thin; build/{paths,loaders,plugins,helpers}.cjs own the seams so the bundler remains evolvable.
  • Multi-entry "component bundles" - only *.entry.js become Webpack entries; partials pull their own CSS/JS. That is a design-system instinct without a framework.
  • Design system as a product page - design_system.hbs ships tokens, components, and patterns next to the portfolio so UI language is documented, not tribal.

Product surfaces

  • Portfolio (index) - fixed navbar with theme toggle and scroll spy; terminal-style hero; about + animated stats from profile.json; technical arsenal; project cards filtered to status === "Done" from projects.json; experience timeline; contact with social links and email obfuscation (encode helper + decode on interaction).
  • Design system - colors, Inter + JetBrains Mono type, buttons/cards/forms, terminal / skill-cloud / timeline / competency-matrix patterns, and usage guidelines.
  • Responsive + themed - Sass breakpoints and scale; runtime light/dark via [data-theme] CSS custom properties and localStorage.
  • freeCodeCamp as ASRs - welcome section, navbar, projects, and profile link treated as architectural constraints, not afterthoughts.

Architecture knowledge management

  • Nine ADRs covering framework, build tools, content, hosting, responsive strategy, navigation, folder structure, Sass, and Babel.
  • ASRs and fitness-function notes - performance and maintainability written as requirements; Performance API probes in the base template as a lightweight runtime fitness signal.
  • Docs as part of the deliverable - vision, goals, technical requirements, and improvement backlog live in-repo so the case for the next version is already evidence-based.

Stack

LayerChoices
AuthoringJSON in src/data/, Handlebars markup + partials
BuildWebpack 5, Handlebars plugin, Sass, Babel, Terser, CSS minimizer
RuntimeVanilla ES6+ modules, CSS variables theme, no framework
DeliveryGitHub Actions -> GitHub Pages (dist/)
GovernanceADRs, ASRs, Dependabot, CODEOWNERS

Design decisions that mattered

  • Constraint as architecture. Choosing vanilla was a recorded decision with accepted risks (UI complexity ceiling, later refactor) - the same discipline later used when v2 chose Next.js and v3 chose SQLite-at-build.
  • Content at build, not copy-paste HTML. Even with incomplete JSON->UI binding on some sections, the pattern (data files in, HTML out) is the direct ancestor of v2's ContentRepository and v3's ingest pipeline.
  • Two surfaces, one pipeline. Portfolio + design system from the same Webpack graph proves the site is a small product platform, not a single landing file.
  • ADRs on a personal project. Decision logs are leadership practice: future changes argue against written rationale instead of reinventing the stack.
  • GitHub Pages only. Correct for v1 cost and FCC scope; named the hosting ceiling early (ADR-004) so v2's dual preview/prod path was a deliberate upgrade, not a surprise.
  • Trade-off named up front. No blog, no typed multi-collection model, no SEO sitemap/IndexNow, no content/app repo split, no tests - accepted for v1, listed as the problem statement that justified v2.

What shipped

  • Live static portfolio on GitHub Pages
  • Two built pages: portfolio showcase and design-system documentation
  • JSON-at-build content for page shells, profile, and projects (with data stubs ready for coursework / arsenal / experience expansion)
  • Multi-entry Webpack asset graph with Sass themes and light/dark toggle
  • CI deploy on main, Dependabot, and a nine-ADR decision log
  • The foundational static-SSG practice that Personal Portfolio V2 and the later content pipeline evolved

Outcome

V1 established an online presence and, more importantly, a habit of platform thinking on a small surface: content separated from markup, build as a contract, design system as a first-class page, decisions written as ADRs. Its limits - co-located content, incomplete data binding, single hosting path, no typed collections or SEO release seam - are exactly what v2 was designed to fix. The line from this Webpack/Handlebars site to a multi-surface content platform is continuous, not a rewrite from zero.

Predecessor to the platform

This is the first generation of the personal engineering site. Related successors: