AEO on Next.js for marketing sites
Next.js gives you every AEO control Framer gives you, and you build each one yourself. Metadata, canonical tags, JSON-LD, a Markdown version of every page, and a log you can read. Budget a day of work on an App Router site that already exists. The Markdown endpoint is the piece Framer ships free and the one worth building first.
Category
Platform
Reading time
6 min

Next.js gives you every AEO control Framer gives you, and you build each one yourself. Metadata, canonical tags, JSON-LD, a Markdown version of every page, and a log you can read. Budget a day of work on an App Router site that already exists. The Markdown endpoint is the piece Framer ships free and the one worth building first.
Every API below is written against Next.js 16.3.4, the current stable release, and the App Router. One file convention was renamed in version 16 and this article uses the new name.
Desses builds in Next.js and React, from $8,000 over about six weeks, and we're also a Framer Partner selling Framer development at the same starting price. We get paid whichever way this decision goes. Read the disqualification section first: most marketing sites that land here belong on Framer.
Server components render to HTML before anything reaches the browser, and HTML is all these crawlers read. The same checklist on Framer is article 08.
What Next.js hands you and what you write yourself
The job | Framer | Next.js | |
|---|---|---|---|
Full text in the initial HTML | Pre-rendered server-side, nothing to configure | Server components by default, and keeping critical content out of client-only components is on you | |
Title, description, canonical, OG | Per page and per CMS item in the UI | A | |
JSON-LD | Custom code in the head with `{{Field \ | json}}` | A script tag rendered by a server component |
Markdown for agents |
| A route handler you write | |
sitemap.xml and robots.txt | Auto-generated |
| |
Redirect when a slug changes | Doesn't happen, you add it by hand |
| |
Server logs | Not available | Whatever your host exposes, or a drain to somewhere durable |
Every one of these is ten minutes of work. All of them together are a system somebody has to own.
The metadata export covers title, description, canonical and OG
Set metadataBase once in the root layout and every relative URL resolves against it. Leave it out and a relative URL is a build error.
Then per route, with generateMetadata. params is a promise and you await it.
Keep themeColor, colorScheme and viewport out of that object. They've been deprecated inside metadata since Next.js 14. The canonical is the line people skip, and a site with tracking parameters and a paginated archive serves one article at four URLs.
JSON-LD goes in a server component, and it needs the sanitizer
Copy the .replace(/</g, '\\u003c') with the rest. It rewrites every < as its unicode escape, which stops a </script> arriving from your CMS from closing the tag early and turning the rest of that field into live markup. Next's own guide is blunt: JSON.stringify "does not sanitize malicious strings used in XSS injection". Any snippet without that line is one untrusted field away from a stored XSS.
It belongs in a server component because the script has to be in the bytes that come back from the first request. Build the same object in a useEffect and it appears after hydration, where no major AI crawler will see it.
The Markdown route handler is the highest-value file on the list
Framer returns a Markdown version of any page from an Accept: text/markdown header or ?md on the URL, with no configuration. Next.js has no equivalent until you write one, and it's about forty lines.
A page is always the leaf of its route subtree, so the handler lives one segment down. Route handlers take a params promise and return a plain Response, which is where the content type gets set.
Then a rewrite, so the Markdown answers at the page's own URL. This is the file that changed name. middleware.ts was deprecated and renamed to proxy.ts in Next.js 16, the exported function is now proxy, and there's a codemod.
One trap worth the paragraph. Negotiating on Accept means one URL has two representations, and a CDN holding the cached HTML will serve it to the next client asking for Markdown, which is what Vary: Accept exists to prevent. The ?md path sidesteps it, since the query string is part of the cache key.
The real cost sits upstream. If your content is MDX or arrives from a CMS as Markdown, post.markdown already exists and this is a morning. If your pages are hand-built JSX, you're writing a serializer.
Whether AI crawlers ever ask for the Markdown is unmeasured, ours included, and llms.txt is the cautionary tale. The argument for this endpoint is that it lives on the page's own URL, where a crawler already is.
When a marketing site shouldn't be in Next.js
Most of the time, for most teams, and this is the section I'd want read out loud on the call.
Everything above is a file in a repository. Changing a meta description means a branch, a review and a deploy, and a marketer who wants to reword a hero on a Tuesday files a ticket. If nobody on the team can open proxy.ts without help, the Markdown endpoint breaks during an upgrade and nobody notices for months. Version 16 renamed that file. Somebody had to run the codemod.
Framer does the same jobs with none of that, editable by whoever is holding the laptop. If a marketing site is the whole scope and there's no engineer on the team, that's the answer, and article 09 compares the platforms properly.
Next.js earns its place on a short list of conditions. The marketing site sits next to a product surface with auth or dashboards. You need numbered pagination URLs or RSS, and Framer produces neither. Your content pipeline is genuinely custom, though Framer's Server API closed most of that gap in February 2026. Picking the heavier tool for a job the lighter one does costs you every month afterwards.
What we'd do on a Next.js marketing site
Curl a page with JavaScript off and read what comes back before touching anything else. Then metadataBase in the root layout, and generateMetadata on every dynamic route with an explicit canonical. Article and Organization JSON-LD from server components, sanitized, matching the visible text.
The Markdown route handler and the proxy rewrite, revalidated in the same webhook as the page. Then app/robots.ts and app/sitemap.ts, generated from code so the search-bot names sit under code review, and a log drain, because server logs are the only method that shows whether retrieval bots reach you at all.
Then Search Console's generative AI report and Bing's grounding queries, the only place any provider shows what was asked before you got cited.
If you want it built for you, our AI and custom development work covers it end to end.
If you're on Next.js and want to know what an agent reads, send me a URL. I'll fetch it with JavaScript off, send back what came out of the initial HTML, and tell you what the Markdown route would take on your setup. I'm around this week.













