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/astro

v0.0.5

Published

AIDP 0.3 publishing channel for Astro 5 — exposes /.well-known/aidp.json and friends, fetches signed content + pointer payloads from SpeakSpec, receives §8.10 cache-invalidation webhooks.

Readme

@speakspec/astro

AIDP 0.3 publishing channel for Astro 5.

An Astro 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, and observes AI-crawler traffic for upload to your dashboard.

Feature-equivalent to @speakspec/nuxt and @speakspec/next.

Install

pnpm add @speakspec/astro

Configure (env vars)

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

Wire the well-known routes

Astro requires output: 'server' (or output: 'hybrid') to serve dynamic API routes. Add one route file per AIDP endpoint:

// src/pages/.well-known/aidp.json.ts
import { aidpEntityRoute } from '@speakspec/astro'
export const GET = aidpEntityRoute()
// src/pages/.well-known/aidp/content/[id].json.ts
import { aidpContentRoute } from '@speakspec/astro'
export const GET = aidpContentRoute()
// src/pages/.well-known/aidp/content/index.ts
import { aidpDirectoryRoute } from '@speakspec/astro'
export const GET = aidpDirectoryRoute()
// src/pages/api/aidp/invalidate.ts  ← NO leading underscore
import { aidpWebhookRoute } from '@speakspec/astro'
export const POST = aidpWebhookRoute()

Astro 5 excludes any path segment starting with _ from routing (treats it as private). Use api/aidp/... (no leading underscore). The path you register with the SpeakSpec dashboard must match.

// src/pages/llms.txt.ts  (optional — serves spec §11.3 llms.txt projection)
import { llmsTxtRoute } from '@speakspec/astro'
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

// src/middleware.ts
import { aidpBotMiddleware } from '@speakspec/astro/middleware'
export const onRequest = aidpBotMiddleware()

If you already have middleware, sequence them:

import { sequence } from 'astro:middleware'
import { aidpBotMiddleware } from '@speakspec/astro/middleware'

export const onRequest = sequence(myExisting, aidpBotMiddleware())

Inject HTML link tags

---
// src/layouts/BaseLayout.astro
import AidpLinks from '@speakspec/astro/components/AidpLinks.astro'
---
<html>
  <head>
    <AidpLinks />
  </head>
  <body><slot /></body>
</html>

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

---
// src/pages/articles/[id].astro
import AidpContent from '@speakspec/astro/components/AidpContent.astro'
const article = await loadArticle(Astro.params.id)
---
<AidpContent contentId={article.id} pathname={`/articles/${article.id}`} />
<article set:html={article.body} />

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

Cache layer

Default in-memory cache. Plug in Redis / fs / etc. at boot:

// src/server-init.ts (called from astro:server:setup integration)
import { setCacheStore } from '@speakspec/astro'
import { redisStore } from './my-cache'

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[]>
}

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

  • Output mode: requires Astro output: 'server' or 'hybrid' for API routes to be dynamic. output: 'static' (the default) bakes all routes at build time and won't update directives without a rebuild.
  • Multi-instance: in-memory cache + impression queue are per-process. Customers running on Cloudflare or similar edge platforms should provide a Redis-backed cache via setCacheStore.
  • 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