feat(about): add personal bio and details to about page

This commit is contained in:
Prad Nukala
2026-06-30 14:28:50 -04:00
parent 2f40cf3bce
commit 0e45270d93
15 changed files with 200 additions and 109 deletions
+53 -19
View File
@@ -2,35 +2,69 @@
import PageLayout from "@/layouts/Base.astro";
const meta = {
description: "I'm a starter theme for Astro.build",
description:
"About Prad Nukala — engineer and founder building decentralized identity infrastructure, W3C and DIF working-group member, and lifelong app developer.",
title: "About",
};
const organizations = [
{
name: "World Wide Web Consortium (W3C)",
role: "Working Group — DIDs, WebAuthn, WASM",
url: "https://www.w3.org/",
},
{
name: "Decentralized Identity Foundation (DIF)",
role: "Working Group — UCAN, DWN",
url: "https://identity.foundation/",
},
];
---
<PageLayout meta={meta}>
<h1 class="title mb-12">About</h1>
<div class="prose prose-sm prose-cactus max-w-none">
<p>
Hi, Im a starter Astro. Im particularly great for getting you started with your own blogging
website.
I'm Prad Nukala — an engineer and founder building decentralized identity infrastructure. As
co-founder and CEO of <a href="https://sonr.io" rel="noreferrer" target="_blank">Sonr</a>, I'm
building a peer-to-peer identity and data network powered by decentralized identifiers (DIDs),
WebAuthn, and IPFS.
</p>
<p>Here are my some of my awesome built in features:</p>
<ul class="list-inside list-disc" role="list">
<li>I'm ultra fast as I'm a static site</li>
<li>I'm fully responsive</li>
<li>I come with a light and dark mode</li>
<li>I'm easy to customise and add additional content</li>
<li>I have Tailwind CSS styling</li>
<li>Shiki code syntax highlighting</li>
<li>Satori for auto generating OG images for blog posts</li>
</ul>
<p>
Clone or fork my <a
class="cactus-link inline-block"
href="https://github.com/chrismwilliams/astro-cactus"
rel="noreferrer"
target="_blank">repo</a
> if you like me!
I help shape the open standards behind user-owned identity as a working-group member at the
W3C (DIDs, WebAuthn, WebAssembly) and the Decentralized Identity Foundation (UCAN, DWN). I've
been shipping software since age 12 — three apps with over 1M downloads, technical writing
read 120k+ times, and bylines in Fast Company.
</p>
<h2>Education</h2>
<p>
<strong>Virginia Commonwealth University</strong><br />
B.S. in Applied Mathematics · 20172020
</p>
<p>
Attended 5 MLH hackathons, published 3 apps in 3 years with over 1M downloads, and wrote 3
articles with over 120k reads. Coursework spanned linear algebra, number theory, topological
data analysis, and vector calculus.
</p>
<h2>Honors &amp; Recognition</h2>
<ul>
<li>Guest Lecturer, <em>Crypto Finance (15.492)</em> — MIT Sloan School of Management</li>
</ul>
<h2>Standards &amp; Organizations</h2>
<ul>
{
organizations.map((org) => (
<li>
<a href={org.url} rel="noreferrer" target="_blank">
{org.name}
</a>{" "}
— {org.role}
</li>
))
}
</ul>
</div>
</PageLayout>
+11 -1
View File
@@ -3,8 +3,10 @@ import { render } from "astro:content";
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
import { Icon } from "astro-icon/components";
import PostPreview from "@/components/blog/PostPreview.astro";
import TagList from "@/components/TagList.astro";
import { getAllExperience, getContentForExperience } from "@/data/experience";
import PageLayout from "@/layouts/Base.astro";
import { breadcrumbList } from "@/utils/seo";
export const getStaticPaths = (async () => {
const experiences = await getAllExperience();
@@ -20,12 +22,18 @@ type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { exp, content } = Astro.props as Props;
const { Content } = await render(exp);
const { startDate, endDate, organization, role, location, url } = exp.data;
const { startDate, endDate, organization, role, location, url, tags } = exp.data;
const range = `${startDate.getFullYear()} ${endDate ? endDate.getFullYear() : "Present"}`;
const meta = {
description: exp.data.description ?? `${role ?? ""} at ${organization}`.trim(),
keywords: tags,
ogImage: `/og-image/experience/${exp.id}.png`,
schema: breadcrumbList([
{ name: "Home", path: "/" },
{ name: "Experience", path: "/experience/" },
{ name: exp.data.title, path: `/experience/${exp.id}/` },
]),
title: exp.data.title,
};
---
@@ -108,4 +116,6 @@ const meta = {
</section>
)
}
<TagList tags={tags} class="border-global-text/10 mt-4 border-t pt-6" />
</PageLayout>
+16
View File
@@ -1,11 +1,27 @@
---
import { getAllExperience } from "@/data/experience";
import PageLayout from "@/layouts/Base.astro";
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
const experiences = await getAllExperience();
const meta = {
description: "Roles and organizations I've worked with",
schema: [
breadcrumbList([
{ name: "Home", path: "/" },
{ name: "Experience", path: "/experience/" },
]),
collectionPageSchema({
name: "Experience",
description: "Roles and organizations I've worked with",
path: "/experience/",
items: experiences.map((e) => ({
name: e.data.role ? `${e.data.role}, ${e.data.organization}` : e.data.organization,
path: `/experience/${e.id}/`,
})),
}),
],
title: "Experience",
};
+16 -27
View File
@@ -5,8 +5,10 @@ import Project from "@/components/project/Project.astro";
import { getAllPosts } from "@/data/post";
import { getAllSpeaking } from "@/data/speaking";
import PageLayout from "@/layouts/Base.astro";
import { siteConfig, socialLinks } from "@/site.config";
import { socialLinks } from "@/site.config";
import { getTagIndex, getTaggedCollections } from "@/data/tags";
import { collectionDateSort, projectDateSort } from "@/utils/date";
import { personSchema, websiteSchema } from "@/utils/seo";
const MAX_POSTS = 4;
const allPosts = await getAllPosts();
@@ -15,36 +17,26 @@ const latestPosts = (allPosts.sort(collectionDateSort) as CollectionEntry<"writi
MAX_POSTS,
);
const MAX_PROJECTS = 2;
const MAX_PROJECTS = 3;
const allProjects = await getCollection("projects");
const latestProjects = allProjects.sort(projectDateSort).slice(0, MAX_PROJECTS);
const MAX_TALKS = 3;
const latestTalks = (await getAllSpeaking()).sort(collectionDateSort).slice(0, MAX_TALKS);
const talks = (await getAllSpeaking()).sort(collectionDateSort);
const social = Object.fromEntries(socialLinks.map((s) => [s.friendlyName, s.link]));
// Skills (from all tagged content) feed the Person `knowsAbout` signal.
const skills = [...getTagIndex(await getTaggedCollections()).values()].map((t) => t.label);
const meta = {
title: "Engineer & Founder",
description:
"Prad Nukala — engineer and founder building decentralized identity infrastructure. Explore my projects, experience, writing, and talks.",
};
const social = Object.fromEntries(socialLinks.map((s) => [s.friendlyName, s.link]));
// Person structured data for richer search results.
const personSchema = {
"@context": "https://schema.org",
"@type": "Person",
name: siteConfig.author,
url: siteConfig.url,
jobTitle: "Engineer & Founder",
description: meta.description,
image: new URL("/avatar.png", siteConfig.url).href,
sameAs: socialLinks.filter((s) => !s.link.startsWith("mailto:")).map((s) => s.link),
schema: [websiteSchema(), personSchema(skills)],
};
---
<PageLayout meta={meta}>
<script type="application/ld+json" set:html={JSON.stringify(personSchema)} is:inline />
<section>
<h1 class="title mb-6">Prad Nukala</h1>
<p class="mb-4">
@@ -52,9 +44,9 @@ const personSchema = {
Here you'll find what I've built, where I've worked, what I write, and the talks I've given.
</p>
<p>
Find me on <a class="cactus-link" href={social.GitHub} rel="noreferrer" target="_blank">Github</a>,
<a class="cactus-link" href={social.X} rel="noreferrer" target="_blank">X</a>,
<a class="cactus-link" href={social.LinkedIn} rel="noreferrer" target="_blank">LinkedIn</a>, or
Find me on <a class="cactus-link" href={social.GitHub} rel="me noreferrer" target="_blank">Github</a>,
<a class="cactus-link" href={social.X} rel="me noreferrer" target="_blank">X</a>,
<a class="cactus-link" href={social.LinkedIn} rel="me noreferrer" target="_blank">LinkedIn</a>, or
<a class="cactus-link" href={social.Email}>contact me directly</a>.
</p>
</section>
@@ -93,13 +85,13 @@ const personSchema = {
</a>
</section>
{
latestTalks.length > 0 && (
talks.length > 0 && (
<section class="mt-16">
<h2 class="title text-accent mb-6 text-xl">
<a href="/speaking/">Speaking</a>
</h2>
<ul class="space-y-4" role="list">
{latestTalks.map((talk) => (
{talks.map((talk) => (
<li>
<a class="cactus-link" href={`/speaking/${talk.id}/`}>
{talk.data.title}
@@ -108,9 +100,6 @@ const personSchema = {
</li>
))}
</ul>
<a class="hover:text-link mt-6 inline-block" href="/speaking/">
See all talks <span aria-hidden="true">→</span>
</a>
</section>
)
}
+2 -2
View File
@@ -60,9 +60,9 @@ export async function getStaticPaths() {
getAllSpeaking(),
]);
// Writing keeps its existing behaviour: skip posts with a custom ogImage.
// Writing: skip posts with a custom ogImage, and external (link-out) articles.
const writing = (await getAllPosts())
.filter((p) => !p.data.ogImage)
.filter((p) => !p.data.ogImage && !p.data.externalUrl)
.map((p) => ({
params: { slug: `writing/${p.id}` },
props: {
+13
View File
@@ -6,6 +6,7 @@ import Pagination from "@/components/Paginator.astro";
import Project from "@/components/project/Project.astro";
import PageLayout from "@/layouts/Base.astro";
import { projectDateSort } from "@/utils/date";
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
export const getStaticPaths = (async ({ paginate }) => {
const MAX_PROJECTS_PER_PAGE = 10;
@@ -21,6 +22,18 @@ const { page } = Astro.props;
const meta = {
description: "Things I've built",
schema: [
breadcrumbList([
{ name: "Home", path: "/" },
{ name: "Projects", path: "/projects/" },
]),
collectionPageSchema({
name: "Projects",
description: "Things I've built",
path: page.url.current,
items: page.data.map((p) => ({ name: p.data.title, path: `/projects/${p.id}/` })),
}),
],
title: "Projects",
};
+14 -2
View File
@@ -3,6 +3,7 @@ import { getCollection } from "astro:content";
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
import Project from "@/components/project/Project.astro";
import PageLayout from "@/layouts/Base.astro";
import { breadcrumbList, creativeWorkSchema } from "@/utils/seo";
// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
export const getStaticPaths = (async () => {
@@ -16,11 +17,22 @@ export const getStaticPaths = (async () => {
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { project } = Astro.props;
const { title, description, link, repo, tags, startDate } = project.data;
const path = `/projects/${project.id}/`;
const meta = {
description: project.data.description || `Project: ${project.data.title}`,
description: description || `Project: ${title}`,
keywords: tags,
ogImage: `/og-image/projects/${project.id}.png`,
title: project.data.title,
schema: [
breadcrumbList([
{ name: "Home", path: "/" },
{ name: "Projects", path: "/projects/" },
{ name: title, path },
]),
creativeWorkSchema({ title, description, path, url: link, repo, dateCreated: startDate, keywords: tags }),
],
title,
};
---
+1 -1
View File
@@ -13,7 +13,7 @@ export const GET = async () => {
title: post.data.title,
description: post.data.description,
pubDate: post.data.publishDate,
link: `writing/${post.id}/`,
link: post.data.externalUrl ?? `writing/${post.id}/`,
})),
});
};
+13
View File
@@ -6,6 +6,7 @@ import Pagination from "@/components/Paginator.astro";
import { getAllSpeaking } from "@/data/speaking";
import PageLayout from "@/layouts/Base.astro";
import { collectionDateSort } from "@/utils/date";
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
export const getStaticPaths = (async ({ paginate }) => {
const talks = await getAllSpeaking();
@@ -20,6 +21,18 @@ const { page } = Astro.props;
const meta = {
description: "Talks and presentations I've given",
schema: [
breadcrumbList([
{ name: "Home", path: "/" },
{ name: "Speaking", path: "/speaking/" },
]),
collectionPageSchema({
name: "Speaking",
description: "Talks and presentations I've given",
path: page.url.current,
items: page.data.map((t) => ({ name: t.data.title, path: `/speaking/${t.id}/` })),
}),
],
title: "Speaking",
};
+27 -14
View File
@@ -3,8 +3,10 @@ import { getCollection, render } from "astro:content";
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
import ExperienceTags from "@/components/ExperienceTags.astro";
import FormattedDate from "@/components/FormattedDate.astro";
import StreamPlayer from "@/components/StreamPlayer.astro";
import TagList from "@/components/TagList.astro";
import YouTube from "@/components/YouTube.astro";
import PageLayout from "@/layouts/Base.astro";
import { breadcrumbList, videoSchema } from "@/utils/seo";
export const getStaticPaths = (async () => {
const talks = await getCollection("speaking");
@@ -18,9 +20,28 @@ type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { talk } = Astro.props as Props;
const { Content } = await render(talk);
const { title, description, videoId, originalSource, event, publishDate, experiences } = talk.data;
const { title, description, youtubeId, event, publishDate, experiences, tags } = talk.data;
const meta = { description, ogImage: `/og-image/speaking/${talk.id}.png`, title };
const path = `/speaking/${talk.id}/`;
const ogImage = `/og-image/speaking/${talk.id}.png`;
const schema = [
breadcrumbList([
{ name: "Home", path: "/" },
{ name: "Speaking", path: "/speaking/" },
{ name: title, path },
]),
videoSchema({
title,
description,
path,
thumbnail: `https://i.ytimg.com/vi/${youtubeId}/hqdefault.jpg`,
uploadDate: publishDate,
embedUrl: `https://www.youtube.com/embed/${youtubeId}`,
contentUrl: `https://www.youtube.com/watch?v=${youtubeId}`,
}),
];
const meta = { description, keywords: tags, ogImage, schema, title };
---
<PageLayout meta={meta}>
@@ -29,19 +50,11 @@ const meta = { description, ogImage: `/og-image/speaking/${talk.id}.png`, title
<p class="text-muted mb-6">
<FormattedDate date={publishDate} />{event && ` · ${event}`}
</p>
<StreamPlayer videoId={videoId} title={title} />
{
originalSource && (
<p class="text-muted mt-3 text-sm">
<a class="cactus-link" href={originalSource} rel="noopener noreferrer" target="_blank">
Watch original on YouTube →
</a>
</p>
)
}
<YouTube id={youtubeId} title={title} />
<div class="prose prose-sm prose-cactus mt-8 max-w-none">
<Content />
</div>
{experiences.length > 0 && <div class="mt-8"><ExperienceTags experiences={experiences} /></div>}
<TagList tags={tags} class="mt-8" />
{experiences.length > 0 && <div class="mt-6"><ExperienceTags experiences={experiences} /></div>}
</article>
</PageLayout>
-31
View File
@@ -1,31 +0,0 @@
---
import { getTagIndex, getTaggedCollections } from "@/data/tags";
import PageLayout from "@/layouts/Base.astro";
const cols = await getTaggedCollections();
const tags = [...getTagIndex(cols).entries()]
.map(([slug, { label, count }]) => ({ slug, label, count }))
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
const meta = {
description: "Browse projects, experience, writing, and talks by skill and topic.",
title: "Tags",
};
---
<PageLayout meta={meta}>
<h1 class="title mb-6">Tags</h1>
<p class="text-muted mb-8">Skills and topics spanning my projects, experience, writing, and talks.</p>
<ul class="flex flex-wrap gap-x-4 gap-y-2">
{
tags.map(({ slug, label, count }) => (
<li>
<a class="cactus-link before:content-['#']" href={`/tags/${slug}/`}>
{label}
</a>
<span class="text-muted text-sm"> {count}</span>
</li>
))
}
</ul>
</PageLayout>
+13
View File
@@ -8,6 +8,7 @@ import { getAllExperience } from "@/data/experience";
import { getAllPosts, groupPostsByYear } from "@/data/post";
import PageLayout from "@/layouts/Base.astro";
import { collectionDateSort } from "@/utils/date";
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
export const getStaticPaths = (async ({ paginate }) => {
const MAX_POSTS_PER_PAGE = 10;
@@ -36,6 +37,18 @@ const { page, experiences, pinnedPosts } = Astro.props;
const meta = {
description: "Essays, notes and articles I've written",
schema: [
breadcrumbList([
{ name: "Home", path: "/" },
{ name: "Writing", path: "/writing/" },
]),
collectionPageSchema({
name: "Writing",
description: "Essays, notes and articles I've written",
path: page.url.current,
items: page.data.map((p) => ({ name: p.data.title, path: `/writing/${p.id}/` })),
}),
],
title: "Writing",
};
+4 -1
View File
@@ -7,7 +7,10 @@ import PostLayout from "@/layouts/BlogPost.astro";
// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
export const getStaticPaths = (async () => {
const blogEntries = await getAllPosts();
return blogEntries.map((post) => ({
// External (PESOS) articles link out to the publisher; no local detail page.
return blogEntries
.filter((post) => !post.data.externalUrl)
.map((post) => ({
params: { slug: post.id },
props: { post },
}));
+11 -8
View File
@@ -3,7 +3,7 @@ import type { SiteConfig } from "@/types";
export const siteConfig: SiteConfig = {
// ! Please remember to replace the following site property with your own domain, used in astro.config.ts
url: "https://prad.nu/",
url: "https://www.pradnukala.com/",
title: "Prad Nukala",
author: "Prad Nukala",
description: "Engineer & founder. Projects, experience, writing and talks.",
@@ -33,25 +33,28 @@ export const socialLinks: {
isWebmention?: boolean;
}[] = [
{ friendlyName: "GitHub", link: "https://github.com/prdlk", name: "mdi:github" },
{ friendlyName: "X", link: "https://x.com/basedprad", name: "mdi:twitter" },
{ friendlyName: "X", link: "https://x.com/basedprad", name: "x" },
{ friendlyName: "LinkedIn", link: "https://linkedin.com/in/pradn", name: "mdi:linkedin" },
{ friendlyName: "Medium", link: "https://medium.com/@prnk28", name: "mdi:medium" },
{
friendlyName: "Discord",
link: "https://discord.com/users/788422222930640936",
name: "mdi:discord",
},
{ friendlyName: "Telegram", link: "https://t.me/prdnk", name: "mdi:telegram" },
{ friendlyName: "Email", link: "mailto:prnk28@gmail.com", name: "mdi:email-outline" },
];
// Used to generate links in both the Header & Footer.
export const menuLinks: { path: string; title: string }[] = [
{
path: "/",
title: "Home",
path: "/experience/",
title: "Experience",
},
{
path: "/projects/",
title: "Projects",
},
{
path: "/experience/",
title: "Experience",
},
{
path: "/writing/",
title: "Writing",
+3
View File
@@ -20,7 +20,10 @@ export interface PaginationLink {
export interface SiteMeta {
articleDate?: string | undefined;
description?: string;
keywords?: string[];
ogImage?: string | undefined;
/** One or more schema.org JSON-LD objects rendered into <head>. */
schema?: Record<string, unknown> | Record<string, unknown>[];
title: string;
}