ry stands for route yes.
An AI router for 404s, built on Cloudflare Clef.
When someone lands on a page that doesn't exist, ry asks Clef which real page they meant and sends them there.
/priceing → /pricing/ typo
/docs/quickstart → /docs/getting-started/ renamed page
/release-notes → /changelog/ synonym
/About/ → /about/ fixed without AI
/our-team → "did you mean About or Careers?"
/wp-admin/setup.php → 404 bot probe, ignored
Clef is a decision model, not a chatbot. You hand it a list of options and it returns a probability for each one. That makes it a good fit here:
- It can't make up a URL. Your pages are the options, so Clef can only pick one of them, or "none".
- The probabilities mean something. ry redirects when Clef is sure, suggests pages when it's torn, and leaves the 404 alone otherwise.
- One call does two jobs. The same request asks whether the visitor looks like a person or a bot scanning for
/wp-admin.
npm i route-yes1. List your pages at build time. This writes route-yes.json with every page's path, title, and description.
| Framework | Add |
|---|---|
| Vite (React, Vue, Svelte, TanStack Router, React Router…) | plugins: [routeYes()] from route-yes/vite |
| Astro | integrations: [routeYes()] from route-yes/astro |
| Anything else | npx route-yes manifest dist after your build |
The Vite plugin reads built HTML, sitemap*.xml, and TanStack's routeTree.gen.ts. Add pages it can't see with routeYes({ routes: ["/pricing"] }).
2. Route 404s in your Worker.
import { withRouteYes } from "route-yes/worker";
// Static site: serves from ASSETS and routes the misses
export default withRouteYes();
// SSR framework: wrap its handler instead
// export default withRouteYes(handler);3. Add the bindings.
Important
not_found_handling must be "none". With 404-page or single-page-application, Cloudflare answers missing pages before your Worker runs. ry still serves your 404.html when it doesn't redirect.
In a single-page app the 404 happens in the browser, so your not-found view asks the Worker:
import { routeYes } from "route-yes/client";
// e.g. in TanStack Router's notFoundComponent
const decision = await routeYes({ navigate: (to) => navigate({ to, replace: true }) });
// No confident match? Show decision.suggestions instead.Add "run_worker_first": ["/_route-yes/*"] to assets so those requests reach the Worker.
- Fix the easy ones. Differences in case, slashes,
.html, or/indexget a 301. - Skip the noise. Asset requests and obvious probes (
.env,.php,wp-admin) keep their 404. - Fix typos. A URL one or two keystrokes from exactly one page redirects immediately.
- Shortlist. On sites with more than 60 pages, ry picks the 60 likeliest by spelling and by meaning (Workers AI embeddings), so
/jobsstill finds/careers. - Ask Clef which page the visitor meant (or none), and whether they look like a person.
- Ask again if unsure. If Clef's top pick is weak, ry asks a second time with only its top 5. With fewer options it can commit.
- Act. ry sends a 302 when the top page scores 0.7 or higher, or 0.5 and three times the runner-up. Weaker matches get a "did you mean" page. The query string is kept on redirects.
- Cache each decision by path in memory, the Cache API, and KV. Redeploying with changed pages clears it.
Steps 1 to 3 don't call AI. On a 662-page test site, ry sends 76% of broken URLs to the right page, and never to the wrong one. Including suggestions, it offers the right page 97% of the time. See eval/RESULTS.md.
Every response gets an x-route-yes header with the decision, the confidence, and how long Clef took. If anything fails, the visitor gets your normal 404.
withRouteYes(handler, {
model: "clef-flash", // or "clef", the larger model
minConfidence: 0.7, // always redirect at or above this
minDominantConfidence: 0.5, // ...or above this when it's
dominance: 3, // 3x likelier than the runner-up
minHumanLikelihood: 0.5, // below this, treat the visitor as a bot
typoFix: true, // redirect obvious typos without calling Clef
shortlist: "hybrid", // or "lexical" (spelling only, no embedding calls)
maxCandidates: 60, // pages sent to Clef; more options dilute its confidence
rerank: true, // ask again with the top 5 when unsure
siteDescription: "…", // one line about your site; helps with vague paths
fallback: "suggest", // or "passthrough" to always show your own 404
renderMiss: (decision) => …, // your own "did you mean" page
cacheTtl: 86400, // seconds; 0 turns caching off
aiStatus: 302, // a model guessed, so not permanent by default
onDecision: (decision) => …, // logging or analytics
});The resolver also works outside Workers, through the REST API:
CLOUDFLARE_ACCOUNT_ID=… CLOUDFLARE_API_TOKEN=… npx route-yes try /priceing- Speed. During launch week, uncached Clef calls took 0.5 to 5 seconds, not the ~40ms Cloudflare advertises. Cached paths skip Clef, so bind KV. The Cache API doesn't work on
*.workers.dev. - Borderline paths can flip. Confidence varies a little between calls, so a path near the threshold might redirect once and show suggestions another time. Whichever answer comes first is cached.
- Your page copy matters. Clef reads your titles and descriptions, falling back to each page's first paragraph. A homepage description that mentions every section pulls guesses toward the homepage.
- Big sites use embeddings. Above 60 pages, each uncached 404 also makes one small embedding call. Each Worker instance embeds the whole page list once (about 1s for 660 pages). Set
shortlist: "lexical"to avoid both. - Cost. Clef only runs for page visits that 404, once per path per cache period. On a public site, add rate limiting so floods of random URLs can't run up your bill.
- Safety and SEO. ry only redirects to pages in your manifest, so it can't be used as an open redirect. AI redirects are 302s, and the suggestions page is
noindex. - Dynamic routes like
/posts/:idaren't redirect targets. Prerendered pages and sitemap entries are.
pnpm install && pnpm test
cd examples/vite-site && pnpm dev # runs the demo against real Clef
