Jason Von Flue
A single-page marketing site for private MMA and grappling training. No database: the coach edits his own copy in a password-protected panel that saves by committing a JSON file back to GitHub, which triggers a rebuild. Every content change is an ordinary commit, and undo is `git revert`.
Live at jasonvonflue.com, deployed from GitHub, with the coach editing his own content through /admin.

- Role
- Art direction and design system · Frontend development · Editing panel and content model · Domain, DNS and deployment
- Year
- 2026
- Industry
- Sport · Coaching
- Technologies
- Next.js 15 · React 19 · TypeScript · Tailwind CSS v4 · GitHub Contents API · Vercel Blob · Vercel
The challenge
A coach with a real name in the sport needed a site that books private sessions — and that he can keep current himself, without a monthly CMS bill or a database to administer.
Almost everything a site like this normally leans on was unavailable or unverified: no confirmed pricing, no confirmed schedule, no photography, no verified professional record.
The domain already existed and was forwarding to a Facebook page, with mail running on the same zone. Getting it wrong would take down the coach's email.
The approach
Ember over near-black. One warm accent against a page that is otherwise ink and steel, so the only orange things on the page are the things you are meant to press.
Oswald for display: a condensed grotesque carries weight at large sizes without the page turning into a poster, and it holds long section titles on one line.
The hardest decision was subtractive. Rather than fill the gaps with plausible-sounding claims, unverified content was left out — pricing reads “on request”, the professional record is absent, there are no testimonials. A site with three true facts outperforms one with twenty unverifiable ones the first time a visitor checks.
One page, seven sections, a sticky booking bar that appears only on phones.
The process
- 01
Copy as data
Every editable string was pulled into a single JSON file and wrapped in types, so the components import named, typed exports rather than reaching into a blob.
- 02
Decide what is not editable
Navigation hrefs are tied to section ids and icon keys map to inline SVG. Both were deliberately locked, because exposing them would let a content edit break the layout.
- 03
Then the panel
Section-by-section forms, each saving independently, built once the content model had stopped moving.
- 04
Cutover
The existing domain forwarding was removed and A/CNAME records repointed, leaving nameservers where they were.
The solution
A one-page site: hero, about, training options, the technique the coach is known for, a career timeline, reasons to train, and a booking form.
An admin panel covering tagline, contacts, the three training cards, career entries, the reasons list and the booking-form options.
Photo upload straight from the browser into blob storage, and video by embed rather than upload.


The design system
The tokens this site is written in. Every colour below is a value from its stylesheet, not an impression of it.
- Ink#0b0a09Page
- Carbon#15120fCards
- Graphite#262019Hairlines
- Steel#9a948cMuted text
- Ember#d9622bAccent
- OswaldDisplay and section titles
- InterBody and forms
Technical perspective
The decisions worth defending. Written to be readable by someone who will never open the repository.
Git as the CMS
The panel reads the content JSON from GitHub and saves it by committing the file back. Vercel is connected to the repository, so the commit rebuilds the site and the change is live in about a minute. The edit history is the git history, and reverting a bad change is `git revert` — no database, no backups to run, no admin UI for version control because git already is one.
Concurrent edits can't clobber each other
Sections save independently, and each save re-reads the newest JSON immediately before committing. Two quick saves merge instead of overwriting; an edit made somewhere else in the meantime surfaces as a 409 asking for a reload rather than silently winning.
Photos take the long way round for a reason
A serverless function body is capped at 4.5 MB and an unedited phone photo is usually larger. The browser downscales to an 1800 px longest edge, re-encodes to JPEG, then uploads straight to blob storage using a short-lived token that `/api/upload` mints only for a signed-in admin. Only the resulting URL is committed, so binaries never enter git history — where every replaced photo would live forever.
Video is embedded, never hosted
Serving video from blob storage means paying bandwidth on every view with no adaptive quality. Links are accepted in any form — `watch?v=`, `youtu.be`, `/shorts/`, `player.vimeo.com`, with or without a scheme — and rejected at save time if they cannot be turned into an embed, rather than silently later.
A player that costs nothing until it is used
The embed component renders a poster frame and only mounts the iframe once the visitor presses play. A bare embed pulls roughly a megabyte of player code on every page view. Playback uses youtube-nocookie and Vimeo's do-not-track flag, so neither service is contacted at all until someone actually watches.
DNS without collateral damage
The domain's A and CNAME records point at Vercel while the nameservers stay with the registrar. Moving nameservers would have taken the whole zone — including the MX records the coach's email depends on.
Outcome
- Databases to maintain
- 0
- From save to live
- ~1 min
- Unverified claims published
- 0
Live and in the coach's hands. The content model is deliberately larger than the site currently shows: pricing, schedule and social links are all editable fields sitting empty, waiting for confirmed information rather than filled with guesses.
What it still needs
Every project here ships with the list of what is still missing. Hiding placeholders would make the rest of this page harder to believe.
- Real photography — the hero and technique images are abstract placeholders.
- A demo video of the coach's signature technique.
- Pricing, location, schedule and social links, all of which are editable fields currently left empty on purpose.