mirror of
https://github.com/prdlk/website.git
synced 2026-08-02 17:31:41 +00:00
feat(about): add personal bio and details to about page
This commit is contained in:
+53
-19
@@ -2,35 +2,69 @@
|
|||||||
import PageLayout from "@/layouts/Base.astro";
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
|
||||||
const meta = {
|
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",
|
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}>
|
<PageLayout meta={meta}>
|
||||||
<h1 class="title mb-12">About</h1>
|
<h1 class="title mb-12">About</h1>
|
||||||
<div class="prose prose-sm prose-cactus max-w-none">
|
<div class="prose prose-sm prose-cactus max-w-none">
|
||||||
<p>
|
<p>
|
||||||
Hi, I’m a starter Astro. I’m particularly great for getting you started with your own blogging
|
I'm Prad Nukala — an engineer and founder building decentralized identity infrastructure. As
|
||||||
website.
|
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>
|
||||||
<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>
|
<p>
|
||||||
Clone or fork my <a
|
I help shape the open standards behind user-owned identity as a working-group member at the
|
||||||
class="cactus-link inline-block"
|
W3C (DIDs, WebAuthn, WebAssembly) and the Decentralized Identity Foundation (UCAN, DWN). I've
|
||||||
href="https://github.com/chrismwilliams/astro-cactus"
|
been shipping software since age 12 — three apps with over 1M downloads, technical writing
|
||||||
rel="noreferrer"
|
read 120k+ times, and bylines in Fast Company.
|
||||||
target="_blank">repo</a
|
|
||||||
> if you like me!
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2>Education</h2>
|
||||||
|
<p>
|
||||||
|
<strong>Virginia Commonwealth University</strong><br />
|
||||||
|
B.S. in Applied Mathematics · 2017–2020
|
||||||
|
</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 & Recognition</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Guest Lecturer, <em>Crypto Finance (15.492)</em> — MIT Sloan School of Management</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Standards & Organizations</h2>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
organizations.map((org) => (
|
||||||
|
<li>
|
||||||
|
<a href={org.url} rel="noreferrer" target="_blank">
|
||||||
|
{org.name}
|
||||||
|
</a>{" "}
|
||||||
|
— {org.role}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { render } from "astro:content";
|
|||||||
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import PostPreview from "@/components/blog/PostPreview.astro";
|
import PostPreview from "@/components/blog/PostPreview.astro";
|
||||||
|
import TagList from "@/components/TagList.astro";
|
||||||
import { getAllExperience, getContentForExperience } from "@/data/experience";
|
import { getAllExperience, getContentForExperience } from "@/data/experience";
|
||||||
import PageLayout from "@/layouts/Base.astro";
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
import { breadcrumbList } from "@/utils/seo";
|
||||||
|
|
||||||
export const getStaticPaths = (async () => {
|
export const getStaticPaths = (async () => {
|
||||||
const experiences = await getAllExperience();
|
const experiences = await getAllExperience();
|
||||||
@@ -20,12 +22,18 @@ type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
|||||||
|
|
||||||
const { exp, content } = Astro.props as Props;
|
const { exp, content } = Astro.props as Props;
|
||||||
const { Content } = await render(exp);
|
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 range = `${startDate.getFullYear()} – ${endDate ? endDate.getFullYear() : "Present"}`;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
description: exp.data.description ?? `${role ?? ""} at ${organization}`.trim(),
|
description: exp.data.description ?? `${role ?? ""} at ${organization}`.trim(),
|
||||||
|
keywords: tags,
|
||||||
ogImage: `/og-image/experience/${exp.id}.png`,
|
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,
|
title: exp.data.title,
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
@@ -108,4 +116,6 @@ const meta = {
|
|||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<TagList tags={tags} class="border-global-text/10 mt-4 border-t pt-6" />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
---
|
---
|
||||||
import { getAllExperience } from "@/data/experience";
|
import { getAllExperience } from "@/data/experience";
|
||||||
import PageLayout from "@/layouts/Base.astro";
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
|
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
|
||||||
|
|
||||||
const experiences = await getAllExperience();
|
const experiences = await getAllExperience();
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
description: "Roles and organizations I've worked with",
|
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",
|
title: "Experience",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+16
-27
@@ -5,8 +5,10 @@ import Project from "@/components/project/Project.astro";
|
|||||||
import { getAllPosts } from "@/data/post";
|
import { getAllPosts } from "@/data/post";
|
||||||
import { getAllSpeaking } from "@/data/speaking";
|
import { getAllSpeaking } from "@/data/speaking";
|
||||||
import PageLayout from "@/layouts/Base.astro";
|
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 { collectionDateSort, projectDateSort } from "@/utils/date";
|
||||||
|
import { personSchema, websiteSchema } from "@/utils/seo";
|
||||||
|
|
||||||
const MAX_POSTS = 4;
|
const MAX_POSTS = 4;
|
||||||
const allPosts = await getAllPosts();
|
const allPosts = await getAllPosts();
|
||||||
@@ -15,36 +17,26 @@ const latestPosts = (allPosts.sort(collectionDateSort) as CollectionEntry<"writi
|
|||||||
MAX_POSTS,
|
MAX_POSTS,
|
||||||
);
|
);
|
||||||
|
|
||||||
const MAX_PROJECTS = 2;
|
const MAX_PROJECTS = 3;
|
||||||
const allProjects = await getCollection("projects");
|
const allProjects = await getCollection("projects");
|
||||||
const latestProjects = allProjects.sort(projectDateSort).slice(0, MAX_PROJECTS);
|
const latestProjects = allProjects.sort(projectDateSort).slice(0, MAX_PROJECTS);
|
||||||
|
|
||||||
const MAX_TALKS = 3;
|
const talks = (await getAllSpeaking()).sort(collectionDateSort);
|
||||||
const latestTalks = (await getAllSpeaking()).sort(collectionDateSort).slice(0, MAX_TALKS);
|
|
||||||
|
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 = {
|
const meta = {
|
||||||
title: "Engineer & Founder",
|
title: "Engineer & Founder",
|
||||||
description:
|
description:
|
||||||
"Prad Nukala — engineer and founder building decentralized identity infrastructure. Explore my projects, experience, writing, and talks.",
|
"Prad Nukala — engineer and founder building decentralized identity infrastructure. Explore my projects, experience, writing, and talks.",
|
||||||
};
|
schema: [websiteSchema(), personSchema(skills)],
|
||||||
|
|
||||||
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),
|
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout meta={meta}>
|
<PageLayout meta={meta}>
|
||||||
<script type="application/ld+json" set:html={JSON.stringify(personSchema)} is:inline />
|
|
||||||
<section>
|
<section>
|
||||||
<h1 class="title mb-6">Prad Nukala</h1>
|
<h1 class="title mb-6">Prad Nukala</h1>
|
||||||
<p class="mb-4">
|
<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.
|
Here you'll find what I've built, where I've worked, what I write, and the talks I've given.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Find me on <a class="cactus-link" href={social.GitHub} rel="noreferrer" target="_blank">Github</a>,
|
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="noreferrer" target="_blank">X</a>,
|
<a class="cactus-link" href={social.X} rel="me noreferrer" target="_blank">X</a>,
|
||||||
<a class="cactus-link" href={social.LinkedIn} rel="noreferrer" target="_blank">LinkedIn</a>, or
|
<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>.
|
<a class="cactus-link" href={social.Email}>contact me directly</a>.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -93,13 +85,13 @@ const personSchema = {
|
|||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
{
|
{
|
||||||
latestTalks.length > 0 && (
|
talks.length > 0 && (
|
||||||
<section class="mt-16">
|
<section class="mt-16">
|
||||||
<h2 class="title text-accent mb-6 text-xl">
|
<h2 class="title text-accent mb-6 text-xl">
|
||||||
<a href="/speaking/">Speaking</a>
|
<a href="/speaking/">Speaking</a>
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="space-y-4" role="list">
|
<ul class="space-y-4" role="list">
|
||||||
{latestTalks.map((talk) => (
|
{talks.map((talk) => (
|
||||||
<li>
|
<li>
|
||||||
<a class="cactus-link" href={`/speaking/${talk.id}/`}>
|
<a class="cactus-link" href={`/speaking/${talk.id}/`}>
|
||||||
{talk.data.title}
|
{talk.data.title}
|
||||||
@@ -108,9 +100,6 @@ const personSchema = {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<a class="hover:text-link mt-6 inline-block" href="/speaking/">
|
|
||||||
See all talks <span aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ export async function getStaticPaths() {
|
|||||||
getAllSpeaking(),
|
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())
|
const writing = (await getAllPosts())
|
||||||
.filter((p) => !p.data.ogImage)
|
.filter((p) => !p.data.ogImage && !p.data.externalUrl)
|
||||||
.map((p) => ({
|
.map((p) => ({
|
||||||
params: { slug: `writing/${p.id}` },
|
params: { slug: `writing/${p.id}` },
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Pagination from "@/components/Paginator.astro";
|
|||||||
import Project from "@/components/project/Project.astro";
|
import Project from "@/components/project/Project.astro";
|
||||||
import PageLayout from "@/layouts/Base.astro";
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
import { projectDateSort } from "@/utils/date";
|
import { projectDateSort } from "@/utils/date";
|
||||||
|
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
|
||||||
|
|
||||||
export const getStaticPaths = (async ({ paginate }) => {
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
const MAX_PROJECTS_PER_PAGE = 10;
|
const MAX_PROJECTS_PER_PAGE = 10;
|
||||||
@@ -21,6 +22,18 @@ const { page } = Astro.props;
|
|||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
description: "Things I've built",
|
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",
|
title: "Projects",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getCollection } from "astro:content";
|
|||||||
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
||||||
import Project from "@/components/project/Project.astro";
|
import Project from "@/components/project/Project.astro";
|
||||||
import PageLayout from "@/layouts/Base.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
|
// 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 () => {
|
export const getStaticPaths = (async () => {
|
||||||
@@ -16,11 +17,22 @@ export const getStaticPaths = (async () => {
|
|||||||
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
const { project } = Astro.props;
|
const { project } = Astro.props;
|
||||||
|
const { title, description, link, repo, tags, startDate } = project.data;
|
||||||
|
const path = `/projects/${project.id}/`;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
description: project.data.description || `Project: ${project.data.title}`,
|
description: description || `Project: ${title}`,
|
||||||
|
keywords: tags,
|
||||||
ogImage: `/og-image/projects/${project.id}.png`,
|
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,
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const GET = async () => {
|
|||||||
title: post.data.title,
|
title: post.data.title,
|
||||||
description: post.data.description,
|
description: post.data.description,
|
||||||
pubDate: post.data.publishDate,
|
pubDate: post.data.publishDate,
|
||||||
link: `writing/${post.id}/`,
|
link: post.data.externalUrl ?? `writing/${post.id}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Pagination from "@/components/Paginator.astro";
|
|||||||
import { getAllSpeaking } from "@/data/speaking";
|
import { getAllSpeaking } from "@/data/speaking";
|
||||||
import PageLayout from "@/layouts/Base.astro";
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
import { collectionDateSort } from "@/utils/date";
|
import { collectionDateSort } from "@/utils/date";
|
||||||
|
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
|
||||||
|
|
||||||
export const getStaticPaths = (async ({ paginate }) => {
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
const talks = await getAllSpeaking();
|
const talks = await getAllSpeaking();
|
||||||
@@ -20,6 +21,18 @@ const { page } = Astro.props;
|
|||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
description: "Talks and presentations I've given",
|
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",
|
title: "Speaking",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { getCollection, render } from "astro:content";
|
|||||||
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
|
||||||
import ExperienceTags from "@/components/ExperienceTags.astro";
|
import ExperienceTags from "@/components/ExperienceTags.astro";
|
||||||
import FormattedDate from "@/components/FormattedDate.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 PageLayout from "@/layouts/Base.astro";
|
||||||
|
import { breadcrumbList, videoSchema } from "@/utils/seo";
|
||||||
|
|
||||||
export const getStaticPaths = (async () => {
|
export const getStaticPaths = (async () => {
|
||||||
const talks = await getCollection("speaking");
|
const talks = await getCollection("speaking");
|
||||||
@@ -18,9 +20,28 @@ type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
|||||||
|
|
||||||
const { talk } = Astro.props as Props;
|
const { talk } = Astro.props as Props;
|
||||||
const { Content } = await render(talk);
|
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}>
|
<PageLayout meta={meta}>
|
||||||
@@ -29,19 +50,11 @@ const meta = { description, ogImage: `/og-image/speaking/${talk.id}.png`, title
|
|||||||
<p class="text-muted mb-6">
|
<p class="text-muted mb-6">
|
||||||
<FormattedDate date={publishDate} />{event && ` · ${event}`}
|
<FormattedDate date={publishDate} />{event && ` · ${event}`}
|
||||||
</p>
|
</p>
|
||||||
<StreamPlayer videoId={videoId} title={title} />
|
<YouTube id={youtubeId} 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div class="prose prose-sm prose-cactus mt-8 max-w-none">
|
<div class="prose prose-sm prose-cactus mt-8 max-w-none">
|
||||||
<Content />
|
<Content />
|
||||||
</div>
|
</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>
|
</article>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -8,6 +8,7 @@ import { getAllExperience } from "@/data/experience";
|
|||||||
import { getAllPosts, groupPostsByYear } from "@/data/post";
|
import { getAllPosts, groupPostsByYear } from "@/data/post";
|
||||||
import PageLayout from "@/layouts/Base.astro";
|
import PageLayout from "@/layouts/Base.astro";
|
||||||
import { collectionDateSort } from "@/utils/date";
|
import { collectionDateSort } from "@/utils/date";
|
||||||
|
import { breadcrumbList, collectionPageSchema } from "@/utils/seo";
|
||||||
|
|
||||||
export const getStaticPaths = (async ({ paginate }) => {
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
const MAX_POSTS_PER_PAGE = 10;
|
const MAX_POSTS_PER_PAGE = 10;
|
||||||
@@ -36,6 +37,18 @@ const { page, experiences, pinnedPosts } = Astro.props;
|
|||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
description: "Essays, notes and articles I've written",
|
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",
|
title: "Writing",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ 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
|
// 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 () => {
|
export const getStaticPaths = (async () => {
|
||||||
const blogEntries = await getAllPosts();
|
const blogEntries = await getAllPosts();
|
||||||
return blogEntries.map((post) => ({
|
// External (PESOS) articles link out to the publisher; no local detail page.
|
||||||
params: { slug: post.id },
|
return blogEntries
|
||||||
props: { post },
|
.filter((post) => !post.data.externalUrl)
|
||||||
}));
|
.map((post) => ({
|
||||||
|
params: { slug: post.id },
|
||||||
|
props: { post },
|
||||||
|
}));
|
||||||
}) satisfies GetStaticPaths;
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|||||||
+11
-8
@@ -3,7 +3,7 @@ import type { SiteConfig } from "@/types";
|
|||||||
|
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig: SiteConfig = {
|
||||||
// ! Please remember to replace the following site property with your own domain, used in astro.config.ts
|
// ! 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",
|
title: "Prad Nukala",
|
||||||
author: "Prad Nukala",
|
author: "Prad Nukala",
|
||||||
description: "Engineer & founder. Projects, experience, writing and talks.",
|
description: "Engineer & founder. Projects, experience, writing and talks.",
|
||||||
@@ -33,25 +33,28 @@ export const socialLinks: {
|
|||||||
isWebmention?: boolean;
|
isWebmention?: boolean;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ friendlyName: "GitHub", link: "https://github.com/prdlk", name: "mdi:github" },
|
{ 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: "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" },
|
{ friendlyName: "Email", link: "mailto:prnk28@gmail.com", name: "mdi:email-outline" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Used to generate links in both the Header & Footer.
|
// Used to generate links in both the Header & Footer.
|
||||||
export const menuLinks: { path: string; title: string }[] = [
|
export const menuLinks: { path: string; title: string }[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/experience/",
|
||||||
title: "Home",
|
title: "Experience",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/projects/",
|
path: "/projects/",
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/experience/",
|
|
||||||
title: "Experience",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/writing/",
|
path: "/writing/",
|
||||||
title: "Writing",
|
title: "Writing",
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export interface PaginationLink {
|
|||||||
export interface SiteMeta {
|
export interface SiteMeta {
|
||||||
articleDate?: string | undefined;
|
articleDate?: string | undefined;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
keywords?: string[];
|
||||||
ogImage?: string | undefined;
|
ogImage?: string | undefined;
|
||||||
|
/** One or more schema.org JSON-LD objects rendered into <head>. */
|
||||||
|
schema?: Record<string, unknown> | Record<string, unknown>[];
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user