Live at https://kunal26das.github.io/resume/
One hand-written source, src/resume.html, and one published file, index.html. Every other
rendition of this document — shorter, re-laid-out, tailored to a role, as a PDF, a Word file,
plain text, Markdown or JSON — is built in your browser at the moment you ask for it. Fonts
are inlined as base64 woff2, so the page renders identically offline, from file://, or
behind any host, with nothing to break when a CDN changes.
/ ships the whole annotated document — every bullet carrying the tags that decide when it
survives — plus a 59 KB engine that does in the browser exactly what the build used to do on
my machine: drop the subtrees a shorter version does not admit, join the bullets that merge
into their neighbour, re-count the sentences that count themselves. About 340 KB, no network calls,
no framework.
The opening view is Full, the detailed career record. Two-page and One-page provide shorter copies. The datasheet layout leads with platform ownership and three concrete proof points; the shorter copies use a concise skills summary and describe the DOOM port rather than reducing it to a project name and link.
The controls live in a panel down the left edge, behind a chip in the corner on a narrow screen. It sits outside the resume — appended to the page, never mounted inside the document it filters — which is why nothing it does can turn up in a file you save, and why the search field keeps your cursor while the document rebuilds underneath it. Every control writes itself into the query string, so whatever view you arrive at is a link you can send.
Jump to at the top of the panel is the document's own table of contents, built from whatever survived the current filter — cut to the one-page length and it drops from eight links to five, because there are five sections left. Whichever section you are reading is marked as you scroll.
| Control | Param | Try it |
|---|---|---|
| Length — full, two-page, one-page | len |
?len=one |
| Layout — datasheet, column, plain | lay |
?lay=plain |
| Lead with — platform, product, android, rn | lead |
?lead=android |
| Only show — 14 topic tags | only |
?only=kmp,ios |
| Companies — hide any of the five | hide |
?hide=none |
| Search — highlights as it filters | q |
?q=gradle |
| Contact — masked or shown on screen | contact |
?contact=show |
| Theme — auto, light, dark, paper, contrast, slate, terminal | theme |
?theme=terminal |
They compose:
?len=short&lead=android&lay=plain
is the two-page Android copy with no colour in it.
Lead and only are different tools. Lead with hides nothing — it shifts a tagged bullet one step up or down the priority order, so at a shorter length the work you care about survives a cut it would otherwise lose, and the spine of the document stays put. Only show is the blunt instrument: it filters, and the counts in the panel tell you how much is left.
Wish starts hidden — it was a contract engagement and most readers do not need it — so the
page you land on shows four companies. ?hide=none brings all five back, and so does the
chip. Hiding a company hides it in your copy of the page, not in the page as served: the
markup is still there. If it needs to be gone, save the file rather than sending the link.
With JavaScript off, the page is the full resume. The engine only ever removes.
Save as PDF at the foot of the panel — or ⌘P — renders whatever is on screen through the
same @media print stylesheet the build asserts page counts against. It is named after the
view it came from, kunal-das-resume-short-android-no-wish.pdf, and it carries the full
contact details whatever the screen is showing. A PDF is what people ask for, so it is the
only button.
The page can also write itself out as a self-contained HTML file, as Word, as plain text, as
Markdown and as JSON Resume — same code, same view, no library and
no network, window.__versions.download("docx") from the console. Those generators are a port
of the Python that used to run at build time and commit these files into the repository, and
were checked byte-for-byte against it — text, Markdown, JSON, and all five parts of the Word
file — before the Python was deleted. They stay because the build still tests them on every
run; they are not in the panel because nobody was going to press them.
There is no PDF here, no .docx, no .txt, no resume.json, and no second HTML page.
If you have a link to one, it no longer resolves: /short/, /column/, /column-short/,
/plain/, /plain-short/, /for-platform/, /for-product/, kunal-das-resume.pdf and its
.docx, .txt and .md siblings are all gone. The page makes any of them.
The build still renders nine PDFs locally on every run — full, two-page, ATS-plain, one-page,
platform, product, android, and dark versions of the first two — and asserts the page count
and the paper size of each. They are now printed from the published page itself
(index.html?len=short&lead=android), so those assertions cover the engine and the print
stylesheet a reader actually gets, which is the reason to render them at all: it is the only
thing that catches a content edit quietly spilling a two-page version onto a third page.
They are simply not committed. Three and a half megabytes of binary per revision is not worth
carrying to publish something the reader's own browser produces better.
The email address and the phone number are not in the served HTML as text. Each sits base64 in
a data-real attribute and comes back when you click it, when you print, or with
?contact=show; every file you save carries them in full regardless. Revealing them is the
engine's job, so with JavaScript off the mask stays on. It is obfuscation and not a lock — it
costs an address-harvesting crawler everything and a human one click.
Kept, but not published. archive/ holds nine versions, newest first, each one a rendered
page next to the source it was built from; ./src/snapshot.sh "what changed" adds the current
build to it. It is gitignored and lives only on my machine — a record of what this document
used to claim is not something a reader needs to diff against what it claims now.
python3 src/build.py # about 25 seconds
# any one-off copy, into gitignored out/ — the same renditions without a browser
python3 src/render.py --len short --lead android --hide wish --theme dark --pdf
python3 src/selftest.py # 32 checks, including backup rotation
# optional focused export regression checks (requires Node.js)
node src/formats-selftest.jsNo dependencies beyond Python 3, and a local Chrome or Chromium — without one the build prints
SKIPPED and still succeeds. Composing the page takes well under a second; the rest of that
time is ten headless Chrome runs — nine PDFs, and one that drives the download generators over
five different views.
Edit src/resume.html, rebuild, commit index.html.
main is the whole repository, and it keeps exactly one commit. The resume is a living
document, the generated files are megabytes per revision, and there is no history worth
preserving in git when /archive/ is the readable version of it — so every change is
squashed into the single root commit and force-pushed over the remote. GitHub Pages serves
the root as-is: no CI, no build step, no second branch.
The fonts account for 147 KB; the self-contained page is about 340 KB, and that page is the site.
None of these is a second document. Every one of them is the same source with subtrees dropped and neighbouring bullets joined, chosen by tier attributes that never reach anything you save. Leading with different work shifts a tagged bullet's tier by one, so a tailored copy can open on different evidence without a word being rewritten. Nothing is ever written twice, so no figure can drift between any version of this.
The build enforces that rather than trusting it. It refuses a variant containing a number that
is not in the source; it refuses to separate a figure from the clause that qualifies it — drop
"green on the branch" and an in-progress upgrade ladder starts reading as shipped work; and it
refuses to let a sentence that counts the document, "the eight I would lead with", go stale
when filtering changes the count. The renditions are made in the browser now, so the build
drives them there: a headless run generates all four downloads across five different views,
then checks the text and Markdown for invented figures, the JSON for a section gone silently
empty, and the Word file for its five parts. Portfolio and app links must survive in text,
Markdown and Word, with clickable links in the latter two. Ten browser checks cover search,
Reset, shared layouts, topic highlights, one-page role coverage and keyboard focus when opening or closing the panel.
src/selftest.py exercises the content rules and checks backup rotation in folders with spaces.
Every figure on the resume is measured from the underlying repository's git history. Nothing is estimated, rounded up, or extrapolated.
Themed through CSS custom properties declared once for light on a bare :root, again under
prefers-color-scheme: dark guarded against an explicit light override, and again for each
named palette — dark, paper, contrast, slate, terminal — so the page is legible whichever way
a browser resolves the question, and the panel's own choice wins over all of them. Every
contrast pair was computed rather than eyeballed. Leave the theme on auto and the page prints
light, which is what a print stylesheet is for; pick one and it prints as you picked it.
Amber is reserved for measured quantities and nothing else, which is what makes the numbers
scannable at a glance; ordinary emphasis is bold. The plain layout has no colour at all,
which keeps the reservation by having nothing to reserve. A layout file may not touch :root
and the build refuses one that does: a layout restyles the document, never the controls
around it.
Typefaces: Archivo for display,
IBM Plex Sans for body and IBM Plex Mono for labels and data —
147 KB of woff2, inlined. Both are SIL OFL 1.1 and the licences ship in src/fonts/. The
plain layout uses system fonts instead, and the ATS-plain PDF is rendered from it.
src/resume.html THE SOURCE edit this
src/layout/*.css one file per layout datasheet, column, plain
src/versions.js the live filter engine
src/formats.js text, Markdown, JSON and Word, written in the browser
src/build.py the build
src/selftest.py tests for the content filter and the content rules
src/browser-selftest.js browser regression checks, run by the build
src/formats-selftest.js optional focused export checks, run with Node.js
src/render.py renders one arbitrary version into out/
src/archive.py snapshots a build into archive/
src/snapshot.sh archive.py + the commands to publish the resume
src/fonts/ woff2 originals + OFL licences
.nojekyll tells Pages to serve the files as-is
index.html the published page — generated, do not edit
kunal26das@gmail.com · linkedin.com/in/kunal26das · github.com/kunal26das