Personal site — a macOS-style desktop built with React + Vite + Tailwind. Lives at https://daviddong.me.
- React 19 + TypeScript + Vite
- Tailwind CSS v4 (macOS/Aqua design tokens mapped into
@theme) - Backend: Cloudflare Worker (Hono) serving both the static SPA and
/api/*routes, via@cloudflare/vite-plugin - Project images served from Cloudflare R2 at
https://assets.daviddong.me
src/
├── data/ # content: projects.ts, profile.ts
├── features/
│ ├── desktop/ # desktop shell: MenuBar, Dock, icons, window manager, wallpaper
│ └── windows/ # window content components (About, Projects, Resume, ...)
├── components/macos # macOS UI component library
└── worker/ # Cloudflare Worker (Hono): serves SPA + /api/* routes
bun install
bun dev # vite dev — runs the Worker + React together (HMR),
# with local access to bindings (R2/D1/KV)
bun run build # production build (vite build, no type-check)
bun run typecheck # optional: full tsc -b type-check (app + node + worker)
bun run cf-typegen # regenerate worker-configuration.d.ts after editing bindings
buildintentionally runs onlyvite build(esbuild transpile, no type checking) so the deploy is not blocked by unused shadcncomponents/ui/files that pull in optional deps. Runbun run typecheckfor types.The
/api/*routes are handled insrc/worker/index.ts(Hono). Everything else falls through to the static SPA via theASSETSbinding.
Deployed as a Cloudflare Worker (name daviddong). The custom domain
daviddong.me is bound to the Worker via a route in wrangler.jsonc.
Git auto-deploy is enabled via Cloudflare Workers Builds. Pushing to
master builds and deploys to daviddong.me automatically.
Cloudflare build settings (dashboard → daviddong Worker → Settings → Builds):
- Production branch:
master - Build command:
bun run build - Deploy command:
npx wrangler deploy - Version command:
npx wrangler versions upload(non-production branches upload a preview version without going live)
bun run build (via @cloudflare/vite-plugin) outputs:
dist/client/— the static SPA (bound to the Worker asASSETS)dist/daviddong/— the bundled Worker + generatedwrangler.json
You can still deploy directly from your machine at any time:
bun run deploy # = vite build && wrangler deployProject images are not bundled — they live in the R2 bucket
daviddong-assets, served via the custom domain assets.daviddong.me.
In src/data/projects.ts, images are referenced with the asset() helper:
asset("projects/my-app/hero.png")
// -> https://assets.daviddong.me/projects/my-app/hero.pngUpload with the helper script, then reference the key in projects.ts:
# Upload ./newpics/* under the "projects/my-app" prefix
scripts/upload-assets.sh ./newpics projects/my-appOr upload a single file directly:
bunx wrangler r2 object put daviddong-assets/projects/my-app/hero.png \
--file ./hero.png --content-type image/png --remoteOverride the asset base URL at build time with VITE_ASSETS_BASE if needed.