SEO & AEO
Search and answer-engine optimization ship in the box. Classic SEO (metadata, canonical URLs, sitemap, structured data) gets you ranked on Google. AEO (answer engine optimization) gets you cited when someone asks ChatGPT, Claude, or Perplexity for a recommendation. Both are wired through a single helper and a few config files, so a fresh site is indexable and quotable on the first deploy.
What ships
- Per-page metadata via one
getSEOTags()helper: title, description, keywords, robots directives, canonical URL, OpenGraph, and Twitter card. - JSON-LD structured data (Organization, WebSite, SoftwareApplication, FAQPage, BlogPosting, BreadcrumbList) via
renderStructuredData(). - Sitemap and robots.txt generated on every build with per-route priority and lastmod.
- OpenGraph images auto-generated at 1200×630 for every share.
- An AI-crawler allowlist and
llms.txtso answer engines can read and cite the site.
appName, appDescription, and domainName in config.ts feed the SEO tags, OG images, sitemap, structured data, and llms.txt. Set those three and every surface follows.Metadata on every page
All SEO tags flow through getSEOTags() in src/lib/seo.tsx. It is already wired into app/layout.tsx, so every route gets sane defaults from config.ts. Each page exports its own metadata to override title, description, and canonical:
import { getSEOTags } from "@/lib/seo";
export const metadata = getSEOTags({
title: "Pricing — AI Web Launcher",
description: "One-time price, unlimited sites. 120–160 characters, primary keyword included.",
canonicalUrlRelative: "/pricing",
// optional: keywords, openGraph.images, noindex
});The helper emits an explicit <meta name="robots" content="index, follow"> on every route (with max-image-preview: large for Googlebot), plus a matching OpenGraph object and a summary_large_image Twitter card. To keep a thin or utility page out of the index, pass noindex: true instead of hand-writing a robots tag.
canonicalUrlRelative on every page. The helper builds the absolute canonical from config.domainName. Leave domainName as the naked (non-www) domain so canonicals do not split between www and apex.Structured data (JSON-LD)
renderStructuredData() in src/lib/seo.tsx emits a single @graph script per page. It is called once site-wide in app/layout.tsx and per page where richer schema helps. Pass the page type:
| Call | Schema emitted |
|---|---|
| renderStructuredData("layout") | Organization + WebSite (site-wide, in layout) |
| renderStructuredData("homepage") | SoftwareApplication + FAQPage |
| renderStructuredData("blog", { article }) | BlogPosting |
| renderStructuredData("blog-index", { breadcrumbs }) | CollectionPage + BreadcrumbList |
| renderStructuredData("about", …) | AboutPage |
| renderStructuredData("features", { breadcrumbs }) | BreadcrumbList |
The SoftwareApplication offer price is read from config.stripe.plans, so the schema never contradicts your live pricing. Never inject structured data as a raw <script> tag; use the helper so the graph stays valid.
Sitemap and robots.txt
next-sitemap runs on the postbuild script and writes public/sitemap.xml and public/robots.txt. Behavior lives in next-sitemap.config.js:
- Per-route priority and changefreq. Home is 1.0/daily, pricing 0.9/weekly, blog 0.8/daily, docs 0.8/weekly, articles and legal pages lower.
- Accurate lastmod. Blog entries pull their date from
src/features/blog/content.tsx; other routes fall back to git history. - Exclusions. Image and icon routes,
/api/*, and/llms.txtare kept out of the sitemap. Add a private page to theexcludearray to hide it.
module.exports = {
siteUrl: process.env.SITE_URL || "https://your-domain.com",
generateRobotsTxt: true,
exclude: ["/api/*", "/llms.txt", "/*.png", "/opengraph-image", /* … */],
transform: async (config, loc) => {
// per-route priority + changefreq + lastmod
},
};siteUrl defaults to https://your-domain.com. Set SITE_URL (and config.domainName) to your production domain so generated URLs are correct.OpenGraph images
app/opengraph-image.tsx generates a 1200×630 share image at the extension-less route /opengraph-image. getSEOTags() points both the OpenGraph and Twitter tags at that route, so every share renders an image with no manual asset work. Blog posts generate their own image from the article definition.
AEO: getting cited in AI answers
An LLM composes its answer from many sources, so ranking third is not the bar anymore. Any page that adds one specific, useful detail can be pulled into the answer. The boilerplate ships the plumbing that lets answer engines read and quote you.
llms.txt
src/app/llms.txt/route.ts is a force-static route that serves /llms.txt, a curated, JavaScript-free Markdown map of your site for language models (spec: llmstxt.org). It reads name, description, domain, and support email from config.ts, so it updates when you rebrand. When you add a high-intent page (pricing, a comparison, a category page), add a line for it in the route; it is the AEO counterpart to sitemap.xml and is deliberately excluded from the sitemap so it is not treated as an indexable HTML page.
AI-crawler allowlist
next-sitemap.config.js (mirrored in public/robots.txt) explicitly allows the 2026 AI-crawler set. Every bot you block is an answer engine you disappear from, so the list stays open unless a content-protection decision says otherwise:
OpenAI GPTBot · OAI-SearchBot · ChatGPT-User
Anthropic ClaudeBot · Claude-SearchBot · anthropic-ai
Google Google-Extended (Gemini / AI Overviews)
Others PerplexityBot · Applebot-Extended · Amazonbot · CCBot · cohere-ai · BytespiderThe -SearchBot and ChatGPT-User agents put you in the AI answer; the rest cover training and indexing. To add a bot, append { userAgent: "NewBot", allow: "/" } to the policies array.
https://your-domain.com/robots.txt after deploy and confirm what is actually served. A correct repo proves nothing about production.FAQs that get quoted
LLMs lean on question-and-answer format and often cite FAQ blocks verbatim. The homepage FAQ lives in a single liveLandingFaqItems array in src/lib/seo.tsx. It is rendered visibly on the page and emitted as FAQPage JSON-LD from the same source, so the two can never drift. Two rules make it work:
- Phrase each question in the exact words a user types into a chatbot, not product-FAQ phrasing.
- Keep each answer standalone in 2 to 4 sentences; the model may quote it without the rest of the page.
Product-FAQ (weak): "Do I get updates?"
Chatbot (strong): "What is the best Next.js boilerplate for AI-built sites?"
"Is there a <competitor> alternative that does <differentiator>?"Pages the schema is built for
These are the page types answer engines cite most. The boilerplate ships the schema helpers (FAQPage, BreadcrumbList) they need; you write the pages:
- Category pages. A "best X for Y" landing page that anchors the exact phrase you want to be recommended for, with a chatbot-phrased FAQ at the bottom.
- Honest comparison pages. A
/vs/<competitor>page that concedes where the competitor genuinely wins. The honesty is the citation mechanism; models have read everyone's marketing and can tell an evaluation from a sales page. Verify competitor pricing at the source on the day you write it. - Use-case pages. One job-to-be-done per page, specific and citable.
llms.txt, and docs. Synonym variation reads as better writing and is worse AEO; it splits your keyword mass across phrasings nobody searches. There is no Search Console for LLM citations, so log your target prompts and check who gets named in ChatGPT, Claude, and Perplexity on a schedule.