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

@speakspec/next

v0.0.5

Published

AIDP 0.3 publishing channel for Next.js (App Router) — exposes /.well-known/aidp.json and friends, fetches signed content + pointer payloads from SpeakSpec, receives §8.10 cache-invalidation webhooks.

Readme

@speakspec/next

AIDP 0.3 publishing channel for Next.js 15 (App Router).

A Next.js package that turns your site into a first-class AIDP source: publishes the entity directive at /.well-known/aidp.json, exposes signed content endpoints + a paginated content directory, injects <link rel="aidp"> head tags, receives cache-invalidation webhooks from SpeakSpec, and observes AI-crawler traffic for upload to your dashboard.

Feature-equivalent to @speakspec/nuxt.

Install

pnpm add @speakspec/next

Configure (env vars)

# .env.local
SPEAKSPEC_ENTITY_ID=your-entity-slug
SPEAKSPEC_API_KEY=aidp_xxxxxxxxxxx
SPEAKSPEC_WEBHOOK_SECRET=...
NEXT_PUBLIC_SPEAKSPEC_SITE_ORIGIN=https://yoursite.com
SPEAKSPEC_BOT_TRACKING=true
SPEAKSPEC_BOT_UPLOAD=true

Wire the well-known routes

Create one route file per AIDP endpoint and re-export the SDK's factory:

// app/.well-known/aidp.json/route.ts
import { aidpEntityRoute } from '@speakspec/next'
export const GET = aidpEntityRoute()
// app/.well-known/aidp/content/[id]/route.ts
import { aidpContentRoute } from '@speakspec/next'
export const GET = aidpContentRoute()
// app/.well-known/aidp/content/route.ts
import { aidpDirectoryRoute } from '@speakspec/next'
export const GET = aidpDirectoryRoute()
// app/api/_aidp/invalidate/route.ts
import { aidpWebhookRoute } from '@speakspec/next'
export const POST = aidpWebhookRoute()
// app/llms.txt/route.ts  (optional — serves spec §11.3 llms.txt projection)
import { llmsTxtRoute } from '@speakspec/next'
export const GET = llmsTxtRoute()

Content inline vs directory (v0.4+)

AIDP v0.4 introduces per-type content strategy. The entity owner can decide, per content type, whether content appears:

  • Inline (inline, default): full content envelopes appear inside /.well-known/aidp.json's content array
  • Directory (directory): the type is omitted from aidp.json.content; AI agents fetch /.well-known/aidp/content/directory.json for the index, and /.well-known/aidp/content/{id}.json for individual envelopes

The content_index field in aidp.json declares which types are inlined vs indexed:

{
  "content_index": {
    "url": "https://example.com/.well-known/aidp/content/directory.json",
    "types_inlined": ["faq", "service"],
    "types_indexed": ["article", "event"],
    "total_by_type": { "article": 1240, "event": 387, "faq": 18, "service": 6 },
    "pinned_count": 3,
    "updated_at": "2026-05-12T10:00:00Z"
  }
}

The SDK proxies the upstream response transparently—no client code change is needed when an entity owner switches strategy. AI consumers should check content_index.types_indexed and pull directory.json when needed.

Pinned content

Any content can be marked pinned: true. Pinned content always appears in aidp.json.content regardless of the type's strategy, sorted first.

Wire the bot-detection middleware

// middleware.ts (project root)
import { aidpBotMiddleware } from '@speakspec/next/middleware'
export default aidpBotMiddleware()

export const config = {
  // Apply to all routes EXCEPT Next internals + the webhook endpoint
  matcher: '/((?!_next/static|_next/image|api/_aidp/invalidate|favicon.ico).*)',
}

Inject HTML link tags

// app/layout.tsx
import { AidpLinks } from '@speakspec/next/react'

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <AidpLinks />
      </head>
      <body>{children}</body>
    </html>
  )
}

For per-page binding on article / product / policy pages:

// app/articles/[id]/page.tsx
import { AidpContent } from '@speakspec/next/react'

export default async function ArticlePage({ params }) {
  const article = await loadArticle((await params).id)
  return (
    <>
      <AidpContent contentId={article.id} pathname={`/articles/${article.id}`} />
      <article>{article.body}</article>
    </>
  )
}

Calling <AidpContent /> registers the (path → content_id) mapping with the SDK so subsequent AI crawler hits on that path get enriched with content_id in the impression.

Cache layer

The SDK ships an in-memory cache by default — fine for single-instance deployments and warm Vercel functions. Multi-instance (or wanting durability across cold starts) customers can plug in a Redis-backed or fs-backed store at boot:

// app/instrumentation.ts
import { setCacheStore } from '@speakspec/next'
import { redisStore } from './my-cache'

export function register() {
  setCacheStore(redisStore)
}

Any object satisfying:

interface FullStore {
  getItem<T>(key: string): Promise<T | null>
  setItem(key: string, value: unknown): Promise<void>
  removeItem(key: string): Promise<void>
  getKeys(base: string): Promise<string[]>  // prefix match
}

works.

Cache tuning

The SDK serves three well-known routes with Cache-Control headers tuned for fast revocation propagation. If you have Cloudflare / CloudFront in front of your site, those headers are what the CDN respects — so they directly bound how long it takes a revoked fact to disappear from AI agent answers.

There are two TTLs to think about:

| Layer | What it does | Default | Affects | |---|---|---|---| | SDK internal | how long the SDK process reuses a fetched bundle before re-fetching from SpeakSpec | 300s | origin load on SpeakSpec | | Cache-Control: max-age | how long downstream caches (CDN + AI agents) reuse the response | 60s (entity/directory), 300s (content) | revocation propagation, CDN cost |

Why entity = 60s but content = 300s by default? The entity directive (/.well-known/aidp.json) is the revocation pivot — when a customer revokes a fact, this is the document AI agents re-fetch first to learn what's still valid. Short max-age keeps revocation fast. Per-content envelopes (/.well-known/aidp/content/[id].json) are content-addressed: each updated_at produces a new signed bundle, so longer caching is safe.

Setting max-age=0 disables CDN caching for that route but does NOT disable stale-while-revalidate — the CDN still serves stale within the SWR window while it revalidates. To fully disable caching, set both *_MAX_AGE=0 and *_SWR=0.

The SDK internal TTL is mostly the safety net for missed webhooks — when an entity is revoked, SpeakSpec sends a webhook that clears the SDK cache instantly. Downstream max-age is the real ceiling on how quickly AI agents see the revocation.

All values are configurable via env vars (seconds):

# SDK internal cache (default 300)
SPEAKSPEC_CACHE_TTL_SEC=300

# /.well-known/aidp.json (default 60 / 300)
SPEAKSPEC_ENTITY_MAX_AGE=60
SPEAKSPEC_ENTITY_SWR=300

# /.well-known/aidp/content/[id] (default 300 / 600)
SPEAKSPEC_CONTENT_MAX_AGE=300
SPEAKSPEC_CONTENT_SWR=600

# /.well-known/aidp/content (default 60 / 300)
SPEAKSPEC_DIRECTORY_MAX_AGE=60
SPEAKSPEC_DIRECTORY_SWR=300

Trade-off: longer max-age means lower origin/CDN bill but slower revocation. Worst-case revocation propagation is bounded by max-age + stale-while-revalidate. If you want sub-minute revocation across Cloudflare, also wire SpeakSpec's webhook to a Cloudflare purge — out of SDK scope.

Caveats vs @speakspec/nuxt

  • Edge runtime: the bot-detect middleware is Edge-safe (no Node-specific APIs); the impression upload queue uses fetch and console.log only — also Edge-safe. However, the webhook receiver uses node:crypto HMAC verification and must run in the Node runtime. Pin it explicitly in app/api/_aidp/invalidate/route.ts:
    import { aidpWebhookRoute } from '@speakspec/next'
    export const runtime = 'nodejs'
    export const POST = aidpWebhookRoute()
  • Multi-instance: in-memory cache + impression queue are per-process. Vercel cold starts may drop in-flight impressions. Acceptable per fire-and-forget design; see setCacheStore for shared persistence.
  • First-hit content_id: <AidpContent /> registers on render, so the very first AI crawler hit on a path lands with content_id=null. Subsequent hits are enriched.

Spec & references

License

MIT