{"_id":"@aeranko/ship","_rev":"2-277ce923a4602d342281cc31ae04d144","name":"@aeranko/ship","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@aeranko/ship","version":"0.1.0","keywords":["aeo","geo","ai-search","crawlers","gptbot","observability","nextjs","express"],"author":{"name":"Aeranko"},"license":"MIT","_id":"@aeranko/ship@0.1.0","maintainers":[{"name":"fellofell","email":"fellofell@gmail.com"}],"homepage":"https://aeranko.com/ship","bugs":{"url":"https://github.com/aeranko/aeranko/issues"},"dist":{"shasum":"7f868fb839145504335ee1f6a557fa4ab7a5b2b6","tarball":"https://registry.npmjs.org/@aeranko/ship/-/ship-0.1.0.tgz","fileCount":22,"integrity":"sha512-iSmAA1dXTBAamt3+jgWPqcn3tiHQgp0fEpqhXxeL8IbbjE0aOH//OmeAAMXN+EUalk6H9ZcXuEWDz3AiIijZFA==","signatures":[{"sig":"MEUCIQCvGxT2ph6V+FmjKVcJXYXbqTxqo8dUNMtQIOfp1Kl4SgIgWKFzY4CdMnzAdpegRyXsfFm+h4yn6Bh68OZDO7fvF8c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":153703},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.17"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./next":{"types":"./dist/next.d.ts","import":"./dist/next.js","require":"./dist/next.cjs"},"./express":{"types":"./dist/express.d.ts","import":"./dist/express.js","require":"./dist/express.cjs"}},"gitHead":"bde7db724374bc4fa3eb219484ad4c07a264d015","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build && npm run test"},"_npmUser":{"name":"fellofell","email":"fellofell@gmail.com"},"repository":{"url":"git+https://github.com/aeranko/aeranko.git","type":"git","directory":"packages/ship"},"_npmVersion":"10.8.2","description":"Drop-in AI crawler tracking and AEO telemetry for Node.js apps. Ships data to Aeranko for AI-visibility analytics.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.9.0","@types/node":"^22.10.0"},"peerDependencies":{"next":">=14"},"peerDependenciesMeta":{"next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ship_0.1.0_1776522989375_0.6396338175784688","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@aeranko/ship","version":"0.2.0","description":"Drop-in AI crawler tracking and AEO telemetry for Node.js apps. Ships data to Aeranko for AI-visibility analytics.","license":"MIT","author":{"name":"Aeranko"},"homepage":"https://aeranko.com/ship","repository":{"type":"git","url":"git+https://github.com/aeranko/aeranko.git","directory":"packages/ship"},"keywords":["aeo","geo","ai-search","crawlers","gptbot","observability","nextjs","express"],"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./next":{"types":"./dist/next.d.ts","import":"./dist/next.js","require":"./dist/next.cjs"},"./express":{"types":"./dist/express.d.ts","import":"./dist/express.js","require":"./dist/express.cjs"},"./schema":{"types":"./dist/schema.d.ts","import":"./dist/schema.js","require":"./dist/schema.cjs"},"./seo":{"types":"./dist/seo.d.ts","import":"./dist/seo.js","require":"./dist/seo.cjs"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"}},"sideEffects":false,"scripts":{"build":"tsup","dev":"tsup --watch","lint":"eslint src","size":"node scripts/check-bundle-size.mjs","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","prepublishOnly":"npm run build && npm run test && npm run size"},"peerDependencies":{"next":">=14","react":">=18","react-dom":">=18"},"peerDependenciesMeta":{"next":{"optional":true},"react":{"optional":true},"react-dom":{"optional":true}},"devDependencies":{"@types/node":"^22.10.0","@types/react":"^19.0.0","@types/react-dom":"^19.0.0","tsup":"^8.3.5","typescript":"^5.9.0","vitest":"^2.1.9"},"publishConfig":{"access":"public"},"engines":{"node":">=18.17"},"_id":"@aeranko/ship@0.2.0","gitHead":"f5529d9f80b74f2d672b923294331f36b74edf2d","bugs":{"url":"https://github.com/aeranko/aeranko/issues"},"_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-Je71TUOh+fkzkchYwAMACXOqltojy0azGbJfyxlyiqWDts3xztqRUvD1Ct3ek+tjYWd3p+EUR9gHsZXrl/ZmzA==","shasum":"dca533b0b38d1e31d4072f860041c08f0fd992a2","tarball":"https://registry.npmjs.org/@aeranko/ship/-/ship-0.2.0.tgz","fileCount":27,"unpackedSize":115648,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICI3YGb1k4IVp6SurqOyf9x8kmPXCgA2QFWGoUsSw+z3AiBShAfy60kJMgz6VEn0NU9QMbDxN1BksKZcRfKTJ/itcg=="}]},"_npmUser":{"name":"fellofell","email":"fellofell@gmail.com"},"directories":{},"maintainers":[{"name":"fellofell","email":"fellofell@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ship_0.2.0_1776536389290_0.04865292491513773"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-18T14:36:29.230Z","modified":"2026-04-18T18:19:49.590Z","0.1.0":"2026-04-18T14:36:29.524Z","0.2.0":"2026-04-18T18:19:49.441Z"},"bugs":{"url":"https://github.com/aeranko/aeranko/issues"},"author":{"name":"Aeranko"},"license":"MIT","homepage":"https://aeranko.com/ship","keywords":["aeo","geo","ai-search","crawlers","gptbot","observability","nextjs","express"],"repository":{"type":"git","url":"git+https://github.com/aeranko/aeranko.git","directory":"packages/ship"},"description":"Drop-in AI crawler tracking and AEO telemetry for Node.js apps. Ships data to Aeranko for AI-visibility analytics.","maintainers":[{"name":"fellofell","email":"fellofell@gmail.com"}],"readme":"# @aeranko/ship\n\nAI-search observability and AEO uplift for JavaScript apps, in a single\nSDK. Drop in, and your site:\n\n1. Exposes **JSON-LD** (`Organization`, `WebSite`, `BreadcrumbList`,\n   `Article`, `FAQPage`, `SpeakableSpecification`) so LLM crawlers can\n   parse what you do.\n2. Serves **`/llms.txt`**, **`/llms-full.txt`**, **`robots.txt`** and\n   **`sitemap.xml`** with AI-bots explicitly allow-listed.\n3. Emits **canonical + OpenGraph + Twitter** metadata with zero\n   ceremony.\n4. Reports every AI-crawler hit (GPTBot, ClaudeBot, PerplexityBot,\n   Google-Extended, …) to your Aeranko dashboard.\n\n```bash\nnpm install @aeranko/ship\n```\n\n---\n\n## Quick start (Next.js 15 / 16 — App Router)\n\n### 1. Add `<Ship />` to your root layout\n\n```tsx\n// app/layout.tsx\nimport { Ship } from \"@aeranko/ship/react\";\nimport { aerankoMetadata } from \"@aeranko/ship/next\";\n\nexport const metadata = aerankoMetadata({\n  siteName: \"Acme Inc.\",\n  title: \"Acme — widgets for serious people\",\n  description: \"We make widgets that just work.\",\n  canonical: \"https://acme.example\",\n});\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Ship\n          config={{\n            organization: {\n              name: \"Acme Inc.\",\n              url: \"https://acme.example\",\n              logo: \"https://acme.example/logo.png\",\n              sameAs: [\"https://twitter.com/acme\"],\n            },\n            website: { name: \"Acme Inc.\", url: \"https://acme.example\" },\n          }}\n        />\n      </head>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n`<Ship />` is a **Server Component** — no `\"use client\"`, no hydration\ncost, and LLM crawlers (which don't execute JS) see the JSON-LD in\nplain HTML.\n\n### 2. Add `app/robots.ts`\n\n```ts\nimport type { MetadataRoute } from \"next\";\nimport { aerankoRobots } from \"@aeranko/ship/next\";\n\nexport default function robots(): MetadataRoute.Robots {\n  return aerankoRobots({ siteUrl: \"https://acme.example\" });\n}\n```\n\nEvery major AI crawler (GPTBot, ClaudeBot, PerplexityBot, Google-Extended,\nApplebot-Extended, Bytespider, CCBot, …) is explicitly allowed.\n\n### 3. Add `app/sitemap.ts`\n\n```ts\nimport type { MetadataRoute } from \"next\";\nimport { aerankoSitemap } from \"@aeranko/ship/next\";\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n  return aerankoSitemap([\n    { url: \"https://acme.example\", lastModified: new Date(), priority: 1 },\n    { url: \"https://acme.example/pricing\", changeFrequency: \"weekly\" },\n  ]);\n}\n```\n\n`lastModified` feeds the Aeranko freshness score — keep it current.\n\n### 4. Add `app/llms.txt/route.ts`\n\n```ts\nimport { createLlmsTxtHandler } from \"@aeranko/ship/next\";\n\nexport const GET = createLlmsTxtHandler({\n  siteName: \"Acme Inc.\",\n  siteUrl: \"https://acme.example\",\n  description: \"Widgets for serious people.\",\n  sections: [\n    { title: \"Pricing\", url: \"https://acme.example/pricing\" },\n    { title: \"Docs\", url: \"https://acme.example/docs\" },\n  ],\n});\n```\n\nPair it with `app/llms-full.txt/route.ts` using `createLlmsFullTxtHandler`\nwhen you have long-form content that LLMs should index.\n\n### 5. Track AI-crawler hits via `proxy.ts`\n\n```ts\n// proxy.ts (Next.js 16) or middleware.ts (Next.js 14/15)\nimport { createAerankoProxy } from \"@aeranko/ship/next\";\n\nexport default createAerankoProxy({\n  apiKey: process.env.AERANKO_API_KEY!,\n});\n\nexport const config = {\n  matcher: [\"/((?!_next/static|_next/image|favicon.ico).*)\"],\n};\n```\n\n---\n\n## Subpath entry points\n\n| Import                  | What you get                                                 | React dep |\n| ----------------------- | ------------------------------------------------------------ | --------- |\n| `@aeranko/ship`         | `createClient`, `detectCrawler`, `isAICrawler`               | none      |\n| `@aeranko/ship/next`    | `createAerankoProxy`, `aerankoMetadata`, `aerankoRobots`, `aerankoSitemap`, `createLlmsTxtHandler`, `createLlmsFullTxtHandler` | none (next is peer) |\n| `@aeranko/ship/express` | `aerankoMiddleware`                                          | none      |\n| `@aeranko/ship/react`   | `<Ship />`, `<ShipFAQ />`, `<ShipSpeakable />`               | peer      |\n| `@aeranko/ship/schema`  | `buildOrganization`, `buildWebSite`, `buildBreadcrumbList`, `buildArticle`, `buildFAQPage`, `buildSpeakable`, `bundleJsonLd` | none |\n| `@aeranko/ship/seo`     | `buildLlmsTxt`, `buildLlmsFullTxt`, `buildRobotsRules`, `renderRobotsTxt`, `DEFAULT_AI_BOTS` | none |\n\nEvery subpath ships both `import` and `require` variants, with full\n`.d.ts` types. `sideEffects: false` so tree-shaking works.\n\n---\n\n## API reference\n\n### `@aeranko/ship/schema`\n\nPure JSON-LD builders. Every function returns a plain object — you can\nstringify it yourself, or pass it to `bundleJsonLd` which produces a\nstring safe to drop into `<script type=\"application/ld+json\">`\n(`</script>` is XSS-escaped automatically).\n\n```ts\nimport { buildOrganization, bundleJsonLd } from \"@aeranko/ship/schema\";\n\nconst html = `<script type=\"application/ld+json\">${bundleJsonLd(\n  buildOrganization({ name: \"Acme\", url: \"https://acme.example\" }),\n)}</script>`;\n```\n\n### `@aeranko/ship/seo`\n\n```ts\nimport {\n  buildLlmsTxt,\n  buildRobotsRules,\n  renderRobotsTxt,\n  DEFAULT_AI_BOTS,\n} from \"@aeranko/ship/seo\";\n\nconst txt = buildLlmsTxt({\n  siteName: \"Acme\",\n  siteUrl: \"https://acme.example\",\n  description: \"Widgets.\",\n  sections: [{ title: \"Docs\", url: \"https://acme.example/docs\" }],\n});\n\nconst robots = buildRobotsRules({ siteUrl: \"https://acme.example\" });\nconst robotsTxt = renderRobotsTxt(robots);\n```\n\n`DEFAULT_AI_BOTS` is exported as a `readonly string[]` — extend or\nfilter as needed, never hardcode your own list.\n\n### `@aeranko/ship/react`\n\nAll components are Server Components. Do **not** add `\"use client\"` —\nthe API key must not leak into the client bundle and the JSON-LD must\nrender in the server HTML.\n\n```tsx\nimport { Ship, ShipFAQ, ShipSpeakable } from \"@aeranko/ship/react\";\n\n// Root layout\n<Ship config={{ organization: { name, url } }} />\n\n// FAQ page\n<ShipFAQ items={[{ question: \"Why?\", answer: \"Because.\" }]}>\n  {/* your UI */}\n</ShipFAQ>\n\n// A \"read this out loud\" region\n<ShipSpeakable className=\"summary\">\n  <p>TL;DR paragraph.</p>\n</ShipSpeakable>\n```\n\n---\n\n## Why Ship?\n\nAI search (ChatGPT, Claude, Perplexity, Google AI Overviews) parses\nyour site differently from Google of 2015. It wants:\n\n- **Structured facts** in JSON-LD so it knows *what* you are.\n- **`/llms.txt`** so it knows *where* to get the summary without\n  crawling a JS bundle.\n- **An explicit bot-allow list** — AI crawlers treat a missing rule as\n  conservative \"probably don't\".\n- **Canonical URLs and `lastmod`** so the answer engine doesn't cite\n  stale versions.\n\nShip gives you every one of those in ≤ 50 lines of code, with a single\ndependency, zero client-bundle cost, and full TypeScript types.\n\n---\n\n## Compatibility\n\n| Runtime       | Supported |\n| ------------- | --------- |\n| Next.js 15    | yes       |\n| Next.js 16    | yes (App Router, `proxy.ts`) |\n| React 18 / 19 | yes       |\n| Node.js       | ≥ 18.17   |\n| Edge runtime  | yes (`/schema`, `/seo`, `/react` have no Node-only APIs) |\n| Cloudflare Workers | yes (`/schema`, `/seo`) |\n\n---\n\n## Privacy\n\nShip does not capture query strings, headers, cookies, or request\nbodies. Only `method`, `path`, `status`, and the crawler's user-agent\nheader are shipped.\n\n## License\n\nMIT © Aeranko\n","readmeFilename":"README.md"}