Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
355c43c
Curate projects listing
schalkneethling Jul 6, 2026
98ae281
Add minimal project detail pages
schalkneethling Jul 6, 2026
603ac95
Add project detail pages
schalkneethling Jul 6, 2026
4a7bbd5
Fix projects masonry card visibility
schalkneethling Jul 6, 2026
e758af1
Merge branch 'codex/curated-projects-listing-1344' into codex/project…
schalkneethling Jul 6, 2026
7a7c079
Restore project card image headers
schalkneethling Jul 6, 2026
55368f4
Merge branch 'codex/curated-projects-listing-1344' into codex/project…
schalkneethling Jul 6, 2026
95300d9
Polish project card metadata and links
schalkneethling Jul 6, 2026
8ebd7c2
Merge branch 'codex/curated-projects-listing-1344' into codex/project…
schalkneethling Jul 6, 2026
756321f
Narrow project detail links
schalkneethling Jul 6, 2026
8be9ce0
Merge pull request #1352 from schalkneethling/codex/project-detail-pa…
schalkneethling Jul 6, 2026
3f13757
Complete starter project content
schalkneethling Jul 6, 2026
8b0dccb
Redesign project detail pages
schalkneethling Jul 6, 2026
3ed784f
Merge pull request #1353 from schalkneethling/codex/complete-starter-…
schalkneethling Jul 6, 2026
5688e3b
Add manual project doc cache refresh
schalkneethling Jul 6, 2026
d2d37e0
Merge pull request #1357 from schalkneethling/codex/manual-repo-doc-c…
schalkneethling Jul 6, 2026
4bacc26
Add project-local projects skill
schalkneethling Jul 6, 2026
2c64f5c
Merge pull request #1358 from schalkneethling/codex/project-local-wha…
schalkneethling Jul 6, 2026
3c52dfb
Refine project detail page design
schalkneethling Jul 6, 2026
2a07735
Merge branch 'main' into codex/curated-projects-listing-1344
schalkneethling Jul 6, 2026
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
61 changes: 61 additions & 0 deletions .codex/skills/what-i-want-to-exist-projects/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
name: what-i-want-to-exist-projects
description: Use when adding, updating, or reviewing entries for the schalkneethling.com "What I want to exist" projects area, including project cards, project detail pages, repo doc cache refreshes, GOAL.md/ROADMAP.md links, and the little demos section.
---

# What I Want To Exist Projects

Use this skill for changes under `src/content/projects/`, `src/pages/projects*`,
`src/components/Projects/`, `src/data/project-doc-cache/`, or tests that protect
the curated project listing and detail pages.

## Workflow

1. Read the current project entry or nearby entries in `src/content/projects/`.
2. If adding or materially updating a project, inspect the repo, cached docs, and
public project page context before writing summaries.
3. Keep public page copy editorial: summarize `GOAL.md`, `ROADMAP.md`, issues,
and repo docs instead of pasting long verbatim source text.
4. Preserve the current card/detail conventions:
- `/projects` remains the listing URL.
- Cards link to internal `/projects/{slug}` pages.
- Every card needs an `imageUrl`.
- `category: "main"` appears in the Projects section.
- `category: "demo"` appears in Little demos.
5. Update tests before implementation when behavior or required content changes.
6. Run the narrow relevant tests first, then the site checks listed below.

## Adding Or Updating A Project

For schema details and a frontmatter checklist, read
`references/project-entry.md`.

Use the project slug as the filename in `src/content/projects/{slug}.md`.
Keep ordering deliberate. Do not automate project ordering unless the user asks.

If repo docs changed, run:

```sh
pnpm run refresh:project-docs
```

Review the generated cache diff under `src/data/project-doc-cache/`. The cache
is source material for editorial summaries, not content rendered directly into
the public pages.

## Validation

Run the smallest meaningful test first. Common commands:

```sh
pnpm exec vitest run tests/projectContent.test.ts
pnpm exec vitest run tests/projectFilters.test.ts tests/projectPages.test.ts
pnpm exec vitest run tests/projectDocCache.test.ts
pnpm exec vitest run tests/projectSkill.test.ts
pnpm run typecheck
pnpm run build
pnpm run test:a11y
```

For listing or detail page UI changes, include the relevant Playwright coverage
in `tests/projects.spec.ts` and verify accessibility.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "What I Want To Exist Projects"
short_description: "Maintain curated project pages"
default_prompt: "Use $what-i-want-to-exist-projects to add or update a project page."
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Project Entry Reference

Project entries live in `src/content/projects/{slug}.md`.

## Required Frontmatter

- `title`: Display name.
- `description`: Short card summary.
- `category`: `"main"` for Projects or `"demo"` for Little demos.
- `order`: Integer used for deliberate listing order.
- `repoUrl`: GitHub repository URL.
- `imageUrl`: Card/detail visual. Prefer the repo OpenGraph image when present.

## Optional Frontmatter

- `language`: Primary language shown on cards/detail facts.
- `stars`: GitHub star count shown on cards.
- `liveUrl`: Live site or demo URL when available.
- `goalDocUrl`: Public GitHub link to `GOAL.md` when available.
- `roadmapDocUrl`: Public GitHub link to `ROADMAP.md` when available.
- `technologies`: Short list of project technologies.
- `whatAndWhy`: Editorial "what and why" detail-page summary.
- `goalSummary`: Editorial summary of the project goal.
- `currentState`: Where the project is now.
- `nextSteps`: Short list of what comes next.
- `contributionGuidance`: How people can help.

## Content Rules

- Use editorial summaries, not pasted repo docs.
- Keep summaries concrete and written in the site’s voice.
- Prefer links to `GOAL.md` and `ROADMAP.md` over duplicating full documents.
- Make missing docs visible only when useful; do not invent links.
- Keep detail content complete for `category: "main"` starter-style projects.

## Tests To Consider

- `tests/projectContent.test.ts`: required project fields, doc links, live URLs.
- `tests/projectFilters.test.ts`: category grouping and ordering.
- `tests/projectPages.test.ts`: internal project URL helpers.
- `tests/projects.spec.ts`: listing/detail rendering and accessibility-related UI.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"setup:playwright": "pnpm exec playwright install chromium",
"build": "pnpm run generate:og && astro build",
"generate:og": "node scripts/generate-og-cards.mjs",
"refresh:project-docs": "node scripts/refresh-project-doc-cache.mjs",
"preview": "astro preview",
"astro": "astro",
"typecheck": "astro check",
Expand Down
3 changes: 3 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: "pnpm run dev --host 127.0.0.1",
env: {
ASTRO_DEV_BACKGROUND: "1",
},
url: "http://localhost:4321",
reuseExistingServer: !process.env.CI,
},
Expand Down
223 changes: 223 additions & 0 deletions scripts/refresh-project-doc-cache.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

const DEFAULT_CONTENT_DIRECTORY = "src/content/projects";
const DEFAULT_CACHE_DIRECTORY = "src/data/project-doc-cache";
const DEFAULT_BRANCH = "main";
const DOC_BRANCHES = [DEFAULT_BRANCH, "master"];
const FETCH_TIMEOUT_MS = 10_000;

const DOCS = [
{ key: "goal", filename: "GOAL.md" },
{ key: "roadmap", filename: "ROADMAP.md" },
];

function parseFrontmatter(contents) {
const match = contents.match(/^---\n(?<frontmatter>[\s\S]*?)\n---/);

if (!match?.groups?.frontmatter) {
return {};
}

return Object.fromEntries(
match.groups.frontmatter
.split("\n")
.map((line) =>
line.match(/^(?<key>[A-Za-z][A-Za-z0-9]*):\s+"?(?<value>[^"]+)"?\s*$/),
)
.filter((match) => match?.groups)
.map((match) => [match.groups.key, match.groups.value]),
);
}

function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}

export function repoUrlToRawDocUrl(
repoUrl,
filename,
branch = DEFAULT_BRANCH,
) {
const url = new URL(repoUrl);

if (url.hostname !== "github.com") {
throw new Error(`Unsupported repository host: ${url.hostname}`);
}

const [owner, repo] = url.pathname.replace(/^\/|\/$/g, "").split("/");

if (!owner || !repo) {
throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
}

return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filename}`;
}

async function readProjectEntries(contentDirectory) {
const filenames = await readdir(contentDirectory);
const markdownFiles = filenames
.filter((filename) => filename.endsWith(".md"))
.sort((a, b) => a.localeCompare(b));

const projects = [];

for (const filename of markdownFiles) {
const id = path.basename(filename, ".md");
const contents = await readFile(path.join(contentDirectory, filename), "utf8");
const frontmatter = parseFrontmatter(contents);

if (!frontmatter.repoUrl) {
continue;
}

projects.push({
id,
repoUrl: frontmatter.repoUrl,
title: frontmatter.title ?? id,
});
}

return projects;
}

async function fetchDoc(project, doc, fetchImpl) {
let sourceUrl = "";

try {
let missingResult;

for (const branch of DOC_BRANCHES) {
sourceUrl = repoUrlToRawDocUrl(project.repoUrl, doc.filename, branch);
const response = await fetchImpl(sourceUrl, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});

if (response.status === 404) {
missingResult = {
filename: doc.filename,
sourceUrl,
status: "missing",
statusCode: response.status,
content: "",
};
continue;
}

if (!response.ok) {
return {
filename: doc.filename,
sourceUrl,
status: "failed",
statusCode: response.status,
content: "",
error: `GitHub returned ${response.status}`,
};
}

return {
filename: doc.filename,
sourceUrl,
status: "fetched",
statusCode: response.status,
content: await response.text(),
};
}

return missingResult;
} catch (error) {
return {
filename: doc.filename,
sourceUrl,
status: "failed",
content: "",
error: getErrorMessage(error),
};
}
}

function updateSummary(summary, docResult) {
if (docResult.status === "fetched") {
summary.fetched += 1;
} else if (docResult.status === "missing") {
summary.missing += 1;
} else {
summary.failed += 1;
}
}

export async function refreshProjectDocCache({
contentDirectory = DEFAULT_CONTENT_DIRECTORY,
cacheDirectory = DEFAULT_CACHE_DIRECTORY,
fetch = globalThis.fetch,
now = new Date(),
} = {}) {
if (!fetch) {
throw new Error("A fetch implementation is required");
}

const projects = await readProjectEntries(contentDirectory);
const refreshedAt = now.toISOString();
const summary = {
projects: projects.length,
fetched: 0,
missing: 0,
failed: 0,
};

await mkdir(cacheDirectory, { recursive: true });

for (const project of projects) {
const docs = {};

for (const doc of DOCS) {
const docResult = await fetchDoc(project, doc, fetch);
docs[doc.key] = docResult;
updateSummary(summary, docResult);
}

await writeFile(
path.join(cacheDirectory, `${project.id}.json`),
`${JSON.stringify(
{
id: project.id,
title: project.title,
repoUrl: project.repoUrl,
refreshedAt,
docs,
},
null,
2,
)}\n`,
);
}

return summary;
}

async function runCli() {
const summary = await refreshProjectDocCache();

console.log(
[
`Refreshed project doc cache for ${summary.projects} projects.`,
`Fetched: ${summary.fetched}.`,
`Missing: ${summary.missing}.`,
`Failed: ${summary.failed}.`,
].join(" "),
);

if (summary.failed > 0) {
process.exitCode = 1;
}
}

const currentFileUrl = pathToFileURL(fileURLToPath(import.meta.url)).href;
const executedFileUrl = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: "";

if (currentFileUrl === executedFileUrl) {
await runCli();
}
2 changes: 1 addition & 1 deletion src/components/Header/Navigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<li><a href="/">Return home</a></li>
<li><a href="/now">What I'm doing now</a></li>
<li><a href="/about">#whoami</a></li>
<li><a href="/projects">Things I build</a></li>
<li><a href="/projects">What I want to exist</a></li>
<li><a href="/blog">My writing</a></li>
<li><a href="/contact">Get in touch</a></li>
</ul>
Expand Down
Loading