Static, dependency-free portfolio site. Terminal/CRT aesthetic, an interactive command interpreter, and — the important part — all copy lives in one JSON file you can edit from your phone.
No build step. No framework. No backend. No AWS.
Every word on this site comes from content.json. The page
fetches it in the browser at runtime, not at build time:
fetch(`https://github.com/ghraw/ram0verflow/portfolio/main/content.json?t=${Date.now()}`)Because the copy is never baked into the deployed bundle, editing
content.json on github.com updates the live site on the next page load —
no redeploy, no CI run, no waiting on Vercel.
- Open
content.jsonon github.com (web editor, or the GitHub mobile app). - Edit. Commit directly to
main. - Reload the site. Done — usually within seconds.
That is the entire CMS. There is no database and no admin panel, on purpose.
github.com/ghraw sends caching headers. Without the cache-busting
timestamp you would commit a change, reload, see the old copy, and reasonably
conclude the commit failed. Do not remove it.
The loader falls back to the content.json shipped alongside the bundle, so
the site still renders if GitHub raw is unreachable or you're offline. If
both fail, a visible error state appears with direct contact details — the
page never renders blank.
Moving the repo? Change the two constants at the top of
src/content-loader.js:
export const CONTENT_REPO = 'ram0verflow/portfolio';
export const CONTENT_BRANCH = 'main';There is no build. Serve the directory over HTTP — file:// will not work,
because ES modules and fetch both require a real origin.
python3 -m http.server 4173Then open http://localhost:4173. Locally the remote fetch 404s until the repo
exists and falls back to your local content.json, which is what you want while
editing.
Any static host works. Vercel and Cloudflare Pages are both free for this.
- Framework preset: None / Other
- Build command: (none)
- Output directory:
/(repo root)
TLS is automatic and free on every one of these hosts, including on a custom domain — you never buy a certificate. They provision and renew Let's Encrypt certs for you.
The shell is the site — it's what loads by default and where the personality
lives. The page view is the same content laid out conventionally, one click
away via the toggle top-right (or the gui command, or ?view=doc).
The page view is not an apology for the terminal. It exists because:
- someone skimming on a phone between meetings will not type commands, and
- crawlers and screen readers get real semantic markup.
Both views render from the same content object, so they physically cannot
disagree. The choice is remembered in localStorage.
On SEO, honestly: the real constraint is not the terminal, it's the runtime
fetch. Because content.json is fetched in the browser, the copy is not in the
HTML source — Googlebot executes JS and will index it, but it's weaker than
static HTML. That is the price of editing the site from your phone without a
redeploy, and it is worth it for a personal site. What offsets it: static
<title>/<meta description>, JSON-LD Person structured data in the source,
and a <noscript> block with the essentials.
index.html page shell + critical CSS + JSON-LD
content.json ALL copy — the CMS
src/
main.js entry point; fetch, then wire shell + page to the content
content-loader.js remote fetch, local fallback, validation
terminal.js the shell: parser, pipelines, completion, history, boot
commands.js ~45 commands with man pages
vfs.js virtual filesystem, generated from content.json
view.js tty/doc toggle, themes, matrix toggle
sections.js renders the page view
banner.js generates the ASCII banner from identity.handle
contact.js modal + Web3Forms submission
effects.js Matrix rain (throttled, pauses when tab hidden)
sound.js key click sounds + persisted mute
styles.css full stylesheet
assets/ keyboard.mp3, button.mp3, glitch.mp3
- The filesystem is generated from the content.
ls,cat,tree,findandgrepwalk a tree built fromcontent.json, so the shell can never drift from the page view — both read the same source. - The ASCII banner is generated, not hardcoded. The old site had
ram0verflowbaked into a static art string, which silently madeidentity.handleun-editable.src/banner.jsrenders it from the handle. - Output colour uses a control-character sentinel, not markup. Commands emit
�a/�g/�dprefixes; the renderer turns them into spans viatextContent. No amount of content incontent.jsoncan inject HTML. Sentinels are stripped when output is piped. - Pipes are real. Commands return strings and receive the previous stage's
output as
stdin, which is what makescat about.txt | grep -i bitcoin | wc -lwork rather than being faked per-command.
Submits to Web3Forms — one POST, no backend, delivers
to email. The access key lives in content.json under links.web3formsKey and
is public by design: it identifies the destination inbox, it does not
authorise anything.
To point the form at a different inbox, get a new key at web3forms.com and change that one field.
Fallback if Web3Forms ever goes away: Formspree has the
same integration shape (free tier, 50 submissions/month) — swap the endpoint
and the payload key name in src/contact.js.
| group | commands |
|---|---|
| portfolio | about experience projects skills achievements contact open gui |
| files | ls cd pwd cat tree find stat file du |
| text | grep wc head tail sort uniq rev echo |
| system | whoami id uname hostname env which uptime df ps date history |
| fun | neofetch figlet banner cowsay fortune sl matrix theme sudo vim |
| shell | help man mute clear exit |
Flags work (ls -la, grep -in, head -n 3, sort -r). So do pipes,
quoting, ~, .., cd -, and $VAR expansion in echo.
Keys: Tab completes commands and paths (longest common prefix, then a
menu), ↑/↓ walks history (persisted to localStorage), Ctrl-L clears,
Ctrl-C abandons the line, Ctrl-U wipes it.
man <command> gives a real manual page for any of them.
Writes are real and ephemeral. touch, mkdir -p, rm -r, cp, mv,
tee, sed and > / >> all work against an IndexedDB-backed scratch layer
that survives a reload and is discarded when the tab closes. Run fs to see
the model, fs reset to clear it.
apt install fetches real code. apt install cowsay dynamic-imports the
actual npm package from a CDN and registers a command backed by it — not by a
reimplementation. apt search lists the catalogue.
lsusb is real WebUSB, not a mock. It lists devices you have granted this
page; usb attach opens the browser's own chooser. Chromium only.
For exactly how far each command goes toward real UNIX, what it's backed by, and which public project to swap in to go further, see SHELL.md.
date fetches the live Bitcoin block height from
mempool.space — a free, unauthenticated public API.
theme amber|green|ice|mono recolours the phosphor; the choice persists.
On phones a row of tappable command chips sits under the terminal, because
typing neofetch on glass is a chore.
The Matrix rain is the only continuous work on the page. It runs on
requestAnimationFrame with a ~20fps budget rather than the old
setInterval(…, 50), stops entirely when the tab is hidden, and uses larger
glyphs (fewer columns) on small screens. It's disabled outright under
prefers-reduced-motion, along with every other animation.