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.
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/.
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.
npx shadcn@latest add @agentblog/blog # writes the files
npx agentblog@latest doctor --fix # wires up what a file copier cannot reach| # | File | What you write |
|---|---|---|
| 1 | components.json |
The @agentblog namespace under registries. Both commands resolve through it, so this one comes first |
| 2 | app/globals.css |
@import '../styles/agentblog.css'; after the Tailwind import. Miss it and article prose renders with no typography and no error at all |
| 3 | agentblog.config.ts |
siteUrl, brand name and logo, brand.sameAs, and a default author slug |
| 4 | content/authors.json |
Yourself. Keep the editorial slug or change it in the seed posts too |
Or one command that does both of the above and edit 3, with prompts:
npx agentblog@latest initRequires 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
the installation guide explains the
tradeoff between the two paths honestly.
Starting from nothing:
npx agentblog@latest create my-blogcreate 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.
app/blog/page.tsx paginated index, Blog schema, real <a href> pagination
app/blog/opengraph-image.tsx social card for every list surface
app/blog/[slug]/page.tsx the post route, every slug prerendered
app/blog/[slug]/opengraph-image.tsx per-post social card
app/blog/category/[slug]/page.tsx indexable hub pages
app/blog/tag/[slug]/page.tsx noindexed below a configurable post count
app/authors/[slug]/page.tsx Person schema with sameAs and knowsAbout
app/sitemap.ts lastModified from real content dates
app/robots.ts environment guarded, AI access declarations
app/feed.xml/route.ts RSS 2.0
app/api/publish/route.ts revalidate plus IndexNow, in the right order
app/editorial-policy/page.tsx the Trust surface almost no blog has
lib/schema.ts typed JSON-LD @graph builders
lib/posts.ts the storage adapter facade
lib/preflight.ts build-time config lint
lib/og-card.tsx the social card layout, in one place
components/blog/* the reading experience
components/mdx/* callouts, stats, tables, FAQ, code blocks
.claude/skills/* the agent layer
agentblog.config.ts one file to configure all of it
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.
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:
curl -s -A "GPTBot" https://your-site.dev/blog/your-post | grep "a sentence from your post"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.
/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.
Posts are MDX files in your repository, which makes them something a coding agent can genuinely maintain.
# via the shadcn registry, into .claude/skills/
npx shadcn@latest add @agentblog/agent-kit
# or as a Claude Code plugin
/plugin marketplace add goldk3y/agentblog
/plugin install agentblog@agentblogSeven skills, and the plugin id is qualified by its marketplace, which is the form Claude Code resolves.
| Skill | What it does |
|---|---|
agentblog-setup |
Finishes the install wiring, then replaces the seed identity, authors, categories, and posts with yours |
dataforseo-research |
Researches queries, competitors, SERPs, and AI citations through DataForSEO, and keeps every run in content/research/ so the next one is a diff |
plan-blog-content |
Builds the entity, the taxonomy, the query clusters, and the link graph, and writes them to an editorial plan |
write-blog-post |
Writes to the GEO playbook: answer capsules, question headings, real tables, cited statistics, internal links that prevent orphans |
refresh-blog-post |
Re-researches an existing post and updates dateModified only when the content actually changed |
agentblog-audit |
Runs the pre-publish gate, including a raw curl -A GPTBot assertion |
publish-blog-post |
Revalidates the post, index, sitemap, and feed, submits to IndexNow, and reads the response code |
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
CONTRIBUTING.md.
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.
One file.
import { defineConfig } from '@/lib/define-config'
import { mdxSource } from '@/lib/sources/mdx'
export default defineConfig({
siteUrl: 'https://yourdomain.com',
brand: {
name: 'Your Brand',
logo: { url: '/logo.png', width: 512, height: 512 },
sameAs: ['https://www.linkedin.com/company/yourbrand', 'https://github.com/yourbrand'],
},
source: mdxSource({ dir: 'content/blog' }),
revalidate: 3600,
postsPerPage: 12,
noindexTagsBelow: 5,
indexnow: { enabled: true },
})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.
Storage is an interface, and MDX on disk is one implementation of it.
interface ContentSource<Strategy extends PrerenderStrategy> {
readonly prerenderStrategy: Strategy
readonly name: string
getAllPosts(opts?: PostQuery): Promise<PublishedPost[]>
getPost(slug: Slug, opts?: PostQuery): Promise<Post | null>
// ...
}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.
| Adapter | Status |
|---|---|
mdxSource |
Shipping. Filesystem plus frontmatter. Zero infrastructure, git versioned, and an agent writing a post is just writing a file |
supabaseSource |
Planned |
convexSource |
Planned |
agentblog init detect, prompt, install, patch, verify
agentblog create <name> scaffold a standalone blog site (see the note above)
agentblog doctor [--fix] verify the install; non-zero exit, so it works in CI
agentblog doctor --url <u> fetch your live site as five different crawlers
agentblog audit [slug] the pre-publish checklist
agentblog audit --stale posts ranked by how overdue a refresh is
agentblog new "<title>" scaffold a post with correct frontmatter
agentblog ping <slug> revalidate plus IndexNow, manually
agentblog revert restore the last patch set
agentblog uninstall revert patches and list what to delete
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
CLI reference 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
Taking an update.
- 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
npx agentblog@latest new "Do AI crawlers run JavaScript?"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:
npx agentblog@latest audit do-ai-crawlers-run-javascriptThe pre-publish gate. It reports every item pass or fail with the observed value, and never claims done on a fail.
| Page | For |
|---|---|
| Installation | Both install paths, and the four edits neither one makes |
| Configuration | Every field in agentblog.config.ts |
| CLI reference | Every command, every flag, and what doctor --fix declines |
| Troubleshooting | The failures that actually happen, and what each looks like |
| The GEO playbook | Why the content format is the content format |
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
Roadmap and non-goals.
apps/web agentblog.dev: landing page, registry host, and the demo blog
registry/ the source of truth for every file AgentBlog ships
apps/docs docs.agentblog.dev: the documentation, built with Fumadocs
apps/fixture-next16 clean Next.js 16 app; CI installs into it and builds
packages/schema Zod schemas, inferred types, ContentSource, contract suite
packages/checks dependency-free config predicates, vendored bot list
packages/cli the `agentblog` npm package
plugins/agentblog the Claude Code plugin
scripts codegen and the CI assertion gates
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 CONTRIBUTING.md.
Three licenses, scoped by directory, and the scoping is the point: the seed posts become your published content, so they carry no attribution requirement.
| Scope | License | Why |
|---|---|---|
| Code | MIT | A restrictive license on a template whose distribution mechanism is copying files into your repository would be incoherent |
Seed posts (apps/web/registry/blog/content/**) |
CC0 | These become your published content. An attribution requirement would mean every user owes credit on their own blog |
| Docs and playbook prose | CC BY 4.0 | Reproducing the playbook requires a credit link, which is exactly the citation this product exists to generate |
Full text in Licensing.
Built on shadcn/ui's registry system, which solved distribution so we did not have to, and on Next.js, whose App Router makes full prerendering the default rather than an achievement.