This repo is my personal website at [www.pradnukala.com](https://www.pradnukala.com) (prad.nu redirects here). It's a static [Astro](https://astro.build) site (originally forked from the [Astro Cactus](https://github.com/chrismwilliams/astro-theme-cactus) theme) reworked into a portfolio organised around four sections:
| **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](https://developers.cloudflare.com/stream/) player and a link to the original recording. |
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.
- [Astro](https://astro.build) v6 (static output, no adapter required)
- [Tailwind CSS](https://tailwindcss.com) v4
- [Content Collections](https://docs.astro.build/en/guides/content-collections/) with type-checked frontmatter (`src/content.config.ts`)
- [Cloudflare Stream](https://developers.cloudflare.com/stream/) for speaking videos
- [Satori](https://github.com/vercel/satori) OG images, [Pagefind](https://pagefind.app/) search, RSS, sitemap, and `Person` JSON-LD on the homepage for SEO
| `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`.
| `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"). |
To surface an article published elsewhere (PESOS), add a stub with no body:
```md
---
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.
-`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 (your `customer-<code>.cloudflarestream.com` subdomain or a Worker/custom domain). The speaking player loads `${streamOrigin}/<videoId>/iframe`.
Static output, so it builds to `./dist/` with no adapter. See the [Astro deploy guides](https://docs.astro.build/en/guides/deploy/). For Cloudflare Workers, point the assets directory at `./dist` with a static [Wrangler config](https://developers.cloudflare.com/workers/framework-guides/web-apps/astro/).