I built Wayoflife, a systematic AI tool that helps developers pick the right tool for each job. It has 21 use cases and 165 tools, and each use case is a short ranked list with a one-line verdict per pick. This post covers the parts that might be useful in your own projects: modeling rankings as validated data, keeping sponsorship out of the ordering in code, making a static site readable by chatbots, and letting an AI agent ship changes without breaking things.
The problem with "AI directories"
Most directories are catalogs: thousands of tools per category, no order, and often paid placement at the top. As a developer I didn't want a catalog. I wanted "for this job, use this, and here's why in one line." That's a data-modeling problem before it's a UI problem.
Rankings as data: Astro content collections + zod
Everything lives in src/content/ as JSON: one file per tool, one per use case, one per release. The schema (trimmed):
// src/content.config.ts const tools = defineCollection({ loader: glob({ pattern: '*.json', base: './src/content/tools' }), schema: z.object({ name: z.string(), url: z.string().url(), affiliateUrl: z.union([z.string().url(), z.literal('')]).optional().default(''), // Paid featured listing: adds a labeled badge. NEVER changes ranking order. featured: z.boolean().optional().default(false), pricing: z.enum(['free', 'freemium', 'paid']), description: z.string(), pros: z.array(z.string()).default([]), cons: z.array(z.string()).default([]), lastReviewed: z.string(), addedAt: z.string(), }), }); // One JSON file per use case; `rankings` order IS the editorial rank. const useCases = defineCollection({ loader: glob({ pattern: '*.json', base: './src/content/use-cases' }), schema: z.object({ name: z.string(), summary: z.string(), order: z.number().default(99), lastUpdated: z.string(), rankings: z.array(z.object({ tool: reference('tools'), verdict: z.string(), bestFor: z.string(), })), }), }); A use case then looks like this (real excerpt from coding-agents.json):
{ "name": "Coding agents", "rankings": [ { "tool": "claude-code", "verdict": "The strongest all-round agent for real codebases; start here if you can live in a terminal.", "bestFor": "Large refactors and multi-file features" }, { "tool": "cursor", "verdict": "The best AI editor for most developers: fast autocomplete plus a capable agent.", "bestFor": "Everyday coding in a familiar IDE" }, { "tool": "openai-codex", "verdict": "Great for firing off many tasks in parallel and reviewing PRs later.", "bestFor": "Async, parallel tasks" } ] } Two things fall out of this design:
- Rank is the array index, and nothing else. No
scorefield to fiddle with and no sort function that could quietly take sponsorship into account. -
reference('tools')makes broken links a build error. If a slug doesn't match a tool file,astro buildfails. That matters a lot once an agent is editing content (more below).
Keeping money out of the order, in code
Sponsorship is a boolean on the tool, while the order lives on the use case. The page renders the ranked list exactly as stored, then a separate, labeled "Featured" box below it. Affiliate links (when set) get rel="sponsored noopener":
export const outbound = (t: Tool) => t.data.affiliateUrl || t.data.url; export const outboundRel = (t: Tool) => (t.data.affiliateUrl ? 'sponsored noopener' : 'noopener'); Because the two concerns are in different files, a reviewer (human or agent) can see from the diff that a sponsorship change can't touch ordering.
Making a static site readable by chatbots
People increasingly ask an assistant "what's the best coding agent?" instead of searching. So the site is built for both:
-
/llms.txtand/llms-full.txt, generated at build time by an Astro endpoint from the same collections:
// src/pages/llms.txt.ts (trimmed) for (const u of useCases) { const top = u.data.rankings.slice(0, 3) .map((r, i) => `#${i + 1} ${tm.get(r.tool.id)!.data.name}`).join(', '); lines.push(`- [Best AI tools for ${u.data.name.toLowerCase()}](${abs(`/use-cases/${u.id}/`)}): ${top}`); } -
ItemListJSON-LD withpositionon every ranking page, so the order is explicit in structured data, not just implied by the HTML. - robots.txt that explicitly allows GPTBot, ClaudeBot, PerplexityBot, Google-Extended and friends, plus a sitemap and IndexNow pings after deploys.
Letting an AI agent ship without breaking things
An AI agent writes most of the code and content updates. What made that safe:
- The schema is the contract. The agent can add 20 tools in one go, and a wrong enum value or missing field fails the build.
- Build + link check before every deploy.
astro buildthen a link checker over the output. In practice this caught more agent mistakes than reading diffs did. - Deploy from a clean tree. Vercel blocked production deploys whose git commit author wasn't on the team. The fix was to deploy from
git archive HEAD | tar -x -C /tmp/deploy(no.git), which also guarantees only committed files ship.
What I'd do differently
- Buy a domain on day one. You can't verify a
*.vercel.appsubdomain with email providers, so newsletter sending waits on that. - Model "last reviewed" per ranking entry, not just per tool, so stale verdicts are easier to find.
Try it, and tell me what's wrong
If you're a developer choosing between coding agents, agent frameworks or vibe-coding builders, the ranked lists are here: wayoflife-ai.vercel.app. It's free, has no paid ranks, and adds a "New this week" feed of releases. I'd love to hear which ranking you'd change.
This article was written with AI assistance and checked against the site's source code.


