{"slug": "rudra-js-llm-designed-product-recommendations-for-react-stores", "title": "Rudra-JS, LLM-designed product recommendations for React stores", "summary": "The open-source TypeScript library rudra-js, published at version 0.8.3 under the MIT license on npm, lets a language model design a product-recommendation block for React storefronts by returning a constrained spec rather than HTML, with @rudra-js/react rendering titles, prices, images and links from the merchant's own catalog at serve time. Because the model is never given a price and the spec has no field for one, rudra-js relies on that structural limit as its main defense, backed by three text checks — pattern matching for prices, discounts, ratings, delivery dates and stock counts, a digit check, and a list of numberless claims like \"top pick\" — that drop any failing field, though the project acknowledges rewordings such as \"Four and a half stars from other hikers\" pass all three. On the project's 500-shopper benchmark, the default mode makes 460 model calls per 1,000 page views, and the block ships in the first HTML response without client-side JavaScript; the public API may change before 1.0, and leaving the provider out lets @rudra-js/core build the block with no model and no API key.", "body_md": "rudra-js is an open-source TypeScript library (MIT) you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it.\n\nIt is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.\n\nIf a language model writes raw HTML, it can make up a price or show a product you don't sell. So rudra-js never lets the model write HTML. The model returns a spec instead: a short description of the block, using only fields rudra-js defines. The model picks the layout. It writes the headline and the copy. There is no field for a price, a product name, an image or a link. The model names products only by SKU, and only from the list you give it.\n\n`@rudra-js/react` turns the spec into HTML. It reads each product's title, price, image and link\nfrom your catalog as the page is served. Anything the model wrote is rendered as escaped text.\n\nrudra-js also checks every word the model writes, in three passes:\n\n- Patterns for prices, discounts, ratings, delivery dates and stock counts.\n- A digit check. Every digit in a sentence must come from your own input, as a category or a tag on one of the products the model was shown.\n- A list of claims that carry no number to check, like \"top pick\" and \"customer favourite\".\n\nAny field that fails a pass is dropped.\n\nTwo of the passes are word lists, so a rewording that's on neither list gets through. The digit check only reads digits, so it misses a number spelled out in words. \"Four and a half stars from other hikers\" gets past all three passes. The tests keep a list of rewordings that still pass, so you can go and read how big the gap is. That's why the real defence is structural. The model is never told a price, and the spec has nowhere to put one.\n\nThe block is in the first HTML response and needs no client-side JavaScript. So a crawler that never runs JavaScript still reads it.\n\nOn our 500-shopper benchmark, the default mode makes 460 model calls per 1,000 page views. Generating for every shopper would make 1,000.\n\n**Status: `0.8.3`, early.** It installs and it works. The Getting started below runs as a test on\nevery commit. The public API might still change between minor versions before `1.0`. The\nchangelog says when it does.\n\nYou don't need an API key to start. Leave the provider out and `@rudra-js/core` builds the block\nitself, with no model. That's a fully supported setting, and the right one until you've settled on\na model.\n\n```\nnpm install @rudra-js/core@latest @rudra-js/react@latest @rudra-js/attested@latest\n```\n\nUse this one command for a new app and for an app that already uses rudra-js. It's also how you\nupgrade. If your app has `@rudra-js/anthropic`, add it to the command with `@latest` too. If it has\n`@rudra-js/a2ui`, use the install line in that package's README instead. That line also names the\ntwo A2UI packages.\n\nThe `@rudra-js` packages are published together. Each one's peer ranges on the others start at its\nown version. With a bare package name, npm stays inside the range your `package.json` already has.\nSo without `@latest`, an app on an older core can get an `ERESOLVE` error.\n\nEvery package is under the `@rudra-js` scope. The unscoped `rudra-js` package on npm belongs to\nsomeone else, so include the `@`.\n\nYou don't need to install zod. `@rudra-js/core` brings zod 4 as a dependency. If your app is\nalready on zod 4, it shares that copy. On zod 3, npm puts a zod 4 under core, and another under\n`@rudra-js/anthropic` if you use it. Your zod 3 can't mix with the schemas core exports.\n`productSchema.array()` works, but your `z.array(productSchema)` won't.\n\nYou need React 18 or 19. You don't need Server Components. `@rudra-js/react` is plain React with no\nhooks. It works with `renderToString` and in a client-rendered app. Where your framework supports\nServer Components, it works as one too. Render it on the server to put the block in the first HTML\nresponse.\n\nYou also need Node 22.12 or later. Node 20 is end of life, so we don't build or test on it.\n\n``` js\nimport { createComponentGenerator, parseTrackingInput } from '@rudra-js/core';\nimport { RudraComponent } from '@rudra-js/react';\n\nconst catalog = [\n  { sku: 'A-1', title: 'Cast iron skillet', category: 'Cookware', price: 39, currency: 'USD' },\n  { sku: 'A-2', title: 'Enamel dutch oven', category: 'Cookware', price: 89, currency: 'USD' },\n  { sku: 'A-3', title: 'Chef knife', category: 'Knives', price: 55, currency: 'USD' },\n];\n\nasync function recommendations() {\n  // Passing no provider means no API key and no spend.\n  // Core builds the block itself, and the same input always gives the same block.\n  const generator = createComponentGenerator({ provider: null });\n\n  // Parsing fills in what you left out and rejects anything that doesn't belong.\n  // Always pass the parsed candidates to the renderer, not your raw objects.\n  const input = parseTrackingInput({\n    user: { id: 'shopper-1' },\n    context: { surface: 'pdp', currentSku: 'A-1', currentCategory: 'Cookware' },\n    candidates: catalog,\n    signals: { cart: [{ sku: 'A-2', at: Date.now() }] },\n  });\n\n  const spec = await generator.generate(input);\n\n  return <RudraComponent spec={spec} products={input.candidates} locale=\"en-US\" />;\n}\n```\n\nA shopper looking at the skillet, with the dutch oven already in their cart, is shown the knife. It sits under the heading \"Goes with your cart\". The product they're viewing and the one in their cart are both left out. No model was asked, and nothing was billed.\n\nIn Next.js, add `export default recommendations` at the bottom of the sample to turn it into a page.\n\nNew Next.js apps from `create-next-app` turn Cache Components on. With it on, the build stops with\n\"Next.js encountered the unstable value `Date.now()` while prerendering\". That's because the sample\nand `generate` read the clock while the page renders. To fix it, create the app with\n`npx create-next-app@latest my-shop --yes --no-cache-components`. Or delete `cacheComponents: true`\nand `partialPrefetching: true` from `next.config.ts`. The\n[quick start](https://rudrajs.com/learn/quick-start#cache-components) has more.\n\nWhen you're ready for a model, add [`@rudra-js/anthropic`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/anthropic) and pass it as the\n`provider`. Everything else above stays the same. The model then writes the wording. It also picks\nthe layout and the emphasis. By default, core still picks the products in a grid or carousel. It\npicks them per request, from the candidates you pass in.\n\nBefore you send real shopper traffic, read\n[Running it in a regulated shop](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/core#running-it-in-a-regulated-shop).\n\nThe Anthropic adapter is one option. You can put any model behind the small interface described in\n[Any provider](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/core#any-provider).\n\nIf an [A2UI](https://a2ui.org) agent builds your pages, add [`@rudra-js/a2ui`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/a2ui) too.\n\n| Package | What it does | \n|---|---|\n| [`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/core) | The schemas and logic that turn one tracking input into one spec you can render. The spec is checked with `@rudra-js/attested` . | \n| [`@rudra-js/react`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/react) | Renders the spec with React, on the server or in the browser. | \n| [`@rudra-js/anthropic`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/anthropic) | Talks to the Anthropic API. It's the only package that makes a billed call. | \n| [`@rudra-js/attested`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/attested) | Checks the model's copy against the facts a shop stands behind. Has no dependencies. | \n| [`@rudra-js/a2ui`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/a2ui) | Lets an [A2UI](https://a2ui.org) agent place the block. Your server still picks the products. | \n\nYou need Node `>=22.12` to work on the repo too. Node 20 also lacks the\n`--experimental-strip-types` flag that `npm run verify:consumer` relies on. `.nvmrc` names an exact\nNode 22. `engine-strict=true` turns a wrong Node version into a readable install error. CI runs the\nchecks on Node 22.12.0 and on the latest Node 22.\n\n```\nnvm use\nnpm install\n\nnpm run check        # all six of the below, in order\n\nnpm run build        # tsc -b across the workspace\nnpm run typecheck    # includes test files, which the build does not\nnpm run lint\nnpm run format:check\nnpm test\nnpm run verify:consumer   # packs every package and uses them from outside the repo\n```\n\nTo see what each generation mode costs under a stub model, run\n`ANTHROPIC_API_KEY= RUDRA_REPLAY_ONLY=1 npm run bench`. [bench/README.md](https://github.com/clivedsouza1010/rudra-js/blob/main/bench/README.md) explains\nthe columns.\n\nIf you keep a key exported in your shell, run the tests as `ANTHROPIC_API_KEY= npm test`.\n`vitest.config.ts` sets `RUDRA_REPLAY_ONLY=1` for every test run. The shop throws at start-up when\nthat flag and a key are both set. So with a key in your shell, the test run fails to load instead\nof spending money.\n\nCI runs all six checks on every pull request. A second job builds the example shop and checks that\na crawler still sees the block as part of the page. CI sets no `RUDRA_SHOP_MODE`, so the shop\nreplays. That job runs the plain build:\n\n```\nnpm run build --workspace @rudra-js/example-shop\nnpm run verify:crawlable\n```\n\nLocally, that build is safe on its own. Building calls no model. Serving the shop only spends money\nwith `RUDRA_SHOP_MODE=record`. The prefix below makes that a rule instead of a default:\n\n```\nANTHROPIC_API_KEY= RUDRA_REPLAY_ONLY=1 npm run build --workspace @rudra-js/example-shop\n```\n\n[`examples/shop`](https://github.com/clivedsouza1010/rudra-js/blob/main/examples/shop) is a small Next.js storefront that runs rudra-js on a real page.\nIts product page asks a language model for a recommendation block. The server renders the block\ninto the same HTML response as the page, so the block isn't fetched after the page loads.\n\n```\nnpm run dev --workspace @rudra-js/example-shop\n# then visit http://localhost:3000/product/RJ-00001?shopper=S-0001\n```\n\nThat replays the transcripts committed under `examples/shop/recordings/`. A transcript is a saved\nmodel call: the prompt and the model's answer. Replaying bills nothing, whatever keys you have in\nyour environment. `RUDRA_SHOP_MODE` is the switch, and `record` is the only value that spends money:\n\n```\nRUDRA_SHOP_MODE=record npm run dev --workspace @rudra-js/example-shop\n```\n\nThat calls Claude once for each page that has no transcript yet. It saves each answer as a new transcript. Pages that already have one still replay. So after the first visit, browsing a page costs nothing.\n\n[`examples/shop/README.md`](https://github.com/clivedsouza1010/rudra-js/blob/main/examples/shop/README.md) covers the rest:\n\n- the environment variables\n- how to re-record a transcript after a prompt change\n- why the files under `recordings/` must only ever hold demo shoppers and demo catalogs\n\n- **The docs** : guides and the API reference at[rudrajs.com](https://rudrajs.com) .\n- **Questions and ideas** : open an[issue](https://github.com/clivedsouza1010/rudra-js/issues) .\n- **Bugs** : open an[issue](https://github.com/clivedsouza1010/rudra-js/issues/new/choose) .\n- **Vulnerabilities** : report them privately. See[SECURITY.md](https://github.com/clivedsouza1010/rudra-js/blob/main/SECURITY.md) .\n\nContributions are welcome. [CONTRIBUTING.md](https://github.com/clivedsouza1010/rudra-js/blob/main/CONTRIBUTING.md) covers setup and the scope the\nproject holds to. It also covers the naming and testing conventions. We expect everyone taking part\nto follow the [code of conduct](https://github.com/clivedsouza1010/rudra-js/blob/main/CODE_OF_CONDUCT.md).\n\nThe [changelog](https://github.com/clivedsouza1010/rudra-js/blob/main/CHANGELOG.md) records the changes.\n\nPatent pending in the US. A provisional application for the cohort caching\ndesign — the coupled cache key and prompt projection in\n[`spec-cache.ts`](https://github.com/clivedsouza1010/rudra-js/blob/main/packages/core/src/spec-cache.ts) — was filed on 20 September\n2026, application 64/158,649.\n\nThis doesn't change how you can use rudra-js. The code is MIT and stays MIT. The application was filed defensively, so that nobody else patents the design and comes after the people using it. A provisional isn't examined and may never become a granted patent.\n\n[MIT](https://github.com/clivedsouza1010/rudra-js/blob/main/LICENSE) © Clive Dsouza", "url": "https://wpnews.pro/news/rudra-js-llm-designed-product-recommendations-for-react-stores", "canonical_source": "https://github.com/clivedsouza1010/rudra-js", "published_at": "2026-10-28 12:31:25+00:00", "updated_at": "2026-10-28 12:42:03.560546+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "developer-tools", "generative-ai", "ai-agents"], "entities": ["rudra-js", "@rudra-js/core", "@rudra-js/react", "@rudra-js/attested", "@rudra-js/anthropic", "@rudra-js/a2ui", "npm", "React"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/rudra-js-llm-designed-product-recommendations-for-react-stores", "markdown": "https://wpnews.pro/news/rudra-js-llm-designed-product-recommendations-for-react-stores.md", "text": "https://wpnews.pro/news/rudra-js-llm-designed-product-recommendations-for-react-stores.txt", "jsonld": "https://wpnews.pro/news/rudra-js-llm-designed-product-recommendations-for-react-stores.jsonld"}}