Kurtyoon's personal developer blog — blog.kurtyoon.me
Posts are plain Markdown files (no CMS, no database), rendered server-side with React Router 7.
- React 19 + React Router 7 (framework mode, SSR)
- TypeScript
- Tailwind CSS 4
- Vite
- fp-ts for the data/utility layer
- pnpm as the package manager
pnpm install
pnpm dev # http://localhost:5173| Script | Description |
|---|---|
pnpm dev |
Start the dev server with HMR |
pnpm build |
Production build → build/ |
pnpm start |
Serve the production build |
pnpm typecheck |
react-router typegen + tsc |
The app follows Feature-Sliced Design, with
imports generally flowing downward
(routes → pages → widgets → features → shared):
app/
├── routes/ # React Router file routes (loaders + meta)
├── pages/ # Page-level composition
├── widgets/ # Self-contained UI blocks (navbar, sidebar, markdown, …)
├── features/ # Domain logic + UI (post, archive)
└── shared/ # Config, constants, lib, locale, layout, post content
Path alias: ~/* → app/*.
-
Create
app/shared/contents/posts/<number>. <slug>.md. The leading number is parsed into the post id and sort order, so use the next number after the highest existing post. -
Add frontmatter:
--- title: My Post Title published: 2025-01-01 description: A short summary tags: [Tag1, Tag2] category: Category thumbnail: https://example.com/cover.png draft: false ---
Set
draft: trueto hide a post from listings. -
Run
pnpm devto preview, thenpnpm typecheckbefore committing.
Site-wide settings live in app/shared/config/blog.config.ts. A few values come
from VITE_* environment variables:
| Variable | Used for |
|---|---|
VITE_DEVLOG_AVATAR_URL |
Profile avatar |
VITE_DEVLOG_BANNERS |
Comma-separated fallback banners |
VITE_UTTERANCE_REPO |
utterances comments repo |
These are read through
import.meta.envand are inlined at build time. They must be present whenpnpm buildruns (or duringdocker build); injecting them only at runtime has no effect.
docker build -t devlog .
docker run -p 3000:3000 devlogThe Dockerfile runs pnpm build and copies the Markdown posts directory into
the runtime image, so the container serves all content without external files.
Because VITE_* values are inlined during the build, provide them via a .env
file in the project root before building — the build stage copies the project
into the image, so import.meta.env picks them up. Values passed only to
docker run (or exported in the shell without a .env) have no effect, since
the Dockerfile declares no matching ARG/ENV.
The image can be deployed to any Docker-capable platform (Cloud Run, Fly.io, Railway, ECS, …).
The built-in server is production-ready, but it needs more than just build/.
At runtime it requires:
package.jsonand the production dependencies (react-router-serve) — install withpnpm install --prod;- the
app/shared/contents/postsdirectory — posts are read at runtime relative to the working directory, not bundled intobuild/.
The simplest setup is to deploy the checked-out repository, then run
pnpm install --prod, pnpm build, and pnpm start.
MIT © kurtyoon