Open project
ohfork.dev
The site you're currently on.
Started · Updated
- You're looking at it.This website

What is it?
My personal playground and portfolio: the projects, the experiments and the jokes, in English and Thai. Polished, editorial design; dry developer humour in the margins.
Why did I build it?
I didn't want a formal portfolio that just lines up screenshots and tech stacks. I wanted a site where people remember both the work and the person who built it.
Can I try it?
You're already using it.
The long version
The briefLink to this section
Look like a design studio, talk like a developer with a sense of humour. The concept document puts a number on it: 80% polished, professional design. 20% unexpected developer humour. The jokes are a bonus, never something you need to get to use the site.
Why I built itLink to this section
I didn't want a formal portfolio that just lines up screenshots and tech stacks. I wanted a site where people remember both the work and the person who built it.
Decisions that shaped itLink to this section
- Self-hosted, no inbound ports. The site is a Docker container on a home server. It reaches the internet through a Cloudflare Tunnel, an outbound-only connection, so the server opens no ports. Merging to
maindeploys it. - Static by default. Next.js (App Router) prerenders every page, including each project page in both languages. Client JavaScript is for small interactive islands only.
- English and Thai, properly. English lives at
/and Thai at/th. Thai copy is written the way Thai developers actually talk, not translated word for word, and a Thai browser is never redirected. Brand puns such asOh fork.andWORKS ON MY MACHINEstay in English, because translating a pun kills it. - Honesty is enforced by the build. Every project has a canonical status (shipped, in progress, experiment, archived) and an optional joke label. The joke may not contradict the truth, and the content validation fails the build if it does:
export const FLAVOR_STATUSES = {
"somehow-works": ["shipped", "wip", "experiment"],
"works-on-my-machine": ["shipped", "wip"],
"wip-forever": ["wip"],
} as const satisfies Record<StatusFlavor, readonly ProjectStatus[]>;
- Private stays private. Linking a repository on the private list also fails the build. Anything still missing is marked
TODO(owner)and shows up on the page with a dashed outline and aPLACEHOLDERtag, so it can't ship unnoticed. - Jokes you can switch off. On the roadmap: a Recruiter Mode that turns every joke off with one switch, and Easter eggs that are opt-in, load lazily, respect reduced motion and never get in the way of finding a project.
Things that were harder (or funnier) than expectedLink to this section
- The concept, the roadmap and the instructions for the coding agents came before any website. The first commit has no code in it.
- With Next's Cache Components, a "not found" from a page rendered on demand turned into an empty error page. Unknown project addresses are now sent to the 404 page before rendering starts, so
/projects/typogets the full, helpful 404. - Thai has vowels and tone marks stacked above and below the line, so every line height grows on Thai pages to keep them from clipping.
- The footer only says
on a Friday. Pray for me.when the build really happened on a Friday, Bangkok time. - This page is in its own showcase, and its demo button says "You're looking at it." instead of linking to itself.