Personal site for Aashish Jagini — engineer turned PM, still building the tools he uses. Built on Astro + Tailwind. Deployed to Cloudflare (Workers Static Assets).
Four routes: /, /experience, /skills, /writing (plus one page per post). Also /rss.xml and /llms.txt. The homepage Brain section and the Brain write-up carry the AI story. No chatbot, no AI fit-checker — both were cut on purpose.
| Framework | Astro 5 (static output) |
| Styling | Tailwind CSS 3 + custom design tokens in src/styles/global.css |
| Content | MDX in src/content/writing/ via Astro Content Collections |
| Fonts | Self-hosted via @fontsource-variable — Source Serif 4, Inter, JetBrains Mono |
| Hosting | Cloudflare Workers Static Assets (wrangler.toml, no Worker script) |
| Package manager | pnpm 9 (pinned in package.json), Node 20 |
pnpm install
pnpm dev # localhost:4321
pnpm build # astro check + astro build → dist/
pnpm preview # serve dist/ locally
pnpm check # astro check (TypeScript + content schema)
pnpm refresh:brain # pull fresh Brain numbers (see below)pnpm build fails on type errors, because it runs astro check first.
.
├── astro.config.mjs # integrations, site URL, trailingSlash: 'always'
├── tailwind.config.mjs # colors via CSS vars, fonts
├── wrangler.toml # Workers Static Assets config (serves dist/)
├── public/
│ ├── favicon.svg # AJ italic monogram
│ ├── og-default.png # 1200x630 social share image
│ ├── llms.txt # plain-text summary for AI crawlers
│ └── robots.txt # AI crawlers explicitly allowed
├── scripts/
│ └── refresh-brain.sh # pulls Brain stats from the home server → JSON
├── src/
│ ├── content/
│ │ ├── config.ts # writing collection schema (Zod)
│ │ └── writing/ # MDX posts
│ ├── components/ # Header, Footer, CommandPalette (⌘K, vanilla JS),
│ │ # BrainStatus, PostCard, ChapterCard, Placeholder
│ ├── data/
│ │ └── brain-status.json # Brain numbers, baked in at build time
│ ├── layouts/
│ │ ├── BaseLayout.astro # site shell — head, JSON-LD, theme, view transitions
│ │ └── PostLayout.astro # /writing/[slug] reading layout
│ ├── pages/ # index, experience, skills, writing/, rss.xml.js, 404
│ └── styles/global.css
└── .github/workflows/
├── ci.yml # typecheck + build
└── purge-cache.yml # purge Cloudflare cache after each deploy
Locked. Don't drift without an explicit ask.
| Token | Light | Dark |
|---|---|---|
paper (page bg) |
#FAF7F0 |
#16181C |
ink (body text) |
#16181C |
#F0EDE5 |
cream (section bg) |
#F0EDE5 |
#1F2127 |
line (hairlines) |
#E8E2D5 |
#2A2D33 |
muted (secondary text) |
#5A5247 (AA on cream) |
#A6ABB4 |
burnt (body accent) |
#B23B0A (AA on paper) |
#EA580C |
burnt-strong (display italic) |
#C2410C |
#F97316 |
Fonts:
- Source Serif 4 Variable — body, headlines (display).
- Inter Variable — UI chrome, navigation, buttons, labels.
- JetBrains Mono Variable — kickers, dates, status tables, mono pull-quotes.
Italic display headlines use <em> and pick up burnt-strong via .h-display em in global.css.
-
Create
src/content/writing/<slug>.mdx:--- title: "Your title here" description: "One-sentence description, 140-160 chars." pubDate: 2026-05-15 tags: ["product-management", "ai"] draft: false --- Body in MDX. Use `**bold**`, regular paragraphs, `code`, fenced code blocks, and any imported components if needed.
-
Frontmatter schema (Zod-validated at build time, see
src/content/config.ts):Field Type Required Notes titlestring ✓ descriptionstring ✓ pubDatedate ( YYYY-MM-DD)✓ Coerced to Date.updatedDatedate Shown on the post page if present. tagsstring[] Defaults to [].draftboolean Defaults to false.truehides from/writingindex but the post is still reachable at/writing/<slug>for preview.canonicalUrlURL string If cross-posting elsewhere; sets <link rel="canonical">.ogImagestring Per-post OG image override; falls back to /og-default.png. -
Set
draft: falseto publish. The/writingindex will pick it up automatically — no manual indexing. -
Reading time auto-calculates from word count at build time (
words / 200, rounded, min 1 minute).
The homepage Brain section shows real numbers (doc count, last capture, a "Tended" date). They are not live. They are baked in at build time from src/data/brain-status.json.
To refresh (monthly):
pnpm refresh:brain
git add src/data/brain-status.json
git commit -m "Refresh Brain status"
git pushrefresh:brain needs SSH access to the home server, so only the owner can run it.
Push to main. Cloudflare builds and deploys in about 90 seconds.
| Setting | Value |
|---|---|
| Build command | pnpm install --frozen-lockfile && pnpm build |
| Output directory | dist (served via the [assets] block in wrangler.toml) |
| Node version | NODE_VERSION=20 |
After each push, purge-cache.yml waits 2 minutes and then clears the Cloudflare cache. It needs two repo secrets: CLOUDFLARE_ZONE_ID and CLOUDFLARE_CACHE_PURGE_TOKEN.
Don't touch brain.aashishj.com. That subdomain belongs to a separate self-hosted server behind a Cloudflare Tunnel. This site owns only the apex domain (www redirects to it).
- Per-post OG images. Build-time with
satori+@resvg/resvg-jsatsrc/pages/og/[slug].png.ts. Deferred. - Search across writing. When posts pass ~20, add Pagefind as a post-build step.
- AI features. Cut on purpose (2026 clichés). If one comes back, it gets a real home — not a "coming soon" route. Adding a Worker means adding
main = "src/worker/..."towrangler.toml.
- The employer is named: McKinsey (since 2026-07-16).
- The
Gapscolumn on/skillsis a feature. Specificity beats self-deprecation. Don't soften entries. - No "passionate," "synergy," "10x," "leveraged."
- No generic openers. Start in the middle of a sentence if you have to.
- Specific numbers and proper nouns carry weight.
- One moment of dry self-awareness on the homepage is allowed and earned.
All rights reserved. The content is Aashish's; the code structure can be referenced freely but please don't clone the design wholesale.