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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +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/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: 1 addition & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ export default defineConfig({
shikiConfig: { theme: 'github-light' }, // code blocks sit on the white page, in a light frame (prose.css)
},
integrations: [sitemap({
// TEMPORARY (#30): /map-preview/ shows the intro and the map until the home uses them; #31 deletes it.
filter: (page) => !/^\/(documents|map-preview)\/?$/.test(new URL(page).pathname),
filter: (page) => !/^\/documents\/?$/.test(new URL(page).pathname),
serialize: (item) => ({ ...item, lastmod: lastModified.get(new URL(item.url).pathname) }),
}), trailingSlashRedirects()],
site: 'https://meaningfool.net',
Expand Down
72 changes: 0 additions & 72 deletions src/components/ArticlesList.astro

This file was deleted.

8 changes: 2 additions & 6 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
---
// The site footer: the wordmark, then About and the profiles elsewhere. On phones the links go under the wordmark.
import Wordmark from './Wordmark.astro';
import { profiles } from '../data/profiles';

const links = [
{ href: '/about/', label: 'About' },
{ href: 'https://www.linkedin.com/in/josselinperrus/', label: 'LinkedIn' },
{ href: 'https://github.com/meaningfool', label: 'GitHub' },
{ href: 'https://x.com/nonils', label: 'X' },
];
const links = [{ href: '/about/', label: 'About' }, ...profiles];
---

<footer class="foot">
Expand Down
4 changes: 4 additions & 0 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';
import { postId } from './lib/posts.mjs';
import { TOPICS } from './data/topics';

const writing = defineCollection({
loader: glob({
Expand All @@ -23,6 +24,9 @@ const writing = defineCollection({
updated: z.date().optional(),
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.
topics: z.array(z.enum(TOPICS)).optional(),
}),
});

Expand Down
6 changes: 6 additions & 0 deletions src/data/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Josselin's profiles elsewhere, linked from the footer and the home's intro.
export const profiles = [
{ href: 'https://www.linkedin.com/in/josselinperrus/', label: 'LinkedIn' },
{ href: 'https://github.com/meaningfool', label: 'GitHub' },
{ href: 'https://x.com/nonils', label: 'X' },
];
54 changes: 54 additions & 0 deletions src/data/topics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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.
//
// 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';
import map from './topic-map.json';

/** The topics' ids, which the map's dots carry and the frontmatter's `topics` field takes. */
export const TOPICS = ['customer-success', 'product', 'ai-agents'] as const;
export type Topic = (typeof TOPICS)[number];

// The map's dots (topic-map.json) are these three topics, in this order.
if (map.topics.map(({ id }) => id).join() !== TOPICS.join()) {
throw new Error(`src/data/topic-map.json's topics should be ${TOPICS.join(', ')}, as in src/data/topics.ts`);
}

/** 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));
}
3 changes: 2 additions & 1 deletion src/lib/share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const HOME_INTRO =
/** The share image of Home and About. The activity logs, which have none of their own, use it too. */
export const SITE_SHARE: Share = { path: '/share.png', alt: `Josselin Perrus. ${HOME_INTRO}` };

const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? '' : 's'}`;
/** "18 articles", "1 project" */
export const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? '' : 's'}`;

/** "May 27, 2026": the date in the content is a day, which UTC keeps whatever the build machine's time zone. */
export function formatDate(date: Date): string {
Expand Down
Loading
Loading