JSPM

@dualmark/nextjs

0.5.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 459
  • Score
    100M100P100Q99814F
  • License Apache-2.0

Next.js 15 App Router adapter for the Dualmark AEO framework. Drop-in middleware + route-handler factory + withDualmark() config wrapper. Same API surface as @dualmark/astro.

Package Exports

  • @dualmark/nextjs

Readme

@dualmark/nextjs

Next.js 15 App Router adapter for the Dualmark AEO framework.

Same one-line install as @dualmark/astrowithDualmark() for the config, createDualmarkMiddleware() for middleware.ts, createDualmarkRouteHandler() for the markdown twin route handler, and createLlmsTxtHandler() for /llms.txt.

Install

bun add @dualmark/nextjs @dualmark/core @dualmark/converters

Usage

import { withDualmark } from "@dualmark/nextjs";

export default withDualmark(
  { reactStrictMode: true },
  {
    siteUrl: "https://example.com",
    collections: {
      blog: {
        converter: "blog",
        getEntries: () => yourPosts,
      },
    },
    llmsTxt: {
      enabled: true,
      brandName: "Acme",
      sections: [{ title: "Pages", links: [{ title: "Home", href: "/" }] }],
    },
  },
);
import { createDualmarkMiddleware } from "@dualmark/nextjs";

const middleware = createDualmarkMiddleware({
  siteUrl: "https://example.com",
});

export default middleware;
export const config = middleware.config;
import { createDualmarkRouteHandler } from "@dualmark/nextjs";
import { POSTS } from "@/lib/posts";

const handler = createDualmarkRouteHandler({
  siteUrl: "https://example.com",
  collections: {
    blog: {
      converter: "blog",
      getEntries: () => POSTS,
    },
  },
  staticPages: [{ pattern: "/", render: () => "# Home\n\nWelcome." }],
});

export const GET = handler.GET;
export const generateStaticParams = handler.generateStaticParams;
export const dynamic = "force-static";
import { createLlmsTxtHandler } from "@dualmark/nextjs";

const handler = createLlmsTxtHandler({
  brandName: "Acme",
  sections: [{ title: "Pages", links: [{ title: "Home", href: "/" }] }],
});

export const GET = handler.GET;
export const dynamic = "force-static";

Architecture

┌─────────────────────────────────────────────────────────────┐
│ middleware.ts                                               │
│   - if path ends in .md     → rewrite to /md/<path>        │
│   - if AI bot UA OR Accept: text/markdown                   │
│       → rewrite to /md/<path>                               │
│   - if Accept rules out html+md → 406                       │
│   - else (HTML) → next() + Link rel=alternate header        │
└─────────────────────────────────────────────────────────────┘

app/
├── ...your existing pages
├── md/[...path]/route.ts   ← createDualmarkRouteHandler (renders .md)
└── llms.txt/route.ts       ← createLlmsTxtHandler (renders /llms.txt)

/md/... is an internal namespace your users never see — middleware rewrites to it, and the route handler dispatches to your collections, static pages, or parameterized routes. Configurable via internalNamespace if you need a different name.

Built-in converter names

blog, case-study, changelog, compare, docs, feature, glossary, legal, pricing, pseo, tool, video. Pass any of them as converter, or pass a function (entry) => string for custom output.

Why Next.js needs getEntries

Astro has astro:content to discover collection entries automatically. Next.js has no equivalent — content can come from the filesystem, a CMS, a database, anything. So you supply getEntries: () => Entry[] | Promise<Entry[]> and the adapter does the rest.

License

Apache 2.0