Building My Own Static Site Generator

#report #design-systems #development #process #portfolio
7 min read

A designer's case for building custom tools instead of reaching for the nearest framework. How an Obsidian vault, a token grammar, and a compiler mindset replaced WordPress, Next.js, and the constant urge to redesign everything.

The Problem With Portfolio Sites

Every designer I know has rebuilt their portfolio at least three times. I have done it five. The pattern is always the same: you pick a tool, fight it into submission, launch something decent, then six months later you want to change one thing and the whole house of cards wobbles. WordPress needs plugins for everything. Squarespace looks like everyone else. Next.js is a rocket ship when all you needed was a bicycle.

The fifth time around, I stopped asking which tool should I use and started asking what does my portfolio actually need to do?

The answer was embarrassingly simple:

  1. Take my project write-ups and media
  2. Turn them into static HTML pages
  3. Deploy them to a server
  4. Never make me think about the plumbing again

No database. No server-side rendering. No hydration. No client framework sitting between the source and the page. Just authored Markdown in, static HTML out.

The Vault: One Place for Everything

The authored pages live in a repository-local Obsidian vault. Every project, art piece, and article has its own folder and Markdown file. An assets/ directory is optional: when it exists, it is an inbox for media waiting to be imported, not the permanent home of the site's image and video bytes.

VAULT/
  projects/
    creatitive/
      creatitive.md
      assets/
    bigboys/
      bigboys.md
      assets/
  writing/
    system-report-1/
      system-report-1.md
  art/
    photography/
      photography.md
      assets/
portfolio-media.json
system/
  templates/
  runtime/
  builder/

The Vault holds the writing. The tracked media manifest preserves each logical assets/... name and connects it to authenticated, compressed objects in the local build cache. Full-size originals live once in a separate archive; ordinary building, previewing, testing, and publishing never need to touch that archive.

There is still no admin panel or content API. I write in Obsidian, then run node system/builder/build.js from the repository root. The compiler validates every current source and compiles Markdown marked publish: true. Releasing is intentionally separate: one guarded publisher verifies the source, builds the complete candidate, shows the exact delta, asks for confirmation, and verifies the public result.

Vault Markdown and the media manifest are the content source of truth. Templates, runtime assets, and compiler code define how that content becomes the site. A source change does not become public until it passes the release boundary.

This separation is freeing. I know where authored copy lives, where logical media identity lives, and which exact production commit is public. Drafts can remain in the Vault without shipping, and a source push is never mistaken for a website release.

The Grammar: Tokens Instead of HTML

Markdown handles text well, but portfolios are image-heavy. Standard Markdown image syntax (![alt](path)) is fragile when authored names must survive media compression, cache restoration, and different build environments. So I built a token grammar instead.

Instead of hardcoding emitted file paths, you write short logical references. A hero image is a media key and a #hero tag. An inline image uses #img. An entire gallery is a folder glob and #imagestrip. The builder resolves those names through the media manifest to authenticated WebP, GIF, or WebM objects. The imagestrip token expands a numbered featured/ set into a scrollable carousel.

This is the core idea: authors write intent, the compiler resolves authenticated media. I do not put cache hashes or generated URLs into my writing. If a token cannot be resolved, media is missing, or captured bytes change during the build, the compiler fails before it can authorize a release.

The grammar also handles metadata inline. Alt text and captions ride alongside the token on the same line with #alt and #cap tags. No separate config files, no CMS fields to fill out. Everything about a piece of media lives in one place.

Frozen Templates and the Compiler Mindset

Most site generators put several abstraction layers between a template and its final HTML. My system keeps a small set of hand-built, deliberately stable templates. Their CSS classes, DOM hierarchy, and JavaScript hooks are governed contracts. They can change, but only as explicit source changes with matching tests and documentation.

The builder does not invent a new page structure on each run. It injects content into captured template bytes. HTML comment markers like CONTENT_MOUNT, NAVBAR_INCLUDE, and JSON_LD define where content goes. The compiler reads those exact template bytes, replaces the markers, and verifies the resulting artifact. This means:

  • The design is stable. I can update content without risking the layout.
  • The CSS is predictable. Class names do not shift between builds.
  • The JavaScript works. DOM hooks are where the runtime expects them to be.

Think of it less like a CMS and more like a compiler. The grammar is the source language. The templates are the target architecture. The builder translates one into the other deterministically, authenticates what it consumed, and refuses output that does not satisfy the contract.

Governance: The Report Comes First

The system has rules, and the rules are enforced. Not by convention, not by good intentions, but by pre-commit hooks and automated checks.

Every meaningful system change travels with:

  1. A changelog entry documenting what changed and why
  2. Updated canon documents if the grammar or contracts changed
  3. Passing tests and a real build that validate the affected behavior

The canon documents are the authority ladder of the project:

  • Grammar defines how content is authored (token syntax, frontmatter fields, file structure)
  • Architecture defines how the system is built (data flow, component responsibilities, build phases)
  • Contract defines the non-negotiable rules (vault safety, template freezing, scope control)
  • Invariants defines the locked decisions that are never revisited

If the code and the canon disagree, the canon wins. The code gets fixed.

The evidence belongs in the same coherent change as the feature. That discipline saves hours of reverse-engineering later because the reason, implementation, and proof stay together. When I return after months away, I do not have to guess which behavior was intentional.

Scaling Without Bloating

The grammar supports multiple layout types, each triggered by different tokens or frontmatter fields:

  • Standard layouts for project case studies with heroes, inline images, and imagestrips
  • Photodump layouts that render a masonry grid from a folder of images
  • Specimen layouts for type specimens and grid-based art
  • Blog layouts for long-form writing like this post

Each layout has a stable template and shares the same compiler pipeline. Adding a layout is deliberately more than dropping in one file: its frontmatter or token rules, analysis, rendering, output planning, optional runtime, tests, and canon all have to agree. The pipeline stays modular, but the new behavior still has to earn its place.

The blog layout, for example, adds article-specific features:

  • Reading time calculated from word count
  • Author byline in the footer
  • Related Writing cards that fill from published posts and hold future slots with placeholders
  • BlogPosting structured data for search engines
  • An RSS feed generated at build time

All of this comes from the same frontmatter and markdown I use for everything else. No separate blogging platform, no plugin ecosystem, no second system to maintain.

Why Not Just Use Next.js?

I get this question a lot. Here is the honest answer: because I do not need it.

Next.js is excellent software. So is Astro, Hugo, Eleventy, and a dozen other tools. But every one of them comes with opinions about how your site should work, a dependency tree that needs maintenance, and abstractions that sit between you and your output.

My system has:

  • No client framework, hydration, or application server
  • A small build core: Node.js 20.9 or newer, marked for Markdown, and Sharp for image processing
  • Explicit media tooling: FFmpeg is needed when importing or rebuilding video variants, but ordinary manifest-backed builds reuse authenticated WebM objects
  • Visible browser dependencies: small first-party scripts plus Adobe Fonts, Google Analytics, and Cloudflare Web Analytics
  • Exact generated-artifact control: the publisher authenticates the compiled tree and then verifies the provider-served release instead of assuming the edge returned unchanged bytes

When something breaks, the ownership boundaries tell me where to look: grammar, capture, analysis, rendering, emission, runtime, or provider. I still maintain dependencies and external services, but I am not debugging a general-purpose application framework that the site does not need.

The tradeoff is real: I maintain my own tooling. But for a portfolio site that changes a few times a year and needs to be rock-solid when a potential client visits, the calculus works. I would rather spend an afternoon writing a new render function than a week debugging why my framework's latest update broke image optimization.

The Point

This system is not the right choice for everyone. If you need a team of content editors, user authentication, or server-side application logic, use a system built for that. But if you are a designer who wants a portfolio that:

  • Loads quickly because the pages are static and media is prepared ahead of time
  • Preserves an intentional design through governed templates and runtime hooks
  • Catches malformed content, missing media, and inconsistent output before release
  • Keeps authored copy, logical media identity, generated output, and production history in explicit places

Then maybe the answer is not picking the best tool. Maybe it is building the one you actually need.

The system does not eliminate maintenance or bugs. It makes them visible, bounded, and testable. The report and the feature travel together, and the public site changes only after both have been verified.

By René Romero