Personal portfolio website built with vanilla JavaScript (ES6 modules), custom reactive system, and Microtastic for minimal build tooling. Content managed through YAML configuration and markdown files.
Key Features: Reactive UI with signals • Path-based SPA routing • Markdown blog & pages • Fuzzy search (Fuse.js) • Light/Dark themes • GitHub integration • Optional comments (giscus) & contact form (EmailJS)
- Core: Vanilla HTML/JS (ES6 modules) • CSS-in-JS (via reactive.js) • Custom reactive system (signals, computed, declarative binding)
- Build: Microtastic (SPA dev server) • Biome 2.3.7 (lint/format) • Node.js test runner (81 tests)
- Content: YAML config + Markdown • Custom YAML parser (~4KB) • Marked.js v17 • Prism.js v1.30
- Features: Fuse.js 7.1 (search) • EmailJS (contact form) • giscus (comments)
- Assets: Raleway fonts • Font Awesome subset (local, no CDNs)
The application follows a modular namespace pattern with reactive components:
Key Modules:
- Core:
main.js(init, event delegation) •utils/reactive.js(signals, components) •Context(state, data, blog utilities) •Router(SPA routing) - Components:
MainContent(main container) •Navbar,Footer,BlogList,BlogPost,Project,Page(self-contained reactive UI - each loads its own data) - Features:
Search(Fuse.js, conditional) •ContactForm(EmailJS, conditional) •Theme(light/dark) - Styles:
styles/reset.styles.js(global reset) •styles/shared.styles.js(shared utilities) •styles/theme.styles.js(CSS variables) •styles/fonts.styles.js(font loading) •styles/main.styles.js(main styles) •[component].styles.js(component-scoped) - Utilities:
Templates(HTML generation) •MarkdownLoader(markdown parsing, copy code buttons) •PrismLoader•YAMLParser•i18n
Prerequisites: Node.js >= 20.0.0, npm >= 9.0.0
Optional: VS Code devcontainer (Alpine Linux, port 8081 auto-forwarded)
npm install # Install dependencies
npm run prepare # Bundle fonts, themes, dependencies
npm run dev # Start dev server (http://localhost:8081)Dev Container: Open in VS Code → "Reopen in Container" → npm install → npm run prepare → npm run dev
- Open
http://localhost:8081
To create an optimized production build:
npm run prodThis will:
- Run code quality checks (
biome check) - Run all tests (81 unit tests)
- Copy assets (fonts, Prism themes) from node_modules
- Bundle and minify dependencies
- Output to
public/directory
Fonts and Prism themes are automatically copied from npm packages when you run npm run prepare. The assetCopy configuration in package.json defines which assets to copy.
Note: app/fonts/ and app/css/prism-themes/ are gitignored as they're auto-generated from npm packages.
The project uses Biome for code formatting and linting:
- Format code:
npm run format - Check code quality:
npm run check - Auto-format: Enabled on save in VS Code
All code changes must pass linting before deployment.
Uses Node.js built-in test runner (81 tests):
npm test # Run all testsTests cover:
- Reactive system (signals, computed, batching, components)
- HTML escaping and template utilities
- Template generation
- Search functionality
- YAML parser
- Routing logic
- Markdown parsing
- Internationalization
- Theme management
- Email controller
- Error handler
- UI utilities
- Prism loader
All tests must pass before production builds.
Custom signals-based reactive system (~5KB) with declarative binding:
export class Counter extends Reactive.Component {
state() {
return {
count: this.signal(0),
doubled: this.computed(() => this.count() * 2),
};
}
template() {
return html`<button data-on-click="increment" data-text="count"></button>`;
}
increment() { this.count(this.count() + 1); }
}Declarative Bindings: data-text, data-html, data-attr-*, data-class-*, data-bool-*, data-visible, data-model, data-on-click/submit
Benefits: Direct DOM updates • Auto-batching • Computed values • No virtual DOM • No build step required
Path-based URLs: /, /blog/, /blog/post-slug, /project/id, /page/id
Dev Server: Microtastic modified (node_modules/microtastic/index.js) to serve index.html for routes without extensions, maintains hot reload
GitHub Pages: Custom 404.html redirects via hash (#!redirect=<path>), main.js restores clean URL with history.replaceState()
Absolute Paths: All resources use root-relative paths (/src/main.js, /data/content.yaml) to work from any route depth
Event Delegation: Dynamic content uses data-action attributes (e.g., <a data-action="email">) handled globally in main.js
All content is managed through app/data/content.yaml. The configuration file supports:
projects:
- id: "my-project"
title: "Cool Project"
tags: ["JavaScript"]
weight: 1
github_repo: "my-project" # Auto-loads README
demo_url: "https://example.com"
youtube_videos: ["videoId"]
links:
- title: "GitHub"
icon: "fab fa-github"
href: "https://github.com/user/repo"Features: Auto-load GitHub READMEs • YouTube embeds • Font Awesome icons • Tag organization
Create posts in app/data/blog/ with frontmatter (title, date, excerpt, tags), register in content.yaml:
blog:
postsPerPage: 5
posts:
- filename: "2025-10-21-post-title.md"
title: "Post Title"
date: "2025-10-21"
excerpt: "Summary"
tags: ["tag1"]Features: GFM markdown • Syntax highlighting • Pagination • Auto-sorting • Tags
Fuse.js fuzzy search across projects and blog posts (conditionally loaded):
site:
search:
enabled: true
minChars: 2Features: Fuzzy matching • Weighted results (title 40%, desc 30%, tags 20%, content 10%) • Clickable tag filters • Real-time with debounce • Up to 8 results • Offline support
Create markdown files in app/data/pages/, configure in content.yaml:
pages:
about:
title: "About"
showInNav: true
order: 1Toggle button with localStorage persistence, system preference support:
site:
theme:
default: "dark" # "dark", "light", "auto"
dark:
primary: "#10B981"
code:
theme: "prism-tomorrow"
light:
primary: "#047857"
code:
theme: "prism-coy"Prism themes: prism-tomorrow, prism-okaidia, prism-dark, prism-coy, prism-solarizedlight (bundled locally)
GitHub Discussions-powered comments via giscus:
site:
comments:
blogEnabled: true
projectsEnabled: true
repo: "username/repo"
repoId: "R_YOUR_REPO_ID"
categoryId: "DIC_YOUR_CATEGORY_ID"Setup: Enable Discussions on repo → Install giscus app → Get IDs from giscus.app
Modal form with email delivery (conditionally loaded):
site:
emailjs:
enabled: true
serviceId: "service_xxx"
templateId: "template_xxx"
publicKey: "your_public_key"Setup: Sign up at emailjs.com → Create service/template with variables {{title}}, {{name}}, {{email}}, {{time}}, {{message}}
Built-in i18n framework (currently English only):
site:
i18n:
defaultLanguage: "en"
availableLanguages: ["en"]
translations:
en:
"nav.projects": "Projects"
"search.placeholder": "Search..."
# ... more translationsTo add a language: Add to availableLanguages (["en", "nl"]), copy en translations and translate values, use i18n.setLanguage('nl') to switch