mirror of
https://github.com/prdlk/prdlk.git
synced 2026-11-05 14:37:48 +00:00
feat(data): add content query helpers and LLMS builder
This commit is contained in:
1 parent
85a65a16a5
commit
bff9f9f999
6 files changed
+313
No files matched your search
@@ -0,0 +1,27 @@
|
|||||||
|
<!-- Parent: ../AGENTS.md -->
|
||||||
|
<!-- Generated: 2026-06-30 | Updated: 2026-06-30 -->
|
||||||
|
|
||||||
|
# data
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Thin query helpers over Content Collections — draft filtering, sorting, and the experience-taxonomy aggregation.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `post.ts` | `getAllPosts()` (returns `CollectionEntry<"writing">`, drafts filtered in prod) and `groupPostsByYear()`. **Naming quirk:** file/functions say "post" but operate on the `writing` collection. |
|
||||||
|
| `experience.ts` | `getAllExperience()` (sorted by `startDate` desc) and `getContentForExperience(id)` which gathers all `projects`/`writing`/`speaking` entries whose `experiences` include `id`. |
|
||||||
|
| `speaking.ts` | `getAllSpeaking()` (drafts filtered in prod). |
|
||||||
|
|
||||||
|
## For AI Agents
|
||||||
|
|
||||||
|
### Working In This Directory
|
||||||
|
- Draft filtering uses `import.meta.env.PROD ? !data.draft : true` — drafts are visible in dev, hidden in production.
|
||||||
|
- `getContentForExperience` matches refs via `entry.data.experiences.some((ref) => ref.id === id)`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- `astro:content` (`getCollection`). Consumed by pages in `src/pages/` and the homepage.
|
||||||
|
|
||||||
|
<!-- MANUAL: -->
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
|
||||||
|
/** All experiences, most recent (by startDate) first. */
|
||||||
|
export async function getAllExperience(): Promise<CollectionEntry<"experience">[]> {
|
||||||
|
const entries = await getCollection("experience");
|
||||||
|
return entries.sort((a, b) => b.data.startDate.valueOf() - a.data.startDate.valueOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExperience = (
|
||||||
|
entry: { data: { experiences: { id: string }[] } },
|
||||||
|
id: string,
|
||||||
|
): boolean => entry.data.experiences.some((ref) => ref.id === id);
|
||||||
|
|
||||||
|
/** Every project, writing & talk filed under a given experience id. */
|
||||||
|
export async function getContentForExperience(id: string) {
|
||||||
|
const [projects, writing, speaking] = await Promise.all([
|
||||||
|
getCollection("projects", (e) => hasExperience(e, id)),
|
||||||
|
getCollection("writing", (e) => (import.meta.env.PROD ? !e.data.draft : true) && hasExperience(e, id)),
|
||||||
|
getCollection("speaking", (e) => (import.meta.env.PROD ? !e.data.draft : true) && hasExperience(e, id)),
|
||||||
|
]);
|
||||||
|
return { projects, writing, speaking };
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import { getAllExperience } from "@/data/experience";
|
||||||
|
import { getAllPosts } from "@/data/post";
|
||||||
|
import { getAllSpeaking } from "@/data/speaking";
|
||||||
|
|
||||||
|
/** Builders for /llms.txt (index) and /llms-full.txt (full content), per https://llmstxt.org */
|
||||||
|
|
||||||
|
const absUrl = (path: string) => new URL(path, import.meta.env.SITE).href;
|
||||||
|
|
||||||
|
const isoDate = (date: Date) => date.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const introHeading = "# Prad Nukala";
|
||||||
|
|
||||||
|
const introSummary =
|
||||||
|
"> Engineer & founder. Co-founder/CEO of Sonr ($4.7M raised). Built Layer-1 + full cryptography stack (ZKP/MPC), gasless onboarding, and led high-ownership teams. Optimized for autonomous, high-speed execution.";
|
||||||
|
|
||||||
|
const yearRange = (start: Date, end?: Date) =>
|
||||||
|
`${start.getFullYear()}–${end ? end.getFullYear() : "Present"}`;
|
||||||
|
|
||||||
|
async function getAllContent() {
|
||||||
|
const [experience, projects, writing, speaking] = await Promise.all([
|
||||||
|
getAllExperience(),
|
||||||
|
getCollection("projects").then((entries) =>
|
||||||
|
entries.sort((a, b) => b.data.startDate.valueOf() - a.data.startDate.valueOf()),
|
||||||
|
),
|
||||||
|
getAllPosts().then((entries) =>
|
||||||
|
entries.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()),
|
||||||
|
),
|
||||||
|
getAllSpeaking().then((entries) =>
|
||||||
|
entries.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
return { experience, projects, writing, speaking };
|
||||||
|
}
|
||||||
|
|
||||||
|
const writingUrl = (post: CollectionEntry<"writing">) =>
|
||||||
|
post.data.externalUrl ?? absUrl(`/writing/${post.id}/`);
|
||||||
|
|
||||||
|
/** Curated markdown index of the whole site. */
|
||||||
|
export async function buildLlmsIndex(): Promise<string> {
|
||||||
|
const { experience, projects, writing, speaking } = await getAllContent();
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
introHeading,
|
||||||
|
"",
|
||||||
|
introSummary,
|
||||||
|
"",
|
||||||
|
"## Experience",
|
||||||
|
"",
|
||||||
|
...experience.map((exp) => {
|
||||||
|
const { organization, role, startDate, endDate } = exp.data;
|
||||||
|
const detail = [role, organization].filter(Boolean).join(", ");
|
||||||
|
return `- [${exp.data.title}](${absUrl(`/experience/${exp.id}/`)}): ${detail} (${yearRange(startDate, endDate)})`;
|
||||||
|
}),
|
||||||
|
"",
|
||||||
|
"## Projects",
|
||||||
|
"",
|
||||||
|
...projects.map((project) => {
|
||||||
|
const { title, description, startDate, endDate } = project.data;
|
||||||
|
const summary = description ? ` ${description}` : "";
|
||||||
|
return `- [${title}](${absUrl(`/projects/${project.id}/`)}):${summary} (${yearRange(startDate, endDate)})`;
|
||||||
|
}),
|
||||||
|
"",
|
||||||
|
"## Writing",
|
||||||
|
"",
|
||||||
|
...writing.map((post) => {
|
||||||
|
const { title, description, publisher, publishDate } = post.data;
|
||||||
|
const via = publisher ? ` — via ${publisher}` : "";
|
||||||
|
return `- [${title}](${writingUrl(post)}): ${description}${via} (${isoDate(publishDate)})`;
|
||||||
|
}),
|
||||||
|
"",
|
||||||
|
"## Speaking",
|
||||||
|
"",
|
||||||
|
...speaking.map((talk) => {
|
||||||
|
const { title, description, event, publishDate } = talk.data;
|
||||||
|
const at = event ? ` — ${event}` : "";
|
||||||
|
return `- [${title}](${absUrl(`/speaking/${talk.id}/`)}): ${description}${at} (${isoDate(publishDate)})`;
|
||||||
|
}),
|
||||||
|
"",
|
||||||
|
"## Optional",
|
||||||
|
"",
|
||||||
|
`- [Full content](${absUrl("/llms-full.txt")}): Every page's full text in one file`,
|
||||||
|
`- [RSS feed](${absUrl("/rss.xml")})`,
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(heading: string, blocks: string[]): string[] {
|
||||||
|
return blocks.length ? [`## ${heading}`, "", ...blocks] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryBlock(header: string, meta: string[], body: string | undefined): string {
|
||||||
|
const content = body?.trim();
|
||||||
|
return [`### ${header}`, "", ...meta, ...(content ? ["", content] : []), ""].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full markdown body of every entry, for direct LLM ingestion. */
|
||||||
|
export async function buildLlmsFull(): Promise<string> {
|
||||||
|
const { experience, projects, writing, speaking } = await getAllContent();
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
`${introHeading} — full content`,
|
||||||
|
"",
|
||||||
|
introSummary,
|
||||||
|
"",
|
||||||
|
...section(
|
||||||
|
"Experience",
|
||||||
|
experience.map((exp) => {
|
||||||
|
const { organization, role, location, url, description, tags, startDate, endDate } =
|
||||||
|
exp.data;
|
||||||
|
return entryBlock(
|
||||||
|
exp.data.title,
|
||||||
|
[
|
||||||
|
`- URL: ${absUrl(`/experience/${exp.id}/`)}`,
|
||||||
|
`- Organization: ${organization}`,
|
||||||
|
...(role ? [`- Role: ${role}`] : []),
|
||||||
|
...(location ? [`- Location: ${location}`] : []),
|
||||||
|
...(url ? [`- Website: ${url}`] : []),
|
||||||
|
`- Period: ${yearRange(startDate, endDate)}`,
|
||||||
|
...(tags.length ? [`- Skills: ${tags.join(", ")}`] : []),
|
||||||
|
...(description ? [`- Summary: ${description}`] : []),
|
||||||
|
],
|
||||||
|
exp.body,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...section(
|
||||||
|
"Projects",
|
||||||
|
projects.map((project) => {
|
||||||
|
const { description, link, repo, tags, startDate, endDate } = project.data;
|
||||||
|
return entryBlock(
|
||||||
|
project.data.title,
|
||||||
|
[
|
||||||
|
`- URL: ${absUrl(`/projects/${project.id}/`)}`,
|
||||||
|
...(link ? [`- Link: ${link}`] : []),
|
||||||
|
...(repo ? [`- Repository: ${repo}`] : []),
|
||||||
|
`- Period: ${yearRange(startDate, endDate)}`,
|
||||||
|
...(tags.length ? [`- Skills: ${tags.join(", ")}`] : []),
|
||||||
|
...(description ? [`- Summary: ${description}`] : []),
|
||||||
|
],
|
||||||
|
project.body,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...section(
|
||||||
|
"Writing",
|
||||||
|
writing.map((post) => {
|
||||||
|
const { description, publisher, tags, publishDate, updatedDate } = post.data;
|
||||||
|
return entryBlock(
|
||||||
|
post.data.title,
|
||||||
|
[
|
||||||
|
`- URL: ${writingUrl(post)}`,
|
||||||
|
...(publisher ? [`- Publisher: ${publisher}`] : []),
|
||||||
|
`- Published: ${isoDate(publishDate)}`,
|
||||||
|
...(updatedDate ? [`- Updated: ${isoDate(updatedDate)}`] : []),
|
||||||
|
...(tags.length ? [`- Tags: ${tags.join(", ")}`] : []),
|
||||||
|
`- Summary: ${description}`,
|
||||||
|
],
|
||||||
|
post.body,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...section(
|
||||||
|
"Speaking",
|
||||||
|
speaking.map((talk) => {
|
||||||
|
const { description, event, youtubeId, tags, publishDate } = talk.data;
|
||||||
|
return entryBlock(
|
||||||
|
talk.data.title,
|
||||||
|
[
|
||||||
|
`- URL: ${absUrl(`/speaking/${talk.id}/`)}`,
|
||||||
|
...(event ? [`- Event: ${event}`] : []),
|
||||||
|
`- Video: https://www.youtube.com/watch?v=${youtubeId}`,
|
||||||
|
`- Date: ${isoDate(publishDate)}`,
|
||||||
|
...(tags.length ? [`- Tags: ${tags.join(", ")}`] : []),
|
||||||
|
`- Summary: ${description}`,
|
||||||
|
],
|
||||||
|
talk.body,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
|
||||||
|
/** filter out draft posts based on the environment */
|
||||||
|
export async function getAllPosts(): Promise<CollectionEntry<"writing">[]> {
|
||||||
|
return await getCollection("writing", ({ data }) => {
|
||||||
|
return import.meta.env.PROD ? !data.draft : true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** groups posts by year, using the year as the key
|
||||||
|
* Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so.
|
||||||
|
*/
|
||||||
|
export function groupPostsByYear(posts: CollectionEntry<"writing">[]) {
|
||||||
|
return Object.groupBy(posts, (post) => post.data.publishDate.getFullYear().toString());
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
|
||||||
|
/** filter out draft talks in production */
|
||||||
|
export async function getAllSpeaking(): Promise<CollectionEntry<"speaking">[]> {
|
||||||
|
return await getCollection("speaking", ({ data }) => {
|
||||||
|
return import.meta.env.PROD ? !data.draft : true;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
|
||||||
|
/** URL-safe slug for a skill/tag (tags are already lowercased by the schema). */
|
||||||
|
export const slugifyTag = (tag: string) =>
|
||||||
|
tag
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
|
||||||
|
const draftFilter = ({ data }: { data: { draft?: boolean } }) =>
|
||||||
|
import.meta.env.PROD ? !data.draft : true;
|
||||||
|
|
||||||
|
/** All tag-bearing content across the four collections (drafts filtered in prod). */
|
||||||
|
export async function getTaggedCollections() {
|
||||||
|
const [projects, experience, writing, speaking] = await Promise.all([
|
||||||
|
getCollection("projects"),
|
||||||
|
getCollection("experience"),
|
||||||
|
getCollection("writing", draftFilter),
|
||||||
|
getCollection("speaking", draftFilter),
|
||||||
|
]);
|
||||||
|
return { projects, experience, writing, speaking };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaggedCollections = Awaited<ReturnType<typeof getTaggedCollections>>;
|
||||||
|
|
||||||
|
type AnyTagged = CollectionEntry<"projects" | "experience" | "writing" | "speaking">;
|
||||||
|
const tagsOf = (entry: AnyTagged) => entry.data.tags ?? [];
|
||||||
|
|
||||||
|
/** Map of slug → { label, count } across every collection, for the tag index. */
|
||||||
|
export function getTagIndex(cols: TaggedCollections) {
|
||||||
|
const index = new Map<string, { label: string; count: number }>();
|
||||||
|
for (const col of Object.values(cols)) {
|
||||||
|
for (const entry of col) {
|
||||||
|
for (const tag of tagsOf(entry)) {
|
||||||
|
const slug = slugifyTag(tag);
|
||||||
|
const cur = index.get(slug) ?? { label: tag, count: 0 };
|
||||||
|
cur.count += 1;
|
||||||
|
index.set(slug, cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every entry, grouped by collection, that carries the given tag label. */
|
||||||
|
export function filterByTag(cols: TaggedCollections, label: string) {
|
||||||
|
const has = (e: AnyTagged) => tagsOf(e).includes(label);
|
||||||
|
return {
|
||||||
|
projects: cols.projects.filter(has),
|
||||||
|
experience: cols.experience.filter(has),
|
||||||
|
writing: cols.writing.filter(has),
|
||||||
|
speaking: cols.speaking.filter(has),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user