Portfolio Project — 25.04.2026
A comprehensive dev log for adotkaya.github.io — a personal portfolio and blog powered by a custom Go static site generator.
Core Philosophy
Minimalism as constraint. Every element must earn its place. No animations, no JavaScript frameworks, no unnecessary dependencies. The site should feel like an extension of my terminal.
Quick Links
- Live Site: https://adotkaya.github.io
- Tech Stack: Go 1.22, vanilla CSS, GitHub Pages
- Build Time: Milliseconds
- Dependencies: 3 direct Go modules (zero npm)
Map of Content
Architecture
- Architecture - Custom Go Generator — Why Go instead of Next.js/Astro, how the generator works, build pipeline.
Design Philosophy
- Design - Visual Philosophy — Dark mode, no accent colors, system fonts, single CSS file.
- Design - Zero JavaScript — Why the site has zero client-side scripts.
Features
- Feature - Content as Data — YAML + Markdown workflow, draft support, separation of concerns.
- Feature - Blog Enhancements — RSS feed, human-readable dates, prev/next navigation.
- Feature - SEO & Social Polish — Open Graph, Twitter Cards, JSON-LD, canonical URLs.
- Feature - Bookshelf Page — Dedicated
/books/page with reading status badges.
Operations
- Security - Static Site Model — No runtime, no secrets, no attack surface.
- Deployment - GitHub Actions — OIDC-based push-to-deploy pipeline.
Reflection
- Lessons Learned — What building this from scratch taught me.
- Future Roadmap — What’s next for the site.
Project Stats
| Metric | Value |
|---|---|
| Total Pages | 8 (Home, Resume, Projects, Blog, Now, Uses, Bookshelf, 404) |
| Blog Posts | 1 (with draft support ready) |
| CSS Size | ~1,070 lines (single file) |
| Go Code | ~550 lines (single main.go) |
| Build Output | Static HTML + CSS + SVG |
| Hosting Cost | $0 |
This note is a living document. Last updated: 25.04.2026
Architecture — Custom Go Generator
What
A single Go program (main.go) reads YAML content files and Markdown blog posts, parses HTML templates, and writes static HTML to disk.
Why Not Next.js / Astro / Hugo?
Most developers default to JavaScript frameworks. I wanted the site to reflect my actual values — simplicity, performance, minimal dependencies. Go is what I write professionally now. The portfolio itself should demonstrate that.
Specific reasons: - Build time: Milliseconds instead of seconds. - Dependency surface: 3 direct Go modules vs. 1,000+ npm packages. - Zero runtime overhead: No client-side hydration, no bundle size anxiety. - Learning value: Deeper understanding of template systems and static site fundamentals.
How It Works
content/*.yaml ──┐
content/blog/*.md ─┼──► main.go ──► public/*.html ──► GitHub Pages
templates/*.html ──┘ ↑
static/* ────────────────────┘
Key packages:
| Package | Purpose |
|---|---|
html/template |
Template rendering |
gopkg.in/yaml.v3 |
YAML content parsing |
gomarkdown/markdown |
Markdown → HTML |
alecthomas/chroma/v2 |
Syntax highlighting |
What It Improves
- Interview value: The generator itself is a project I can discuss. It shows systems thinking, not just framework usage.
- Maintenance: No version conflicts, no deprecated packages, no security advisories for frontend tooling.
- Speed: The entire site builds in under a second.
Build Pipeline
go run main.go # Generates public/
# Commit & push # GitHub Actions deploys automatically
No build tool. No webpack.config.js. No vite.config.ts. Just Go.
Related: Design - Zero JavaScript, Security - Static Site Model
Deployment — GitHub Actions
What
A GitHub Actions workflow that builds the site on every push to main and deploys to GitHub Pages automatically.
Why
Push-to-deploy is the ideal developer experience. I write, commit, push, and the site updates. No manual FTP, no server management, no deployment scripts on my machine.
How
Workflow file: .github/workflows/deploy.yml
Pipeline:
Push to main ──► Checkout ──► Setup Go ──► go run main.go ──► Upload artifact ──► Deploy to Pages
Key details:
- Trigger: Push to main or manual workflow_dispatch.
- Go version: 1.21 (workflow) — note: go.mod specifies 1.22, minor drift.
- Authentication: OIDC (id-token: write). No PATs, no secrets.
- Permissions: Minimal — contents: read, pages: write, id-token: write.
What It Improves
- Zero manual deployment: The site updates in ~30 seconds after push.
- Auditable: Build and deploy history is visible in GitHub’s UI.
- Secure: OIDC is the modern standard. No long-lived tokens to rotate.
- Free: GitHub Pages costs $0 for public repos.
The Build Step
- name: Build site
run: |
go mod download
go run main.go
This downloads the 3 dependencies and generates the entire site in milliseconds.
Future Improvement
Update go-version: '1.21' → '1.22' in the workflow to match go.mod. Not urgent, but avoids confusion.
Related: Security - Static Site Model, Architecture - Custom Go Generator
Design — Visual Philosophy
Dark Mode by Default
What: Near-black background (#0a0a0a), soft grays, no pure white.
Why: I live in terminals and dark-themed editors. A bright white portfolio feels foreign to my daily environment.
How: CSS custom properties make the palette consistent:
:root {
--bg: #0a0a0a;
--bg-elevated: #141414;
--text-primary: #e5e5e5;
--text-secondary: #a3a3a3;
--text-muted: #737373;
--border: #262626;
}
What it improves: - Authenticity — the site feels like my workspace. - Reduced eye strain for visitors who prefer dark themes. - Easier to maintain one theme than supporting a toggle.
No Accent Colors, No Animations, No Distractions
What: No primary brand color. No hover effects beyond subtle underline and color shifts. No particles, no scroll-jacking, no cookie banners.
Why: Minimalism as a forcing function. If an element doesn’t serve the content, it doesn’t belong.
How: The only interactive effects are opacity and color transitions on links. No JavaScript animations. No transform tricks.
What it improves: - Load speed — no animation libraries, no heavy CSS. - Accessibility — reduced motion is the default. - Professional tone — the content speaks, not the chrome. - Time saved — zero hours spent on animation tuning.
System Font Stack
What: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, ...
Why: Custom web fonts add HTTP requests and layout shift. System fonts are already on the user’s machine.
How: Defined once in body {}. No @font-face declarations.
What it improves: - Zero font loading time. - No flash of unstyled text (FOUT). - Native feel — the site matches the OS typography. - Saves bandwidth for users on slow connections.
Single CSS File, No Frameworks
What: One static/style.css (~1,070 lines). No Tailwind, no Bootstrap, no Sass.
Why: A portfolio doesn’t need a component library. I know exactly what elements exist.
How: Hand-written CSS. Mobile breakpoint at 640px. Print styles for resume.
What it improves: - No build step for CSS. - Zero unused styles. - Full control — no fighting framework defaults. - Browsers parse one small file instantly.
Mobile-First Responsive
What: The site works on all screen sizes. Navigation collapses on small screens.
Why: ~60% of web traffic is mobile.
How: Flexbox and grid adapt naturally. Single breakpoint.
What it improves: - Professional appearance on all devices. - No separate mobile site to maintain.
Related: Design - Zero JavaScript, Architecture - Custom Go Generator
Design — Zero JavaScript
What
No <script> tags anywhere. The site is pure HTML + CSS.
Why
JavaScript is powerful but often unnecessary. For a content site, it’s overhead.
Specific reasons: - I don’t need interactivity beyond links and navigation. - Every byte of JS is a byte that must be parsed, compiled, and executed. - No JS means no runtime bugs, no hydration mismatches, no framework updates.
How
Everything that could be done with JS is done at build time instead:
| Task | Typical JS Approach | My Approach |
|---|---|---|
| Reading time | Client-side calculation | Build-time word count / 200 wpm |
| Date formatting | toLocaleDateString() |
Go’s time.Format() at build |
| Navigation | React Router | Server-rendered <a href> |
| Syntax highlighting | Prism.js on load | Chroma at build |
| Mobile menu toggle | JS event listener | CSS-only (future: checkbox hack) |
What It Improves
- Performance: No parse/compile/execute step on load.
- Security: No XSS surface from client-side scripts.
- Reliability: No broken features if a script fails to load or is blocked.
- Battery life: Especially important on mobile devices.
- Simplicity: The mental model is “request → HTML → render.” Nothing else.
The Exception That Proves the Rule
The only JavaScript-adjacent feature I considered was client-side search. If the blog grows beyond ~10 posts, I may add a single vanilla JS file that queries a search.json generated at build time. Even then, it would be one small script, not a framework.
Related: Architecture - Custom Go Generator, Design - Visual Philosophy
Feature — Blog Enhancements
RSS / Atom Feed
What: Auto-generated Atom feed at /feed.xml containing all posts with title, URL, date, and excerpt.
Why: RSS is the open web’s native subscription protocol. It doesn’t require platforms (Twitter, LinkedIn) to distribute content.
How: generateFeed() in main.go writes XML directly. <link rel="alternate"> in every page <head> for auto-discovery.
What it improves: - Readers can subscribe without creating accounts anywhere. - Platform independence — my content is mine. - SEO signal — active feeds indicate fresh content to crawlers.
Human-Readable Dates
What: 2026-04-25 → April 25, 2026 everywhere dates appear.
Why: Machine-readable ISO dates are for datetime attributes. Humans prefer natural language.
How: formatDate() in main.go using Go’s time.Parse + time.Format. Stored as DateFormatted on the BlogPost struct.
What it improves:
- Warmth — “April 25, 2026” feels human.
- No DD/MM vs. MM/DD confusion.
- Still accessible — datetime attribute preserves machine-readability for screen readers and crawlers.
Previous / Next Post Navigation
What: At the bottom of each blog post, links to chronologically adjacent posts.
Why: Single-post pages are dead ends without navigation. Readers who finish one post should be invited to read another.
How: Added PrevPost and NextPost pointers to PageData during render loop. Conditionally rendered in template footer.
What it improves: - Increased time on site. - Better internal linking (SEO benefit). - Reader-friendly — no need to return to the blog list.
Auto-Heading IDs
What: Markdown headings automatically get id attributes for direct linking.
Why: Readers should be able to link to specific sections of long posts.
How: parser.AutoHeadingIDs extension in gomarkdown.
What it improves: - Deep linking without manual HTML. - Table of contents can be generated automatically in the future. - Better anchor navigation.
Syntax Highlighting with Chroma
What: Code blocks in blog posts are highlighted using the Monokai theme.
Why: Technical blog posts need readable code. Plain <pre> blocks are hard to scan.
How: A regex in main.go feeds code to chroma, which returns inline-styled HTML. No external CSS file needed.
What it improves: - Colors work immediately — styles are inline. - Monokai fits the dark theme perfectly. - Language auto-detection falls back gracefully.
Related: Feature - Content as Data, Feature - SEO & Social Polish
Feature — Bookshelf Page
What
A dedicated /books/ page listing books with title, author, and reading status badges.
Why
Reading lists signal intellectual curiosity. A bookshelf page makes the portfolio feel more personal and less like a resume.
The /now page concept extended: Just as /now shows what I’m doing, /books shows what I’m learning.
How
Data model:
- title: "Designing Data-Intensive Applications"
author: "Martin Kleppmann"
status: "finished"
Template: templates/books.html follows the same minimal design as other pages. Uses the existing .book-list CSS class.
Status badges: .book-status CSS rule renders a small pill (reusing the .tech-tag pattern):
- reading — currently reading
- finished — completed
- to-read — on the list
Generator wiring:
- Added Status string to Book struct.
- Added render call in main.go for public/books/index.html.
- Added /books/ to sitemap and navigation on all 8 existing templates.
What It Improves
- Personality: Shows what I care about learning, not just what I’ve built.
- Dynamic feel: Status badges make the page feel alive and current.
- Return visits: Visitors might check back to see what I’ve finished.
- Content reuse:
books.yamlalready existed but was never rendered. This finally surfaces it.
Design Detail
The status badge uses the same visual language as tech tags on the resume page — muted background, subtle border, capitalized text. Consistency without introducing new patterns.
Related: Feature - Content as Data, Design - Visual Philosophy
Feature — Content as Data
What
All content lives in content/*.yaml and content/blog/*.md. Templates are pure presentation. The two never mix.
Why
Separation of concerns. I can update my experience or add a blog post without touching HTML or CSS.
The alternative: Hardcoding content in templates. Every edit requires opening HTML files, finding the right tags, not breaking structure. That’s friction.
How
YAML files for structured data:
- config.yaml — name, bio, social links
- resume.yaml — summary, skills, education
- experience.yaml — work history
- projects.yaml — project list with tech stacks
- books.yaml — reading list with status
- now.yaml — current activities
- uses.yaml — tools and workflow
Markdown for long-form:
- content/blog/*.md with YAML frontmatter
Build process:
data.Config = mustParseYAML[Config]("content/config.yaml")
data.Posts = loadBlogPosts()
renderTemplate("templates/index.html", "public/index.html", data)
What It Improves
- Writing friction is near zero: Add a Markdown file, run
go run main.go, push. - Content is version-controlled: No separate CMS database.
- Portable: YAML is universal. I’m not locked into any platform.
- Enables draft support:
draft: truein frontmatter excludes a post from all renders.
Draft Post Support
What: draft: true in frontmatter excludes the post from home, blog list, RSS, and prevents individual page generation.
Why: I need a place to iterate on posts without publishing them.
How: Added Draft bool to frontmatter struct. Early return if draft.
What it improves: - I can commit work-in-progress posts to git without them going live. - No risk of accidentally publishing unfinished thoughts. - Draft files live alongside published ones.
Related: Feature - Blog Enhancements, Architecture - Custom Go Generator
Feature — SEO & Social Polish
What
A comprehensive set of meta tags and structured data to make the site discoverable and shareable.
Why
Without these, sharing a link on Twitter/LinkedIn/Discord produces a bare URL. Search engines struggle to understand page structure and authorship.
How
Open Graph Tags (Every Page)
<meta property="og:title" content="...">
<meta property="og:description" content="...">
<meta property="og:type" content="website|article">
<meta property="og:url" content="...">
<meta property="og:image" content=".../og-image.svg">
Twitter Cards (Every Page)
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="...">
<meta name="twitter:description" content="...">
JSON-LD Structured Data
- Home:
Personschema (name, url, sameAs links to GitHub/LinkedIn, jobTitle) - Blog Posts:
BlogPostingschema (headline, author, datePublished, url, description) - Other Pages:
WebPageschema (name, url)
Canonical URLs
<link rel="canonical" href="https://adotkaya.github.io/.../">
Sitemap
Auto-generated sitemap.xml with priorities and change frequencies for every page and post.
Favicon & OG Image
favicon.svg—@glyph on dark backgroundog-image.svg— 1200×630 dark@mark for social previews
What It Improves
- Social shares: Rich previews with title, description, and image.
- Search rankings: Search engines understand who I am, what I write, and how pages relate.
- Click-through rate: Structured data can produce rich snippets in search results.
- SEO hygiene: Canonical URLs prevent duplicate content penalties.
Page-Type Specifics
| Page | og:type |
JSON-LD Schema |
|---|---|---|
| Home | website |
Person |
| Blog Post | article |
BlogPosting |
| All Others | website |
WebPage |
Related: Feature - Blog Enhancements, Design - Visual Philosophy
Future Roadmap
High Priority
Table of Contents for Blog Posts
Auto-generate from Markdown headings (h2, h3). Injected at the top of long posts. Pure HTML, zero JS. Becomes valuable once posts exceed 1,000 words.
Tags & Tag Index Pages
tags: [go, architecture] in frontmatter. Generate /blog/tag/go/ index pages. Valuable once there are 5+ posts.
Tools Section on Home Page
tools.yaml exists with 11 tools + SimpleIcons. A visual grid below Projects would add color and context to the homepage.
Medium Priority
Live Reload Dev Server
A simple file-watcher loop in Go that rebuilds on content/ or templates/ changes. Huge quality-of-life improvement for writing sessions.
Client-Side Search
main.go generates public/search.json with post titles, excerpts, tags. A single vanilla JS file (static/search.js) powers a /search/ page. No external service. Valuable once blog exceeds ~10 posts.
Makefile
Standard conventions: make build, make serve, make clean.
Low Priority / Polish
Accessibility Improvements
- Skip navigation link (“Skip to main content”) for keyboard users.
focus-visiblestyles for clearer keyboard navigation.- Normalize
<time>element usage across all pages.
HTML Minification
Strip whitespace from generated HTML. Saves ~10-20% file size. Marginal gain for a small site.
Self-Host SimpleIcons
Download tech stack icons into static/ to remove external CDN dependency entirely.
Content Security Policy
Add <meta http-equiv="Content-Security-Policy" ...> for extra XSS hardening. Overkill for a static site with no scripts, but good hygiene.
Related: Lessons Learned, Feature - Blog Enhancements
Lessons Learned
Go’s Template System Is Elegant
Building the generator taught me more about Go’s html/template than any tutorial. The block and define patterns for layout inheritance are powerful once understood. Auto-escaping prevents XSS by default — a security feature that’s easy to take for granted.
Static Files Are Surprisingly Sufficient
Modern frontend frameworks abstract away an enormous amount of complexity. Building from scratch made me question whether that complexity is always necessary. For a content site, the answer is often no.
Constraints Force Clarity
The decision to use zero JavaScript and zero CSS frameworks meant every feature had to be justified. Could it be done at build time? Could it be done with pure CSS? If not, did I really need it? This discipline produced a cleaner result than unconstrained development would have.
Content-First Means Less Code
By separating content (YAML/Markdown) from presentation (templates), I spend more time writing and less time debugging layout issues. The generator handles the rest.
A Portfolio Is Never “Done”
This site will evolve as I learn and build. The architecture supports that: new blog posts are just Markdown files. New pages are just templates and YAML. The foundation is solid enough to grow without rewriting.
What I’d Do Differently
Nothing major. If I were starting from absolute zero again, I might:
- Add the /books/ page earlier (the data was there, unused, for multiple commits).
- Consider a Makefile from day one (make build, make serve).
But these are polish, not regrets. The core decisions hold up.
Related: Future Roadmap, Architecture - Custom Go Generator
Security — Static Site Model
What
The deployed site is static HTML + CSS + SVG. No backend, no database, no API keys, no runtime code.
Why
Every additional service is an attack surface and a maintenance burden. For a personal portfolio, this attack surface should be zero.
How
No secrets in repository:
- Scanned git history: no API keys, tokens, passwords, or credentials committed.
- No .env files. No config files with hidden values.
No runtime: - Go generator runs at build time only. - The deployed artifact is HTML/CSS/SVG. - Nothing executes on the server or in the browser.
GitHub Actions security:
- Uses OIDC (id-token: write) for Pages deployment.
- No long-lived personal access tokens stored as secrets.
- Permissions are minimal: contents: read, pages: write.
What It Improves
- Nothing to hack: No SQL injection, no XSS from user input, no auth bypass.
- Nothing to leak: No API keys, no database credentials.
- Nothing to patch: No runtime dependencies to update for security advisories.
- No costs: GitHub Pages is free and scales infinitely for static content.
Minor Considerations
| Risk | Mitigation |
|---|---|
| External CDN (SimpleIcons) | Icons load as <img>, not <script>. XSS risk is minimal. |
| Email in git commits | Optional: enable GitHub’s “Keep my email addresses private” setting. |
| Intentional PII exposure | Full name, location, work history are by design for a public portfolio. |
Verdict
Zero meaningful security risk. This is the safest possible architecture for a public website.
Related: Architecture - Custom Go Generator, Deployment - GitHub Actions