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:

  1. Rank is the array index, and nothing else. No score field to fiddle with and no sort function that could quietly take sponsorship into account.
  2. reference('tools') makes broken links a build error. If a slug doesn't match a tool file, astro build fails. 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.txt and /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}`); } 
  • ItemList JSON-LD with position on 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 build then 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.app subdomain 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.