9.1 KiB
pradnukala.com
The personal CV & portfolio site of Prad Nukala — engineer and founder.
What this is
This repo is my personal website at www.pradnukala.com (prad.nu redirects here). It's a static Astro site (originally forked from the Astro Cactus theme) reworked into a portfolio organised around four sections:
| Section | Route | Purpose |
|---|---|---|
| Projects | /projects |
Things I've built, with links to live sites and source. |
| Experience | /experience |
Resume-style roles, which also act as a taxonomy (see below). |
| Writing | /writing |
Essays, notes, and articles. |
| Speaking | /speaking |
Talks, with an embedded Cloudflare Stream player and a link to the original recording. |
Experience as a taxonomy
Experience entries are the connective tissue of the site. Projects, writing, and talks each declare an experiences array in their frontmatter that references experience entries by id. Each experience page then renders its own write-up plus every project, post, and talk filed under it — so the content cross-links automatically.
Tech
- Astro v6 (static output, no adapter required)
- Tailwind CSS v4
- Content Collections with type-checked frontmatter (
src/content.config.ts) - Cloudflare Stream for speaking videos
- Satori OG images, Pagefind search, RSS, sitemap, and
PersonJSON-LD on the homepage for SEO - Dark / light mode, accessible semantic markup
Commands
| Command | Action |
|---|---|
pnpm install |
Install dependencies |
pnpm dev |
Start the local dev server |
pnpm build |
Build the production site to ./dist/ |
pnpm postbuild |
Build the Pagefind static search index |
pnpm preview |
Preview the production build locally |
pnpm sync |
Generate types from the content config |
Content
Content lives in src/content/<collection> as .md/.mdx files; the filename becomes the slug. The schema for each collection is defined in src/content.config.ts.
Projects (src/content/projects)
| Property | Required | Notes |
|---|---|---|
title |
✓ | Max 60 chars. |
startDate |
✓ | Date. |
endDate |
Omit for ongoing projects. Projects are sorted by endDate, most recent first (ongoing on top). |
|
description |
Used for meta description. | |
link |
URL to the live project. | |
repo |
URL to the source repository. | |
tags |
Skills/technologies — double as tags for cross-project browsing at /projects/tags/<tag>. |
|
experiences |
Array of experience ids this project is filed under. |
Experience (src/content/experience)
| Property | Required | Notes |
|---|---|---|
title |
✓ | Display name of the experience (also its id via filename). |
organization |
✓ | Company / org. |
startDate |
✓ | Date. |
endDate |
Omit for current/ongoing. | |
role |
Job title. | |
location |
e.g. "Remote". | |
url |
Org website. | |
description |
Short summary. |
Writing (src/content/writing)
| Property | Required | Notes |
|---|---|---|
title |
✓ | Max 60 chars. |
description |
✓ | Meta description. |
publishDate |
✓ | Date. |
updatedDate |
Date. | |
coverImage |
{ src, alt }. |
|
ogImage |
Skip Satori generation with a custom OG image. | |
tags |
String array. | |
experiences |
Array of experience ids. | |
externalUrl |
If set, the entry links out to this URL (an article published on Fast Company, Medium, LinkedIn, …) instead of rendering a local page. | |
publisher |
Publisher name shown as a badge next to external entries (e.g. "Fast Company"). | |
draft |
true excludes the post from production builds. |
|
pinned |
Pin to the top of /writing. |
To surface an article published elsewhere (PESOS), add a stub with no body:
---
title: "Why user-owned identity matters"
description: "A short summary of the piece."
publishDate: "2025-01-15"
externalUrl: "https://www.fastcompany.com/…"
publisher: "Fast Company"
tags: ["identity"]
---
It appears in /writing, RSS, and the /skills taxonomy, links straight to the publisher (new tab, with a ↗ Publisher badge), and gets no local detail page — so there's no duplicate-content risk.
Speaking (src/content/speaking)
| Property | Required | Notes |
|---|---|---|
title |
✓ | Max 60 chars. |
description |
✓ | Meta description. |
publishDate |
✓ | Date. |
videoId |
✓ | Cloudflare Stream video UID. |
originalSource |
Link to the original (e.g. YouTube) recording. | |
event |
Conference / event name. | |
tags |
String array. | |
experiences |
Array of experience ids. | |
draft |
true excludes the talk from production builds. |
Configure
Most site-wide settings live in src/site.config.ts:
siteConfig— title, author, description,url(used for canonical URLs and OG tags), locale.socialLinks— GitHub / X / LinkedIn / email, shown in the footer and homepage intro.streamOrigin— the Cloudflare Stream origin (yourcustomer-<code>.cloudflarestream.comsubdomain or a Worker/custom domain). The speaking player loads${streamOrigin}/<videoId>/iframe.menuLinks— header/footer navigation.
Replace public/avatar.png (header logo) and public/social-card.png (default OG image) with your own.
Deploy
Static output, so it builds to ./dist/ with no adapter. See the Astro deploy guides. For Cloudflare Workers, point the assets directory at ./dist with a static Wrangler config.
Acknowledgment
Built on the Astro Cactus theme by Chris Williams.
License
MIT