Merge pull request #1 from prdlk/feat/astro-migrate

Refactor into Prad Nukala's CV/portfolio site
This commit is contained in:
Prad Nukala
2026-06-30 14:48:51 -04:00
committed by GitHub
191 changed files with 7627 additions and 34524 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
+3
View File
@@ -0,0 +1,3 @@
WEBMENTION_API_KEY=
WEBMENTION_URL=
WEBMENTION_PINGBACK=#optional
+17 -27
View File
@@ -1,38 +1,28 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # build output
dist/
.output/
# dependencies # dependencies
data node_modules/
node_modules
.pnp
.pnp.js
tmp/
# testing # logs
coverage
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log*
# local env files
# environment variables
.env .env
.env.local .env.production
.env.development.local
.env.test.local
.env.production.local
# turbo # macOS-specific files
.turbo .DS_Store
# vercel # misc
*.pem
.cache
.astro
tmp/
.vercel .vercel
.env*.local
+1
View File
@@ -0,0 +1 @@
22
+8
View File
@@ -0,0 +1,8 @@
*.min.js
node_modules
# cache-dirs
**/.cache
pnpm-lock.yaml
dist
+23
View File
@@ -0,0 +1,23 @@
/** @type {import("@types/prettier").Options} */
export default {
printWidth: 100,
semi: true,
singleQuote: false,
tabWidth: 2,
useTabs: true,
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss" /* Must come last */],
overrides: [
{
files: "**/*.astro",
options: {
parser: "astro",
},
},
{
files: ["*.mdx", "*.md"],
options: {
printWidth: 80,
},
},
],
};
-23
View File
@@ -1,23 +0,0 @@
{
"explorer.excludeGitIgnore": false,
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#95ee3a",
"activityBar.background": "#95ee3a",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#3290ed",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#15202b99",
"sash.hoverBorder": "#95ee3a",
"statusBar.background": "#7ce114",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#62b210",
"statusBarItem.remoteBackground": "#7ce114",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#7ce114",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#7ce11499",
"titleBar.inactiveForeground": "#15202b99"
},
"peacock.color": "#7ce114"
}
+47
View File
@@ -0,0 +1,47 @@
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# prad.nu
## Purpose
Personal CV / portfolio site for Prad Nukala (https://prad.nu). A static [Astro](https://astro.build) v6 site (forked from the Astro Cactus theme) organised into four content sections — **Projects**, **Experience**, **Writing**, **Speaking** — where Experience entries double as a cross-linking taxonomy. See `README.md` for the human-facing overview.
## Key Files
| File | Description |
|------|-------------|
| `package.json` | Dependencies and scripts (pnpm). |
| `astro.config.ts` | Astro integrations (MDX, sitemap, icon, robots, webmanifest, expressive-code), markdown remark/rehype pipeline, Satori OG fonts. |
| `tailwind.config.ts` | Tailwind v4 config (most styling is in `src/styles/global.css`). |
| `tsconfig.json` | TypeScript config; defines the `@/*``src/*` path alias. |
| `biome.json` | Biome linter/formatter config (`pnpm check` / `pnpm lint`). |
| `mise.toml` / `pnpm-workspace.yaml` | Toolchain + workspace config. |
| `README.md` | Project purpose, content model, frontmatter reference. |
## Subdirectories
| Directory | Purpose |
|-----------|---------|
| `src/` | All application source (see `src/AGENTS.md`). |
| `public/` | Static assets served as-is (see `public/AGENTS.md`). |
## For AI Agents
### Working In This Directory
- Package manager is **pnpm**. Output is **static** (`output: 'static'`, `./dist`) — no SSR adapter.
- Import from source via the `@/` alias (e.g. `@/site.config`), not long relative paths.
- Most site-wide config (title, url, `socialLinks`, `streamOrigin`, `menuLinks`) lives in `src/site.config.ts`.
### Testing Requirements
- `pnpm build` is the primary correctness gate — it runs `astro check`-level content validation and fails on bad frontmatter, broken collection refs, or type errors.
- `pnpm check` runs `astro check` + Biome. `pnpm dev` serves locally for visual checks.
### Common Patterns
- Content lives in Content Collections; schema is defined once in `src/content.config.ts`.
- `is:inline` is required on `<script type="application/ld+json">` and similar non-bundled scripts.
## Dependencies
### External
- `astro` v6, `tailwindcss` v4 — framework + styling.
- `@astrojs/mdx`, `@astrojs/rss`, `@astrojs/sitemap`, `astro-icon`, `astro-webmanifest`, `astro-robots-txt`, `astro-expressive-code`.
- `satori` + `sharp` — OG image generation. `pagefind` — static search. `@mux`? no — speaking uses Cloudflare Stream via an iframe (no player dep).
<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
-98
View File
@@ -1,98 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a personal portfolio/blog website built with Next.js 13.4.16 (App Router), TypeScript, and Tailwind CSS. It's based on the Tailwind UI "Spotlight" template and features a modern, responsive design with dark mode support.
## Essential Commands
```bash
# Install dependencies
npm install
# Run development server (http://localhost:3000)
npm run dev
# Build for production
npm run build
# Start production server
npm start
# Run linting
npm run lint
# Type checking (via build process)
npm run build
```
## Architecture & Key Patterns
### Directory Structure
- **`/src/app/`** - Next.js App Router pages and layouts
- Each page is in its own directory with `page.tsx` or `page.mdx`
- Articles are MDX files in subdirectories under `/articles/`
- RSS feed generation at `/feed.xml/`
- **`/src/components/`** - Reusable React components
- Layout components extend from `Layout.tsx`
- All components use TypeScript with proper type definitions
- Components use Tailwind CSS classes for styling
- **`/src/lib/`** - Utility functions
- `articles.ts` - Functions for loading and managing MDX articles
- `formatDate.ts` - Date formatting utilities
### Content Management
- Articles are written in MDX format in `/src/app/articles/[slug]/page.mdx`
- Each article directory can contain images alongside the MDX file
- Article metadata is defined in the MDX frontmatter
- The `ArticleLayout` component wraps all article content
### Styling Approach
- Tailwind CSS utility classes for all styling
- Dark mode support via `next-themes` package
- Typography plugin for prose content styling
- Custom Tailwind configuration in `tailwind.config.js`
- Code syntax highlighting with Prism CSS
### TypeScript Configuration
- Strict mode enabled
- Path alias `@/*` maps to `./src/*`
- All components and utilities should have proper TypeScript types
### Important Files
- `next.config.mjs` - Next.js configuration with MDX support
- `tailwind.config.js` - Tailwind CSS customization
- `.env.local` - Must set `NEXT_PUBLIC_SITE_URL` for production
## Development Guidelines
### Adding New Pages
1. Create a new directory under `/src/app/`
2. Add a `page.tsx` file with proper TypeScript types
3. Use the `SimpleLayout` or custom layout components
4. Follow existing page patterns for consistency
### Adding New Articles
1. Create a new directory under `/src/app/articles/[article-name]/`
2. Add a `page.mdx` file with frontmatter including title, description, author, and date
3. Place any images in the same directory
4. Articles automatically appear in the articles list and RSS feed
### Component Development
- Always use TypeScript with proper type definitions
- Follow existing component patterns for props and structure
- Use Tailwind CSS classes; avoid inline styles
- Components should be reusable and properly abstracted
### Environment Setup
Before running the project, create a `.env.local` file:
```
NEXT_PUBLIC_SITE_URL=https://your-domain.com
```
## No Testing Framework
This project does not currently have a testing framework configured. Consider adding Jest and React Testing Library if test coverage is needed.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Chris Williams
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-129
View File
@@ -1,129 +0,0 @@
# Tailwind UI License
## Personal License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
- Share your access to the Components and Templates with any other individuals.
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a personal website by yourself.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
Examples of usage **not allowed** by the license:
- Creating a repository of your favorite Tailwind UI components or templates (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
- Create a Figma or Sketch UI kit based on the Tailwind UI component designs.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
In simple terms, use Tailwind UI for anything you like as long as it doesn't compete with Tailwind UI.
### Personal License Definitions
Licensee is the individual who has purchased a Personal License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Team License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a website for your company.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available.
Examples of use **not allowed** by the license:
- Creating a repository of your favorite Tailwind UI components or template (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
- Creating a theme or template using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind UI license to build their own websites or side projects.
### Team License Definitions
Licensee is the business entity who has purchased a Team License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Employee is a full-time or part-time employee of the Licensee.
Contractor is an individual or business entity contracted to perform services for the Licensee.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Enforcement
If you are found to be in violation of the license, access to your Tailwind UI account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
## Liability
Tailwind Labs Inc.s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
## Questions?
Unsure which license you need, or unsure if your use case is covered by our licenses?
Email us at [support@tailwindui.com](mailto:support@tailwindui.com) with your questions.
+139 -27
View File
@@ -1,42 +1,154 @@
# Spotlight <!--toc:start-->
- [What this is](#what-this-is)
- [Experience as a taxonomy](#experience-as-a-taxonomy)
- [Tech](#tech)
- [Commands](#commands)
- [Content](#content)
- [Projects (`src/content/projects`)](#projects-srccontentprojects)
- [Experience (`src/content/experience`)](#experience-srccontentexperience)
- [Writing (`src/content/writing`)](#writing-srccontentwriting)
- [Speaking (`src/content/speaking`)](#speaking-srccontentspeaking)
- [Configure](#configure)
- [Deploy](#deploy)
- [Acknowledgment](#acknowledgment)
- [License](#license)
<!--toc:end-->
Spotlight is a [Tailwind UI](https://tailwindui.com) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org). <div align="center">
<img alt="Prad Nukala" src="./public/avatar.png" width="96" />
</div>
<h1 align="center">pradnukala.com</h1>
## Getting started <p align="center">The personal CV &amp; portfolio site of <strong>Prad Nukala</strong> — engineer and founder.</p>
To get started with this template, first install the npm dependencies: ## What this is
```bash 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:
npm install
| 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 YouTube player. |
### 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](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`)
- Embedded YouTube players 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
- 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:
```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"]
---
``` ```
Next, create a `.env.local` file in the root of your project and set the `NEXT_PUBLIC_SITE_URL` variable to your site's public URL: 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`)
NEXT_PUBLIC_SITE_URL=https://example.com
```
Next, run the development server: | Property | Required | Notes |
| ---------------- | :------: | ------------------------------------------------- |
| `title` | ✓ | Max 60 chars. |
| `description` | ✓ | Meta description. |
| `publishDate` | ✓ | Date. |
| `youtubeId` | ✓ | YouTube video ID (the `v=` value). |
| `event` | | Conference / event name. |
| `tags` | | String array. |
| `experiences` | | Array of experience ids. |
| `draft` | | `true` excludes the talk from production builds. |
```bash ## Configure
npm run dev
```
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. Most site-wide settings live in **`src/site.config.ts`**:
## Customizing - `siteConfig` — title, author, description, `url` (used for canonical URLs and OG tags), locale.
- `socialLinks` — GitHub / X / LinkedIn / Medium / Discord / Telegram / email, shown in the footer and homepage intro.
- `menuLinks` — header/footer navigation.
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files. 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](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/).
## Acknowledgment
Built on the [Astro Cactus](https://github.com/chrismwilliams/astro-theme-cactus) theme by Chris Williams.
## License ## License
This site template is a commercial product and is licensed under the [Tailwind UI license](https://tailwindui.com/license). MIT
## Learn more
To learn more about the technologies used in this site template, see the following resources:
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
- [MDX](https://mdxjs.com) - the MDX documentation
+112
View File
@@ -0,0 +1,112 @@
import fs from "node:fs";
import { rehypeHeadingIds } from "@astrojs/markdown-remark";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import tailwind from "@tailwindcss/vite";
import { defineConfig, envField } from "astro/config";
import expressiveCode from "astro-expressive-code";
import icon from "astro-icon";
import robotsTxt from "astro-robots-txt";
import webmanifest from "astro-webmanifest";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeExternalLinks from "rehype-external-links";
import rehypeUnwrapImages from "rehype-unwrap-images";
import remarkDirective from "remark-directive"; /* Handle ::: directives as nodes */
import { remarkAdmonitions } from "./src/plugins/remark-admonitions";
import { remarkGithubCard } from "./src/plugins/remark-github-card";
import { remarkReadingTime } from "./src/plugins/remark-reading-time";
import { expressiveCodeOptions, siteConfig } from "./src/site.config";
// https://astro.build/config
export default defineConfig({
site: siteConfig.url,
image: {
domains: ["webmention.io"],
},
integrations: [
expressiveCode(expressiveCodeOptions),
icon(),
sitemap(),
mdx(),
robotsTxt(),
webmanifest({
// See: https://github.com/alextim/astro-lib/blob/main/packages/astro-webmanifest/README.md
name: siteConfig.title,
description: siteConfig.description,
lang: siteConfig.lang,
icon: "public/icon.svg", // the source for generating favicon & icons
icons: [
{
src: "icons/apple-touch-icon.png", // used in src/components/BaseHead.astro L:26
sizes: "180x180",
type: "image/png",
},
{
src: "icons/icon-192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "icons/icon-512.png",
sizes: "512x512",
type: "image/png",
},
],
start_url: "/",
background_color: "#1d1f21",
theme_color: "#2bbc8a",
display: "standalone",
config: {
insertFaviconLinks: false,
insertThemeColorMeta: false,
insertManifestLink: false,
},
}),
],
markdown: {
rehypePlugins: [
rehypeHeadingIds,
[rehypeAutolinkHeadings, { behavior: "wrap", properties: { className: ["not-prose"] } }],
[
rehypeExternalLinks,
{
rel: ["noreferrer", "noopener"],
target: "_blank",
},
],
rehypeUnwrapImages,
],
remarkPlugins: [remarkReadingTime, remarkDirective, remarkGithubCard, remarkAdmonitions],
remarkRehype: {
footnoteLabelProperties: {
className: [""],
},
},
},
vite: {
plugins: [tailwind(), rawFonts([".ttf", ".woff"])],
},
env: {
schema: {
WEBMENTION_API_KEY: envField.string({ context: "server", access: "secret", optional: true }),
WEBMENTION_URL: envField.string({ context: "client", access: "public", optional: true }),
WEBMENTION_PINGBACK: envField.string({ context: "client", access: "public", optional: true }),
},
},
});
function rawFonts(ext: string[]) {
return {
name: "vite-plugin-raw-fonts",
// @ts-expect-error:next-line
transform(_, id) {
if (ext.some((e) => id.endsWith(e))) {
const buffer = fs.readFileSync(id);
return {
code: `export default ${JSON.stringify(buffer)}`,
map: null,
};
}
},
};
}
+83
View File
@@ -0,0 +1,83 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"includes": [
"**",
"!**/build",
"!**/dist",
"!**/node_modules",
"!**/.vercel",
"!**/.netlify",
"!**/.vscode",
"!**/.astro"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"lineEnding": "lf",
"lineWidth": 100,
"expand": "auto"
},
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"noSvgWithoutTitle": "off"
},
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error",
"useImportType": "error"
},
"suspicious": { "noExplicitAny": "warn" }
}
},
"javascript": {
"formatter": {
"trailingCommas": "all",
"semicolons": "always",
"bracketSameLine": false
}
},
"html": { "formatter": { "selfCloseVoidElements": "always" } },
"css": {
"parser": {
"tailwindDirectives": true
}
},
"overrides": [
{
"includes": ["**/*.astro"],
"linter": {
"rules": {
"style": {
"useConst": "off",
"useImportType": "off"
},
"correctness": {
"noUnusedVariables": "off",
"noUnusedImports": "off"
}
}
}
}
]
}
+1574
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
import { type MDXComponents } from 'mdx/types'
export function useMDXComponents(components: MDXComponents) {
return {
...components,
}
}
+2
View File
@@ -0,0 +1,2 @@
[tools]
pnpm = "latest"
-5
View File
@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
-52
View File
@@ -1,52 +0,0 @@
import rehypePrism from '@mapbox/rehype-prism'
import nextMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
redirects: async () => {
return [
{
source: '/twitter',
destination: 'https://twitter.com/thisisprad',
permanent: true,
},
{
source: '/github',
destination: 'https://github.com/prnk28',
permanent: true,
},
{
source: '/linkedin',
destination: 'https://www.linkedin.com/in/pradn/',
permanent: true,
},
{
source: '/medium',
destination: 'https://medium.com/@prnk28',
permanent: true,
},
{
source: '/cal',
destination: 'https://cal.com/pradn',
permanent: true,
},
{
source: '/telegram',
destination: 'https://t.me/prnk28',
permanent: true,
}
]
}
}
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
},
})
export default withMDX(nextConfig)
-17617
View File
File diff suppressed because it is too large Load Diff
+59 -36
View File
@@ -1,43 +1,66 @@
{ {
"name": "tailwindui-template", "name": "prad.nu",
"version": "0.1.0", "type": "module",
"private": true, "version": "7.2.0",
"private": false,
"scripts": { "scripts": {
"dev": "next dev", "postinstall": "npm rebuild sharp --force",
"build": "next build", "dev": "astro dev",
"start": "next start", "start": "astro dev",
"lint": "next lint" "build": "astro build",
"postbuild": "pagefind --site dist",
"preview": "astro preview",
"check": "astro check && biome check",
"lint": "biome check --write",
"format": "prettier -w . --cache"
}, },
"browserslist": "defaults, not ie <= 11",
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.13", "@astrojs/markdown-remark": "^7.1.2",
"@mapbox/rehype-prism": "^0.8.0", "@astrojs/mdx": "5.0.6",
"@mdx-js/loader": "^2.3.0", "@astrojs/rss": "4.0.18",
"@mdx-js/react": "^2.3.0", "@astrojs/sitemap": "3.7.2",
"@next/mdx": "13.4.16", "@pagefind/component-ui": "^1.5.2",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/vite": "4.3.0",
"@types/node": "20.4.7", "@vercel/analytics": "^2.0.1",
"@types/react": "18.2.18", "astro": "6.3.3",
"@types/react-dom": "18.2.7", "astro-expressive-code": "^0.42.0",
"@types/webpack-env": "^1.18.1", "astro-icon": "^1.1.5",
"autoprefixer": "^10.4.12", "astro-robots-txt": "^1.0.0",
"cheerio": "^1.0.0-rc.12", "astro-webmanifest": "^1.0.0",
"clsx": "^1.2.1", "cssnano": "^8.0.1",
"fast-glob": "^3.2.11", "hastscript": "^9.0.0",
"feed": "^4.2.2", "mdast-util-directive": "^3.0.0",
"next": "13.4.16", "mdast-util-to-markdown": "^2.1.2",
"next-themes": "^0.2.1", "mdast-util-to-string": "^4.0.0",
"react": "18.2.0", "rehype-autolink-headings": "^7.1.0",
"react-dom": "18.2.0", "rehype-external-links": "^3.0.0",
"remark-gfm": "^3.0.1", "rehype-unwrap-images": "^1.0.0",
"tailwindcss": "^3.3.3", "remark-directive": "^4.0.0",
"typescript": "5.1.6" "satori": "0.26.0",
"sharp": "^0.34.5",
"tailwindcss": "^4.3.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "8.45.0", "@astrojs/check": "^0.9.9",
"eslint-config-next": "13.4.16", "@biomejs/biome": "^2.4.15",
"prettier": "^3.0.1", "@iconify-json/mdi": "^1.2.2",
"prettier-plugin-tailwindcss": "^0.5.2", "@pagefind/default-ui": "^1.5.2",
"sharp": "^0.32.0" "@tailwindcss/typography": "^0.5.19",
} "@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/node": "^25.8.0",
"autoprefixer": "^10.5.0",
"pagefind": "^1.5.2",
"prettier": "^3.8.3",
"prettier-plugin-astro": "0.14.1",
"prettier-plugin-tailwindcss": "^0.8.0",
"reading-time": "^1.5.0",
"typescript": "^6.0.3"
},
"overrides": {
"sharp": "^0.34.5"
},
"trustedDependencies": ["@biomejs/biome", "@tailwindcss/oxide", "esbuild", "sharp"]
} }
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
-6
View File
@@ -1,6 +0,0 @@
/** @type {import('prettier').Options} */
module.exports = {
singleQuote: true,
semi: false,
plugins: ['prettier-plugin-tailwindcss'],
}
+22
View File
@@ -0,0 +1,22 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# public
## Purpose
Static assets copied verbatim to the site root at build time (served at `/`).
## Key Files
| File | Description |
|------|-------------|
| `avatar.png` | Personal avatar used as the header logo (`src/components/layout/Header.astro`, referenced as `/avatar.png`) and in the homepage `Person` JSON-LD. |
| `icon.svg` | Source icon; `astro-webmanifest` generates favicons / apple-touch / manifest icons from it at build (referenced in `BaseHead.astro`). |
| `social-card.png` | Default Open Graph image when a page/post supplies no `ogImage`. |
## For AI Agents
### Working In This Directory
- Reference these with absolute root paths (`/avatar.png`), not the `@/` alias — they are not processed by Astro's asset pipeline.
- Replacing `icon.svg` changes all generated favicons/manifest icons; keep it roughly square (build warns otherwise).
<!-- MANUAL: -->
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

+936
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

+50
View File
@@ -0,0 +1,50 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# src
## Purpose
All application source: configuration, content collections, pages/routes, layouts, components, data helpers, styles, and build plugins.
## Key Files
| File | Description |
|------|-------------|
| `site.config.ts` | Site metadata (`siteConfig`), `socialLinks`, `menuLinks`, and Expressive Code options. Primary place to edit site-wide settings. |
| `content.config.ts` | Content Collection definitions + Zod schemas for `writing`, `projects`, `experience`, `speaking`. Experience refs are validated via `reference("experience")`. |
| `types.ts` | Shared TS types (`SiteConfig`, `SiteMeta`, webmention types, `AdmonitionType`). |
| `env.d.ts` | Ambient type declarations. |
## Subdirectories
| Directory | Purpose |
|-----------|---------|
| `assets/` | Build-time assets, currently OG fonts (see `assets/AGENTS.md`). |
| `components/` | Reusable `.astro` components (see `components/AGENTS.md`). |
| `content/` | Markdown content for the four collections (see `content/AGENTS.md`). |
| `data/` | Collection query helpers (see `data/AGENTS.md`). |
| `layouts/` | Page shells (see `layouts/AGENTS.md`). |
| `pages/` | File-based routes + endpoints. **No `AGENTS.md` here** — any `.md` under `src/pages` becomes a public route, so docs for this dir live in this file (see "Routing & OG images" below). |
| `plugins/` | Custom remark plugins (see `plugins/AGENTS.md`). |
| `styles/` | Global CSS + block/component styles (see `styles/AGENTS.md`). |
| `utils/` | Pure helper functions (see `utils/AGENTS.md`). |
## For AI Agents
### Working In This Directory
- Collection **keys match their content dir and route**: `writing``/writing`, `projects``/projects`, `experience``/experience`, `speaking``/speaking`.
- When changing a collection schema in `content.config.ts`, run `pnpm build` to regenerate `astro:content` types and catch frontmatter mismatches.
### Routing & OG images (`src/pages`)
- Any `.md`/`.mdx`/`.astro` under `src/pages` becomes a route — do **not** put `AGENTS.md` or other docs there (they'd ship as public pages). Document `pages/` here instead.
- Each section has `[...page].astro` (paginated list) + `[...slug].astro` (detail); plus `rss.xml.ts` feeds and a dynamic OG-image endpoint.
- `og-image/[...slug].png.ts` generates per-entry social images for **all** collections (namespaced `/og-image/<type>/<id>.png`) via Satori + Sharp, cached by `_cacheUtil.ts` (bump `CACHE_VERSION` when changing `_ogMarkup.ts`). Files prefixed `_` are private modules, not routes.
- The `/experience/[...slug]` page is the taxonomy hub (`getContentForExperience`), listing related projects/writing/talks.
### Common Patterns
- The `experiences` frontmatter array (on projects/writing/speaking) holds `reference("experience")` entries, resolved with `getEntries`.
## Dependencies
### Internal
- Nearly everything imports `@/site.config` and `@/content.config`-derived collection types.
<!-- MANUAL: -->
-130
View File
@@ -1,130 +0,0 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { Container } from '@/components/Container'
import {
GitHubIcon,
InstagramIcon,
LinkedInIcon,
MediumIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import portraitImage from '@/images/portrait.jpg'
function SocialLink({
className,
href,
children,
icon: Icon,
}: {
className?: string
href: string
icon: React.ComponentType<{ className?: string }>
children: React.ReactNode
}) {
return (
<li className={clsx(className, 'flex')}>
<Link
href={href}
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
>
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
<span className="ml-4">{children}</span>
</Link>
</li>
)
}
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
/>
</svg>
)
}
export const metadata: Metadata = {
title: 'About',
description:
'I\'m Prad Nukala, founder of Sonr and blockchain identity pioneer. Building the future of decentralized identity in New York City.',
}
export default function About() {
return (
<Container className="mt-16 sm:mt-32">
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
<div className="lg:pl-20">
<div className="max-w-xs px-2.5 lg:max-w-none">
<Image
src={portraitImage}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
/>
</div>
</div>
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
I\'m Prad Nukala. I live in New York City, where I\'m building the future of digital identity.
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<p>
I\'ve been passionate about technology since the introduction of the App Store, where I made one
of the first 5,000 apps at age 12. This early start in mobile development shaped my understanding
of how technology can empower individuals and sparked my lifelong commitment to building tools that
put users first.
</p>
<p>
My journey in blockchain began with a simple realization: the internet\'s identity layer is fundamentally
broken. Users have no control over their digital selves, leading to privacy breaches, data exploitation,
and a web that serves corporations over people. This inspired me to found Sonr in 2020, where we\'re
building an IBC-enabled blockchain specifically designed for decentralized identity.
</p>
<p>
As a W3C Working Group Member for DIDs, WebAuthn, and WASM, I\'m actively shaping the standards that
will define the future of digital identity. My technical contributions speak to my commitment - with over
6,000 GitHub contributions from 2021-2023, I was ranked as the 4th most active contributor worldwide.
Before Sonr, I self-developed over 10 projects that garnered more than 1 million downloads combined.
</p>
<p>
Today, I lead Sonr with a clear mission: to create a peer-to-peer identity and asset management system
that leverages DID documents, WebAuthn, and IPFS. We\'ve raised $4.7M in funding and onboarded over 120
first-time blockchain developers to our platform. Our technology features passkey-based user accounts,
DKLS-MPC powered wallets (no seed phrases), and lightning-fast 600ms wallet generation. I believe the
future of the internet is one where users own their identity, control their data, and interact on their
own terms.
</p>
</div>
</div>
<div className="lg:pl-20">
<ul role="list">
<SocialLink href="https://twitter.com/thisisprad" icon={TwitterIcon}>
Follow on Twitter
</SocialLink>
<SocialLink href="https://medium.com/@prnk28" icon={MediumIcon} className="mt-4">
Follow on Medium
</SocialLink>
<SocialLink href="https://github.com/prnk28" icon={GitHubIcon} className="mt-4">
Follow on GitHub
</SocialLink>
<SocialLink href="https://linkedin.com/in/pradn" icon={LinkedInIcon} className="mt-4">
Follow on LinkedIn
</SocialLink>
<SocialLink
href="mailto:prad.nukala@sonr.io"
icon={MailIcon}
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
>
prad.nukala@sonr.io
</SocialLink>
</ul>
</div>
</div>
</Container>
)
}
@@ -1,90 +0,0 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const article = {
author: 'Prad Nukala',
date: '2024-01-15',
title: 'Building Sonr: The Journey to Decentralized Identity',
description:
'How we\'re creating an IBC-enabled blockchain that puts users in control of their digital identity, eliminating passwords and seed phrases while maintaining true self-sovereignty.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
When I founded Sonr in 2020, I had a simple but ambitious vision: create a world where people truly own their digital identity. After years of watching data breaches, privacy violations, and the centralization of user data, I knew there had to be a better way.
## The Problem with Digital Identity Today
The internet wasn\'t designed with identity in mind. We\'ve patched together solutions using usernames, passwords, and centralized databases, but these approaches are fundamentally broken:
- **No User Control**: Your identity lives on servers you don\'t control
- **Privacy Violations**: Companies monetize your personal data without consent
- **Security Nightmares**: Password breaches affect billions of users annually
- **Platform Lock-in**: Your identity is fragmented across countless services
## Enter Sonr: A New Paradigm
Sonr is an IBC-enabled blockchain specifically designed for decentralized identity. We\'re not just building another blockchain - we\'re creating the identity layer the internet should have had from the beginning.
### Key Innovations
1. **Passkey-Based Authentication**: No more passwords. Users authenticate with biometrics or device-based keys, making phishing virtually impossible.
2. **No Seed Phrases**: Using DKLS-MPC (Distributed Key Lifetime Security - Multi-Party Computation), we\'ve eliminated the need for users to manage complex seed phrases while maintaining true ownership.
3. **Lightning-Fast Performance**: 600ms wallet generation means users can onboard as quickly as traditional web apps.
4. **IBC Integration**: As part of the Cosmos ecosystem, Sonr identities work seamlessly across multiple blockchains.
## Technical Architecture
At its core, Sonr leverages several cutting-edge technologies:
```typescript
// Example: Creating a Sonr identity
const identity = await Sonr.createIdentity({
authentication: 'passkey',
storage: 'ipfs',
recovery: 'social'
});
// The user now has a DID (Decentralized Identifier)
console.log(identity.did); // did:sonr:abc123...
```
### DID Documents
Every Sonr user has a DID document stored on-chain that contains their public keys and service endpoints. This document is the source of truth for their identity.
### IPFS Integration
User data is stored on IPFS, ensuring it\'s distributed, permanent, and under user control. Only the user\'s keys can decrypt their personal information.
### UCAN Tokens
We use User Controlled Authorization Networks (UCAN) for fine-grained access control. Users can delegate specific permissions without sharing their private keys.
## Real-World Impact
Since launching, we\'ve:
- Raised $4.7M in funding to accelerate development
- Onboarded over 120 first-time blockchain developers
- Created tools that make blockchain accessible to mainstream developers
- Built partnerships across the Cosmos ecosystem
## The Road Ahead
We\'re currently preparing for our public testnet launch, with mainnet coming soon. Our goal is to onboard the next billion users to Web3 - not by forcing them to understand blockchain, but by making it invisible.
The future of the internet is one where:
- Users own their identity and data
- Privacy is default, not optional
- Security doesn\'t sacrifice usability
- Interoperability is built-in, not bolted-on
## Join the Revolution
If you\'re a developer interested in building on Sonr, check out our [documentation](https://docs.sonr.io) and [GitHub](https://github.com/sonr-io). We\'re always looking for contributors who share our vision of a more equitable internet.
The identity layer of the internet is broken, but together, we can fix it. Welcome to Sonr - where your identity truly belongs to you.
@@ -1,131 +0,0 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const article = {
author: 'Prad Nukala',
date: '2023-09-10',
title: 'The Future of Digital Identity: Self-Sovereign and Interoperable',
description:
'Digital identity is broken. Here\'s how decentralized identifiers (DIDs), verifiable credentials, and blockchain technology will create a future where you truly own your identity.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
Every day, billions of people prove who they are online using systems designed in the 1960s. Usernames and passwords were meant to be temporary solutions, yet here we are, six decades later, still typing "password123" and hoping for the best.
As someone who\'s been building technology since creating one of the first App Store apps, and now as a W3C Working Group Member for DIDs and WebAuthn, I\'ve seen firsthand how broken our identity systems are. It\'s time for a fundamental shift.
## The Current Identity Crisis
Today\'s digital identity landscape is a disaster:
- **2.5 billion** records were exposed in data breaches last year
- The average person has **100+ passwords** to manage
- Identity theft affects **1 in 15 people** annually
- Users have **zero control** over their personal data
But the real problem isn\'t just security - it\'s ownership. Your digital identity doesn\'t belong to you. It belongs to Facebook, Google, your bank, your employer, and dozens of other entities.
## Enter Self-Sovereign Identity
Self-sovereign identity (SSI) flips the script. Instead of companies owning your identity, you do. Here\'s how it works:
### Decentralized Identifiers (DIDs)
A DID is like a phone number for your identity - unique, permanent, and owned by you. Unlike usernames, DIDs aren\'t controlled by any company.
```
did:sonr:1234567890abcdef
```
This simple string represents your entire digital identity, secured by cryptography rather than corporate databases.
### Verifiable Credentials
Imagine your driver\'s license, but digital and cryptographically verifiable. That\'s a verifiable credential. Your university can issue you a degree credential, your employer can issue an employment credential, and you store them in your digital wallet.
The magic? You can prove things about yourself without revealing everything. Need to prove you\'re over 21? You don\'t need to share your birthdate - just a cryptographic proof that you meet the requirement.
## The Sonr Implementation
At Sonr, we\'re not just theorizing about SSI - we\'re building it. Our approach combines:
1. **IBC-Enabled Blockchain**: Your identity works across multiple chains, not locked to one ecosystem
2. **IPFS Storage**: Your data is distributed and permanent, not sitting in vulnerable databases
3. **WebAuthn Integration**: Authenticate with your fingerprint, not passwords
4. **UCAN Authorization**: Grant specific permissions without sharing your keys
## Real-World Applications
This isn\'t just technology for technology\'s sake. Here\'s how SSI will change everyday life:
### Healthcare
- Carry your medical records with you
- Share only relevant information with providers
- Maintain privacy while ensuring continuity of care
### Finance
- Instant KYC/AML compliance without repeated paperwork
- Prove creditworthiness without exposing financial details
- Access global financial services with one identity
### Education
- Verifiable diplomas that can\'t be faked
- Instant credential verification for employers
- Lifelong learning records that follow you
### Travel
- Digital passports that speed up border crossings
- Vaccination records that are instantly verifiable
- One identity for all your travel needs
## The Technical Foundation
Building SSI requires solving complex technical challenges:
```typescript
// Creating a verifiable credential
const credential = await Sonr.createCredential({
type: 'UniversityDegree',
subject: userDID,
claims: {
degree: 'Computer Science',
graduationDate: '2023-05-15'
}
});
// Selective disclosure - prove graduation without revealing degree
const proof = await credential.generateProof({
reveal: ['graduationDate']
});
```
## Privacy by Design
Unlike current systems where privacy is an afterthought, SSI builds privacy in from the ground up:
- **Minimal Disclosure**: Share only what\'s necessary
- **User Consent**: Every data share requires explicit permission
- **Pseudonymity**: Use different identifiers for different contexts
- **Revocation**: Withdraw access to your data anytime
## The Path Forward
Transitioning to SSI won\'t happen overnight, but the momentum is building:
- W3C standards for DIDs and Verifiable Credentials are finalized
- Governments are piloting digital identity programs
- Major tech companies are adopting WebAuthn
- The EU\'s eIDAS 2.0 regulation mandates digital wallets by 2024
## Join the Identity Revolution
The future of identity isn\'t about better passwords or more secure databases. It\'s about fundamentally reimagining the relationship between individuals and their digital selves.
At Sonr, we\'re building this future today. Whether you\'re a developer wanting to integrate SSI, an organization looking to issue credentials, or just someone tired of password resets, there\'s a place for you in this revolution.
The question isn\'t whether self-sovereign identity will replace current systems - it\'s how quickly we can make it happen. The technology is here. The standards are set. Now we need builders, advocates, and early adopters to make it reality.
Your identity belongs to you. It\'s time we built an internet that respects that.
-60
View File
@@ -1,60 +0,0 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import { type ArticleWithSlug, getAllArticles } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
function Article({ article }: { article: ArticleWithSlug }) {
return (
<article className="md:grid md:grid-cols-4 md:items-baseline">
<Card className="md:col-span-3">
<Card.Title href={`/articles/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="md:hidden"
decorate
>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="mt-1 hidden md:block"
>
{formatDate(article.date)}
</Card.Eyebrow>
</article>
)
}
export const metadata: Metadata = {
title: 'Articles',
description:
'All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order.',
}
export default async function ArticlesIndex() {
let articles = await getAllArticles()
return (
<SimpleLayout
title="Writing on software design, company building, and the blockchain industry."
intro="All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order."
>
<div className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
<div className="flex max-w-3xl flex-col space-y-16">
{articles.map((article) => (
<Article key={article.slug} article={article} />
))}
</div>
</div>
</SimpleLayout>
)
}
@@ -1,97 +0,0 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const article = {
author: 'Prad Nukala',
date: '2023-11-20',
title: 'Why Blockchain Needs Better UX (And How We\'re Fixing It)',
description:
'The biggest barrier to blockchain adoption isn\'t scalability or regulation - it\'s user experience. Here\'s how we\'re making Web3 as easy as Web2.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
Ask any blockchain developer about the biggest challenge facing our industry, and you\'ll hear about scalability, regulation, or interoperability. But after years of building in this space and onboarding over 120 developers to blockchain, I\'ve learned the real barrier is much simpler: user experience.
## The UX Crisis in Web3
Let\'s be honest about the current state of blockchain UX:
- **Seed Phrases**: Asking users to write down 24 random words and never lose them is a non-starter for mainstream adoption
- **Gas Fees**: Imagine if every click on the internet cost money and required manual approval
- **Addresses**: 42-character hexadecimal strings are not how humans think about identity
- **Transaction Times**: Waiting minutes for confirmations kills any semblance of modern app experience
These aren\'t technical limitations - they\'re design choices we\'ve accepted as "the way blockchain works."
## Learning from History
When I created one of the first 5,000 apps on the App Store at age 12, the iPhone had just made computing accessible to everyone. Not by making people understand computers, but by hiding the complexity behind intuitive interfaces.
Blockchain needs its iPhone moment.
## The Sonr Approach
At Sonr, we\'ve rethought blockchain UX from first principles:
### 1. No Seed Phrases
Using DKLS-MPC technology, users never see or manage private keys. Account recovery uses familiar methods like email or social recovery - not a piece of paper in a safe.
### 2. Instant Onboarding
Our 600ms wallet generation means users can start using blockchain apps as quickly as they\'d sign up for any web service. No downloads, no extensions, no friction.
### 3. Passkey Authentication
Leveraging WebAuthn standards, users authenticate with their fingerprint or face - the same way they unlock their phones. It\'s more secure than passwords and infinitely more user-friendly.
### 4. Invisible Blockchain
Users shouldn\'t need to understand consensus mechanisms any more than they need to understand TCP/IP to browse the web. Blockchain should be infrastructure, not interface.
## Real Code, Real Solutions
Here\'s how simple it is to integrate Sonr:
```javascript
// Traditional Web3
const accounts = await ethereum.request({
method: 'eth_requestAccounts'
});
// User must have MetaMask installed
// User must understand gas fees
// User must protect seed phrase
// With Sonr
const user = await Sonr.authenticate();
// That\'s it. No wallet required.
// No seed phrases. Just works.
```
## The Broader Impact
Better UX isn\'t just about convenience - it\'s about accessibility and inclusion. When we make blockchain easier to use, we:
- **Reduce Scams**: Simpler interfaces mean fewer opportunities for user error
- **Increase Adoption**: Lower barriers mean more users and developers
- **Enable Innovation**: When basics are solved, builders can focus on novel applications
## Looking Forward
As a W3C Working Group Member, I\'m helping shape standards that will make these UX improvements universal. The future of blockchain isn\'t about teaching billions of people new concepts - it\'s about making those concepts invisible.
## The Challenge to Builders
If you\'re building in Web3, ask yourself:
- Would my parents be able to use this?
- Does it require reading documentation?
- Are there more than 3 steps to get started?
If the answer to any of these is yes, we have work to do.
## Join the UX Revolution
The blockchain industry has brilliant technologists, but we need more designers, more product thinkers, and more people who put users first. If you\'re interested in making blockchain accessible to everyone, [reach out](mailto:prad.nukala@sonr.io).
The future of the internet depends on making it usable by everyone, not just the technically sophisticated. Let\'s build that future together.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

-70
View File
@@ -1,70 +0,0 @@
import assert from 'assert'
import * as cheerio from 'cheerio'
import { Feed } from 'feed'
export async function GET(req: Request) {
let siteUrl = process.env.NEXT_PUBLIC_SITE_URL
if (!siteUrl) {
throw Error('Missing NEXT_PUBLIC_SITE_URL environment variable')
}
let author = {
name: 'Prad Nukala',
email: 'spencer@planetaria.tech',
}
let feed = new Feed({
title: author.name,
description: 'Your blog description',
author,
id: siteUrl,
link: siteUrl,
image: `${siteUrl}/favicon.ico`,
favicon: `${siteUrl}/favicon.ico`,
copyright: `All rights reserved ${new Date().getFullYear()}`,
feedLinks: {
rss2: `${siteUrl}/feed.xml`,
},
})
let articleIds = require
.context('../articles', true, /\/page\.mdx$/)
.keys()
.filter((key) => key.startsWith('./'))
.map((key) => key.slice(2).replace(/\/page\.mdx$/, ''))
for (let id of articleIds) {
let url = String(new URL(`/articles/${id}`, req.url))
let html = await (await fetch(url)).text()
let $ = cheerio.load(html)
let publicUrl = `${siteUrl}/articles/${id}`
let article = $('article').first()
let title = article.find('h1').first().text()
let date = article.find('time').first().attr('datetime')
let content = article.find('[data-mdx-content]').first().html()
assert(typeof title === 'string')
assert(typeof date === 'string')
assert(typeof content === 'string')
feed.addItem({
title,
id: publicUrl,
link: publicUrl,
content,
author: [author],
contributor: [author],
date: new Date(date),
})
}
return new Response(feed.rss2(), {
status: 200,
headers: {
'content-type': 'application/xml',
'cache-control': 's-maxage=31556952',
},
})
}
-39
View File
@@ -1,39 +0,0 @@
import { type Metadata } from 'next'
import { Providers } from '@/app/providers'
import { Layout } from '@/components/Layout'
import '@/styles/tailwind.css'
export const metadata: Metadata = {
title: {
template: '%s - Prad Nukala',
default:
'Prad Nukala - Founder & CEO of Sonr, Blockchain Identity Pioneer',
},
description:
'I\'m Prad Nukala, founder and CEO of Sonr, an IBC-enabled blockchain for decentralized identity. W3C Working Group Member for DIDs, WebAuthn, and WASM. Building the future of self-sovereign identity and empowering users with control over their digital assets.',
alternates: {
types: {
'application/rss+xml': `${process.env.NEXT_PUBLIC_SITE_URL}/feed.xml`,
},
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="h-full antialiased" suppressHydrationWarning>
<body className="flex h-full bg-zinc-50 dark:bg-black">
<Providers>
<div className="flex w-full">
<Layout>{children}</Layout>
</div>
</Providers>
</body>
</html>
)
}
-23
View File
@@ -1,23 +0,0 @@
import { Button } from '@/components/Button'
import { Container } from '@/components/Container'
export default function NotFound() {
return (
<Container className="flex h-full items-center pt-16 sm:pt-32">
<div className="flex flex-col items-center">
<p className="text-base font-semibold text-zinc-400 dark:text-zinc-500">
404
</p>
<h1 className="mt-4 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-400">
Sorry, we couldnt find the page youre looking for.
</p>
<Button href="/" variant="secondary" className="mt-4">
Go back home
</Button>
</div>
</Container>
)
}
-328
View File
@@ -1,328 +0,0 @@
import Image, { type ImageProps } from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { Button } from '@/components/Button'
import { Card } from '@/components/Card'
import { Container } from '@/components/Container'
import {
GitHubIcon,
InstagramIcon,
LinkedInIcon,
MediumIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import logoAirbnb from '@/images/logos/spacebolt.svg'
import logoFacebook from '@/images/logos/voluntree.svg'
import logoPlanetaria from '@/images/logos/sonr.svg'
import logoStarbucks from '@/images/logos/superball.svg'
import image1 from '@/images/photos/image-1.jpg'
import image2 from '@/images/photos/image-2.jpg'
import image3 from '@/images/photos/image-3.jpg'
import image4 from '@/images/photos/image-4.jpg'
import image5 from '@/images/photos/image-5.jpg'
import { type ArticleWithSlug, getAllArticles } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path
d="M2.75 7.75a3 3 0 0 1 3-3h12.5a3 3 0 0 1 3 3v8.5a3 3 0 0 1-3 3H5.75a3 3 0 0 1-3-3v-8.5Z"
className="fill-zinc-100 stroke-zinc-400 dark:fill-zinc-100/10 dark:stroke-zinc-500"
/>
<path
d="m4 6 6.024 5.479a2.915 2.915 0 0 0 3.952 0L20 6"
className="stroke-zinc-400 dark:stroke-zinc-500"
/>
</svg>
)
}
function BriefcaseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path
d="M2.75 9.75a3 3 0 0 1 3-3h12.5a3 3 0 0 1 3 3v8.5a3 3 0 0 1-3 3H5.75a3 3 0 0 1-3-3v-8.5Z"
className="fill-zinc-100 stroke-zinc-400 dark:fill-zinc-100/10 dark:stroke-zinc-500"
/>
<path
d="M3 14.25h6.249c.484 0 .952-.002 1.316.319l.777.682a.996.996 0 0 0 1.316 0l.777-.682c.364-.32.832-.319 1.316-.319H21M8.75 6.5V4.75a2 2 0 0 1 2-2h2.5a2 2 0 0 1 2 2V6.5"
className="stroke-zinc-400 dark:stroke-zinc-500"
/>
</svg>
)
}
function ArrowDownIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
d="M4.75 8.75 8 12.25m0 0 3.25-3.5M8 12.25v-8.5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function Article({ article }: { article: ArticleWithSlug }) {
return (
<Card as="article">
<Card.Title href={`/articles/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow as="time" dateTime={article.date} decorate>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
)
}
function SocialLink({
icon: Icon,
...props
}: React.ComponentPropsWithoutRef<typeof Link> & {
icon: React.ComponentType<{ className?: string }>
}) {
return (
<Link className="group -m-1 p-1" {...props}>
<Icon className="h-6 w-6 fill-zinc-500 transition group-hover:fill-zinc-600 dark:fill-zinc-400 dark:group-hover:fill-zinc-300" />
</Link>
)
}
function Newsletter() {
return (
<form
action="/thank-you"
className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40"
>
<h2 className="flex text-sm font-semibold text-zinc-900 dark:text-zinc-100">
<MailIcon className="h-6 w-6 flex-none" />
<span className="ml-3">Stay up to date</span>
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
Get notified about updates on Sonr, blockchain technology insights, and decentralized identity developments.
</p>
<div className="mt-6 flex">
<input
type="email"
placeholder="Email address"
aria-label="Email address"
required
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
/>
<Button type="submit" className="ml-4 flex-none">
Join
</Button>
</div>
</form>
)
}
interface Role {
company: string
title: string
logo: ImageProps['src']
start: string | { label: string; dateTime: string }
end: string | { label: string; dateTime: string }
}
function Role({ role }: { role: Role }) {
let startLabel =
typeof role.start === 'string' ? role.start : role.start.label
let startDate =
typeof role.start === 'string' ? role.start : role.start.dateTime
let endLabel = typeof role.end === 'string' ? role.end : role.end.label
let endDate = typeof role.end === 'string' ? role.end : role.end.dateTime
return (
<li className="flex gap-4">
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-full shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image src={role.logo} alt="" className="h-7 w-7 rounded-full" unoptimized />
</div>
<dl className="flex flex-auto flex-wrap gap-x-2">
<dt className="sr-only">Company</dt>
<dd className="w-full flex-none text-sm font-medium text-zinc-900 dark:text-zinc-100">
{role.company}
</dd>
<dt className="sr-only">Role</dt>
<dd className="text-xs text-zinc-500 dark:text-zinc-400">
{role.title}
</dd>
<dt className="sr-only">Date</dt>
<dd
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
aria-label={`${startLabel} until ${endLabel}`}
>
<time dateTime={startDate}>{startLabel}</time>{' '}
<span aria-hidden="true"></span>{' '}
<time dateTime={endDate}>{endLabel}</time>
</dd>
</dl>
</li>
)
}
function Resume() {
let resume: Array<Role> = [
{
company: 'Sonr',
title: 'Founder & CEO',
logo: logoPlanetaria,
start: '2020',
end: {
label: 'Present',
dateTime: new Date().getFullYear().toString(),
},
},
{
company: 'W3C Working Group',
title: 'Member - DIDs, WebAuthn, WASM',
logo: logoAirbnb,
start: '2021',
end: {
label: 'Present',
dateTime: new Date().getFullYear().toString(),
},
},
{
company: 'Open Source',
title: '4th Most GitHub Contributions Worldwide',
logo: logoFacebook,
start: '2021',
end: '2023',
},
{
company: 'Early iOS Developer',
title: 'One of the first 5,000 App Store apps',
logo: logoStarbucks,
start: '2008',
end: '2012',
},
]
return (
<div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40">
<h2 className="flex text-sm font-semibold text-zinc-900 dark:text-zinc-100">
<BriefcaseIcon className="h-6 w-6 flex-none" />
<span className="ml-3">Experience</span>
</h2>
<ol className="mt-6 space-y-4">
{resume.map((role, roleIndex) => (
<Role key={roleIndex} role={role} />
))}
</ol>
<Button href="#" variant="secondary" className="group mt-6 w-full">
Download CV
<ArrowDownIcon className="h-4 w-4 stroke-zinc-400 transition group-active:stroke-zinc-600 dark:group-hover:stroke-zinc-50 dark:group-active:stroke-zinc-50" />
</Button>
</div>
)
}
function Photos() {
let rotations = ['rotate-2', '-rotate-2', 'rotate-2', 'rotate-2', '-rotate-2']
return (
<div className="mt-16 sm:mt-20">
<div className="-my-4 flex justify-center gap-5 overflow-hidden py-4 sm:gap-8">
{[image1, image2, image3, image4, image5].map((image, imageIndex) => (
<div
key={image.src}
className={clsx(
'relative aspect-[9/10] w-44 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800 sm:w-72 sm:rounded-2xl',
rotations[imageIndex % rotations.length],
)}
>
<Image
src={image}
alt=""
sizes="(min-width: 640px) 18rem, 11rem"
className="absolute inset-0 h-full w-full object-cover"
/>
</div>
))}
</div>
</div>
)
}
export default async function Home() {
let articles = (await getAllArticles()).slice(0, 4)
return (
<>
<Container className="mt-9">
<div className="max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Blockchain pioneer, founder, and decentralized identity architect.
</h1>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
I\'m Prad Nukala, founder and CEO of Sonr, an IBC-enabled blockchain for decentralized identity.
As a W3C Working Group Member for DIDs, WebAuthn, and WASM, I\'m building the future where users
control their digital identity and assets. With over 6000+ GitHub contributions, I\'m passionate
about making blockchain technology accessible to everyone.
</p>
<div className="mt-6 flex gap-6">
<SocialLink
href="https://twitter.com/thisisprad"
aria-label="Follow on Twitter"
icon={TwitterIcon}
/>
<SocialLink
href="https://medium.com/@prnk28"
aria-label="Follow on Medium"
icon={MediumIcon}
/>
<SocialLink
href="https://github.com/prnk28"
aria-label="Follow on GitHub"
icon={GitHubIcon}
/>
<SocialLink
href="https://linkedin.com/in/pradn"
aria-label="Follow on LinkedIn"
icon={LinkedInIcon}
/>
</div>
</div>
</Container>
<Photos />
<Container className="mt-24 md:mt-28">
<div className="mx-auto grid max-w-xl grid-cols-1 gap-y-20 lg:max-w-none lg:grid-cols-2">
<div className="flex flex-col gap-16">
{articles.map((article) => (
<Article key={article.slug} article={article} />
))}
</div>
<div className="space-y-10 lg:pl-16 xl:pl-24">
<Newsletter />
<Resume />
</div>
</div>
</Container>
</>
)
}
-118
View File
@@ -1,118 +0,0 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import logoSuperball from '@/images/logos/superball.svg'
import logoVoluntree from '@/images/logos/voluntree.svg'
import logoSpacebolt from '@/images/logos/spacebolt.svg'
import logoYue from '@/images/logos/yue.svg'
import logoSonr from '@/images/logos/sonr.svg'
const projects = [
{
name: 'Sonr',
created: '2020 - Present',
description:
'IBC-enabled blockchain for decentralized identity. Features passkey-based accounts, DKLS-MPC wallets, and 600ms wallet generation.',
link: { href: 'https://sonr.io', label: 'sonr.io' },
logo: logoSonr,
},
{
name: 'DID & WebAuthn Standards',
created: '2021 - Present',
description:
'Contributing to W3C Working Groups for DIDs, WebAuthn, and WASM standards that define the future of digital identity.',
link: { href: 'https://www.w3.org/TR/did-core/', label: 'w3.org' },
logo: logoYue,
},
// {
// name: 'ML Livestock Detection',
// description:
// 'Real-time video streaming library, optimized for interstellar transmission.',
// link: { href: '#', label: 'github.com' },
// logo: logoHelioStream,
// },
// {
// name: 'AR Whiteboard',
// description:
// 'The operating system that powers our Sonr space shuttles.',
// link: { href: '#', label: 'github.com' },
// logo: logoCosmos,
// },
{
name: 'Spacebolt',
created: '2017 - 2018',
description:
'Unity-based mobile game featuring innovative physics-based gameplay. Part of my journey in mobile development.',
link: { href: 'https://github.com/prnk28', label: 'github.com' },
logo: logoSpacebolt,
},
{
name: 'VolunTree',
created: '2015 - 2016',
description:
'iOS app for tracking volunteer hours and connecting volunteers with organizations. Simplified community service engagement.',
link: { href: 'https://github.com/prnk28', label: 'github.com' },
logo: logoVoluntree,
},
{
name: 'Superball',
created: '2010 - 2011',
description:
'One of the first 10,000 apps published on the App Store. A physics-based game that started my journey at age 12.',
link: { href: 'https://github.com/prnk28', label: 'github.com' },
logo: logoSuperball,
},
]
function LinkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="M15.712 11.823a.75.75 0 1 0 1.06 1.06l-1.06-1.06Zm-4.95 1.768a.75.75 0 0 0 1.06-1.06l-1.06 1.06Zm-2.475-1.414a.75.75 0 1 0-1.06-1.06l1.06 1.06Zm4.95-1.768a.75.75 0 1 0-1.06 1.06l1.06-1.06Zm3.359.53-.884.884 1.06 1.06.885-.883-1.061-1.06Zm-4.95-2.12 1.414-1.415L12 6.344l-1.415 1.413 1.061 1.061Zm0 3.535a2.5 2.5 0 0 1 0-3.536l-1.06-1.06a4 4 0 0 0 0 5.656l1.06-1.06Zm4.95-4.95a2.5 2.5 0 0 1 0 3.535L17.656 12a4 4 0 0 0 0-5.657l-1.06 1.06Zm1.06-1.06a4 4 0 0 0-5.656 0l1.06 1.06a2.5 2.5 0 0 1 3.536 0l1.06-1.06Zm-7.07 7.07.176.177 1.06-1.06-.176-.177-1.06 1.06Zm-3.183-.353.884-.884-1.06-1.06-.884.883 1.06 1.06Zm4.95 2.121-1.414 1.414 1.06 1.06 1.415-1.413-1.06-1.061Zm0-3.536a2.5 2.5 0 0 1 0 3.536l1.06 1.06a4 4 0 0 0 0-5.656l-1.06 1.06Zm-4.95 4.95a2.5 2.5 0 0 1 0-3.535L6.344 12a4 4 0 0 0 0 5.656l1.06-1.06Zm-1.06 1.06a4 4 0 0 0 5.657 0l-1.061-1.06a2.5 2.5 0 0 1-3.535 0l-1.061 1.06Zm7.07-7.07-.176-.177-1.06 1.06.176.178 1.06-1.061Z"
fill="currentColor"
/>
</svg>
)
}
export const metadata: Metadata = {
title: 'Projects',
description: 'Blockchain innovations and decentralized identity solutions I\'ve built to empower users.',
}
export default function Projects() {
return (
<SimpleLayout
title="Building the infrastructure for a decentralized future."
intro="From creating one of the first App Store apps at age 12 to founding Sonr, I\\'ve developed over 10 projects with 1M+ downloads. These represent my journey from mobile development to blockchain innovation, all focused on empowering users with better technology."
>
<ul
role="list"
className="grid grid-cols-1 gap-x-12 gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
>
{projects.map((project) => (
<Card as="li" key={project.name}>
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image
src={project.logo}
alt=""
className="h-8 w-8 rounded-full"
unoptimized
/>
</div>
<h2 className="mt-6 text-base font-semibold text-zinc-800 dark:text-zinc-100">
<Card.Link href={project.link.href}>{project.name}</Card.Link>
</h2>
<Card.Description>{project.description}</Card.Description>
<p className="relative z-10 mt-6 flex text-sm font-medium text-zinc-400 transition group-hover:text-teal-500 dark:text-zinc-200">
<LinkIcon className="h-6 w-6 flex-none" />
<span className="ml-2">{project.link.label}</span>
</p>
</Card>
))}
</ul>
</SimpleLayout>
)
}
-55
View File
@@ -1,55 +0,0 @@
'use client'
import { createContext, useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'
import { ThemeProvider, useTheme } from 'next-themes'
function usePrevious<T>(value: T) {
let ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
function ThemeWatcher() {
let { resolvedTheme, setTheme } = useTheme()
useEffect(() => {
let media = window.matchMedia('(prefers-color-scheme: dark)')
function onMediaChange() {
let systemTheme = media.matches ? 'dark' : 'light'
if (resolvedTheme === systemTheme) {
setTheme('system')
}
}
onMediaChange()
media.addEventListener('change', onMediaChange)
return () => {
media.removeEventListener('change', onMediaChange)
}
}, [resolvedTheme, setTheme])
return null
}
export const AppContext = createContext<{ previousPathname?: string }>({})
export function Providers({ children }: { children: React.ReactNode }) {
let pathname = usePathname()
let previousPathname = usePrevious(pathname)
return (
<AppContext.Provider value={{ previousPathname }}>
<ThemeProvider attribute="class" disableTransitionOnChange>
<ThemeWatcher />
{children}
</ThemeProvider>
</AppContext.Provider>
)
}
-105
View File
@@ -1,105 +0,0 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function SpeakingSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<div className="space-y-16">{children}</div>
</Section>
)
}
function Appearance({
title,
description,
event,
cta,
href,
}: {
title: string
description: string
event: string
cta: string
href: string
}) {
return (
<Card as="article">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Eyebrow decorate>{event}</Card.Eyebrow>
<Card.Description>{description}</Card.Description>
<Card.Cta>{cta}</Card.Cta>
</Card>
)
}
export const metadata: Metadata = {
title: 'Speaking',
description:
'Speaking at prestigious institutions and conferences about blockchain, decentralized identity, and the future of the web.',
}
export default function Speaking() {
return (
<SimpleLayout
title="Sharing the vision of decentralized identity with the world."
intro="From guest lecturing at MIT alongside Vitalik Buterin to speaking at blockchain conferences, I\'m passionate about educating developers and entrepreneurs about the future of digital identity and how blockchain technology can empower users."
>
<div className="space-y-20">
<SpeakingSection title="Guest Lectures & Conferences">
<Appearance
href="#"
title="The Future of Decentralized Identity"
description="Guest lecture at MIT Sloan School alongside Vitalik Buterin, Alex Mashinsky, Anthony Pompliano, and Yat Siu on blockchain innovation and decentralized identity."
event="MIT Sloan School of Management, 2023"
cta="Learn more"
/>
<Appearance
href="#"
title="Building IBC-Enabled Blockchains for Identity"
description="Technical deep-dive into Sonr\'s architecture, showcasing how we leverage IBC for cross-chain identity management and our innovative approach to wallet generation."
event="Cosmos Ecosystem Conference, 2023"
cta="Watch video"
/>
<Appearance
href="#"
title="WebAuthn and the Future of Authentication"
description="Presenting our work with W3C standards for DIDs and WebAuthn, demonstrating how passkey-based authentication can replace traditional passwords."
event="W3C Workshop on Web Authentication, 2022"
cta="View presentation"
/>
</SpeakingSection>
<SpeakingSection title="Podcasts & Interviews">
<Appearance
href="#"
title="From App Store Pioneer to Blockchain Innovator"
description="My journey from creating one of the first 5,000 iOS apps at age 12 to founding Sonr and becoming the 4th most active GitHub contributor worldwide."
event="The Blockchain Founders Podcast, 2023"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Raising $4.7M to Build Decentralized Identity"
description="How we raised funding for Sonr, onboarded 120+ first-time blockchain developers, and our vision for making blockchain accessible to everyone."
event="Web3 Startup Stories, 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="The Technical Architecture of Self-Sovereign Identity"
description="Deep technical discussion on DID documents, IPFS integration, UCAN tokens, and our approach to eliminating seed phrases with DKLS-MPC wallets."
event="Decentralized Tech Talks, 2022"
cta="Listen to podcast"
/>
</SpeakingSection>
</div>
</SimpleLayout>
)
}
-17
View File
@@ -1,17 +0,0 @@
import { type Metadata } from 'next'
import { SimpleLayout } from '@/components/SimpleLayout'
export const metadata: Metadata = {
title: 'You\'re subscribed',
description: 'Thanks for subscribing to my newsletter.',
}
export default function ThankYou() {
return (
<SimpleLayout
title="Thanks for subscribing."
intro="I\'ll send you an email any time I publish a new blog post, release a new project, or have anything interesting to share that I think you\'d want to hear about. You can unsubscribe at any time, no hard feelings."
/>
)
}
-151
View File
@@ -1,151 +0,0 @@
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function ToolsSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<ul role="list" className="space-y-16">
{children}
</ul>
</Section>
)
}
function Tool({
title,
href,
children,
}: {
title: string
href?: string
children: React.ReactNode
}) {
return (
<Card as="li">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Description>{children}</Card.Description>
</Card>
)
}
export const metadata = {
title: 'Uses',
description: 'Tools and technologies I use for blockchain development, building Sonr, and staying productive.',
}
export default function Uses() {
return (
<SimpleLayout
title="Tools for building the future of decentralized identity."
intro="From blockchain development to managing a startup, here are the tools and technologies that power my work at Sonr and help me maintain my position as one of the world\'s most active open-source contributors."
>
<div className="space-y-20">
<ToolsSection title="Workstation">
<Tool title='16" MacBook Pro, M2 Max, 96GB RAM'>
When you\'re compiling blockchain nodes and running multiple test networks locally,
RAM is everything. This machine handles everything I throw at it, from Rust compilation
to running full Cosmos SDK nodes.
</Tool>
<Tool title="LG UltraFine 5K Display (x2)">
Dual 5K displays give me the screen real estate I need for coding, monitoring
blockchain metrics, and keeping up with the constant stream of GitHub notifications
from maintaining one of the world\'s most active contribution streaks.
</Tool>
<Tool title="Keychron Q1 with Gateron Brown Switches">
Mechanical keyboards aren\'t just for gaming. The tactile feedback helps me maintain
accuracy during long coding sessions, crucial when every character matters in smart contracts.
</Tool>
<Tool title="Herman Miller Embody Chair">
After years of 12+ hour coding days, investing in proper ergonomics isn\'t optional.
This chair has been perfect for those long sessions building Sonr\'s infrastructure.
</Tool>
</ToolsSection>
<ToolsSection title="Development Tools">
<Tool title="VS Code with Rust Analyzer">
Essential for Cosmos SDK development. The Rust analyzer extension is particularly
crucial for blockchain development where type safety can prevent costly errors.
</Tool>
<Tool title="Warp Terminal">
The AI-powered terminal that understands context. Perfect for managing multiple
blockchain nodes, SSH sessions, and the complex command sequences involved in
Cosmos SDK development.
</Tool>
<Tool title="Docker & Kubernetes">
Running local testnets and managing blockchain infrastructure requires serious
containerization. Docker for local development, K8s for production deployments.
</Tool>
<Tool title="Lens IDE">
The best Kubernetes IDE I\'ve found. Essential for managing Sonr\'s infrastructure
across multiple environments and monitoring our validator nodes.
</Tool>
<Tool title="Tendermint/CometBFT Tools">
The entire Cosmos SDK toolkit is essential for building IBC-enabled chains.
From Ignite CLI to CosmWasm, these tools form the backbone of Sonr\'s development.
</Tool>
</ToolsSection>
<ToolsSection title="Blockchain & Web3 Tools">
<Tool title="Keplr Wallet">
The premier wallet for Cosmos ecosystem development. Essential for testing
IBC transactions and interacting with Sonr\'s testnet.
</Tool>
<Tool title="Remix IDE">
While Sonr uses Cosmos SDK, understanding EVM is crucial. Remix helps me
stay current with Ethereum development and test cross-chain scenarios.
</Tool>
<Tool title="IPFS Desktop">
Since Sonr integrates IPFS for distributed storage, having a local IPFS
node is essential for development and testing.
</Tool>
<Tool title="Postman">
API testing is crucial when building blockchain infrastructure. Postman
helps me test our REST and gRPC endpoints thoroughly.
</Tool>
</ToolsSection>
<ToolsSection title="Design & Documentation">
<Tool title="Figma">
Designing intuitive blockchain UX requires constant iteration. Figma helps
us prototype wallet interfaces and visualize complex identity flows.
</Tool>
<Tool title="Excalidraw">
Perfect for sketching blockchain architectures and explaining complex
technical concepts. Most of our technical documentation starts here.
</Tool>
<Tool title="Notion">
Our entire company knowledge base lives in Notion. From technical specs
to meeting notes, it\'s our single source of truth.
</Tool>
</ToolsSection>
<ToolsSection title="Productivity & Communication">
<Tool title="Raycast">
More than just an app launcher - it\'s my command center. Custom scripts
for blockchain operations, quick access to documentation, and AI features
for code generation.
</Tool>
<Tool title="Linear">
Issue tracking designed for modern software teams. Perfect for managing
Sonr\'s development across multiple repositories and coordinating with our
distributed team.
</Tool>
<Tool title="Cal.com">
Open-source scheduling that respects privacy. Important when you\'re building
privacy-first technology and need to practice what you preach.
</Tool>
<Tool title="Discord">
Where the blockchain community lives. Essential for staying connected with
the Cosmos ecosystem and supporting our developer community.
</Tool>
<Tool title="GitHub Copilot">
AI pair programming at its finest. Particularly helpful when working with
new Cosmos SDK modules or implementing complex cryptographic functions.
</Tool>
</ToolsSection>
</div>
</SimpleLayout>
)
}
+25
View File
@@ -0,0 +1,25 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# assets
## Purpose
Build-time assets imported by source code (processed by Astro/Vite), as opposed to `public/` which is served verbatim.
## Key Files
| File | Description |
|------|-------------|
| `roboto-mono-regular.ttf` | Font (weight 400) embedded into Satori-generated OG images. |
| `roboto-mono-700.ttf` | Font (weight 700) for OG image titles. |
## For AI Agents
### Working In This Directory
- These fonts are imported as raw buffers in `src/pages/og-image/[...slug].png.ts` (via the `vite-plugin-raw-fonts` config in `astro.config.ts`). Renaming requires updating that import.
## Dependencies
### Internal
- `src/pages/og-image/` — consumes these fonts.
<!-- MANUAL: -->
Binary file not shown.
Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# components
## Purpose
Reusable `.astro` components for layout, content rendering, theming, search, and SEO.
## Key Files
| File | Description |
|------|-------------|
| `BaseHead.astro` | All `<head>` tags: title, meta, canonical, Open Graph/Twitter, favicons, RSS auto-discovery, webmentions. |
| `ExperienceTags.astro` | Renders linked chips for an entry's `experiences` refs; resolves them with `getEntries` and links to `/experience/<id>`. Used by Masthead, Project, and the speaking page. |
| `YouTube.astro` | Responsive YouTube embed (`youtube-nocookie.com/embed/<id>`). Used on speaking detail pages. No external player dependency. |
| `FormattedDate.astro` | `<time>` element formatted per `siteConfig.date`. |
| `Paginator.astro` | Prev/next pagination links. |
| `Search.astro` | Pagefind search UI (client-side, works only after build). |
| `ThemeProvider.astro` / `ThemeToggle.astro` | Dark/light mode state + toggle button. |
| `SkipLink.astro` | Accessibility skip-to-content link. |
## Subdirectories
| Directory | Purpose |
|-----------|---------|
| `blog/` | Writing/post rendering: masthead, preview, table of contents, webmentions (see `blog/AGENTS.md`). |
| `layout/` | Site chrome: header + footer (see `layout/AGENTS.md`). |
| `project/` | Project rendering (see `project/AGENTS.md`). |
## For AI Agents
### Working In This Directory
- Components consume Content Collection types like `CollectionEntry<"writing">`; keep prop types in sync with `src/content.config.ts`.
- A CSS `filter` (e.g. `grayscale`) on an element makes it a containing block for absolutely-positioned children — this matters for the header logo (see `layout/AGENTS.md`).
### Common Patterns
- Polymorphic `as` prop (via `astro/types` `Polymorphic`) for heading-level control (e.g. `PostPreview`, `Project`).
- `data-pagefind-body` marks content for the search index.
## Dependencies
### External
- `astro-icon` (`mdi` icon set) for SVG icons.
<!-- MANUAL: -->
-70
View File
@@ -1,70 +0,0 @@
'use client'
import { useContext } from 'react'
import { useRouter } from 'next/navigation'
import { AppContext } from '@/app/providers'
import { Container } from '@/components/Container'
import { Prose } from '@/components/Prose'
import { type ArticleWithSlug } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
function ArrowLeftIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
d="M7.25 11.25 3.75 8m0 0 3.5-3.25M3.75 8h8.5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function ArticleLayout({
article,
children,
}: {
article: ArticleWithSlug
children: React.ReactNode
}) {
let router = useRouter()
let { previousPathname } = useContext(AppContext)
return (
<Container className="mt-16 lg:mt-32">
<div className="xl:relative">
<div className="mx-auto max-w-2xl">
{previousPathname && (
<button
type="button"
onClick={() => router.back()}
aria-label="Go back to articles"
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
>
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
</button>
)}
<article>
<header className="flex flex-col">
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{article.title}
</h1>
<time
dateTime={article.date}
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
>
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
<span className="ml-3">{formatDate(article.date)}</span>
</time>
</header>
<Prose className="mt-8" data-mdx-content>
{children}
</Prose>
</article>
</div>
</div>
</Container>
)
}
+103
View File
@@ -0,0 +1,103 @@
---
import { WEBMENTION_PINGBACK, WEBMENTION_URL } from "astro:env/client";
import { siteConfig, socialLinks } from "@/site.config";
import type { SiteMeta } from "@/types";
import "@/styles/global.css";
type Props = SiteMeta;
const { articleDate, description, keywords, ogImage, schema, title } = Astro.props;
const titleSeparator = "•";
const siteTitle = `${title} ${titleSeparator} ${siteConfig.title}`;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const socialImageURL = new URL(ogImage ? ogImage : "/social-card.png", Astro.url).href;
const xLink = socialLinks.find((s) => s.friendlyName === "X")?.link;
const twitterHandle = xLink ? `@${xLink.split("/").pop()}` : undefined;
const schemas = schema ? (Array.isArray(schema) ? schema : [schema]) : [];
---
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>{siteTitle}</title>
{/* Icons */}
<link href="/icon.svg" rel="icon" type="image/svg+xml" />
{
import.meta.env.PROD && (
<>
{/* Favicon & Apple Icon, Generated at build */}
<link rel="icon" href="/favicon-32x32.png" type="image/png" />
<link href="/icons/apple-touch-icon.png" rel="apple-touch-icon" />
{/* Manifest */}
<link href="/manifest.webmanifest" rel="manifest" />
</>
)
}
{/* Canonical URL */}
<link href={canonicalURL} rel="canonical" />
{/* Primary Meta Tags */}
<meta content={siteTitle} name="title" />
<meta content={description} name="description" />
{keywords && keywords.length > 0 && <meta content={keywords.join(", ")} name="keywords" />}
<meta content={siteConfig.author} name="author" />
{/* Tell crawlers to index and allow full image/snippet/video previews */}
<meta
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
name="robots"
/>
{/* Open Graph / Facebook */}
<meta content={articleDate ? "article" : "website"} property="og:type" />
<meta content={title} property="og:title" />
<meta content={description} property="og:description" />
<meta content={canonicalURL} property="og:url" />
<meta content={siteConfig.title} property="og:site_name" />
<meta content={siteConfig.ogLocale} property="og:locale" />
<meta content={socialImageURL} property="og:image" />
<meta content={`${title} — ${siteConfig.title}`} property="og:image:alt" />
<meta content="1200" property="og:image:width" />
<meta content="630" property="og:image:height" />
{
articleDate && (
<>
<meta content={siteConfig.author} property="article:author" />
<meta content={articleDate} property="article:published_time" />
</>
)
}
{/* Twitter */}
<meta content="summary_large_image" property="twitter:card" />
<meta content={canonicalURL} property="twitter:url" />
<meta content={title} property="twitter:title" />
<meta content={description} property="twitter:description" />
<meta content={socialImageURL} property="twitter:image" />
{twitterHandle && <meta content={twitterHandle} property="twitter:creator" />}
{twitterHandle && <meta content={twitterHandle} property="twitter:site" />}
{/* Structured data (schema.org JSON-LD) */}
{schemas.map((s) => <script type="application/ld+json" is:inline set:html={JSON.stringify(s)} />)}
{/* Sitemap */}
<link href="/sitemap-index.xml" rel="sitemap" />
{/* RSS auto-discovery */}
<link href="/rss.xml" title="Writing" rel="alternate" type="application/rss+xml" />
<link href="/projects/rss.xml" title="Projects" rel="alternate" type="application/rss+xml" />
{/* Webmentions */}
{
WEBMENTION_URL && (
<>
<link href={WEBMENTION_URL} rel="webmention" />
{WEBMENTION_PINGBACK && <link href={WEBMENTION_PINGBACK} rel="pingback" />}
</>
)
}
<meta content={Astro.generator} name="generator" />
-34
View File
@@ -1,34 +0,0 @@
import Link from 'next/link'
import clsx from 'clsx'
const variantStyles = {
primary:
'bg-zinc-800 font-semibold text-zinc-100 hover:bg-zinc-700 active:bg-zinc-800 active:text-zinc-100/70 dark:bg-zinc-700 dark:hover:bg-zinc-600 dark:active:bg-zinc-700 dark:active:text-zinc-100/70',
secondary:
'bg-zinc-50 font-medium text-zinc-900 hover:bg-zinc-100 active:bg-zinc-100 active:text-zinc-900/60 dark:bg-zinc-800/50 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:active:bg-zinc-800/50 dark:active:text-zinc-50/70',
}
type ButtonProps = {
variant?: keyof typeof variantStyles
} & (
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
| React.ComponentPropsWithoutRef<typeof Link>
)
export function Button({
variant = 'primary',
className,
...props
}: ButtonProps) {
className = clsx(
'inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none',
variantStyles[variant],
className,
)
return typeof props.href === 'undefined' ? (
<button className={className} {...props} />
) : (
<Link className={className} {...props} />
)
}
-124
View File
@@ -1,124 +0,0 @@
import Link from 'next/link'
import clsx from 'clsx'
function ChevronRightIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
d="M6.75 5.75 9.25 8l-2.5 2.25"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function Card<T extends React.ElementType = 'div'>({
as,
className,
children,
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'className'> & {
as?: T
className?: string
}) {
let Component = as ?? 'div'
return (
<Component
className={clsx(className, 'group relative flex flex-col items-start')}
>
{children}
</Component>
)
}
Card.Link = function CardLink({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Link>) {
return (
<>
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
<Link {...props}>
<span className="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span className="relative z-10">{children}</span>
</Link>
</>
)
}
Card.Title = function CardTitle<T extends React.ElementType = 'h2'>({
as,
href,
children,
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'href'> & {
as?: T
href?: string
}) {
let Component = as ?? 'h2'
return (
<Component className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
</Component>
)
}
Card.Description = function CardDescription({
children,
}: {
children: React.ReactNode
}) {
return (
<p className="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
{children}
</p>
)
}
Card.Cta = function CardCta({ children }: { children: React.ReactNode }) {
return (
<div
aria-hidden="true"
className="relative z-10 mt-4 flex items-center text-sm font-medium text-teal-500"
>
{children}
<ChevronRightIcon className="ml-1 h-4 w-4 stroke-current" />
</div>
)
}
Card.Eyebrow = function CardEyebrow<T extends React.ElementType = 'p'>({
as,
decorate = false,
className,
children,
...props
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'decorate'> & {
as?: T
decorate?: boolean
}) {
let Component = as ?? 'p'
return (
<Component
className={clsx(
className,
'relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500',
decorate && 'pl-3.5',
)}
{...props}
>
{decorate && (
<span
className="absolute inset-y-0 left-0 flex items-center"
aria-hidden="true"
>
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
</span>
)}
{children}
</Component>
)
}
-39
View File
@@ -1,39 +0,0 @@
import { forwardRef } from 'react'
import clsx from 'clsx'
export const ContainerOuter = forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(function OuterContainer({ className, children, ...props }, ref) {
return (
<div ref={ref} className={clsx('sm:px-8', className)} {...props}>
<div className="mx-auto w-full max-w-7xl lg:px-8">{children}</div>
</div>
)
})
export const ContainerInner = forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(function InnerContainer({ className, children, ...props }, ref) {
return (
<div
ref={ref}
className={clsx('relative px-4 sm:px-8 lg:px-12', className)}
{...props}
>
<div className="mx-auto max-w-2xl lg:max-w-5xl">{children}</div>
</div>
)
})
export const Container = forwardRef<
React.ElementRef<typeof ContainerOuter>,
React.ComponentPropsWithoutRef<typeof ContainerOuter>
>(function Container({ children, ...props }, ref) {
return (
<ContainerOuter ref={ref} {...props}>
<ContainerInner>{children}</ContainerInner>
</ContainerOuter>
)
})
+25
View File
@@ -0,0 +1,25 @@
---
import { getEntries } from "astro:content";
import type { CollectionEntry } from "astro:content";
interface Props {
experiences: CollectionEntry<"projects" | "writing" | "speaking">["data"]["experiences"];
}
const { experiences } = Astro.props;
const entries = experiences.length ? await getEntries(experiences) : [];
---
{
entries.length > 0 && (
<ul class="flex flex-wrap gap-2">
{entries.map((exp) => (
<li>
<a class="cactus-link inline-block" href={`/experience/${exp.id}/`}>
{exp.data.title}
</a>
</li>
))}
</ul>
)
}
-45
View File
@@ -1,45 +0,0 @@
import Link from 'next/link'
import { ContainerInner, ContainerOuter } from '@/components/Container'
function NavLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
return (
<Link
href={href}
className="transition hover:text-teal-500 dark:hover:text-teal-400"
>
{children}
</Link>
)
}
export function Footer() {
return (
<footer className="mt-32 flex-none">
<ContainerOuter>
<div className="border-t border-zinc-100 pb-16 pt-10 dark:border-zinc-700/40">
<ContainerInner>
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800 dark:text-zinc-200">
<NavLink href="/about">About</NavLink>
<NavLink href="/projects">Projects</NavLink>
<NavLink href="/speaking">Speaking</NavLink>
<NavLink href="/uses">Uses</NavLink>
</div>
<p className="text-sm text-zinc-400 dark:text-zinc-500">
&copy; {new Date().getFullYear()} Prad Nukala. All rights
reserved.
</p>
</div>
</ContainerInner>
</div>
</ContainerOuter>
</footer>
)
}
+16
View File
@@ -0,0 +1,16 @@
---
import type { HTMLAttributes } from "astro/types";
import { getFormattedDate } from "@/utils/date";
type Props = HTMLAttributes<"time"> & {
date: Date;
dateTimeOptions?: Intl.DateTimeFormatOptions;
};
const { date, dateTimeOptions, ...attrs } = Astro.props;
const postDate = getFormattedDate(date, dateTimeOptions);
const ISO = date.toISOString();
---
<time datetime={ISO} title={ISO} {...attrs}>{postDate}</time>
-461
View File
@@ -1,461 +0,0 @@
'use client'
import { Fragment, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useTheme } from 'next-themes'
import { Popover, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { Container } from '@/components/Container'
import avatarImage from '@/images/avatar.jpg'
function CloseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="m17.25 6.75-10.5 10.5M6.75 6.75l10.5 10.5"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function ChevronDownIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 8 6" aria-hidden="true" {...props}>
<path
d="M1.75 1.75 4 4.25l2.25-2.5"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function SunIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path d="M8 12.25A4.25 4.25 0 0 1 12.25 8v0a4.25 4.25 0 0 1 4.25 4.25v0a4.25 4.25 0 0 1-4.25 4.25v0A4.25 4.25 0 0 1 8 12.25v0Z" />
<path
d="M12.25 3v1.5M21.5 12.25H20M18.791 18.791l-1.06-1.06M18.791 5.709l-1.06 1.06M12.25 20v1.5M4.5 12.25H3M6.77 6.77 5.709 5.709M6.77 17.73l-1.061 1.061"
fill="none"
/>
</svg>
)
}
function MoonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="M17.25 16.22a6.937 6.937 0 0 1-9.47-9.47 7.451 7.451 0 1 0 9.47 9.47ZM12.75 7C17 7 17 2.75 17 2.75S17 7 21.25 7C17 7 17 11.25 17 11.25S17 7 12.75 7Z"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function MobileNavItem({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
return (
<li>
<Popover.Button as={Link} href={href} className="block py-2">
{children}
</Popover.Button>
</li>
)
}
function MobileNavigation(
props: React.ComponentPropsWithoutRef<typeof Popover>,
) {
return (
<Popover {...props}>
<Popover.Button className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
Menu
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
</Popover.Button>
<Transition.Root>
<Transition.Child
as={Fragment}
enter="duration-150 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-150 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Overlay className="fixed inset-0 z-50 bg-zinc-800/40 backdrop-blur-sm dark:bg-black/80" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="duration-150 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-150 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Popover.Panel
focus
className="fixed inset-x-4 top-8 z-50 origin-top rounded-3xl bg-white p-8 ring-1 ring-zinc-900/5 dark:bg-zinc-900 dark:ring-zinc-800"
>
<div className="flex flex-row-reverse items-center justify-between">
<Popover.Button aria-label="Close menu" className="-m-1 p-1">
<CloseIcon className="h-6 w-6 text-zinc-500 dark:text-zinc-400" />
</Popover.Button>
<h2 className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
Navigation
</h2>
</div>
<nav className="mt-6">
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800 dark:divide-zinc-100/5 dark:text-zinc-300">
<MobileNavItem href="/about">About</MobileNavItem>
<MobileNavItem href="/articles">Writing</MobileNavItem>
<MobileNavItem href="/projects">Projects</MobileNavItem>
<MobileNavItem href="/speaking">Speaking</MobileNavItem>
<MobileNavItem href="/uses">Uses</MobileNavItem>
</ul>
</nav>
</Popover.Panel>
</Transition.Child>
</Transition.Root>
</Popover>
)
}
function NavItem({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
let isActive = usePathname() === href
return (
<li>
<Link
href={href}
className={clsx(
'relative block px-3 py-2 transition',
isActive
? 'text-teal-500 dark:text-teal-400'
: 'hover:text-teal-500 dark:hover:text-teal-400',
)}
>
{children}
{isActive && (
<span className="absolute inset-x-1 -bottom-px h-px bg-gradient-to-r from-teal-500/0 via-teal-500/40 to-teal-500/0 dark:from-teal-400/0 dark:via-teal-400/40 dark:to-teal-400/0" />
)}
</Link>
</li>
)
}
function DesktopNavigation(props: React.ComponentPropsWithoutRef<'nav'>) {
return (
<nav {...props}>
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
<NavItem href="/about">About</NavItem>
<NavItem href="/articles">Writing</NavItem>
<NavItem href="/projects">Projects</NavItem>
<NavItem href="/speaking">Speaking</NavItem>
<NavItem href="/uses">Uses</NavItem>
</ul>
</nav>
)
}
function ThemeToggle() {
let { resolvedTheme, setTheme } = useTheme()
let otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark'
let [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return (
<button
type="button"
aria-label={mounted ? `Switch to ${otherTheme} theme` : 'Toggle theme'}
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
onClick={() => setTheme(otherTheme)}
>
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-teal-500 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-teal-600" />
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
</button>
)
}
function clamp(number: number, a: number, b: number) {
let min = Math.min(a, b)
let max = Math.max(a, b)
return Math.min(Math.max(number, min), max)
}
function AvatarContainer({
className,
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
className={clsx(
className,
'h-10 w-10 rounded-full bg-white/90 p-0.5 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:ring-white/10',
)}
{...props}
/>
)
}
function Avatar({
large = false,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<typeof Link>, 'href'> & {
large?: boolean
}) {
return (
<Link
href="/"
aria-label="Home"
className={clsx(className, 'pointer-events-auto')}
{...props}
>
<Image
src={avatarImage}
alt=""
sizes={large ? '4rem' : '2.25rem'}
className={clsx(
'rounded-full bg-zinc-100 object-cover dark:bg-zinc-800',
large ? 'h-16 w-16' : 'h-9 w-9',
)}
priority
/>
</Link>
)
}
export function Header() {
let isHomePage = usePathname() === '/'
let headerRef = useRef<React.ElementRef<'div'>>(null)
let avatarRef = useRef<React.ElementRef<'div'>>(null)
let isInitial = useRef(true)
useEffect(() => {
let downDelay = avatarRef.current?.offsetTop ?? 0
let upDelay = 64
function setProperty(property: string, value: string) {
document.documentElement.style.setProperty(property, value)
}
function removeProperty(property: string) {
document.documentElement.style.removeProperty(property)
}
function updateHeaderStyles() {
if (!headerRef.current) {
return
}
let { top, height } = headerRef.current.getBoundingClientRect()
let scrollY = clamp(
window.scrollY,
0,
document.body.scrollHeight - window.innerHeight,
)
if (isInitial.current) {
setProperty('--header-position', 'sticky')
}
setProperty('--content-offset', `${downDelay}px`)
if (isInitial.current || scrollY < downDelay) {
setProperty('--header-height', `${downDelay + height}px`)
setProperty('--header-mb', `${-downDelay}px`)
} else if (top + height < -upDelay) {
let offset = Math.max(height, scrollY - upDelay)
setProperty('--header-height', `${offset}px`)
setProperty('--header-mb', `${height - offset}px`)
} else if (top === 0) {
setProperty('--header-height', `${scrollY + height}px`)
setProperty('--header-mb', `${-scrollY}px`)
}
if (top === 0 && scrollY > 0 && scrollY >= downDelay) {
setProperty('--header-inner-position', 'fixed')
removeProperty('--header-top')
removeProperty('--avatar-top')
} else {
removeProperty('--header-inner-position')
setProperty('--header-top', '0px')
setProperty('--avatar-top', '0px')
}
}
function updateAvatarStyles() {
if (!isHomePage) {
return
}
let fromScale = 1
let toScale = 36 / 64
let fromX = 0
let toX = 2 / 16
let scrollY = downDelay - window.scrollY
let scale = (scrollY * (fromScale - toScale)) / downDelay + toScale
scale = clamp(scale, fromScale, toScale)
let x = (scrollY * (fromX - toX)) / downDelay + toX
x = clamp(x, fromX, toX)
setProperty(
'--avatar-image-transform',
`translate3d(${x}rem, 0, 0) scale(${scale})`,
)
let borderScale = 1 / (toScale / scale)
let borderX = (-toX + x) * borderScale
let borderTransform = `translate3d(${borderX}rem, 0, 0) scale(${borderScale})`
setProperty('--avatar-border-transform', borderTransform)
setProperty('--avatar-border-opacity', scale === toScale ? '1' : '0')
}
function updateStyles() {
updateHeaderStyles()
updateAvatarStyles()
isInitial.current = false
}
updateStyles()
window.addEventListener('scroll', updateStyles, { passive: true })
window.addEventListener('resize', updateStyles)
return () => {
window.removeEventListener('scroll', updateStyles)
window.removeEventListener('resize', updateStyles)
}
}, [isHomePage])
return (
<>
<header
className="pointer-events-none relative z-50 flex flex-none flex-col"
style={{
height: 'var(--header-height)',
marginBottom: 'var(--header-mb)',
}}
>
{isHomePage && (
<>
<div
ref={avatarRef}
className="order-last mt-[calc(theme(spacing.16)-theme(spacing.3))]"
/>
<Container
className="top-0 order-last -mb-3 pt-3"
style={{
position:
'var(--header-position)' as React.CSSProperties['position'],
}}
>
<div
className="top-[var(--avatar-top,theme(spacing.3))] w-full"
style={{
position:
'var(--header-inner-position)' as React.CSSProperties['position'],
}}
>
<div className="relative">
<AvatarContainer
className="absolute left-0 top-3 origin-left transition-opacity"
style={{
opacity: 'var(--avatar-border-opacity, 0)',
transform: 'var(--avatar-border-transform)',
}}
/>
<Avatar
large
className="block h-16 w-16 origin-left"
style={{ transform: 'var(--avatar-image-transform)' }}
/>
</div>
</div>
</Container>
</>
)}
<div
ref={headerRef}
className="top-0 z-10 h-16 pt-6"
style={{
position:
'var(--header-position)' as React.CSSProperties['position'],
}}
>
<Container
className="top-[var(--header-top,theme(spacing.6))] w-full"
style={{
position:
'var(--header-inner-position)' as React.CSSProperties['position'],
}}
>
<div className="relative flex gap-4">
<div className="flex flex-1">
{!isHomePage && (
<AvatarContainer>
<Avatar />
</AvatarContainer>
)}
</div>
<div className="flex flex-1 justify-end md:justify-center">
<MobileNavigation className="pointer-events-auto md:hidden" />
<DesktopNavigation className="pointer-events-auto hidden md:block" />
</div>
<div className="flex justify-end md:flex-1">
<div className="pointer-events-auto">
<ThemeToggle />
</div>
</div>
</div>
</Container>
</div>
</header>
{isHomePage && (
<div
className="flex-none"
style={{ height: 'var(--content-offset)' }}
/>
)}
</>
)
}
-19
View File
@@ -1,19 +0,0 @@
import { Footer } from '@/components/Footer'
import { Header } from '@/components/Header'
export function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<div className="fixed inset-0 flex justify-center sm:px-8">
<div className="flex w-full max-w-7xl lg:px-8">
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
</div>
</div>
<div className="relative flex w-full flex-col">
<Header />
<main className="flex-auto">{children}</main>
<Footer />
</div>
</>
)
}
+29
View File
@@ -0,0 +1,29 @@
---
import type { PaginationLink } from "@/types";
interface Props {
nextUrl?: PaginationLink;
prevUrl?: PaginationLink;
}
const { nextUrl, prevUrl } = Astro.props;
---
{
(prevUrl || nextUrl) && (
<nav class="mt-8 flex items-center gap-x-4">
{prevUrl && (
<a class="hover:text-accent me-auto py-2" href={prevUrl.url}>
{prevUrl.srLabel && <span class="sr-only">{prevUrl.srLabel}</span>}
{prevUrl.text ? prevUrl.text : "Previous"}
</a>
)}
{nextUrl && (
<a class="hover:text-accent ms-auto py-2" href={nextUrl.url}>
{nextUrl.srLabel && <span class="sr-only">{nextUrl.srLabel}</span>}
{nextUrl.text ? nextUrl.text : "Next"}
</a>
)}
</nav>
)
}
-10
View File
@@ -1,10 +0,0 @@
import clsx from 'clsx'
export function Prose({
className,
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div className={clsx(className, 'prose dark:prose-invert')} {...props} />
)
}
+19
View File
@@ -0,0 +1,19 @@
---
import "@/styles/blocks/search.css";
---
<script>
(() => {
if (import.meta.env.DEV) return;
const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
onIdle(async () => {
await import("@pagefind/component-ui");
});
})();
</script>
<pagefind-config
baseUrl={`${import.meta.env.BASE_URL}`}
bundle-path={`${import.meta.env.BASE_URL.replace(/\/$/, "")}/pagefind/`}></pagefind-config>
<pagefind-modal-trigger id="search" class="ms-auto" hide-shortcut compact></pagefind-modal-trigger>
<pagefind-modal></pagefind-modal>
-28
View File
@@ -1,28 +0,0 @@
import { useId } from 'react'
export function Section({
title,
children,
}: {
title: string
children: React.ReactNode
}) {
let id = useId()
return (
<section
aria-labelledby={id}
className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40"
>
<div className="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
<h2
id={id}
className="text-sm font-semibold text-zinc-800 dark:text-zinc-100"
>
{title}
</h2>
<div className="md:col-span-3">{children}</div>
</div>
</section>
)
}
-25
View File
@@ -1,25 +0,0 @@
import { Container } from '@/components/Container'
export function SimpleLayout({
title,
intro,
children,
}: {
title: string
intro: string
children?: React.ReactNode
}) {
return (
<Container className="mt-16 sm:mt-32">
<header className="max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{title}
</h1>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
{intro}
</p>
</header>
{children && <div className="mt-16 sm:mt-20">{children}</div>}
</Container>
)
}
+3
View File
@@ -0,0 +1,3 @@
<a class="sr-only focus:not-sr-only focus:fixed focus:start-1 focus:top-1.5" href="#main"
>skip to content
</a>
-44
View File
@@ -1,44 +0,0 @@
export function TwitterIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M20.055 7.983c.011.174.011.347.011.523 0 5.338-3.92 11.494-11.09 11.494v-.003A10.755 10.755 0 0 1 3 18.186c.308.038.618.057.928.058a7.655 7.655 0 0 0 4.841-1.733c-1.668-.032-3.13-1.16-3.642-2.805a3.753 3.753 0 0 0 1.76-.07C5.07 13.256 3.76 11.6 3.76 9.676v-.05a3.77 3.77 0 0 0 1.77.505C3.816 8.945 3.288 6.583 4.322 4.737c1.98 2.524 4.9 4.058 8.034 4.22a4.137 4.137 0 0 1 1.128-3.86A3.807 3.807 0 0 1 19 5.274a7.657 7.657 0 0 0 2.475-.98c-.29.934-.9 1.729-1.713 2.233A7.54 7.54 0 0 0 22 5.89a8.084 8.084 0 0 1-1.945 2.093Z" />
</svg>
)
}
export function MediumIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 64 64" aria-hidden="true" {...props}>
<path d="M 18.998047 15 A 17.002 17.002 0 0 0 18.998047 49.003906 A 17.002 17.002 0 0 0 18.998047 15 z M 45.498047 16 A 8.502 16.002 0 0 0 45.498047 48.003906 A 8.502 16.002 0 0 0 45.498047 16 z M 58.5 17 A 3.5 15.002 0 1 0 58.5 47.003906 A 3.5 15.002 0 1 0 58.5 17 z"></path>
</svg>
)
}
export function InstagramIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M12 3c-2.444 0-2.75.01-3.71.054-.959.044-1.613.196-2.185.418A4.412 4.412 0 0 0 4.51 4.511c-.5.5-.809 1.002-1.039 1.594-.222.572-.374 1.226-.418 2.184C3.01 9.25 3 9.556 3 12s.01 2.75.054 3.71c.044.959.196 1.613.418 2.185.23.592.538 1.094 1.039 1.595.5.5 1.002.808 1.594 1.038.572.222 1.226.374 2.184.418C9.25 20.99 9.556 21 12 21s2.75-.01 3.71-.054c.959-.044 1.613-.196 2.185-.419a4.412 4.412 0 0 0 1.595-1.038c.5-.5.808-1.002 1.038-1.594.222-.572.374-1.226.418-2.184.044-.96.054-1.267.054-3.711s-.01-2.75-.054-3.71c-.044-.959-.196-1.613-.419-2.185A4.412 4.412 0 0 0 19.49 4.51c-.5-.5-1.002-.809-1.594-1.039-.572-.222-1.226-.374-2.184-.418C14.75 3.01 14.444 3 12 3Zm0 1.622c2.403 0 2.688.009 3.637.052.877.04 1.354.187 1.67.31.421.163.72.358 1.036.673.315.315.51.615.673 1.035.123.317.27.794.31 1.671.043.95.052 1.234.052 3.637s-.009 2.688-.052 3.637c-.04.877-.187 1.354-.31 1.67-.163.421-.358.72-.673 1.036a2.79 2.79 0 0 1-1.035.673c-.317.123-.794.27-1.671.31-.95.043-1.234.052-3.637.052s-2.688-.009-3.637-.052c-.877-.04-1.354-.187-1.67-.31a2.789 2.789 0 0 1-1.036-.673 2.79 2.79 0 0 1-.673-1.035c-.123-.317-.27-.794-.31-1.671-.043-.95-.052-1.234-.052-3.637s.009-2.688.052-3.637c.04-.877.187-1.354.31-1.67.163-.421.358-.72.673-1.036.315-.315.615-.51 1.035-.673.317-.123.794-.27 1.671-.31.95-.043 1.234-.052 3.637-.052Z" />
<path d="M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6Zm0-7.622a4.622 4.622 0 1 0 0 9.244 4.622 4.622 0 0 0 0-9.244Zm5.884-.182a1.08 1.08 0 1 1-2.16 0 1.08 1.08 0 0 1 2.16 0Z" />
</svg>
)
}
export function GitHubIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.475 2 2 6.588 2 12.253c0 4.537 2.862 8.369 6.838 9.727.5.09.687-.218.687-.487 0-.243-.013-1.05-.013-1.91C7 20.059 6.35 18.957 6.15 18.38c-.113-.295-.6-1.205-1.025-1.448-.35-.192-.85-.667-.013-.68.788-.012 1.35.744 1.538 1.051.9 1.551 2.338 1.116 2.912.846.088-.666.35-1.115.638-1.371-2.225-.256-4.55-1.14-4.55-5.062 0-1.115.387-2.038 1.025-2.756-.1-.256-.45-1.307.1-2.717 0 0 .837-.269 2.75 1.051.8-.23 1.65-.346 2.5-.346.85 0 1.7.115 2.5.346 1.912-1.333 2.75-1.05 2.75-1.05.55 1.409.2 2.46.1 2.716.637.718 1.025 1.628 1.025 2.756 0 3.934-2.337 4.806-4.562 5.062.362.32.675.936.675 1.897 0 1.371-.013 2.473-.013 2.82 0 .268.188.589.688.486a10.039 10.039 0 0 0 4.932-3.74A10.447 10.447 0 0 0 22 12.253C22 6.588 17.525 2 12 2Z"
/>
</svg>
)
}
export function LinkedInIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M18.335 18.339H15.67v-4.177c0-.996-.02-2.278-1.39-2.278-1.389 0-1.601 1.084-1.601 2.205v4.25h-2.666V9.75h2.56v1.17h.035c.358-.674 1.228-1.387 2.528-1.387 2.7 0 3.2 1.778 3.2 4.091v4.715zM7.003 8.575a1.546 1.546 0 01-1.548-1.549 1.548 1.548 0 111.547 1.549zm1.336 9.764H5.666V9.75H8.34v8.589zM19.67 3H4.329C3.593 3 3 3.58 3 4.297v15.406C3 20.42 3.594 21 4.328 21h15.338C20.4 21 21 20.42 21 19.703V4.297C21 3.58 20.4 3 19.666 3h.003z" />
</svg>
)
}
+27
View File
@@ -0,0 +1,27 @@
---
import { slugifyTag } from "@/data/tags";
interface Props {
tags: string[];
class?: string;
}
const { tags, class: className } = Astro.props;
---
{
tags.length > 0 && (
<ul class:list={["flex flex-wrap gap-x-3 gap-y-1", className]}>
{tags.map((tag) => (
<li>
<a
class="text-muted hover:text-link text-xs before:content-['#']"
href={`/skills/${slugifyTag(tag)}/`}
>
{tag}
</a>
</li>
))}
</ul>
)
}
+48
View File
@@ -0,0 +1,48 @@
{/* Inlined to avoid FOUC. This is a parser blocking script. */}
<script is:inline>
const lightModePref = window.matchMedia("(prefers-color-scheme: light)");
function getUserPref() {
const storedTheme = typeof localStorage !== "undefined" && localStorage.getItem("theme");
return storedTheme || (lightModePref.matches ? "light" : "dark");
}
function setTheme(newTheme) {
if (newTheme !== "light" && newTheme !== "dark") {
return console.warn(
`Invalid theme value '${newTheme}' received. Expected 'light' or 'dark'.`,
);
}
const root = document.documentElement;
// root already set to newTheme, exit early
if (newTheme === root.getAttribute("data-theme")) {
return;
}
root.setAttribute("data-theme", newTheme);
if (typeof localStorage !== "undefined") {
localStorage.setItem("theme", newTheme);
}
}
// initial setup
setTheme(getUserPref());
// listen for pageshow event, browser restored page from bfcache
window.addEventListener("pageshow", (e) => {
if (e.persisted) {
setTheme(getUserPref());
}
});
// listen for theme-change custom event, fired in src/components/ThemeToggle.astro
document.addEventListener("theme-change", (e) => {
setTheme(e.detail.theme);
});
// listen for prefers-color-scheme change.
lightModePref.addEventListener("change", (e) => setTheme(e.matches ? "light" : "dark"));
</script>
+90
View File
@@ -0,0 +1,90 @@
<theme-toggle class="ms-2 sm:ms-4">
<button class="hover:text-accent relative h-9 w-9 cursor-pointer p-2" type="button">
<span class="sr-only">Dark Theme</span>
<svg
aria-hidden="true"
class="absolute inset-s-1/2 top-1/2 h-7 w-7 -translate-x-1/2 -translate-y-1/2 scale-100 opacity-100 transition-all dark:scale-0 dark:opacity-0"
fill="none"
focusable="false"
id="sun-svg"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"></path>
<path d="M22 12L23 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
></path>
<path d="M12 2V1" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M12 23V22" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
></path>
<path d="M20 20L19 19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
></path>
<path d="M20 4L19 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
></path>
<path d="M4 20L5 19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
></path>
<path d="M4 4L5 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
></path>
<path d="M1 12L2 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
></path>
</svg>
<svg
aria-hidden="true"
class="absolute inset-s-1/2 top-1/2 h-7 w-7 -translate-x-1/2 -translate-y-1/2 scale-0 opacity-0 transition-all dark:scale-100 dark:opacity-100"
fill="none"
focusable="false"
id="moon-svg"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 0h24v24H0z" fill="none" stroke="none"></path>
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"
></path>
<path d="M17 4a2 2 0 0 0 2 2a2 2 0 0 0 -2 2a2 2 0 0 0 -2 -2a2 2 0 0 0 2 -2"></path>
<path d="M19 11h2m-1 -1v2"></path>
</svg>
</button>
</theme-toggle>
<script>
// Note that if you fire the theme-change event outside of this component, it will not be reflected in the button's aria-checked attribute. You will need to add an event listener if you want that.
import { rootInDarkMode } from "@/utils/domElement";
class ThemeToggle extends HTMLElement {
constructor() {
super();
const button = this.querySelector<HTMLButtonElement>("button");
if (button) {
// set aria role value
button.setAttribute("role", "switch");
button.setAttribute("aria-checked", String(rootInDarkMode()));
// button event
button.addEventListener("click", () => {
// invert theme
let themeChangeEvent = new CustomEvent("theme-change", {
detail: {
theme: rootInDarkMode() ? "light" : "dark",
},
});
// dispatch event -> ThemeProvider.astro
document.dispatchEvent(themeChangeEvent);
// set the aria-checked attribute
button.setAttribute("aria-checked", String(rootInDarkMode()));
});
} else {
console.warn("Theme Toggle: No button found");
}
}
}
customElements.define("theme-toggle", ThemeToggle);
</script>
+19
View File
@@ -0,0 +1,19 @@
---
interface Props {
id: string;
title?: string;
}
const { id, title = "YouTube video player" } = Astro.props;
---
<div class="aspect-video overflow-hidden rounded-md">
<iframe
class="h-full w-full border-0"
src={`https://www.youtube-nocookie.com/embed/${id}`}
title={title}
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen></iframe>
</div>
+32
View File
@@ -0,0 +1,32 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# blog
## Purpose
Components for rendering Writing (the `writing` collection) — post mastheads, list previews, table of contents, and webmentions. (Folder is named `blog` for historical reasons; it serves the `/writing` section.)
## Key Files
| File | Description |
|------|-------------|
| `Masthead.astro` | Post header: cover image, title, date, reading time, updated badge, and `ExperienceTags` (linked experience chips — replaces the old tag links). |
| `PostPreview.astro` | One-line writing entry (date + linked title) used in lists; links to `/writing/<id>`. |
| `TOC.astro` / `TOCHeading.astro` | Table of contents built from rendered headings. |
## Subdirectories
| Directory | Purpose |
|-----------|---------|
| `webmentions/` | Webmention display components (see `webmentions/AGENTS.md`). |
## For AI Agents
### Working In This Directory
- These consume `CollectionEntry<"writing">`.
- `Masthead` renders `data.experiences` via `ExperienceTags`; it no longer links freeform `tags` to a `/tags` route (that route was removed).
## Dependencies
### Internal
- `@/components/ExperienceTags.astro`, `@/components/FormattedDate.astro`, `@/utils/generateToc`.
<!-- MANUAL: -->
+62
View File
@@ -0,0 +1,62 @@
---
import { Image } from "astro:assets";
import type { CollectionEntry } from "astro:content";
import ExperienceTags from "@/components/ExperienceTags.astro";
import FormattedDate from "@/components/FormattedDate.astro";
import TagList from "@/components/TagList.astro";
interface Props {
content: CollectionEntry<"writing">;
readingTime: string;
}
const {
content: { data },
readingTime,
} = Astro.props;
const dateTimeOptions: Intl.DateTimeFormatOptions = {
month: "long",
};
---
{
data.coverImage && (
<div class="mb-6 aspect-video">
<Image
alt={data.coverImage.alt}
layout="constrained"
width={748}
height={420}
priority
src={data.coverImage.src}
/>
</div>
)
}
{data.draft ? <span class="text-base text-red-500">(Draft)</span> : null}
<h1 class="title">
{data.title}
</h1>
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
<p class="font-semibold">
<FormattedDate date={data.publishDate} dateTimeOptions={dateTimeOptions} /> /{" "}
{readingTime}
</p>
{
data.updatedDate && (
<span class="bg-quote/5 text-quote rounded-lg px-2 py-1">
Updated:
<FormattedDate class="ms-1" date={data.updatedDate} dateTimeOptions={dateTimeOptions} />
</span>
)
}
</div>
{
!!data.experiences?.length && (
<div class="mt-3">
<ExperienceTags experiences={data.experiences} />
</div>
)
}
<TagList tags={data.tags} class="mt-3" />
+35
View File
@@ -0,0 +1,35 @@
---
import type { CollectionEntry } from "astro:content";
import type { HTMLTag, Polymorphic } from "astro/types";
import FormattedDate from "@/components/FormattedDate.astro";
type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }> & {
post: CollectionEntry<"writing">;
withDesc?: boolean;
};
const { as: Tag = "div", post, withDesc = false } = Astro.props;
const { externalUrl, publisher } = post.data;
const href = externalUrl ?? `/writing/${post.id}/`;
---
<FormattedDate class="text-muted min-w-30 font-semibold" date={post.data.publishDate} />
<Tag>
{post.data.draft && <span class="text-red-500">(Draft) </span>}
<a
class="cactus-link"
href={href}
{...externalUrl ? { rel: "noopener noreferrer", target: "_blank" } : {}}
>
{post.data.title}
</a>
{
externalUrl && (
<span class="text-muted text-xs whitespace-nowrap">
{" ↗ "}
{publisher ?? "External"}
</span>
)
}
</Tag>
{withDesc && <q class="line-clamp-3 italic">{post.data.description}</q>}
+22
View File
@@ -0,0 +1,22 @@
---
import type { MarkdownHeading } from "astro";
import { generateToc } from "@/utils/generateToc";
import TOCHeading from "./TOCHeading.astro";
interface Props {
headings: MarkdownHeading[];
}
const { headings } = Astro.props;
const toc = generateToc(headings);
---
<details open class="lg:sticky lg:top-12 lg:order-2 lg:-me-32 lg:basis-64">
<summary class="title hover:marker:text-accent cursor-pointer text-lg">Table of Contents</summary>
<nav class="ms-4 lg:w-full">
<ol class="mt-4">
{toc.map((heading) => <TOCHeading heading={heading} />)}
</ol>
</nav>
</details>
+27
View File
@@ -0,0 +1,27 @@
---
import type { TocItem } from "@/utils/generateToc";
interface Props {
heading: TocItem;
}
const {
heading: { children, depth, slug, text },
} = Astro.props;
---
<li class={`${depth > 2 ? "ms-2" : ""}`}>
<a
class={`line-clamp-2 hover:text-accent ${depth <= 2 ? "mt-3" : "mt-2 text-xs"}`}
href={`#${slug}`}><span aria-hidden="true" class="me-0.5">#</span>{text}</a
>
{
!!children.length && (
<ol>
{children.map((subheading) => (
<Astro.self heading={subheading} />
))}
</ol>
)
}
</li>
+26
View File
@@ -0,0 +1,26 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# webmentions
## Purpose
Display [webmentions](https://webmention.io/) (likes and comments) fetched for a post.
## Key Files
| File | Description |
|------|-------------|
| `index.astro` | Entry component; gates rendering on the `WEBMENTION_URL` env var and fetches mentions. |
| `Likes.astro` | Renders "like" webmentions. |
| `Comments.astro` | Renders comment/reply webmentions. |
## For AI Agents
### Working In This Directory
- Webmentions are optional and only active when the `WEBMENTION_*` client env vars are set (see `astro:env/client` usage in `BaseHead.astro`). Fetch/caching logic lives in `@/utils/webmentions.ts`.
## Dependencies
### Internal
- `@/utils/webmentions.ts`, `@/types` (webmention types).
<!-- MANUAL: -->
@@ -0,0 +1,87 @@
---
import { Image } from "astro:assets";
import { Icon } from "astro-icon/components";
import type { WebmentionsChildren } from "@/types";
interface Props {
mentions: WebmentionsChildren[];
}
const { mentions } = Astro.props;
const validComments = ["mention-of", "in-reply-to"];
const comments = mentions.filter(
(mention) => validComments.includes(mention["wm-property"]) && mention.content?.text,
);
---
{
!!comments.length && (
<div>
<p class="text-accent-2 mb-0">
<strong>{comments.length}</strong> Mention{comments.length > 1 ? "s" : ""}
</p>
<ul class="divide-global-text/20 mt-0 divide-y ps-0" role="list">
{comments.map((mention) => (
<li class="p-comment h-cite my-0 flex items-start gap-x-5 py-5">
{mention.author?.photo && mention.author.photo !== "" ? (
mention.author.url && mention.author.url !== "" ? (
<a
class="u-author not-prose ring-global-text hover:ring-link focus-visible:ring-link shrink-0 overflow-hidden rounded-full ring-2 hover:ring-4 focus-visible:ring-4"
href={mention.author.url}
rel="noreferrer"
target="_blank"
title={mention.author.name}
>
<Image
alt={mention.author?.name}
class="u-photo my-0 h-12 w-12"
height={48}
src={mention.author?.photo}
width={48}
/>
</a>
) : (
<Image
alt={mention.author?.name}
class="u-photo my-0 h-12 w-12 rounded-full"
height={48}
src={mention.author?.photo}
width={48}
/>
)
) : null}
<div class="flex-auto">
<div class="p-author h-card flex items-center justify-between gap-x-2">
<p class="p-name text-accent-2 my-0 line-clamp-1 font-semibold">
{mention.author?.name}
</p>
<a
aria-labelledby="cmt-source"
class="u-url not-prose hover:text-link"
href={mention.url}
rel="noreferrer"
target="_blank"
>
<span class="hidden" id="cmt-source">
Visit the source of this webmention
</span>
<Icon
aria-hidden="true"
class="h-5 w-5"
focusable="false"
name="mdi:open-in-new"
/>
</a>
</div>
<p class="comment-content mt-1 mb-0 [word-break:break-word] whitespace-pre-wrap">
{mention.content?.text}
</p>
</div>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,52 @@
---
import { Image } from "astro:assets";
import type { WebmentionsChildren } from "@/types";
interface Props {
mentions: WebmentionsChildren[];
}
const { mentions } = Astro.props;
const MAX_LIKES = 10;
const likes = mentions.filter((mention) => mention["wm-property"] === "like-of");
const likesToShow = likes
.filter((like) => like.author?.photo && like.author.photo !== "")
.slice(0, MAX_LIKES);
---
{
!!likes.length && (
<div>
<p class="text-accent-2 mb-0">
<strong>{likes.length}</strong>
{likes.length > 1 ? " People" : " Person"} liked this
</p>
{!!likesToShow.length && (
<ul class="flex list-none flex-wrap overflow-hidden ps-2" role="list">
{likesToShow.map((like) => (
<li class="p-like h-cite -ms-2">
<a
class="u-url not-prose ring-global-text hover:ring-link focus-visible:ring-link relative inline-block overflow-hidden rounded-full ring-2 hover:z-10 hover:ring-4 focus-visible:z-10 focus-visible:ring-4"
href={like.author?.url}
rel="noreferrer"
target="_blank"
title={like.author?.name}
>
<span class="p-author h-card">
<Image
alt={like.author!.name}
class="u-photo my-0 inline-block h-12 w-12"
height={48}
src={like.author!.photo}
width={48}
/>
</span>
</a>
</li>
))}
</ul>
)}
</div>
)
}
@@ -0,0 +1,23 @@
---
import { getWebmentionsForUrl } from "@/utils/webmentions";
import Comments from "./Comments.astro";
import Likes from "./Likes.astro";
const url = new URL(Astro.url.pathname, Astro.site);
const webMentions = await getWebmentionsForUrl(`${url}`);
// Return if no webmentions
if (!webMentions.length) return;
---
<hr class="border-solid" />
<h2 class="mb-8 before:hidden">Webmentions for this post</h2>
<div class="space-y-10">
<Likes mentions={webMentions} />
<Comments mentions={webMentions} />
</div>
<p class="mt-8">
Responses powered by{" "}
<a href="https://webmention.io" rel="noreferrer" target="_blank">Webmentions</a>
</p>
+30
View File
@@ -0,0 +1,30 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# layout
## Purpose
Site-wide chrome rendered on every page by `src/layouts/Base.astro`.
## Key Files
| File | Description |
|------|-------------|
| `Header.astro` | Top nav: avatar logo, site title, `menuLinks` nav, search, theme toggle, mobile menu button. |
| `Footer.astro` | Three centered rows — nav links, social icon row (from `socialLinks`), copyright — with a top border. |
## For AI Agents
### Working In This Directory
- **Header logo positioning:** the avatar is `position: absolute` and overflows left into the page margin. It is centered on the **header** block (not just the title) — this only works because the logo's containing block is the `<header>` (which is `relative`). Do **not** put a `filter`/`transform`/`position: relative` on the wrapping `<a>`, or it becomes the containing block and the logo mis-centers. The grayscale/hover effect therefore lives on the `<img>`, not the `<a>`.
- Logo geometry was tuned against the header center; if you change the logo size (`sm:size-36`) or header padding (`sm:ps-28`), re-verify vertical centering and the left overflow (`sm:-start-[48px]`).
- `menuLinks` and `socialLinks` come from `src/site.config.ts` — edit there, not here.
### Testing Requirements
- Verify visually at desktop and mobile widths (the logo is inline on mobile, absolute/overflowing on `sm+`). Chrome DevTools MCP works for measuring element rects.
## Dependencies
### Internal
- `@/site.config` (`menuLinks`, `socialLinks`, `siteConfig`), `astro-icon` for social/RSS icons.
<!-- MANUAL: -->
+45
View File
@@ -0,0 +1,45 @@
---
import { Icon } from "astro-icon/components";
import { menuLinks, siteConfig, socialLinks } from "@/site.config";
const year = new Date().getFullYear();
---
<footer
class="text-muted border-global-text/15 mt-auto flex w-full flex-col items-center gap-y-6 border-t pt-8 pb-6 text-center text-sm font-semibold"
>
{/* Row 1: site nav */}
<nav aria-labelledby="footer_links" class="flex flex-wrap justify-center gap-x-1 gap-y-1">
<p id="footer_links" class="sr-only">More on this site</p>
<a class="hover:text-global-text px-3 py-1 hover:underline" href="/skills/">Skills</a>
{
menuLinks.map((link) => (
<a class="hover:text-global-text px-3 py-1 hover:underline" href={link.path}>
{link.title}
</a>
))
}
</nav>
{/* Row 2: social icons */}
<ul class="flex items-center justify-center gap-x-4">
{
socialLinks.map(({ friendlyName, isWebmention, link, name }) => (
<li class="flex">
<a
class="hover:text-link inline-block"
href={link}
rel={`me noreferrer ${isWebmention ? "authn" : ""}`}
target={link.startsWith("mailto:") ? undefined : "_blank"}
>
<Icon aria-hidden="true" class="h-6 w-6" focusable="false" name={name} />
<span class="sr-only">{friendlyName}</span>
</a>
</li>
))
}
</ul>
{/* Row 3: copyright */}
<p class="text-xs font-normal">&copy; {year} {siteConfig.author}</p>
</footer>
+104
View File
@@ -0,0 +1,104 @@
---
import Search from "@/components/Search.astro";
import ThemeToggle from "@/components/ThemeToggle.astro";
import { menuLinks } from "@/site.config";
import { siteConfig } from "../../site.config";
---
<header class="group relative mb-28 flex items-center sm:ps-28" id="main-header">
<div class="flex sm:flex-col">
<a
aria-current={Astro.url.pathname === "/" ? "page" : false}
class="inline-flex items-center sm:inline-block"
href="/"
>
<img
alt={`${siteConfig.title} logo`}
class="me-3 size-14 rounded-full sm:absolute sm:-start-[48px] sm:top-1/2 sm:me-0 sm:size-36 sm:-translate-y-1/2 sm:grayscale sm:hover:filter-none"
height="144"
width="144"
src="/avatar.png"
/>
<span class="text-xl font-bold sm:text-2xl">{siteConfig.title}</span>
</a>
<nav
aria-label="Main menu"
class="bg-global-bg absolute -inset-x-4 top-12 hidden flex-col divide-y px-2 py-4 group-[.menu-open]:z-50 group-[.menu-open]:flex sm:static sm:z-auto sm:-ms-4 sm:mt-1 sm:flex sm:flex-row sm:divide-x sm:divide-y-0 sm:bg-transparent sm:p-0"
id="navigation-menu"
>
{
menuLinks.map((link) => (
<a
aria-current={Astro.url.pathname === link.path ? "page" : false}
class="text-accent px-2 py-4 font-semibold sm:px-4 sm:py-0 sm:underline-offset-2 sm:hover:underline"
href={link.path}
>
{link.title}
</a>
))
}
</nav>
</div>
<Search />
<ThemeToggle />
<mobile-button>
<button
aria-expanded="false"
aria-haspopup="menu"
class="group relative ms-4 h-7 w-7 sm:invisible sm:hidden"
id="toggle-navigation-menu"
type="button"
>
<span class="sr-only">Open main menu</span>
<svg
aria-hidden="true"
class="absolute start-1/2 top-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 transition-all group-aria-expanded:scale-0 group-aria-expanded:opacity-0"
fill="none"
focusable="false"
id="line-svg"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3.75 9h16.5m-16.5 6.75h16.5" stroke-linecap="round" stroke-linejoin="round"
></path>
</svg>
<svg
aria-hidden="true"
class="text-accent absolute start-1/2 top-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 scale-0 opacity-0 transition-all group-aria-expanded:scale-100 group-aria-expanded:opacity-100"
class="text-accent"
fill="none"
focusable="false"
id="cross-svg"
stroke="currentColor"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
</mobile-button>
</header>
<script>
import { toggleClass } from "@/utils/domElement";
class MobileNavBtn extends HTMLElement {
#menuOpen: boolean = false;
connectedCallback() {
const headerEl = document.getElementById("main-header")!;
const mobileButtonEl = this.querySelector<HTMLButtonElement>("button");
mobileButtonEl?.addEventListener("click", () => {
if (headerEl) toggleClass(headerEl, "menu-open");
this.#menuOpen = !this.#menuOpen;
mobileButtonEl.setAttribute("aria-expanded", this.#menuOpen.toString());
});
}
}
customElements.define("mobile-button", MobileNavBtn);
</script>
+25
View File
@@ -0,0 +1,25 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# project
## Purpose
Rendering for the `projects` collection.
## Key Files
| File | Description |
|------|-------------|
| `Project.astro` | Polymorphic component for a project. In `isPreview` mode it renders a compact card linking to `/projects/<id>`; in full mode it shows the title, date, `link`/`repo` buttons, rendered body, and `ExperienceTags`. |
## For AI Agents
### Working In This Directory
- Prop is `project: CollectionEntry<"projects">` (note: the source dir is `src/content/projects`).
- Used by `src/pages/projects/[...page].astro` (preview list), `[...slug].astro` (full), and the homepage.
## Dependencies
### Internal
- `@/components/ExperienceTags.astro`, `@/components/FormattedDate.astro`.
<!-- MANUAL: -->
+67
View File
@@ -0,0 +1,67 @@
---
import { type CollectionEntry, getEntries, render } from "astro:content";
import type { HTMLTag, Polymorphic } from "astro/types";
import TagList from "@/components/TagList.astro";
type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }> & {
project: CollectionEntry<"projects">;
isPreview?: boolean | undefined;
};
const { as: Tag = "div", project, isPreview = false } = Astro.props;
const { Content } = await render(project);
const { title, startDate, endDate, link, repo, tags, experiences } = project.data;
const range = `${startDate.getFullYear()} ${endDate ? endDate.getFullYear() : "Present"}`;
const expEntries = experiences.length ? await getEntries(experiences) : [];
---
<article
class:list={[isPreview && "bg-global-text/5 inline-grid rounded-md px-4 py-3"]}
data-pagefind-body={isPreview ? false : true}
>
<Tag class="title" class:list={{ "text-base": isPreview }}>
{
isPreview ? (
<a class="cactus-link" href={`/projects/${project.id}/`}>
{title}
</a>
) : (
<>{title}</>
)
}
</Tag>
<p class="text-muted text-sm">
<span>{range}</span>{expEntries.length > 0 && (
<span> ({expEntries.map((exp, i) => (
<Fragment>{i > 0 && ", "}<a class="cactus-link" href={`/experience/${exp.id}/`}>{exp.data.title}</a></Fragment>
))})</span>
)}
</p>
{
!isPreview && (link || repo) && (
<div class="mt-2 flex flex-wrap gap-x-4 gap-y-1">
{link && (
<a class="cactus-link" href={link} rel="noopener noreferrer" target="_blank">
Visit →
</a>
)}
{repo && (
<a class="cactus-link" href={repo} rel="noopener noreferrer" target="_blank">
Source →
</a>
)}
</div>
)
}
<div
class="prose prose-sm prose-cactus mt-4 max-w-none [&>p:last-of-type]:mb-0"
class:list={{ "line-clamp-6": isPreview }}
>
<Content />
</div>
{
!isPreview && tags.length > 0 && (
<TagList tags={tags} class="border-global-text/10 mt-4 border-t pt-3" />
)
}
</article>
+105
View File
@@ -0,0 +1,105 @@
import { defineCollection, reference } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";
function removeDupsAndLowerCase(array: string[]) {
return [...new Set(array.map((str) => str.toLowerCase()))];
}
const titleSchema = z.string().max(60);
const baseSchema = z.object({
title: titleSchema,
});
// Experiences double as a taxonomy: projects, writing & speaking reference them
// by id so each experience page can render everything filed under it.
const experienceRefs = z.array(reference("experience")).default([]);
// /writing section.
const writing = defineCollection({
loader: glob({ base: "./src/content/writing", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
baseSchema.extend({
description: z.string(),
coverImage: z
.object({
alt: z.string(),
src: image(),
})
.optional(),
draft: z.boolean().default(false),
ogImage: z.string().optional(),
tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
experiences: experienceRefs,
// External articles (PESOS): link out to the publisher instead of a local page.
externalUrl: z.url().optional(),
publisher: z.string().optional(), // e.g. "Fast Company", "Medium"
publishDate: z
.string()
.or(z.date())
.transform((val) => new Date(val)),
updatedDate: z
.string()
.optional()
.transform((str) => (str ? new Date(str) : undefined)),
pinned: z.boolean().default(false),
}),
});
// /projects section.
const projects = defineCollection({
loader: glob({ base: "./src/content/projects", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
baseSchema.extend({
description: z.string().optional(),
coverImage: z
.object({
alt: z.string(),
src: image(),
})
.optional(),
link: z.url().optional(),
repo: z.url().optional(),
// Skills double as tags for cross-project categorization.
tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
experiences: experienceRefs,
startDate: z.coerce.date(),
endDate: z.coerce.date().optional(), // omit for ongoing projects
}),
});
// Experience = resume entries that also act as the taxonomy for the site.
const experience = defineCollection({
loader: glob({ base: "./src/content/experience", pattern: "**/*.{md,mdx}" }),
schema: baseSchema.extend({
organization: z.string(),
role: z.string().optional(),
location: z.string().optional(),
url: z.url().optional(),
description: z.string().optional(),
// Skills double as site-wide tags (see /tags).
tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
startDate: z.coerce.date(),
endDate: z.coerce.date().optional(), // omit for current/ongoing
}),
});
// Speaking = talks/presentations rendered with an embedded YouTube player.
const speaking = defineCollection({
loader: glob({ base: "./src/content/speaking", pattern: "**/*.{md,mdx}" }),
schema: baseSchema.extend({
description: z.string(),
youtubeId: z.string(), // YouTube video ID
event: z.string().optional(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
experiences: experienceRefs,
publishDate: z
.string()
.or(z.date())
.transform((val) => new Date(val)),
}),
});
export const collections = { writing, projects, experience, speaking };
+27
View File
@@ -0,0 +1,27 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
# content
## Purpose
Markdown/MDX content for the four collections. Each subdirectory is a collection; the filename is the entry id/slug. Schemas live in `src/content.config.ts`.
## Subdirectories
| Directory | Collection key | Route | Notes |
|-----------|----------------|-------|-------|
| `projects/` | `projects` | `/projects` | Things built. Fields: `title`, `startDate`, `endDate?` (omit = ongoing), `description?`, `link?`, `repo?`, `tags?` (skills; browsable site-wide at `/skills/<skill>`), `experiences?`. Sorted by `endDate` desc, ongoing first. |
| `writing/` | `writing` | `/writing` | Posts. Fields incl. `description`, `coverImage?`, `ogImage?`, `tags?`, `experiences?`, `draft`, `pinned`, `externalUrl?`/`publisher?` (PESOS — entry links out to the publisher, no local detail page). |
| `experience/` | `experience` | `/experience` | Resume entries + taxonomy. Fields: `title`, `organization`, `startDate`, `endDate?`, `role?`, `location?`, `url?`, `description?`. |
| `speaking/` | `speaking` | `/speaking` | Talks. Fields: `title`, `description`, `publishDate`, `youtubeId` (YouTube video ID), `event?`, `tags?`, `experiences?`, `draft`. |
## For AI Agents
### Working In This Directory
- **Experience taxonomy:** entries in `projects`, `writing`, and `speaking` reference experiences via an `experiences: [<id>]` array (validated by `reference("experience")`). The id is the experience filename without extension (e.g. `experiences: ["sonr"]``experience/sonr.md`). Adding a ref to a non-existent experience fails the build.
- `draft: true` (writing/speaking) hides an entry from production builds.
- After editing frontmatter, `pnpm build` validates it against the schema.
### Common Patterns
- Some `writing/` samples (`markdown-elements`, `testing/`) are leftover theme demos showcasing markdown rendering — safe to delete once real content exists.
<!-- MANUAL: -->
+15
View File
@@ -0,0 +1,15 @@
---
title: "Britetwist"
organization: "Britetwist"
role: "iOS Developer"
location: "Ashburn, VA, USA"
tags: ["Executive Leadership", "iOS Development", "Game Development", "C#", "Unity", "Product Management", "User Interface Design"]
startDate: "2016-07-01"
endDate: "2019-05-01"
description: "Founded an independent mobile app studio, driving end-to-end product development—from ideation and UI/UX design to full-stack programming—for three successful applications."
---
- Founded an independent mobile app studio, driving end-to-end product development—from ideation and UI/UX design to full-stack programming—for three successful applications.
- Scaled the company's portfolio to over 1 million total downloads, encompassing a peer-to-peer volunteer network (VolunTree) and highly engaging mobile games (Space Bolt, Yuē).
- Achieved peak user engagement metrics, including 15,000 Daily Active Users (DAU) for Space Bolt and 1,250 Monthly Active Users (MAU) for VolunTree.
- Fostered strong developer community engagement by authoring technical architecture and game development articles that accumulated over 40,000 reads.
+14
View File
@@ -0,0 +1,14 @@
---
title: "Hobsons"
organization: "Hobsons"
role: "Software Engineering Intern"
location: "Arlington, VA, USA"
tags: ["Java", "AngularJS", "Spring Boot", "REST API", "Backend Development", "Algorithms"]
startDate: "2018-05-01"
endDate: "2018-08-01"
description: "Architected a robust backend monitoring system using Java and the Spring Boot framework to track data synchronization between CommonApp documents and the Naviance platform."
---
- Architected a robust backend monitoring system using Java and the Spring Boot framework to track data synchronization between CommonApp documents and the Naviance platform.
- Built an internal frontend dashboard using JavaScript and AngularJS to seamlessly consume, display, and filter REST API JSON responses.
- Empowered the sales team to independently troubleshoot and answer customer QA inquiries by creating self-serve tools, drastically reducing response times from several days to just a few minutes.
+14
View File
@@ -0,0 +1,14 @@
---
title: "Leidos"
organization: "Leidos"
role: "Mobile App Developer"
location: "Herndon, VA, USA"
tags: ["Flutter", "Dart", "Cross-Platform Development", "Mobile App Development", "Agile Methodologies", "Mobile Security"]
startDate: "2020-08-01"
endDate: "2021-05-01"
description: "Engineered and deployed features for the official Customs and Border Protection (CBP) Mobile Passport application, a high-visibility tool utilized by millions of travelers to expedite entry into the United States."
---
- Engineered and deployed features for the official Customs and Border Protection (CBP) Mobile Passport application, a high-visibility tool utilized by millions of travelers to expedite entry into the United States.
- Leveraged the Flutter framework and Dart to build and maintain a seamless, high-performance cross-platform experience for both iOS and Android users.
- Collaborated within a highly regulated, hybrid agile environment to ensure the application met strict federal security, privacy, and accessibility standards.
+19
View File
@@ -0,0 +1,19 @@
---
title: "Sonr"
organization: "Sonr"
role: "Co-Founder / CEO"
location: "New York, USA"
url: "https://sonr.io"
tags: ["Executive Leadership", "Startup Fundraising", "Cosmos SDK", "React Native", "Kubernetes", "Docker", "Google Cloud", "Terraform", "Ansible", "Protocol Buffers", "Decentralized Identifiers (DIDs)", "WebAuthn", "WebAssembly (WASM)", "IPFS", "Matrix Protocol", "Technology Strategy", "DevOps", "Linux Administration"]
startDate: "2021-03-01"
description: "Founded the startup and successfully led executive fundraising efforts, securing $4.7M in capital to build a peer-to-peer identity and asset management system leveraging decentralized identifiers (DIDs), WebAuthn, and IPFS."
---
> Founder building Decentralized Identity | W3C Working Group Member for DIDs, WebAuthn, and WASM | 6000+ Github Contributions 2021-2023
- Founded the startup and successfully led executive fundraising efforts, securing $4.7M in capital to build a peer-to-peer identity and asset management system leveraging decentralized identifiers (DIDs), WebAuthn, and IPFS.
- Directed technical strategy and led a cross-functional engineering team in the co-development of a React Native mobile chat application powered by the decentralized Matrix protocol.
- Architected a custom Layer 1 blockchain using the Cosmos SDK, optimizing data serialization and network communication with Protobufs.
- Orchestrated advanced DevOps and server deployments across Google Cloud (GCP), managing complex network infrastructure using Kubernetes (K8s), Docker, Terraform, Ansible, and comprehensive Linux administration.
- Drove massive open-source community growth, achieving the 4th highest global GitHub contributions (Oct 2021 Oct 2022) for the platform's MVP, and successfully onboarding over 120 first-time blockchain developers to the Motor SDKs during DevNet.
- Actively shaped global web standards as a W3C Working Group Member for DIDs, WebAuthn, and WebAssembly (WASM).
+16
View File
@@ -0,0 +1,16 @@
---
title: "Unissant"
organization: "Unissant"
role: "Data Science Intern"
location: "Herndon, VA, USA"
tags: ["Machine Learning", "Data Science", "AWS Lambda", "React.js", "AWS SageMaker", "Python", "Serverless Computing"]
startDate: "2019-05-01"
endDate: "2019-08-01"
description: "Engineered and deployed a machine learning fraud detection model utilizing AWS SageMaker to identify irregular patterns in healthcare data."
---
- Engineered and deployed a machine learning fraud detection model utilizing AWS SageMaker to identify irregular patterns in healthcare data.
- Developed a custom, client-facing web application using React, integrated with serverless AWS Lambda functions to interact with the predictive model in real-time.
- Delivered a successful proof-of-concept (POC) to a consulting client, demonstrating the firm's technical capabilities in advanced data management and automated fraud prevention.
+14
View File
@@ -0,0 +1,14 @@
---
title: "Layer-1 Blockchain & Decentralized Identity Ecosystem"
startDate: "2022-01-01"
description: "Engineered a revolutionary \"Gasless Onboarding\" pipeline by writing custom Cosmos AnteHandlers to intercept and validate WebAuthn (Passkeys/Biometrics) credentials directly at the mempool level, completely abstracting away seed phrases and transaction fees for new users."
repo: "https://github.com/sonr-io/sonr"
tags: ["Go", "Kubernetes", "WebAuthn", "Decentralized Identifiers (DIDs)", "IPFS", "Distributed Networking", "Blockchain Architecture"]
experiences: ["sonr"]
---
- Engineered a revolutionary "Gasless Onboarding" pipeline by writing custom Cosmos AnteHandlers to intercept and validate WebAuthn (Passkeys/Biometrics) credentials directly at the mempool level, completely abstracting away seed phrases and transaction fees for new users.
- Designed a capability-based authorization system integrating User-Controlled Authorization Networks (UCAN) and Decentralized Web Nodes (DWN) to provide granular, time-bound, and delegatable access to IPFS-backed encrypted data vaults.
- Bridged Web3 ecosystems by implementing full EVM compatibility alongside CosmWasm smart contracts, supporting multi-chain token mappings and cross-chain operations via IBC protocols.
- Built enterprise-grade DevOps infrastructure leveraging Kubernetes (Starship), Docker, and Devbox to orchestrate multi-node testnets and local development environments, utilizing GoReleaser for automated, multi-architecture CI/CD pipelines.
- Integrated automated legal compliance tooling directly into the blockchain CLI, generating cryptographic proofs and automated filings for Wyoming DAO LC amendments.
+14
View File
@@ -0,0 +1,14 @@
---
title: "Beam — E2EE Decentralized Messaging"
startDate: "2022-08-01"
endDate: "2025-08-01"
description: "Led the architecture and hands-on development of Beam, a cross-platform end-to-end encrypted (E2EE) mobile messaging application built in React Native, directing a 6-engineer cross-functional team across mobile, infrastructure, and product."
link: "https://www.youtube.com/watch?v=plaYwlGl4NM"
tags: ["React Native", "Matrix Protocol", "WebAuthn", "Decentralized Identifiers (DIDs)", "Cryptography"]
experiences: ["sonr"]
---
- Led the architecture and hands-on development of Beam, a cross-platform end-to-end encrypted (E2EE) mobile messaging application built in React Native, directing a 6-engineer cross-functional team across mobile, infrastructure, and product.
- Architected and managed a self-hosted Matrix homeserver, customizing the decentralized Matrix protocol to deliver end-to-end encrypted (E2EE), censorship-resistant messaging with self-custodied message history.
- Engineered a passkey-based authentication layer as a drop-in replacement for the Matrix authorization server, swapping centralized email/phone login for WebAuthn passkey wallets and Sonr Decentralized Identifiers (DIDs) to enable passwordless, self-sovereign onboarding and cryptographic key exchange.
- Abstracted decentralized routing and cryptographic key management behind a Web2-grade chat UX, guiding frontend and product teams to deliver a frictionless messaging experience over fully decentralized identity and transport infrastructure.
+14
View File
@@ -0,0 +1,14 @@
---
title: "Client SDK & Motr Cryptographic Enclave"
startDate: "2022-06-01"
endDate: "2023-08-01"
description: "Architected \"Motr,\" a WebAssembly (WASM) cryptographic enclave compiled from Go, designed to run entirely in client-side Web Workers. This sandboxed environment securely handles Multi-Party Computation (MPC), data signing, and UCAN token generation without ever exposing private keys to the main application thread."
link: "https://pub.dev/packages/motor_flutter"
tags: ["API Design", "React.js", "Flutter", "WebAuthn", "Cryptography", "WebAssembly (WASM)", "TypeScript", "Developer Relations"]
experiences: ["sonr"]
---
- Architected "Motr," a WebAssembly (WASM) cryptographic enclave compiled from Go, designed to run entirely in client-side Web Workers. This sandboxed environment securely handles Multi-Party Computation (MPC), data signing, and UCAN token generation without ever exposing private keys to the main application thread.
- Built an edge-native orchestration layer using Cloudflare Workers, utilizing Durable Objects, KV namespaces, and Service Bindings to manage persistent cross-platform sessions, smart routing, and state synchronization across the decentralized network.
- Pioneered a "Web2-to-Web3" authentication bridge by integrating WebAuthn (Passkeys/Biometrics) and standard OAuth 2.0 flows directly into the SDK, allowing developers to offer passwordless, seed-phrase-free onboarding to their users.
- Implemented account-isolated client persistence leveraging Dexie.js (IndexedDB) to create a robust offline caching layer for cryptographic states, delegated capabilities, and decentralized web node (DWN) records.
@@ -0,0 +1,14 @@
---
title: "Core Cryptography Library (ZKP & MPC Infrastructure)"
startDate: "2022-01-01"
endDate: "2026-02-01"
description: "Implemented advanced Zero-Knowledge Proof (ZKP) systems, including Bulletproofs (Range Proofs and Inner Product Proofs) and cryptographic accumulators, leveraging the Fiat-Shamir heuristic for efficient, non-interactive verification."
repo: "https://github.com/sonr-io/crypto"
tags: ["Go", "Security Testing", "Quantum Computing", "Cryptography"]
experiences: ["sonr"]
---
- Implemented advanced Zero-Knowledge Proof (ZKP) systems, including Bulletproofs (Range Proofs and Inner Product Proofs) and cryptographic accumulators, leveraging the Fiat-Shamir heuristic for efficient, non-interactive verification.
- Designed Multi-Party Computation (MPC) and Distributed Key Generation (DKG) protocols, enabling decentralized infrastructure through Threshold ECDSA/Ed25519, FROST, and Gennaro signature schemes without single points of failure.
- Engineered highly secure, constant-time elliptic curve arithmetic across multiple standards (secp256k1, Ed25519, BLS12-381, NIST P-Curves), strictly enforcing safeguards against side-channel timing attacks, signature malleability, and nonce reuse.
- Hardened system security by integrating AES-GCM authenticated encryption and Argon2id memory-hard key derivation, backed by an exhaustive suite of automated security tests, race-condition checks, and performance benchmarks.
@@ -0,0 +1,15 @@
---
title: "Decentralized File-Sharing Platform"
startDate: "2020-10-01"
endDate: "2021-12-01"
description: "Engineered deep native OS integrations by writing custom iOS Share Extensions (Swift) and Android Intent filters, allowing users to seamlessly share photos, videos, and files directly from the OS-level share sheet into the decentralized libp2p network."
link: "https://www.youtube.com/watch?v=HeBcg4P3Ipw"
tags: ["Flutter", "Dart", "Swift", "Kotlin", "Java", "FFI (Foreign Function Interface)", "Decentralized Identifiers (DIDs)", "SQLite", "Continuous Integration and Deployment (CI/CD)", "Libp2p"]
experiences: ["sonr"]
---
- Engineered deep native OS integrations by writing custom iOS Share Extensions (Swift) and Android Intent filters, allowing users to seamlessly share photos, videos, and files directly from the OS-level share sheet into the decentralized libp2p network.
- Integrated a custom Go-based networking core using Gomobile, creating strict type-conversion bindings to translate low-level libp2p data streams, cryptographic keypairs, and Namebase decentralized routing into Dart.
- Designed a reactive, high-performance UI/UX utilizing the GetX framework for state management, combined with complex Lottie and Rive animations to provide intuitive, real-time feedback during peer discovery and file transfers.
- Built a robust local caching layer using Moor (SQLite) to securely store decentralized identity profiles, transfer history, and media metadata offline.
- Automated the entire CI/CD pipeline by configuring custom Makefiles, Fastlane lanes, and GitHub Actions to fetch core framework submodules, manage code-generation (build_runner), and seamlessly deploy App Bundles and iOS Archives to the Play Store and App Store TestFlight.
+15
View File
@@ -0,0 +1,15 @@
---
title: "Web3 Developer Ecosystem & DevNet Launch"
startDate: "2022-01-01"
endDate: "2022-06-01"
description: "Designed and hosted hands-on technical workshops aimed at abstracting away the friction of Web3 development, successfully onboarding dozens of first-time blockchain developers to build decentralized applications (dApps) without requiring deep cryptographic expertise."
link: "https://www.youtube.com/watch?v=zy6Xkh9Mvcs"
tags: ["Developer Relations", "Technical Workshops", "Web3 Ecosystem", "Public Speaking", "API Design"]
experiences: ["sonr"]
---
Spearheaded the launch of the Sonr DevNet at premier collegiate hackathons, including PennApps XXII and UC Berkeleys CalHacks, to drive grassroots adoption of decentralized identity protocols.
- Designed and hosted hands-on technical workshops aimed at abstracting away the friction of Web3 development, successfully onboarding dozens of first-time blockchain developers to build decentralized applications (dApps) without requiring deep cryptographic expertise.
- Provided direct architectural mentorship and SDK support (utilizing the Sonr Motor API and DID modules), enabling developers to seamlessly integrate decentralized identity, secure messaging, and token marketplaces within 48-hour constraints.
- Fostered a thriving early-stage ecosystem that produced highly innovative dApps, including Ptolemy (an on-chain/off-chain validator reputation scoring system), Faiza (a decentralized, token-incentivized health forum), and WalkWithMe (a secure, DID-authenticated peer-to-peer transit network).
+13
View File
@@ -0,0 +1,13 @@
---
title: "Floadt"
startDate: "2013-05-01"
endDate: "2015-10-01"
description: "Developed a unified social media client designed to aggregate and display Instagram, Facebook, and Twitter feeds within a single, Pinterest-style user interface."
repo: "https://github.com/prdlk/floadt"
tags: ["REST APIs", "User Interface Design", "Social Media Integration", "Network Architecture", "Frontend Development"]
experiences: ["britetwist"]
---
- Developed a unified social media client designed to aggregate and display Instagram, Facebook, and Twitter feeds within a single, Pinterest-style user interface.
- Engineered advanced social management features, including cross-platform scheduled publishing, feed filtering to hide specific users, and mutual-follower verification.
- Gained deep, hands-on experience in network architecture and complex REST API integration prior to major platform policy changes that restricted third-party developer access.
+13
View File
@@ -0,0 +1,13 @@
---
title: "Like.ly"
startDate: "2017-05-01"
endDate: "2017-08-01"
description: "Engineered a predictive analytics tool leveraging TensorFlow and supervised machine learning algorithms to forecast Instagram post engagement and likes."
repo: "https://github.com/prdlk/like.ly"
tags: ["React Native", "Machine Learning", "TensorFlow", "Computer Vision", "Web Scraping"]
---
- Engineered a predictive analytics tool leveraging TensorFlow and supervised machine learning algorithms to forecast Instagram post engagement and likes.
- Developed an intuitive user interface by building a cross-platform mobile application and a cohesive messenger bot using React Native.
- Customized a web scraper to systematically aggregate public user data to train the model and optimize prediction accuracy.
- Integrated a Computer Vision API to analyze visual post content and extract key features for the machine learning pipeline.
+13
View File
@@ -0,0 +1,13 @@
---
title: "Pool"
startDate: "2019-08-01"
endDate: "2019-08-01"
description: "Designed and engineered the frontend architecture for an event-sharing mobile application utilizing the Flutter framework and Dart."
repo: "https://github.com/prdlk/pool-tutorial-part-1"
tags: ["Flutter", "Dart", "Cross-Platform Development", "User Interface Design", "Technical Writing"]
---
- Designed and engineered the frontend architecture for an event-sharing mobile application utilizing the Flutter framework and Dart.
- Translated complex Dribbble UI/UX designs into fully functional programmatic widget hierarchies, implementing custom TableView cells, dynamic navigation, and animated backgrounds using the simple-animations library.
- Authored and published a comprehensive technical article ("A Deep Dive into Flutter") detailing the application's structural development to educate developers on cross-platform best practices.
- Architected a scalable, modular codebase designed to seamlessly integrate with future Node.js and MongoDB backend implementations.
+13
View File
@@ -0,0 +1,13 @@
---
title: "Space Bolt"
startDate: "2016-08-01"
endDate: "2018-07-01"
description: "Designed and developed an endless-runner space game, successfully scaling the player base to 15,000 daily active users and 850,000 total downloads by 2018."
repo: "https://github.com/prdlk/spacebolt"
tags: ["Game Development", "C#", "Social Integration", "API Design", "Microtransactions"]
experiences: ["britetwist"]
---
- Designed and developed an endless-runner space game, successfully scaling the player base to 15,000 daily active users and 850,000 total downloads by 2018.
- Engineered a custom API and server-side caching system to generate, retrieve, and distribute shareable gameplay GIFs.
- Integrated Facebook leaderboards to drive user engagement, retention, and global competition among players.
+13
View File
@@ -0,0 +1,13 @@
---
title: "Superball"
startDate: "2011-12-01"
endDate: "2012-04-01"
description: "Developed and launched a puzzle-based casual game for iOS using the Cocos2D framework and Objective-C at age 12."
link: "https://vimeo.com/172015296"
tags: ["Game Development", "Objective-C", "Cocos2d"]
experiences: ["britetwist"]
---
- Developed and launched a puzzle-based casual game for iOS using the Cocos2D framework and Objective-C at age 12.
- Navigated the constraints of early iOS development by successfully managing memory allocation manually, prior to the introduction of built-in automated memory management.
- Designed the complete game loop, mechanics, and user interface tailored specifically for the mobile casual gaming market.

Some files were not shown because too many files have changed in this diff Show More