npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@caprail-dev/agent-pages

v0.4.0

Published

Markdown-first pages for AI agents — co-located page.md twins served inline via Accept-header content negotiation for Next.js.

Downloads

456

Readme

@caprail-dev/agent-pages

Markdown-first pages for AI agents in Next.js: co-locate a page.md next to each page.tsx and serve it via Accept-header content negotiation — plus generated /llms.txt and /llms-full.txt.

A GET/HEAD request is served the markdown twin when its Accept header lists text/markdown with q > 0 and at least as strongly as text/html (a bare */* is not a preference — browsers send it). Everything else gets HTML. A negotiated response Varys on Accept so caches key the two representations separately. A direct .md URL (/terms.md) and the /llms.txt / /llms-full.txt aggregates are served unconditionally.

Install

npm i @caprail-dev/agent-pages   # or bun add / pnpm add / yarn add

Setup

1. Wrap your next.config.js — runs codegen on every next dev / next build (and watches page.md files in dev):

import { withAgentPages } from "@caprail-dev/agent-pages/config";

export default withAgentPages(nextConfig, {
  siteUrl: process.env.BASE_URL ?? "http://localhost:3000"
});

2. Co-locate page.md twins next to the pages you want agent-readable:

src/app/page.tsx        →  src/app/page.md        (served at /index.md)
src/app/terms/page.tsx  →  src/app/terms/page.md  (served at /terms.md)

Optional frontmatter feeds the /llms.txt link list:

---
title: Example
description: One-liner shown in /llms.txt.
---

# Example — the markdown twin

3. Add the middleware — spread the generated AGENT_PAGES bundle straight into the helper. The file is proxy.ts (Next's current naming; middleware.ts on older Next), exporting proxy / middleware respectively:

// proxy.ts
import { createAgentPagesMiddleware } from "@caprail-dev/agent-pages/next";
import { AGENT_PAGES } from "@/app/_agent-pages/manifest";

export const proxy = createAgentPagesMiddleware({ ...AGENT_PAGES });
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

The helper serves every doc inline from the manifest — a direct .md URL (/terms.md), an HTML page whose twin an agent wants as markdown (/terms), and the /llms.txt / /llms-full.txt aggregates — and HTML (with Vary) otherwise.

Composing with analytics or other middleware

observe runs developer-provided middleware for its side effects alongside serving. It's any Next-middleware-shaped function — analytics, a logger, a feature-flag tap — and its return value is ignored (the helper owns the response). Each observer receives the request whose pathname is what was actually served (the .md twin on the markdown branch), so a beacon records the served path without you cloning the URL by hand:

import { createAgentPagesMiddleware } from "@caprail-dev/agent-pages/next";
import { createCaprailMiddleware } from "@caprail-dev/analytics/next";
import { AGENT_PAGES } from "@/app/_agent-pages/manifest";

export const proxy = createAgentPagesMiddleware({
  ...AGENT_PAGES,
  observe: createCaprailMiddleware(), // or [collector, logger, …]
});

Composing with a routing middleware (next-intl, …)

observe is for middleware that only watches. A routing middleware like next-intl's createMiddleware(routing) owns the response (locale redirect/rewrite, NEXT_LOCALE cookie), so it goes in the html slot — it runs instead of NextResponse.next() on the HTML branch:

import { createAgentPagesMiddleware } from "@caprail-dev/agent-pages/next";
import { createCaprailMiddleware } from "@caprail-dev/analytics/next";
import createMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
import { AGENT_PAGES } from "@/app/_agent-pages/manifest";

export const proxy = createAgentPagesMiddleware({
  ...AGENT_PAGES,
  observe: createCaprailMiddleware(), // side effects (analytics, logging)
  html: createMiddleware(routing),    // owns the HTML response (i18n routing)
});

Agent-pages decides first, on the unprefixed path, so AI agents get the one canonical markdown twin while humans fall through to localized HTML — a router that rewrites /terms/en/terms never hides a twin. The negotiation Vary is appended to the router's response, so its own Vary / Link survive.

Need full control? Compose by hand with the decideAgentPages / serveAgentPages primitives — createAgentPagesMiddleware is just their batteries-included form.

What gets generated

A single file inside your app dir (commit it — deterministic output keeps diffs meaningful and shows exactly what agents will read):

  • _agent-pages/manifest.ts — the full doc registry with each page.md's content inlined. Exports AGENT_DOCS (the docs), SITE_URL / LLMS_INTRO, and the AGENT_PAGES bundle ({ docs, llms }) you spread into the middleware. There are no per-page .md route handlers and no separate rewrites file — everything is served inline from this manifest.

The file is marker-headed; the codegen refuses to overwrite a manifest without the marker and removes generated entries whose page.md was deleted.

Options

withAgentPages(nextConfig, {
  siteUrl: "https://example.com",
  appDir: "src/app", // default: src/app, else app
  llms: {
    intro: "# Example\n\n> …", // /llms.txt preamble before "## Docs"
  },
  extraDocs: [
    // Docs whose markdown lives in app code (route stays hand-written);
    // included in /llms.txt + /llms-full.txt via an emitted import.
    {
      mdPath: "/install.md",
      title: "Install guide",
      description: "Agent-readable install instructions.",
      source: { module: "@/lib/install-doc", export: "INSTALL_DOC" },
    },
  ],
});

Notes & limitations

  • Dynamic segments ([slug]/page.md) are unsupported in v1 — skipped with a warning.
  • Interpolations are not supported in page.md — flatten values to literals and guard them with a test (e.g. assert the file contains your TERMS_VERSION constant).
  • The dev watcher uses fs.watch(…, { recursive: true }) (Node ≥ 20 on Linux). Deleting a whole directory may not fire the page.md filter — restart next dev to clean up (a restart with no changes writes nothing).
  • next.config.js consumes the compiled dist/config.js — in monorepos, build this package before next dev / next build.
  • A blanket /* eslint-disable */ heads every generated file; with reportUnusedDisableDirectives enabled you may see warnings on clean files — harmless.

License

MIT