AgentBlog
A production blog for Next.js that AI search engines can actually read, installed with one command, with an agent layer that knows how to write for it.
· · ·
The problem
Search and AI answer engines are the two organic channels almost every team knows it should have and almost nobody sets up properly. Not because the individual pieces are hard, but because there are about forty of them and every one fails quietly.
A blog can look completely correct and still be invisible. Client-rendered
content returns an empty shell to a crawler that does not execute JavaScript.
Metadata streams into <body> instead of <head> for exactly the bots you care
about. og:site_name disappears from every post because Next.js merges metadata
shallowly. A new post never reaches sitemap.xml because that route is cached.
author is a bare string, so the entity link that E-E-A-T depends on does not
exist. None of that shows up in a browser. All of it shows up in curl, which is
why every claim on this page has an assertion script behind it in scripts/.
Install
Two commands, then four edits. The commands are ours. Every edit is a decision only you can make, which is why none of them is automated.
Or one command that does both of the above and edit 3, with prompts:
Requires Next.js 16.3 or newer with the App Router, React 19, Tailwind v4, and a
shadcn-initialised project. init checks all four and refuses with instructions
rather than guessing. If you do not have components.json yet, run
npx shadcn@latest init first: that command picks a component base and a base
colour, and those are your choices to make, not ours.
Worth knowing before you run either command: adding @agentblog to your
components.json authorises shadcn add to fetch source files, npm
dependencies, and CSS from agentblog.dev and write them into your app, every
time it runs. That is the same trust you extend to an npm publisher, without a
lockfile. --dry-run prints everything it would write first, and
explains the
tradeoff between the two paths honestly.
Starting from nothing:
create scaffolds a new Next.js project and installs the blog into it. Two
things it does not do yet: it does not apply @agentblog/theme, and it does not
add the standalone site items. You get a working blog inside a default shadcn
project, and the reading theme is a shadcn add away.
What you get
Resolving @agentblog/blog through its dependency graph writes 81 files. All of
them yours after install, none of them a dependency you have to keep upgrading.
What makes it correct
The crawler gets the whole article in the first response byte.
generateStaticParams returns every slug, with no slicing and no pagination. No
client component withholds content from the HTML. The two that exist,
TableOfContents and ShareButtons, render their full markup server-side;
hydration only adds scroll state. The FAQ and the table of contents use
<details> and CSS rather than a conditional mount. Verify it the way we do:
htmlLimitedBots is a union, not a replacement. This config key overrides
the Next.js default bot list rather than extending it, so writing only the AI
crawlers silently drops Googlebot, Bingbot, Applebot, Twitterbot, LinkedInBot,
Slackbot, Discordbot, and WhatsApp from HTML-limited treatment. AgentBlog vendors
a pinned copy of the Next.js default list, unions it with the AI crawler list,
and CI diffs the vendored copy against upstream so the day Next.js adds a bot we
find out from a red build.
Structured data is a real @graph, and it is typed. BlogPosting,
WebPage, BreadcrumbList, Person, Organization, WebSite, ImageObject,
and FAQPage, cross-referenced by @id from a single helper so the nodes cannot
drift apart and stop linking. Built with schema-dts, so a property name cannot
be a typo. The bare-string author is closed off by us rather than by the library:
schema-dts 2.0.0 models Person as Person | string, because schema.org lets a
text node stand in for an entity, so lib/schema.ts declares
type Person = Exclude<PersonOrText, string> and the builders take an @id
reference. FAQPage is emitted only when the FAQs render visibly, because
marking up invisible content is the fastest way to earn a manual action.
Metadata defaults survive. Next.js merges metadata shallowly, so a page that
sets openGraph at all replaces its parent's entire object. Every route spreads
shared defaults from lib/metadata.ts, and CI asserts og:site_name and
max-snippet are still in the built HTML. That last one matters: max-snippet: -1 is what permits full snippets in AI answers, so losing it is a visibility
regression, not a formatting one.
Publishing invalidates the metadata routes. sitemap.xml and feed.xml are
cached route handlers, and generateStaticParams does not re-run during ISR. A
publish webhook that revalidates only the page routes leaves the new post out of
both files until the next deploy, then pings IndexNow about a URL the sitemap
does not list. AgentBlog revalidates both explicitly, uses
revalidateTag(tag, { expire: 0 }) rather than a stale-while-revalidate profile
on the publish path, and for database-backed sources fires a deploy hook and
waits for it before pinging.
Dates carry an offset, and slugs carry none. IsoDateTime is a branded type,
so a timestamp without a UTC offset is unconstructible. Google falls back to
Googlebot's timezone when the offset is missing, which shifts every published
date. Slug rejects a leading date, because a slug that advertises the post's
age makes an evergreen refresh look stale.
Content is written for retrieval. Answer capsules of 40 to 60 words under the H1 and under each H2, question-format headings, sections of 150 to 300 words that stand alone, entity names repeated instead of pronouns, real tables instead of prose comparisons, and citations as first-class frontmatter. The two seed posts demonstrate all of it, because they are the format specification.
The failure modes are loud. lib/preflight.ts reads your next.config at
build time and warns on every next dev and next build when the wiring is
missing. agentblog doctor reports the same findings and can fix most of them.
agentblog doctor --url fetches your live site as GPTBot, ClaudeBot,
PerplexityBot, OAI-SearchBot, and Googlebot, because a CDN in front of your
origin can block all of them before your robots.txt is ever read, and nothing
else in the toolchain catches that. Run it from CI or from your own machine
rather than from inside the deployment: it is an ordinary fetch from wherever
the CLI runs, and a request originating inside the network can bypass the very
CDN rule you are testing for.
One exception, worth stating because the paragraph above claims the opposite.
Nothing checks that you imported styles/agentblog.css. htmlLimitedBots has
four redundant channels warning you about it and this has none, so the quietest
remaining failure in the product is the one where the prose renders unstyled and
every check still passes. It is edit 2 in the install table above for that reason.
It inherits your design system
/blog should look like the rest of your product. AgentBlog composes on top of
your existing shadcn primitives using bare-name registry dependencies, which
resolve against your components.json, your component base, and your own
customised Card. Every colour in the block is a semantic token, so it tracks
your theme automatically. In Mode A it installs no base, no theme, and no font.
CI proves it with a static source lint. scripts/assert-theme-conformance.mjs
reads every file under apps/web/registry/blog/** and rejects any palette
utility, any colour literal, and any dark: colour variant, plus an
hsl(var(--token)) wrapper in the CSS, which is the Tailwind v3 idiom that
produces an invalid colour under v4 and inherits silently. Planned, not yet
wired: a second fixture with a custom base colour, radius, and font, built
alongside the default one so the two snapshots can be asserted to differ.
The agent layer
Posts are MDX files in your repository, which makes them something a coding agent can genuinely maintain.
Seven skills, and the plugin id is qualified by its marketplace, which is the form Claude Code resolves.
agentblog-audit and publish-blog-post set disable-model-invocation: true,
so an agent cannot decide on its own to run a gate or submit a URL to an external
service. You invoke those two.
The skills carry two prohibitions in always-loaded context rather than in a
reference file: never fabricate a statistic, a quotation, or a source, and never
use an em dash. Both are checked by agentblog audit and by CI. The full copy
style rules and the script that enforces them are in
.
AGENTS.md gets a short block telling every agent tool the invariants that must
not be broken, written strictly outside the region Next.js manages itself.
Configuration
One file.
brand.sameAs earns its own mention. It maps to Organization.sameAs, which is
how an answer engine resolves "the company that published this" to a real entity
rather than a string.
Content sources
Storage is an interface, and MDX on disk is one implementation of it.
Swapping storage is one line in agentblog.config.ts. Every adapter must pass
runSourceContractTests from @agentblog/schema/contract, which asserts the
eight properties the rest of the blog assumes: schema conformance, draft
handling, single-round-trip hydration, callability with no request context, null
for an absent post, editorial-first related posts, a declared prerender strategy,
and stable ordering.
prerenderStrategy is enforced by the compiler. Point your config at a
database-backed source without supplying a deployHook and the file stops type
checking, which turns a silent staleness bug into a build error.
CLI
init, create, doctor --fix, revert, and uninstall all take --dry-run,
print a unified diff before touching anything, back up every file they modify to
.agentblog/backup/, and are idempotent. Running init twice is a no-op, and CI
asserts it. --dry-run, --yes, --force, and --json belong to specific
commands rather than to the CLI as a whole; the
has the table.
Taking a later fix into an install you have already edited is
npx shadcn@latest add @agentblog/blog --diff to see what moved, then
--overwrite on the items you want, then npx agentblog@latest doctor --fix for
the config half. See
.
Requirements
- Next.js 16.3 or newer, App Router
- React 19
- Tailwind CSS v4 (v3 is not supported; see the docs for why)
- A shadcn-initialised project (
components.jsonpresent) - Node 20.9 or newer
Your first post
That writes content/blog/do-ai-crawlers-run-javascript.mdx with complete
frontmatter, today's date with a UTC offset, and your default author. It does not
write the post. Ask a coding agent to, and the write-blog-post skill you already
installed takes over.
Two things to know before the first one, because both are build failures rather
than warnings. The file name is the slug, and a slug in frontmatter silently
overrides it. And author and category are references: each must name a record
that exists in content/authors.json and content/categories.json, which is why
edit 4 above is worth doing before you write anything.
Then:
The pre-publish gate. It reports every item pass or fail with the observed value, and never claims done on a fail.
Documentation
Non-goals for v1
Stated as decisions rather than gaps: multi-locale routing, Tailwind v3, an
llms.txt inside the blog block, and certified monorepo support. Each one has a
reason and a reserved seam where there is one. See
.
Contributing
How this repository is built
The demo blog at agentblog.dev/blog imports the same modules the registry
ships, so shipped code cannot drift from tested code. The highest-value job in CI
installs the registry into a clean Next.js app and builds it, because a registry
can pass every schema check and still produce a project that does not compile.
Setup, the copy style rules, and the review gates are in .
Licensing
Three licenses, scoped by directory, and the scoping is the point: the seed posts become your published content, so they carry no attribution requirement.
Full text in .
Acknowledgements
Built on 's registry system, which solved distribution so we did not have to, and on , whose App Router makes full prerendering the default rather than an achievement.