Next.js App Router is the stack this site runs on, so it is the platform we can describe in the most detail, and the one where our own score is the evidence. Its default is the right one for agents: every component is a server component unless you say otherwise, so the HTML a plain fetch receives carries the page. Where a Next site loses points is almost always something added on top of that default, and most of it is reversible. This guide covers what the App Router does for you, what it tends to get wrong, and the fixes in the order the rubric's effort ratings suggest. The Next.js documentation is at nextjs.org/docs; our live report is at /site/agentfriendlyrank.com.
What the App Router does by default
Components under app/ are React server components. They run on the server, produce HTML, and ship no JavaScript of their own. Only a file that begins with "use client" becomes a client component, and as of this writing Next still prerenders client components to HTML on the server before hydrating them. A fresh App Router project therefore passes A7 without anyone thinking about it: the visible text in the raw HTML matches the rendered DOM, which is exactly what the two fetches explains an agent depends on.
The framework also inlines a serialised copy of the component tree into the document, in <script> elements, so the client can hydrate without a second round trip. That payload duplicates the page text and can be larger than the page itself. It is why rubric v1.0.1 changed B5 to exclude script and style contents from the denominator: measured against total bytes, the check was penalising the server-rendered architecture A7 rewards, and on our own homepage the payload was 60% of the document. The change is recorded on the changelog and explained in clean text ratio. The practical consequence for a Next site is that B5 now measures your markup, not your framework.
Metadata is declarative. A metadata export or a generateMetadata function on a layout or page writes the title, meta description, canonical link and Open Graph tags that B4 reads. A title.template in the root layout appends the site name to every page. File conventions handle the rest: robots.ts produces /robots.txt, sitemap.ts produces /sitemap.xml, opengraph-image.tsx produces a per-route OG image, and icon.svg produces the favicon D5 expects to resolve.
What a Next site tends to get wrong
Content that exists only after hydration. The default is server-first, but the escape hatches are easy to reach for: a "use client" component that fetches its data in useEffect, a dynamic(() => import(...), { ssr: false }) wrapper, or a mounted guard that renders nothing until the browser is ready. Each of those leaves a hole in the raw HTML that the rendered DOM fills in. A7 compares the two and pays five points for a ratio at or above 0.8, three at 0.5, and nothing below that. A pricing table loaded from an API on the client is the commonest way to drop into the middle band.
Third-party scripts. Consent managers, chat widgets and A/B testing tags are usually added through the root layout, which puts them on every page. A consent dialog is what A6 looks for. A widget that injects an overlay is more markup on every page for B5 and, if it moves content around, more divergence for A7. Each one is also a host you must open in your Content-Security-Policy.
A CDN in front of the origin. Nothing in Next causes this, but a great many Next sites sit behind an edge that blocks AI crawlers by default, and the result is a 403 to the three probes in A5 while robots.ts says allow. Our domain's nameservers point at the host directly for exactly this reason.
A page that declares its own openGraph and loses its image. In our experience, once a page or layout exports an openGraph object, the file-convention opengraph-image for that route is no longer merged in automatically, and the page ships og:title and og:description without og:image. B4 wants all three on every crawled page. Every page on this site passes images: explicitly for that reason.
sitemap.ts and time. In our experience the sitemap.ts convention prerendered once at build regardless of a revalidate export, which meant a sitemap that did not see content published after the deploy. Our /sitemap.xml is a plain route handler with revalidate = 3600, and the lastmod values D4 reads come from the same registry as llms.txt.
Fixes, in order of effort
Low
robots.ts is the place to name the crawlers rather than rely on the * default. Ours lists each agent explicitly with the same disallow list, and declares the registry sitemap first, because the scanner follows the first sitemap in the file.
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: "*", allow: "/", disallow: ["/admin"] },
...["GPTBot", "OAI-SearchBot", "ClaudeBot", "Claude-User", "PerplexityBot", "Google-Extended"]
.map((userAgent) => ({ userAgent, allow: "/", disallow: ["/admin"] })),
],
sitemap: "https://example.com/sitemap.xml",
};
}
Security headers go in next.config.ts under headers(). D1 wants Strict-Transport-Security present; the CSP is not scored, but it is the header that keeps you honest about third-party scripts, and Next's hydration payload needs 'unsafe-inline' on script-src.
async headers() {
return [{
source: "/:path*",
headers: [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self' 'unsafe-inline'" },
],
}];
}
llms.txt, llms-full.txt and agents.json can each be a route handler that renders from the same list of pages as the sitemap, which is what we do, or a static file under public/. In our experience Next does not route a path segment that begins with a dot, so /.well-known/security.txt needs a rewrites() entry pointing at a handler on an undotted path. Canonicals come from metadataBase plus alternates.canonical, and a redirects() rule with a host condition collapses www onto the apex for B7.
Medium
JSON-LD belongs in the root layout as a server component that writes a <script type="application/ld+json">, with Organization and WebSite in one @graph, and page-level types added by each route. B1 reads the raw HTML, so a client-injected block does not count. Landmarks are a layout decision too: a <main id="main"> around children, a <nav> in the header and a <footer> cover B3 on every page at once. Removing a consent banner means removing the reason for it, which for us meant cookieless analytics.
High
Moving content from client components back to server components is the A7 fix, and its cost depends on how much state the page really needs. The useful discipline is the one we keep: the only client component on this site renders nothing. It listens for a server event and calls router.refresh(); the page it sits on is a complete server-rendered document with or without it. Machine endpoints for C4 are route handlers, and the check looks for links to them in the homepage HTML, which is why our footer carries /docs and /api/v1/openapi.json on every page.
What is not fixable without leaving the platform
Very little. The two constraints we hit, the dotted path and the sitemap convention, both had in-place workarounds. The constraint that matters is not the framework but what sits in front of it: a host or CDN that refuses crawlers at the edge overrides everything in this guide, and the fix is a setting there, not a line of code here.
Check your own site
Run the free scan against your Next site and read the A7 evidence first: rawTextLength against renderedTextLength tells you whether anything is hiding behind hydration. Then compare against our own report and the discovery and understanding definitions on the methodology page.