A browsable catalog of what you can build with software: Application Ideas (59 things worth building) and Types of Applications (the 24 shapes software takes). Every card expands to how you would actually approach it.
This is the discovery-catalog branch — a different design from main, which
holds the Proposal and the interactive Map. Here the whole site is discovery:
the catalog is the argument, and there is no prose to read first.
Built with Astro and hostable free on GitHub Pages. The
catalog lives in typed
content collections
(markdown under src/content/) rather than a database — there is no server to
run and nothing to pay for.
git clone <this repo>
npm run setup # installs dependencies and the Playwright browser
npm run dev # http://127.0.0.1:4321Other commands:
npm run build # type-check + static build into dist/
npm run test # unit tests (vitest)
npm run check # astro type-check only/ — Types of Applications. 24 types, ordered as a climb rather than
alphabetically: each roughly adds one capability and one class of problem to the
one before — and the cards are numbered 01 to 24 so that ordering is visible,
since in a three-column grid the eye reads across rather than down the argument.
Expanding a card gives five sections in one column at a readable measure: examples, what it is, good for, the trade-offs, and the
technologies typically used.
Every field in the schema is rendered by the surface that reads it. Frontmatter nothing displays is a maintenance cost with no reader, so a new field should arrive with the markup that shows it.
Examples lead, because a type is an abstraction and the fastest way to
understand one is to see things you recognise built that way. The list is the
type's curated exampleIdeas followed by every other idea that names this type,
capped at ten with a link to the rest — so adding an idea needs no edit to any
type.
Types lead because the taxonomy is the frame: knowing that a static site is free and unbreakable, and that a database is what buys you shared state, is what makes any individual idea legible.
/ideas — Application Ideas. 59 ideas, from a personal website you finish
this afternoon to firmware on a chip. Each card carries a one-line pitch, its
family, a rough effort, and how many ways there are to build it. Expanding it
gives two sections in one column: what it is, and how you can build it —
three or four approaches, easiest first, each with a summary, its pros and cons,
and a link to the application type it corresponds to.
The approaches are where the two collections meet, and they are the argument of the whole catalog: an idea is not one thing to build. A run tracker is a form you fill in after a run, or an installable web page, or a native app, or a phone that syncs to a web app — four different costs for four different versions of the same sentence. There are 199 approaches across the 59 ideas.
These are two real routes, not a JavaScript tab toggle — each is separately linkable and the browser Back button undoes a switch.
Ideas and types point at each other by slug, in two places.
Each approach names its type. That is the load-bearing link: an idea says "you could build this as a browser-only app, or as a native mobile app", and each of those is a real type with a card of its own. Inverting that mapping is where a type's example ideas come from — a type claims an idea exactly when that idea describes building it that way, so the two directions cannot drift.
Each idea also has a shape: one type slug, the form it most naturally
takes, which is where its category colour and its complexity are read
from. It is a single value on purpose. An idea used to carry a types array of
every shape it might take, and once the approaches existed that array was wrong
for 55 of 59 ideas — the approaches already enumerate the options, and far more
honestly, since they say what each one costs.
A type also lists a few curated exampleIdeas, which lead its derived ones.
Zod cannot check a reference into a sibling collection, so src/lib/catalog.ts
resolves all of these at build time and drops any that point at nothing — a
typo costs one link, not the build.
Families are the nine buckets in src/lib/families.ts that the filter, the
coloured rail on every card, and the --fam-* colour tokens all key off. They
are defined in code rather than content because adding a tenth is a design
decision.
Four facets — Category, Time, Money, Complexity — each a labelled button showing its current answer, opening a modal picker of options with live counts. A facet names its question out loud rather than asking the reader to infer a grouping from nine unlabelled colour chips, which is what the first version did and why the catalog read as denser than it is.
Options within one facet are OR-ed; facets are AND-ed with each other and with the search box, so every question answered narrows the result. Option counts are recomputed against everything else that is selected, so opening "Time" while "Games" is on says how many games take a weekend.
Every facet a card can be filtered by is also displayed on that card — category, complexity, time, and money all appear in the collapsed head. A result that cannot explain why it matched makes a filter feel broken even when it is correct.
Where the values come from:
| Facet | Types | Ideas |
|---|---|---|
| Category | its family |
the family of its shape |
| Time | time in frontmatter |
time in frontmatter |
| Money | money in frontmatter |
money in frontmatter |
| Complexity | complexity (1–5) |
the complexity of its shape |
time and money are enums, deliberately coarse — "a weekend" and "a week or
two" are different decisions, "nine days" and "eleven days" are not. They sit
alongside the free-text effort and hosting sentences a card actually
displays: the sentence is for reading, the band is for filtering. The enums in
src/content/config.ts and the option lists in src/lib/facets.ts have to
agree — a value with no option is unreachable, an option with no value filters
to nothing.
Every card is rendered into the page up front; the filter only hides the ones
that do not match, which is why the catalog stays a static file with no data
fetching. The rule itself is a pure function in src/lib/filter.ts
(unit-tested, no DOM), and the script in FilterBar.astro is a thin binding
layer.
Selections are mirrored into the URL, so a filtered view is a link:
/ideas?category=hardware, /?time=weekend&money=free. That is what lets a
type card link to "all database-backed ideas" without knowing anything about the
other page's markup.
Without JavaScript there is nothing to filter with, so the bar ships hidden
and the script reveals it: the page degrades to the complete catalog rather than
to a row of dead buttons, and <details> still opens on click.
src/
content/
config.ts # typed collection schemas (the data contract)
types/ # the 24 application types — one markdown file each
ideas/ # the 59 ideas — one markdown file each
lib/
families.ts # the nine families, and resolving an idea into one
facets.ts # the four filter questions and their options
filter.ts # the catalog filter, as pure functions
catalog.ts # collections -> view models, cross-links resolved
collections.ts # shared ordering helper
layouts/
BaseLayout.astro # shell: masthead + <head> meta + footer
components/
SiteHeader.astro # wordmark + the two-route switcher
Hero.astro # kicker, headline, standfirst
FilterBar.astro # facet buttons, modal pickers, search, URL sync
Legend.astro # what the nine family colours mean
ComplexityMeter.astro # the shared 1-5 machinery meter
IdeaCard.astro # one idea, collapsed then expanded
TypeCard.astro # one type, with its complexity meter
Detail.astro # one labelled block inside an expanded card
OpenOnHash.astro # makes a link to a card open that card
pages/
index.astro # Types of Applications (home)
ideas.astro # Application Ideas
styles/
tokens.css # design tokens as CSS custom properties
scripts/
og-template.html # source for the social preview image
og.mjs # renders it to public/og.png
Adding an idea or a type is a content edit, not a code change: add a
markdown file under src/content/ideas/ or src/content/types/ with the
frontmatter the schema in src/content/config.ts requires. The filename is the
slug that cross-links point at.
order decides the sequence. For types it is the editorial climb — rung 9
follows rung 8 because the argument says so, not because of how the files are
named. Everything the cards render lives in frontmatter; neither collection uses
a markdown body.
Dark, dense, and monospace-inflected, because the job is browsing several dozen
cards at once rather than reading one document. Colour is semantic: the nine
--fam-* tokens are the families and nothing else, and --accent means
interactive state. The site is deliberately single-theme, with no webfonts and
no external assets.
Every text colour clears 4.5:1 against every surface it sits on, which is the constraint that matters most here — the smallest text on the site is 11px monospace carrying the effort, the cost, and every section label.
Both catalogs are navigable by heading: the page is an h1, every card title an
h2, every section inside a card an h3. Card titles are real headings rather
than styled spans, so the 24 types and 59 ideas exist in the document outline.
The social preview image is generated, not drawn: edit
scripts/og-template.html and run node scripts/og.mjs to rewrite
public/og.png. It uses the same tokens as the site, so it cannot drift from
the design by hand.
Already configured. Pushing to main triggers
.github/workflows/deploy.yml, which builds the site and publishes it to
https://codeyam-ai.github.io/codeyam-academy-site/.
The base path. A GitHub Pages project site is served from a repo subpath,
so the build needs base: '/codeyam-academy-site' — but a base path applies to
the dev server too, and with it always on npm run dev serves at that subpath
and every codeyam scenario pointing at / or /ideas 404s. So astro.config.mjs
reads the base from SITE_BASE, which only the deploy workflow sets. Dev stays
at the root; the deployed build gets the prefix.
To reproduce exactly what CI builds:
SITE_BASE=/codeyam-academy-site npm run buildThis is also why internal links go through withBase() in src/lib/urls.ts
rather than being written as plain /ideas — see
The filter.
Custom domain. Add a CNAME file to public/ containing the domain, drop
SITE_BASE from the workflow so the base returns to /, point site at the
domain, and update DNS. No markup changes are needed: the links collapse back to
plain paths on their own. DEPLOY_SETUP.md has the exact A
records and CNAME value.
An optional CMS (so the curriculum can be edited without touching markdown) is documented in CMS_SETUP.md; it is not wired up by default.
This project is built with codeyam-editor — code and runnable data scenarios are authored side by side against a live preview.
# Clone the repo
git clone https://github.com/codeyam-ai/codeyam-academy-site && cd codeyam-academy-site
# Install codeyam-editor
npm install -g @codeyam-editor/codeyam-editor@latest
# Launch the editor (split-screen terminal + live preview)
codeyam-editor startThe captured scenarios in .codeyam/ still describe the Proposal and Map
surfaces this branch replaced, so the gallery is empty until they are recaptured
against the catalog.