Personal portfolio website built with GoFront using .templ component architecture. Content managed through YAML configuration and markdown files.
Key Features: GoFront templ components • Path-based SPA routing • Markdown blog & pages • Fuzzy search (Fuse.js) • Light/Dark themes • GitHub integration • Optional comments (giscus) & contact form (EmailJS)
- Core: GoFront (
.templcomponents, Go-inspired frontend architecture) - Build:
gofront prep&npm run prod• Biome (lint/format) • Playwright (10 E2E suites) - Content: YAML config (compiled to JSON) + Markdown • Marked.js • Prism.js v1.30 • Mermaid (lazy-loaded from jsDelivr for
```mermaidfences) - Features: Fuse.js (search) • EmailJS (contact form) • giscus (comments)
- Assets: Raleway fonts • Inline SVG icons (local, no CDNs)
The application is written in GoFront under app/src/ and compiles to native JavaScript ES modules.
flowchart LR
subgraph Source ["Source (app/src/)"]
SRC["Go & .templ Files"]
DATA["YAML & Markdown"]
end
subgraph Build ["Build Pipeline"]
GF["GoFront Compiler"]
PREP["gofront prep"]
end
subgraph Runtime ["Browser Runtime"]
APP["app.js (Router & UI)"]
VENDOR["vendor.js (Libs)"]
DOM["DOM (gom.Mount)"]
end
SRC --> GF --> APP --> DOM
DATA -.-> APP
PREP --> VENDOR --> DOM
flowchart TD
NAV["Navigation Event<br/>(Link click or popstate)"] --> ROUTE["Router<br/>Parse path & match route"]
ROUTE --> RESOLVE{"Content Cached?"}
RESOLVE -->|Yes| RENDER["Render View<br/>Mount .templ components to DOM"]
RESOLVE -->|No| FETCH["Fetch & Parse Markdown<br/>(Marked.js & Prism)"] --> RENDER
- Source (
app/src/):main.go(init, global event delegation) •ui.go(region renders, overlay state reconciliation) •store.go(state & data store, JSON hydration,contentCache) •view.go(ViewState+ pure route resolvers) •router.go(parseRoute, SPA routing,loadRoutefetch/cache/render path) •theme.go(theme persistence & CSS variables) •markdown.go(markdown & code highlighting) •search.go(Fuse.js search) •email.go(EmailJS integration) •icons.go(SVG icon registry) •comments.go(giscus comments) - Components (
app/src/*.templ):app.templ•navbar.templ•blog.templ•projects.templ•page.templ•footer.templ•search.templ•contact.templ•icons.templ - Styles (
app/css/app.css): single plain stylesheet linked fromindex.html, formatted and linted by Biome; loads in parallel with the JS bundles
Prerequisites: Node.js >= 24.0.0, npm >= 11.0.0
npm install # Install dependencies
npm run prepare # Bundle fonts, themes, dependencies
npm run dev # Start dev server (http://localhost:8181)Open http://localhost:8181 in your browser.
To create an optimized production build:
npm run prodThis will:
- Run code quality checks (
biome check) - Let
gofront buildreplace the previouspublic/output so removed posts and routes never linger - Compile YAML configuration to
content.json - Bundle and minify vendor dependencies (
gofront prep --minify) - Compile, minify, and mangle GoFront application bundle to
public/app.js - Let GoFront copy standard web assets to
public/, then copy selective blog/page Markdown topublic/data/;404.htmlis emitted as a copy of the processedindex.html - Generate
sitemap.xmlandrss.xml - Output complete, self-contained site to
public/directory
- Vendor Assets (
assetCopy): Fonts and Prism themes are copied from npm packages toapp/duringnpm run preparevia GoFront'sassetCopyconfiguration inpackage.json. Note:app/fonts/andapp/css/prism-themes/are gitignored as they are generated from npm packages. - Production Assets:
gofront buildsynchronizes standard web files fromapp/topublic/. Runtime data is finalized separately:content.jsonplus only Markdown or files referenced by live routes are copied topublic/data/(nevercontent.yaml).
The project uses Biome for code formatting and linting (JavaScript in scripts/ and tests/build/, and the stylesheet app/css/app.css):
- 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.
The test suite covers unit tests and full end-to-end integration tests:
npm run test:gofront # Run Go unit tests (gofront test, jsdom-backed for DOM code)
npm run test:e2e # Run E2E tests (Playwright, requires dev server)
npm run test:build # Run the production build and verify public/ output (node:test; rebuilds public/)
npm run test:all # Run all tests (GoFront + Playwright + build; runs last, rebuilds public/)Tests cover:
- Go Unit Tests (
app/src/*_test.go): route parsing,ViewStateresolvers (cache hit/miss, not found, repo-less projects),loadRoute(stubbedfetch, superseded navigation), content JSON hydration and date sorting, theme precedence/persistence and CSS variable application (jsdom), search guards and result mapping (fake Fuse index), frontmatter stripping, contact validation, render helpers - End-to-End Tests (
tests/e2e/): 10 Playwright test suites across Chromium and Firefox:- Navigation, history, deep-linking, and route transitions
- Markdown blog rendering, pagination, and code syntax highlighting
- Project showcase, links, and tags
- Light/Dark theme switching and localStorage persistence
- Fuse.js search modal and query matching
- EmailJS contact form validation and submission
- Mobile hamburger navigation drawer
- 404 and error state fallbacks
- Type Checking: GoFront type checker (
npx gofront check)
All quality gates and tests must pass before production builds.
The frontend is built with GoFront using declarative .templ components and Go:
package main
templ BlogPostCard(post BlogPost) {
<article class="blog-post-card">
<h2 class="post-title">
<a href={ "/blog/" + post.Slug } data-action="nav">{ post.Title }</a>
</h2>
<div class="post-meta">
<time>{ post.Date }</time>
</div>
<p class="post-excerpt">{ post.Excerpt }</p>
</article>
}Architecture Highlights:
- Zero Runtime Overhead:
.templfiles compile directly to native DOM manipulation calls (createElement,setAttribute,appendChild) with no virtual DOM diffing. - Go Syntax & Type Safety: Components receive typed props and compile to clean ES modules.
- Dynamic Content Injection: Markdown generated from
markedis injected using@templ.Raw(doc.HTML). - Mermaid Diagrams:
```mermaidfences in blog posts, pages and project READMEs are rendered as SVG. Mermaid is fetched from jsDelivr only when a rendered page contains a diagram, and diagrams are re-drawn with the matchingdark/defaulttheme when the theme toggles. - Global Event Delegation: Handled via
data-actionattributes registered centrally on#appinapp/src/main.go.
Path-based URLs: /, /blog/, /blog/post-slug, /project/id, /page/id
Dev Server: npm run dev (gofront dev, port 8181) provides live reload and built-in SPA route fallback (serving index.html on clean paths)
GitHub Pages: Every known route gets a pre-generated index.html stub (200 with route-specific meta). 404.html is the app shell itself — GitHub Pages serves it at the requested URL without redirecting, so app/src/router.go reads location.pathname directly for unknown deep links. No hash redirect.
Absolute Paths: All resources use root-relative paths (/app.js, /data/content.json) to work from any route depth
Event Delegation: Dynamic content uses data-action attributes (e.g., <a data-action="nav">, <button data-action="toggle-theme">) handled centrally in app/src/main.go
All content is managed through app/data/content.yaml. The configuration file supports:
projects:
- id: "my-project"
title: "Cool Project"
tags: ["JavaScript"]
order: 1
github_repo: "my-project" # Auto-loads README
demo_url: "https://example.com"
youtube_videos: ["videoId"]
links:
- title: "GitHub"
icon: "github"
href: "https://github.com/user/repo"Features: Auto-load GitHub READMEs • YouTube embeds • SVG 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