DEV Community

Cover image for I tried to catch Next.js contradicting itself. It never did.
Ajisafe Victor Oluwapelumi
Ajisafe Victor Oluwapelumi

Posted on

I tried to catch Next.js contradicting itself. It never did.

Sanity Challenge Path One Submission

This is a submission for the Sanity Challenge, Path One: Ship an Agent That Queries Real Content

What I Built

Next.js has two routers. The Pages Router and the App Router answer the same questions in incompatible ways, and both sets of documentation are live, indexed, and correct. Neither page carries a banner saying "this is the old way." Search does not tell you which one you landed on.

So an AI assistant or an old tutorial hands you getServerSideProps, you paste it into an App Router project, and Next.js rejects it. The answer was not wrong. It was for the other router.

Router Rosetta takes a snippet or a question, identifies which router it belongs to, and gives you the equivalent in the other one. Every claim carries the documentation entry it came from, and every equivalence carries a citation to the official migration guide.

It also refuses. If the knowledge base does not cover something, it says so rather than filling the gap from the model's memory of Next.js.

Demo

Live: https://router-rosetta.vercel.app

Paste code, or use one of the four examples on the page.

It translates, with receipts

Given this:

export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/posts')
  return {props: {posts: await res.json()}}
}
Enter fullscreen mode Exit fullscreen mode

It answers:

This code uses Pages Router, as identified by the use of getServerSideProps [data_fetching/pages_static].

In the App Router, getServerSideProps is replaced by fetching data directly inside an async Server Component page [migration, data_fetching/app_router]. In the App Router, fetch calls inside Server Components run on the server during rendering [data_fetching/app_router], mapping to getServerSideProps dynamic request behavior [migration].

Then it writes the App Router version, citing the target entry and the migration
guide for the equivalence.

It refuses, with partial credit

Ask it something out of scope:

How do I configure Redis session storage with a custom Kubernetes ingress in Next.js?

The knowledge base does not contain information on configuring Redis session storage or custom Kubernetes ingress routing in Next.js.

The only Kubernetes and deployment details supported by the knowledge base are:

  • Next.js supports deployment to Docker-compatible providers, including Kubernetes, using either a Node.js server or Docker container build [setup/configuration].
  • For Docker and containerized environments like Kubernetes, Next.js provides an output: "standalone" configuration mode in next.config.js to create a minimal production image containing only the required runtime files [setup/configuration].

Because Redis session storage and custom Kubernetes ingress configurations are not covered in the knowledge base, no router-specific mapping or implementation code can be provided.

Code

Router Rosetta

Translates Next.js code and questions between the Pages Router and the App Router, citing a knowledge base entry for every claim.

Paste getServerSideProps and it tells you this is Pages Router code, gives you the async Server Component equivalent, and cites the docs entry behind each statement. Ask "how do I read search params in each router?" and it answers for both. Every factual claim carries a citation chip — the point is that you can check the answer rather than trust it.

The model is instructed not to answer from its own memory of Next.js. If the knowledge base does not cover a mapping, it says so instead of guessing.

Getting started

npm install
cp .env.local.example .env.local   # then fill in the values below
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000.

Environment

The two Sanity variables are required. At least one model provider key is also required…

How I Used Sanity

What I pointed Context at

Three website sources, all on nextjs.org:

Source Pages Role
/docs/pages/building-your-application 21 The superseded way
/docs/app/getting-started 20 The current way
/docs/app/guides/migrating/app-router-migration 1 The arbiter

42 pages, no dataset or uploaded files.

One setting mattered more than everything else: Max depth. Left empty, the crawler reads the seed URL and follows nothing. My first build pulled 7 pages out of a 38 page target and I spent a while blaming my include patterns. Setting Max depth to 3 took it to 42.

What the build produced

Context turned those 42 pages into 16 entries, and it organised them into pairs:

Topic Pages Router App Router
Data fetching data_fetching/pages_static data_fetching/app_router
Routing routing/pages_router routing/app_router
Rendering rendering/pages_router_modes caching

Plus migration, holding the deprecated-to-current API mapping.

Each entry also carries an excludes: line pointing at its counterpart:

data_fetching/app_router [peripheral]
  App Router data fetching: Server Components with fetch API or ORM/database...
  excludes: Pages Router methods (getStaticProps, getStaticPaths,
            getServerSideProps); cache/revalidation config.
Enter fullscreen mode Exit fullscreen mode

That line is the whole product: the knowledge base records where the other half of each answer lives, and I did not model that pairing myself. The build wrote it from the source material.

Which tools the agent uses

The endpoint serves Knowledge Base mode, so two tools:

  • initial_context for the outline, which is small enough to hold for a whole conversation
  • knowledge_base_read for the entries themselves, several paths at a time

The system prompt forces the pairing to be used rather than hoped for:

Read the entry for that router AND its paired entry for the other router. Read
the migration entry whenever you assert an equivalence.

Without that line, the model answers from whichever entry it reads first and produces a confident, one-sided answer.

Why this needs structured content

Keyword search for getServerSideProps returns the Pages Router documentation. It is the right page, current and accurate, and it will not tell you that a newer router replaced it or what the replacement looks like. The answer requires knowing which of two parallel systems a claim belongs to and what it maps onto, a relation between sources rather than a string inside any one of them.

What did not work

I started by trying to surface contradictions between the two routers, because the Sanity docs describe conflict detection and it sounded like a strong demo.

The first build, the starved one with 7 pages, raised a real conflict about getStaticPaths fallback values.

Then I fixed the crawl, rebuilt with all 42 pages, and got zero conflicts.

That result is correct. With the full picture, including the migration guide explaining that one router supersedes the other, Context concluded these are two coherent systems rather than one system contradicting itself. More context produced better reconciliation. The conflict I had been excited about was an artifact of an incomplete index.

Next.js does not contradict itself, it documents two things in parallel, and that only became visible once the crawl was complete. I kept the same 42 pages and the same paired structure, and changed the product from adjudication to
translation.

Build notes

Stack: Next.js 16, Vercel AI SDK v7, @ai-sdk/mcp connecting to Context MCP, Groq as the model with Gemini as a fallback.

The agent is provider-agnostic, and that is the point. All the domain knowledge lives in the knowledge base rather than in the model. Neither provider knows anything about Next.js routing beyond what it reads through MCP.

Free tiers forced real engineering, not just a config swap. Gemini's free tier caps at 20 requests per day per model, which a demo exhausts in minutes, so Groq is the primary and Gemini is the fallback. Three things make that survivable in production: the client mirrors the limit with a sliding window so you aretold before a request is wasted, first-turn answers are cached for a day so the example buttons do not re-derive the same output, and a failed request actually switches provider mid-run rather than just erroring, using the AI SDK's prepareStep so the swap sticks for the rest of the agent's steps.

Errors were invisible until I made them loud. The AI SDK's default failure message is "An error occurred," identical for every cause. Wiring onError to report the real cause was one of the first things worth doing, well before therate limits above ever came up.

Sanity Project Details

Project ID: 3bpjqog6
Dataset: production (public)
Knowledge Base ID: kbm133o2qNEI

The project is deliberately empty. The knowledge base is built entirely from website sources, so no dataset content was needed. The structure worth looking at is in the Knowledge Base entries, not in the Content Lake.

Top comments (0)