A CV is a list of claims. An operating system is a thing you can use. That's the whole premise behind KM/OS, my portfolio: instead of a page that tells you I can build things, it's a desktop OS running in a browser tab: windows, a dock, a real virtual filesystem behind a Terminal app, a menu bar where every section of a normal portfolio is an application that actually works.
No framework, on purpose
Vanilla JavaScript in ES modules, bundled by esbuild into a single file. Hand-written CSS over one set of design tokens. Two dependencies total, and both only at build time.
For an interface built almost entirely from bespoke pieces (custom window chrome, custom drag/resize physics, a custom terminal emulator), a framework would have added a runtime, a build step, and a set of conventions in exchange for very little the project actually needed. A few decisions that came out of that:
- Windows are cloned from a
<template>and driven by state classes, so all the open/close/minimize motion lives in CSS, not JS. - Pointer Events handle every gesture (drag, resize, swipe), so behavior is identical across mouse, pen, and touch without separate code paths.
- One virtual filesystem is shared by the Terminal, a Finder-style file explorer, and a notes app. Edit a file in one, and it changes in the others.
- A service worker uses content-hashed bundle URLs (
app.js?b=<build-id>, regenerated every build). This came from a real bug: a stale cached bundle once ran against a freshly-deployed HTML document and silently broke the mobile UI, because the old JS was looking for DOM nodes the new shell no longer had. Giving the bundle's address a build id, not just its cache headers, means a stale cache has nothing to serve at a URL it's never seen.
What leaves the browser
Worth being explicit about, since it's a portfolio and not a toy: only two things call out to anything else. A question typed into the built-in AI assistant goes to the site's own server, then to a language-model provider. The API key never reaches the browser. The contact form posts to a forwarding service. Everything else is same-origin, enforced by a real Content-Security-Policy HTTP header (not a meta tag: meta tags can't express frame-ancestors), which is also why there's no analytics script and no font CDN.
Two inline <script> tags that do need to run are pinned by SHA-256 hash instead of unsafe-inline, and there's a test that recomputes both hashes from the HTML source and fails the build loudly if either script's content ever changes without the hash being updated. That's a tripwire against the failure mode where an edited script just silently stops executing in production with zero build error.
There's a public chat room too, which is the one place text from strangers gets rendered back to other visitors. Messages are length-capped, stripped of control characters, rate-limited server-side, and always written into the DOM as text content, never as markup.
The part I didn't expect to spend time on: GEO
Once the site worked, the next problem was that nobody could find it: a name search for "Karim Masmoudi" is dominated by an unrelated, actively-covered public figure with years of accumulated backlinks. That's not a code problem. But there was a real code problem hiding next to it: the site was shipping noindex, nofollow on every page, which meant none of the SEO work mattered until that came off first.
After that, the more interesting part was optimizing for something that barely existed as a discipline a year ago: being cited by AI answer engines (ChatGPT, Perplexity, Google AI Overviews), not just ranked by classic search. Concretely:
- A
FAQPageJSON-LD block, but built from real, already-written content rather than freehand copy: I split the AI assistant's own one-paragraph bio into sentences and mapped them to its own suggested questions, so the "answers" are literally the same facts the assistant already gives a human visitor. -
llms.txtand/.well-known/ai.txt,/ai/summary.json,/ai/faq.json: the GEO-era equivalents ofrobots.txt, generated at build time from the same data files, not maintained by hand. - A JSON-LD
@graphwhere entities reference each other by@id(Person → CollegeOrUniversity → WebSite) instead of nesting everything inline, since that's what lets a search engine resolve "who is this person" as a connected set of facts instead of a string that happens to appear on a page. -
dateModifiedsourced fromgit log -1, not a hardcoded string that silently goes stale.
The one thing I deliberately didn't add: a SearchAction schema advertising site search to Google, because the site doesn't support URL-parameter search deep-linking. A rich-result annotation for a capability that doesn't work is worse than no annotation at all.
Top comments (0)