v0.5.0 — @dualmark/nextjs
New: `@dualmark/nextjs` — first-class Next.js 15 App Router adapter
Closes #4. Same one-line install as `@dualmark/astro`, with a small surface area:
- `withDualmark(nextConfig, options)` — wraps `next.config.mjs`
- `createDualmarkMiddleware(options)` — drop-in `middleware.ts`
- `createDualmarkRouteHandler(options)` — catch-all markdown twin route handler with `generateStaticParams`
- `createLlmsTxtHandler(options)` — `/llms.txt` route handler
The `collections` / `staticPages` / `parameterizedRoutes` config shape mirrors `@dualmark/astro` so users can copy their config across frameworks. All 12 built-in converters (`blog`, `case-study`, `changelog`, `compare`, `docs`, `feature`, `glossary`, `legal`, `pricing`, `pseo`, `tool`, `video`) work identically. Tree-shakeable, ESM + CJS, zero runtime deps beyond `@dualmark/core` + `@dualmark/converters`. 47 vitest tests cover config validation, middleware negotiation, route dispatch, and `generateStaticParams`.
Install
```bash
bun add @dualmark/nextjs @dualmark/core @dualmark/converters
```
Minimal setup
```ts
// middleware.ts
import { createDualmarkMiddleware } from "@dualmark/nextjs";
export default createDualmarkMiddleware({ siteUrl: "https://example.com\" });
export const config = {
matcher: [
{
source: "/((?!_next/|favicon.ico|md/).*)",
missing: [{ type: "header", key: "next-router-prefetch" }],
},
],
};
```
```ts
// app/md/[...path]/route.ts
import { createDualmarkRouteHandler } from "@dualmark/nextjs";
import { POSTS } from "@/lib/posts";
const handler = createDualmarkRouteHandler({
siteUrl: "https://example.com\",
collections: {
blog: { converter: "blog", getEntries: () => POSTS.map(toEntry) },
},
});
export const dynamic = "force-static";
export const GET = handler.GET;
export const generateStaticParams = handler.generateStaticParams;
```
That's it. Bot UAs get markdown, browsers get HTML with `Link rel="alternate"`, direct `.md` URLs serve markdown.
Migration from manual `@dualmark/core` setup
If you wired `@dualmark/core` into Next.js by hand before this release, the migration is mechanical:
| Before (`@dualmark/core` only) | After (`@dualmark/nextjs`) |
|---|---|
| Hand-rolled `middleware.ts` with `detectAIBot` + `negotiateFormat` + manual rewrite | `createDualmarkMiddleware({ siteUrl })` |
| Hand-rolled `app/md/[...path]/route.ts` with `if`-chains | `createDualmarkRouteHandler({ siteUrl, collections, staticPages, parameterizedRoutes })` |
| Hand-rolled `app/llms.txt/route.ts` calling `renderLlmsTxt` | `createLlmsTxtHandler({ brandName, sections })` |
| Manual `transpilePackages: [...]` | `withDualmark(nextConfig, options)` |
The internal namespace, response headers, and conformance score are unchanged. Full guide: docs.dualmark.dev/docs/integrations/nextjs.
Reference example
`examples/nextjs-app-router` is now built on the new package — ~50 lines instead of ~120 hand-rolled, same 120/125 conformance score under both `next dev` and `next start`. The CI `nextjs-app-router` job verifies this on every PR.
Coordinated patch bumps
The linked `@dualmark/*` changeset group means every package gets a coordinated version bump:
- `@dualmark/nextjs` 0.5.0 (new)
- `@dualmark/core` 0.3.1 → 0.5.0
- `@dualmark/converters` 0.3.1 → 0.5.0
- `@dualmark/astro` 0.3.1 → 0.5.0
- `@dualmark/cloudflare` 0.3.1 → 0.5.0
- `@dualmark/cli` 0.3.1 → 0.5.0
No source-level changes to the other five packages — just version metadata bumps so internal `workspace:*` deps resolve cleanly when consumers install `@dualmark/nextjs`.
Verified
- Full monorepo: 313 tests across 6 packages, build + test + typecheck green on CI
- `examples/nextjs-app-router` E2E in CI: `next dev` + `dualmark verify` → 120/125
- Local verification: `next dev` and `next start` both score 120/125
- Docs site dogfood (`apps/docs`): every doc page ≥ 105/125