docs(content): add documentation for content directory structure and collections

This commit is contained in:
Prad Nukala
2026-06-30 13:05:08 -04:00
parent 40e107706f
commit eaf2a86df0
27 changed files with 437 additions and 25 deletions
+27
View File
@@ -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: -->
+54
View File
@@ -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),
};
}