Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Josselin Perrus's personal site: an Astro 7 static site served by Cloudflare Wor
- The page background (`src/components/Background.astro`: the paper texture and the corner pattern) and the topic map (`TopicMap.astro`, inside the `Intro` of Home and About) are drawn on canvases from small images in `src/assets/background/`. `scripts/make-background.py` makes those images, from the map's land in `src/data/topic-map.json`; rerun it after changing either, and commit its output. The site loads no shader library.
- `src/content/writing/`: git submodule of [meaningfool-writing](https://github.com/meaningfool/meaningfool-writing), which holds every article, activity log and image. Read its `AGENTS.md` before editing content.
- `src/content.config.ts`: the content collection, built from `articles/*.md` and `daily-logs/*.md` in the submodule.
- `src/pages/index.astro`: the home. Its Writing list holds the articles, not the activity logs, and picking a topic on the map shows only the articles about it. An article's topics are its `topics` frontmatter field (`customer-success`, `product`, `ai-agents`: each one that makes up 30% or more of it), or else its entry in `src/data/topics.ts`.
- `src/pages/index.astro`: the home. Its Writing list holds the articles, not the activity logs, and picking a topic on the map shows only the articles about it. An article's topics are its `topics` frontmatter field (`customer-success`, `product`, `ai-agents`: each one that makes up 30% or more of it).
- `src/pages/articles/[slug].astro`: the page of every article and activity log. The Markdown goes through the plugins in `src/lib/` set in `astro.config.mjs`: the `# Title` line is dropped, the headings are renumbered from h2, and `rehype-article.mjs` shapes figures, YouTube embeds (moved to youtube-nocookie.com) and tables for `src/styles/prose.css`. In a table, a row whose cells are all bold is highlighted, and a paragraph starting with `Table:` right after it, past a blank line, is its caption. An image alone in its paragraph is captioned with its title, or with its alt text when that has a space.
- `src/data/projects.ts`: the projects, with their screenshots in `src/assets/projects/`, and which articles belong to each, by article id.
- `docs/document-sharing.md`: how the encrypted portfolio links on `/documents/` work and how to create them.
Expand Down
3 changes: 2 additions & 1 deletion src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const writing = defineCollection({
description: z.string().optional(),
tags: z.array(z.string()).optional(),
// The topics an article is about, each one 30% or more of it: customer-success, product, ai-agents. The home's
// topic map filters Writing with them. Articles without the field take theirs from src/data/topics.ts.
// topic map filters Writing with them. Activity logs don't need the field; the home warns about an article
// without it.
topics: z.array(z.enum(TOPICS)).optional(),
}),
});
Expand Down
43 changes: 4 additions & 39 deletions src/data/topics.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// The site's three topics, the three orange dots of the topic map, and which of them each article is about. Picking a
// topic on the home's map shows only the articles about it.
// The site's three topics, the three orange dots of the topic map. Picking a topic on the home's map shows only the
// articles about it.
//
// An article belongs to a topic when that topic makes up 30% or more of it, so it can belong to more than one. An
// article's topics come from its `topics` frontmatter field in meaningfool-writing when it has one, for example
// `topics: ["product", "ai-agents"]`. Until every article has that field, the list below gives the topics of the 18
// articles the prototype read (docs/redesign/prototype/maps.js, the weights in each article's `w`), by article id.
import type { CollectionEntry } from 'astro:content';
// article's topics are its `topics` frontmatter field in meaningfool-writing, for example
// `topics: ["product", "ai-agents"]`, which takes the ids below.
import map from './topic-map.json';

/** The topics' ids, which the map's dots carry and the frontmatter's `topics` field takes. */
Expand All @@ -19,36 +17,3 @@ if (map.topics.map(({ id }) => id).join() !== TOPICS.join()) {

/** The topics' names, as the map shows them: "Customer success", "Product", "AI agents". */
export const TOPIC_NAMES = Object.fromEntries(map.topics.map(({ id, name }) => [id, name])) as Record<Topic, string>;

// The prototype's weights are [customer success, product, AI agents]; each topic at 0.3 or more is listed.
const fromPrototype: Record<string, Topic[]> = {
'edit-collections-generative-ai': ['ai-agents'], // 0.1, 0.15, 0.75
'structured-extraction': ['ai-agents'], // 0.2, 0.1, 0.7
'thoughts-on-human-ai-collaboration': ['customer-success', 'product', 'ai-agents'], // 0.3, 0.3, 0.4
'detecting-bounding-boxes': ['ai-agents'], // 0.05, 0.1, 0.85
'agent-frameworks-for-the-rest-of-us': ['ai-agents'], // 0.05, 0.15, 0.8
'how-long-for-that-change': ['product'], // 0.15, 0.7, 0.15
'january-in-review': ['ai-agents'], // 0.05, 0.25, 0.7
'prolifics-and-frugals': ['product'], // 0.2, 0.7, 0.1
'the-slicing-fallacy': ['product'], // 0.25, 0.7, 0.05
'we-cant-stop-planning': ['product'], // 0.2, 0.75, 0.05
cost_of_change_is_agile_ceiling: ['product'], // 0.15, 0.75, 0.1
compounding_dev_process: ['product', 'ai-agents'], // 0.05, 0.5, 0.45
antigravity_is_onto_something: ['ai-agents'], // 0.05, 0.25, 0.7
'semi-async_valley_of_death': ['product', 'ai-agents'], // 0.1, 0.3, 0.6
'spec-driven-development-has-it-backwards': ['product', 'ai-agents'], // 0.1, 0.5, 0.4
'ai-engineer-paris-conference-notes': ['ai-agents'], // 0.05, 0.15, 0.8
'voice-ai-maven-course-notes': ['ai-agents'], // 0.05, 0.1, 0.85
'faster-answers-better-questions': ['customer-success', 'product'], // 0.3, 0.5, 0.2
};

/** An article's topics: its `topics` frontmatter field, or else the list above; undefined when it has neither. */
export function topicsOf(article: CollectionEntry<'writing'>): Topic[] | undefined {
return article.data.topics ?? fromPrototype[article.id];
}

/** The ids in the list above that match no article, for example after an article's file was renamed. */
export function unknownTopicIds(articleIds: Iterable<string>): string[] {
const ids = new Set(articleIds);
return Object.keys(fromPrototype).filter((id) => !ids.has(id));
}
10 changes: 4 additions & 6 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
---
// The home: the intro beside the topic map, then Projects and Writing. Writing lists the articles, newest first; the
// activity logs aren't listed, but their pages keep their URLs. Picking an orange dot on the map shows only the
// articles about its topic (src/data/topics.ts), and picking it again, or "Show all", shows them all again.
// articles about its topic (each article's `topics` frontmatter field), and picking it again, or "Show all", shows
// them all again.
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro';
import Intro from '../components/Intro.astro';
import JsonLd from '../components/JsonLd.astro';
import { projects, projectOf } from '../data/projects';
import { profiles } from '../data/profiles';
import { TOPIC_NAMES, topicsOf, unknownTopicIds } from '../data/topics';
import { TOPIC_NAMES } from '../data/topics';
import { newestFirst } from '../lib/sort-posts';
import { readingMinutes } from '../lib/reading-time.mjs';
import { HOME_INTRO, articlesOnly, plural } from '../lib/share';
Expand All @@ -19,11 +20,8 @@ const description =
"Josselin Perrus's site: a product manager in Paris building AI products with AI agents. His projects, and writing on customer success, product and AI agents.";

const articles = articlesOnly(await getCollection('writing')).sort(newestFirst);
for (const id of unknownTopicIds(articles.map((article) => article.id))) {
console.warn(`src/data/topics.ts gives topics to an article that doesn't exist: ${id}`);
}
const rows = articles.map((article) => {
const topics = topicsOf(article);
const topics = article.data.topics;
// At 30% or more, every article has at least one topic: three shares under 30% can't make a whole.
if (!topics?.length) console.warn(`"${article.id}" has no topics, so picking a topic on the home's map hides it: give it a topics field.`);
return {
Expand Down
Loading