11 KiB
👾 dvorakmortonxd.dev
The source code for my personal website, dvorakmortonxd.dev.
This is not a normal portfolio page. It ships two fully interactive shells and picks which one to show based on the saved theme and the device, all from the same content:
skeuo(default) — a KDE Plasma / Windows-style desktop simulation: draggable snap-to-grid icons, resizable/floating windows, a Start menu, taskbar and live clock. On phones it becomes an iOS-style home screen with a widget grid, dock, drag-to-reorder and an iOS launch animation.terminal— a Gruvbox TUI version of the site: TUI panels, an animated ASCII train background, a clickable easter-egg logo, and a working in-browser terminal (Alacritty) with commands likehelp,ls,cat,neofetch,emerge,sshand more.
Both shells surface the same personal content: socials, self-hosted services with live up/down health checks, a hardware collection, coding projects, a GitDXD contribution heatmap, a gym SBD panel and links to friends' sites.
✨ Features
- Two themes / shells — KDE-style desktop and Gruvbox terminal, toggled from either UI
and persisted in
localStorage(applied pre-paint to avoid a flash). - Phone vs. desktop detection — a user-agent sniff on
<html data-device>decides whether the desktop or the mobile home screen is shown. - Draggable desktop icons — free placement that snaps to a measured grid, one icon per cell, with a follow-the-cursor drag ghost. Icons shrink on cramped viewports so they never collide.
- Multi-window manager — draggable, resizable, minimizable, maximizable, focus/z-order and animated open/close (with reduced-motion support). The taskbar shrinks its window buttons as more apps open and scrolls when they still don't fit, so every open app stays reachable. Windows are clamped to the viewport so they never open off-screen on a small monitor and can't be dragged off any edge.
- Working terminal app — command input with history (
↑/↓,Ctrl+Lto clear) and a hiddenssheaster egg. - Live service status — each self-hosted service is health-checked through the site's own API; Minecraft is checked through mcstatus.io.
- Embedded live services — opening a frameable self-hosted service (Llama.cpp/Open WebUI,
Jellyfin, Navidrome, GitDXD) from the desktop shows the real site inside the window (an
iframe). A service must not send a restrictive
X-Frame-Options: Navidrome needsND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALLand Forgejo needs[cors] X_FRAME_OPTIONS = unset. A spinner covers the frame while the site boots. Discord sendsDENYand keeps a link card instead. - Minecraft-style SMP window — the SMP shortcut opens a join menu modelled on Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled from mcstatus.io through the site's own ping API.
- Live GitDXD stats — contribution heatmap + latest commit activity proxied server-side from the self-hosted Forgejo instance.
- Extras — a playable
Revival Inc.game embedded at/revival-inc,llms.txt,robots.txtand asitemap.xml.
🛠️ Tech Stack
- Framework: Next.js 16 (App Router, Turbopack)
- Language: TypeScript, React 19 (client components everywhere interactivity lives)
- Styling: hand-written CSS —
src/app/page.css(terminal theme),src/app/skeuo.css(desktop + mobile),src/app/globals.css(base/reset). Tailwind v4 is imported for the base layer but the UI is custom CSS. - Fonts: Monocraft (self-hosted in
public/, used by the terminal theme; the desktop theme uses the system sans stack) - Images:
next/image - Version:
2.1.0(seepackage.json)
🚀 Running Locally
Requires Node.js >= 20.9.0 (Next.js 16).
git clone https://git.dvorakmortonxd.dev/dvorakmortonxd/dvorakmortonxd.dev
cd dvorakmortonxd.dev
npm install
npm run dev
Then open http://localhost:3000.
One-shot setup on Debian/Ubuntu
install-dependencies-deb.sh installs the system packages, Node.js LTS (>= 20.9.0) and npm,
then runs npm install and npm run build:
./install-dependencies-deb.sh # system deps + npm install + build
./install-dependencies-deb.sh --no-build # system deps + npm install only
./install-dependencies-deb.sh --no-npm # system packages only
Scripts
npm run dev # node scripts/dev-server.mjs (dev + live updates)
npm run dev:next # next dev --turbopack (raw dev server, unprotected)
npm run build # next build --turbopack
npm run start # next start (production)
npm run lint # eslint
Handy checks before shipping: npx tsc --noEmit, npx eslint <files>, npm run build.
Testing from a phone on your LAN
next.config.ts sets allowedDevOrigins so the dev server can be reached from other devices
on the local network and from the beta.dvorakmortonxd.dev tunnel. Without it Next.js
rejects the dev asset requests and the HMR WebSocket, which leaves the page un-hydrated
(stacked icons, dead buttons).
A plain next dev server is meant for localhost and exposes unauthenticated debug endpoints
(/__nextjs* — open-a-file-in-editor, stack frames, source maps, debugger attach, restart)
that you don't want on a tunnel. npm run dev therefore runs scripts/dev-server.mjs, a
custom server that keeps the app and live updates (HMR / Fast Refresh) but returns 404 for
those endpoints and *.map for any request that isn't a direct localhost connection.
Localhost keeps the full dev experience; the LAN and tunnel get a plain, live-reloading
site. To force the raw server instead, use npm run dev:next (don't point a tunnel at it).
Running it as a service (survives reboot)
On this host the dev server runs as a system systemd unit, so it starts at boot on
its own — no login or linger needed. The unit lives in the repo at
deploy/dvorakmortonxd-dev.system.service:
# installs /etc/systemd/system/dvorakmortonxd-dev.service and enables it
sudo bash deploy/install-system-service.sh # or: doas bash deploy/install-system-service.sh
Manage it with systemctl status|restart|stop dvorakmortonxd-dev and follow logs with
journalctl -u dvorakmortonxd-dev -f. It shares .next/dev/lock with a manual
npm run dev, so stop the service before starting one by hand.
No root? Use the per-user unit instead
deploy/dvorakmortonxd-dev.service is an equivalent
user unit you can install without root:
mkdir -p ~/.config/systemd/user
cp deploy/dvorakmortonxd-dev.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now dvorakmortonxd-dev.service
loginctl enable-linger "$USER" # once, so it starts at boot
Drive it with systemctl --user … and journalctl --user -u …. Run only one of the two —
they contend for :3000 and .next/dev/lock.
📡 API Routes
| Route | Method | What it does |
|---|---|---|
/api/heatmap |
GET |
Proxies the user's contribution heatmap from git.dvorakmortonxd.dev. |
/api/activities |
GET |
Fetches the GitDXD activity feed and keeps only the user's own events. |
/api/ping?url=… |
GET |
Health-checks an allow-listed service (the site's own *.dvorakmortonxd.dev hosts and the friends' sites). Handles minecraft://host:port via mcstatus.io; treats any non-5xx response as "up". Other hosts and schemes are rejected and redirects are never followed, so it can't be used to probe the LAN (SSRF guard). |
📁 Project Structure
src/app/
page.tsx → renders <ThemeRoot/>, imports the two theme stylesheets
layout.tsx → html shell + metadata, injects ThemeScript
globals.css → base/reset styles + Tailwind import
page.css → terminal theme styles
skeuo.css → desktop + mobile home styles
data.ts → socials, codingProjects, techProjects, friends, wallpapers
revival-inc/page.tsx → embeds the bundled game at /public/revival-inc/index.html
api/
heatmap/route.ts → contribution heatmap proxy
activities/route.ts → GitDXD activity feed proxy
ping/route.ts → service health check (HTTP + Minecraft)
components/
ThemeRoot.tsx → picks which shell(s) to mount
ThemeScript.tsx → pre-paint script: theme + device detection
useTheme.ts → theme state via useSyncExternalStore
TerminalLayout.tsx → the Gruvbox terminal shell
WindowsDesktop.tsx → desktop shell: icons, windows, taskbar, Start menu
MobileHome.tsx → phone shell: iOS home screen + app view
WinWindow.tsx → one draggable/resizable window
apps.tsx → APPS registry, window contents, in-app terminal
useDesktopIcons.ts → desktop icon grid + drag/drop logic
useDragOrder.ts → list reordering (mobile grid/dock)
ServiceButton.tsx → service link + live status dot
ContributionHeatmap.tsx → heatmap + recent commits widget
Clock.tsx → live clock
JokerLogo.tsx → logo with click easter egg
SlBackground.tsx → animated ASCII train background
scripts/
dev-server.mjs → custom dev server: live updates without the dev-only debug surface
deploy/
dvorakmortonxd-dev.system.service → systemd unit (system-wide) so the dev server starts at boot
dvorakmortonxd-dev.service → equivalent per-user unit (no root; needs linger)
install-system-service.sh → installs the system unit (run with sudo/doas)
public/ → fonts, wallpapers, icons, friend pages, /revival-inc game, llms.txt, robots.txt, sitemap.xml
🌍 Deploying?
In order to deploy the website you'd want to set up a reverse proxy and something that runs it as a service. Me personally, I use cloudflared and pm2. You could also use something like Vercel, but I prefer self-hosting. Also, if you want to deploy it, you gotta change it enough so that you don't directly copy my website, and also follow the GPL3.0-only license, meaning you gotta make your code open source too.
Detailed step-by-step production instructions for PM2 + npm on Debian and Alpine Linux
(including the Cloudflare Tunnel / Nginx reverse-proxy setup) are in
DEPLOYMENT.md.
📬 Contact
To see my projects or get in touch, visit the live site or email me at dvorak@dvorakmortonxd.dev.
📄 License
GPL-3.0-only — see LICENSE.