- Astro 48.4%
- JavaScript 25.8%
- CSS 8.6%
- TypeScript 7.7%
- Shell 3.6%
- Other 5.9%
| deploy/bare-metal | ||
| public | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .woodpecker.yaml | ||
| astro.config.mjs | ||
| compose.yaml | ||
| DEPLOY.md | ||
| Dockerfile | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
pbminimal
A minimal, dark-first personal site for Prashanth Bairu — Astro, no framework, one page: hero → timeline → about → skills → contact.
Built in the spirit of a clean engineering portfolio, but the markup, CSS and the animated P mark are original, and every word of content is Prashanth's own.
Run
npm install
npm run dev # http://localhost:4321
npm run build # static output in dist/
npm run preview
npm run security # gate a deploy: asserts zero off-origin requests, live CSP
npm run csp # regenerate the CSP hashes after changing an inline script
DEPLOY.md is the runbook: container stack in /opt/stacks/pbdev,
Woodpecker pipeline, Forgejo registry, and the redeploy loop.
SECURITY.md covers the security posture and the limits of
"nothing traceable". Read both before the first deploy — in particular the note
that CSP script hashes go stale whenever an inline script changes.
Output is fully static and ships as a container: a Node stage builds it, a Caddy
stage serves it, and the runtime image holds no Node, no npm, and no source.
make run builds and runs it locally on :8080.
Where things live
| Path | What |
|---|---|
src/data/profile.ts |
All content. Name, links, timeline entries, about paragraphs, skill groups. Edit here first. |
src/styles/global.css |
Design tokens (dark + light), layout, shared chip/section styles. |
src/layouts/Base.astro |
<head>, meta/OG tags, pre-paint theme script, reveal + scroll-spy observers. |
src/components/Logo.astro |
The animated P mark. |
src/components/Nav.astro |
Sticky nav, scroll-spy underline, theme toggle, reading-progress bar. |
src/components/Hero.astro |
Name, lede, facts row, large P. |
src/components/Timeline.astro |
Career as a vertical spine that fills as you scroll. |
src/components/About.astro |
Three paragraphs + a facts sidebar. |
src/components/Skills.astro |
Six separated cards, each with a blurb and tag chips. |
src/components/Contact.astro |
Email / LinkedIn / GitHub rows. |
public/favicon.svg |
Static version of the P mark. |
public/fonts/ |
Self-hosted Inter + JetBrains Mono (SIL OFL, licences included). |
Dockerfile |
Two-stage build; generates the runtime Caddyfile from the dist it bakes in. |
compose.yaml, .env.example |
The stack for /opt/stacks/pbdev. Real .env never leaves the server. |
.woodpecker.yaml |
CI: verify, publish to the registry, deploy over SSH. All values from secrets. |
Makefile |
make run / make check / make image for local work. |
deploy/bare-metal/ |
The non-container alternative: nginx vhost, Caddyfile, rsync script. |
DEPLOY.md, SECURITY.md |
Deploy runbook; security posture and its limits. |
scripts/csp.mjs |
Regenerates the CSP script hashes from the build. |
scripts/security-check.mjs |
Deploy gate: asserts zero off-origin requests. |
scripts/verify.mjs |
Overflow / console-error sweep at three viewports, both themes. |
The P mark
Logo.astro draws a geometric P from three stroked paths — stem, bowl, base.
Each has pathLength="1", so a single stroke-dashoffset: 1 → 0 keyframe draws
them in on load, staggered 50 / 250 / 500 ms.
On hover or keyboard focus of the enclosing link, the three parts translate along
their own axes into an exploded view and a pin dot appears in the counter. Add the
class pmark-burst to any ancestor to trigger the same state without a pointer.
Everything is disabled under prefers-reduced-motion.
Theming
Every colour is an RGB triple custom property in global.css, so :root and
:root[data-theme='light'] swap the whole palette in one place. An inline script
in Base.astro applies the stored choice before first paint (no flash); the site
follows the OS until the visitor clicks the toggle, then remembers.
Retune a colour by changing the triple, not the call sites. The accent is
--accent: 255 106 61 (dark) / 199 63 18 (light) — the light value is darkened
so it clears 4.5:1 on the light background.
Accessibility
- Skip link, visible focus rings on everything interactive.
- Scroll-spy sets
aria-currenton the active nav link. - Sections carry
scroll-margin-topso anchors don't land under the sticky nav. - All motion — draw-in, reveal, spine fill, explode — respects
prefers-reduced-motion. - Verified: no horizontal overflow at 1440 / 834 / 390, no page errors, both themes.
Notes
profile.githubis empty on purpose — set it and the GitHub row appears in Contact and nowhere else needs touching. Leave it empty and the row is omitted rather than linking somewhere dead.- Content is deliberately free of employer project detail, client names, and metrics.
Keep it that way when editing
profile.ts. - Timezone is deliberately absent. It appeared twice — a live IST clock in the hero and an
Asia/Kolkatarow in the About sidebar that only restated Location — and both were cut. - Skill groups carry no
01-06codes. Skills aren't a sequence the way the timeline is, so numbering them would encode nothing. The section eyebrows (01 / Timeline…04 / Contact) stay, because page sections genuinely are ordered. - External links open in a new tab.
Contact.astrosetstarget="_blank"andrel="me noopener noreferrer"in markup for anyhttp(s)href, so it works with JS off;mailto:and in-page anchors are deliberately excluded (amailto:withtarget="_blank"leaves a dead tab behind). Each one also carries a visually-hidden "(opens in a new tab)" for screen readers.Base.astrohas a small runtime pass that applies the same attributes to any off-origin link added later that forgot them — belt and braces, not the mechanism. - Fonts are self-hosted from
public/fonts/— latin-subset variable WOFF2, declared with@font-faceat the top ofglobal.css. The page makes no off-origin requests at all, which is asserted bynpm run security. Don't reintroduce a<link>to a font CDN; it would hand every visitor's IP to a third party on page load. astro.config.mjssetssite: 'https://me.prashanthbairu.dev'. It is baked into the build as the canonical link and the Open Graph URLs, so change it there and rebuild before deploying anywhere else.