{"_id":"@balladlabs/next","_rev":"6-3adb944450a14c4bc033720ffbf16150","name":"@balladlabs/next","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@balladlabs/next","version":"0.1.0","keywords":["ballad","nextjs","blog","headless","content","app-router"],"license":"MIT","_id":"@balladlabs/next@0.1.0","maintainers":[{"name":"troygoode","email":"troygoode@gmail.com"}],"homepage":"https://www.balladlabs.com/docs/nextjs-blog","bugs":{"url":"https://github.com/BalladAI/next/issues"},"dist":{"shasum":"f17b93b871eee37edd362df9092341e26cb19a76","tarball":"https://registry.npmjs.org/@balladlabs/next/-/next-0.1.0.tgz","fileCount":22,"integrity":"sha512-bH4dCUb/LUpn/wIigaUB4c5XL/BpUiqgrytMWxbKPrnub1cHblKrUDYBddgTan+AMJCc3ex6MHpa3/17m+2M+Q==","signatures":[{"sig":"MEQCIGK0cQ+5FWaxSf8o6kBbLSEt2lT0aYHc5zvIGofg58NUAiAU2lMGSqiVNOjyxFOJVJN547o/hzULZe0cY3GOD0YPRA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115401},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./pages":{"types":"./dist/pages.d.ts","import":"./dist/pages.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js"},"./styles.css":"./dist/styles.css"},"gitHead":"2ed03a6a48f074c2c7ef351c509805935a924a55","scripts":{"test":"vitest run","build":"tsup && cp src/styles.css dist/styles.css","verify":"npm run typecheck && npm test && npm run build","typecheck":"tsc --noEmit","prepublishOnly":"npm run verify"},"_npmUser":{"name":"troygoode","email":"troygoode@gmail.com"},"repository":{"url":"git+https://github.com/BalladAI/next.git","type":"git"},"_npmVersion":"12.0.2","description":"A Ballad-powered blog for Next.js: typed client for the content API, block renderer, SEO metadata, RSS, sitemap, and refresh-on-publish — with minimal code and full override points.","directories":{},"sideEffects":["*.css"],"_nodeVersion":"24.15.0","dependencies":{"remark-gfm":"^4.0.1","react-markdown":"^10.1.0"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.0.0","tsup":"^8.3.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.7.0","@types/node":"^22.20.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"next":">=15","react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/next_0.1.0_1789053733897_0.517561054833835","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@balladlabs/next","version":"0.1.1","keywords":["ballad","nextjs","blog","headless","content","app-router"],"license":"MIT","_id":"@balladlabs/next@0.1.1","maintainers":[{"name":"troygoode","email":"troygoode@gmail.com"}],"homepage":"https://www.balladlabs.com/docs/nextjs-blog","bugs":{"url":"https://github.com/BalladAI/next/issues"},"dist":{"shasum":"42de7b0db02e8f6d342d059edd361d9adbed22a1","tarball":"https://registry.npmjs.org/@balladlabs/next/-/next-0.1.1.tgz","fileCount":22,"integrity":"sha512-axn1HTux0sqdv6Syk8ALCXY26eQ660xq840n0ulErmJbjopaMKs7IMEzu71KMvFjs4hMeIJdI9S+4R/r9bUNaQ==","signatures":[{"sig":"MEQCIF4AsBugJEL8wXrdce+tvgzChxiY6oNXrSwqgLdqJ9S2AiAsTuXNUokyLjQ+nlZn6rD2K9Xjj6S/y9dS8YifRG3pHg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":117184},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./pages":{"types":"./dist/pages.d.ts","import":"./dist/pages.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js"},"./styles.css":"./dist/styles.css"},"gitHead":"4bbefe34a98b5cb7b4c5f6fd5e9be95a4309cde3","scripts":{"test":"vitest run","build":"tsup && cp src/styles.css dist/styles.css","verify":"npm run typecheck && npm test && npm run build","typecheck":"tsc --noEmit","prepublishOnly":"npm run verify"},"_npmUser":{"name":"troygoode","email":"troygoode@gmail.com"},"repository":{"url":"git+https://github.com/BalladAI/next.git","type":"git"},"_npmVersion":"12.0.2","description":"A Ballad-powered blog for Next.js: typed client for the content API, block renderer, SEO metadata, RSS, sitemap, and refresh-on-publish — with minimal code and full override points.","directories":{},"sideEffects":["*.css"],"_nodeVersion":"24.15.0","dependencies":{"remark-gfm":"^4.0.1","react-markdown":"^10.1.0"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.0.0","tsup":"^8.3.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.7.0","@types/node":"^22.20.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"next":">=15","react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/next_0.1.1_1789054010949_0.7451638569562724","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@balladlabs/next","version":"0.1.2","keywords":["ballad","nextjs","blog","headless","content","app-router"],"license":"MIT","_id":"@balladlabs/next@0.1.2","maintainers":[{"name":"troygoode","email":"troygoode@gmail.com"}],"homepage":"https://www.balladlabs.com/docs/nextjs-blog","bugs":{"url":"https://github.com/BalladAI/next/issues"},"dist":{"shasum":"b07987e311262bc26f103cb98b613e3ea0d21ea4","tarball":"https://registry.npmjs.org/@balladlabs/next/-/next-0.1.2.tgz","fileCount":22,"integrity":"sha512-/aPp6G0DEwvK6+UnjxEiJly5dYJXlmhvZlT5HGCK0ldP3mgpvWK1j7qOURCHCFrJaEYbuz5mEeD9dAFzpjaRMA==","signatures":[{"sig":"MEYCIQDvBQ7cznYtH+W3C1QZ0DYuZeIF7cj53CvqYKI/Exq6WQIhAIRdOUb7LvKhtvMB2NZ1nJG7Ui52Y7jFdeIb6e35DGI5","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":123310},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./pages":{"types":"./dist/pages.d.ts","import":"./dist/pages.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js"},"./styles.css":"./dist/styles.css"},"gitHead":"1c6bd0b4003d6855f7e971abaa55895e5d05cd98","scripts":{"test":"vitest run","build":"tsup && cp src/styles.css dist/styles.css","verify":"npm run typecheck && npm test && npm run build","typecheck":"tsc --noEmit","prepublishOnly":"npm run verify"},"_npmUser":{"name":"troygoode","email":"troygoode@gmail.com"},"repository":{"url":"git+https://github.com/BalladAI/next.git","type":"git"},"_npmVersion":"12.0.2","description":"A Ballad-powered blog for Next.js: typed client for the content API, block renderer, SEO metadata, RSS, sitemap, and refresh-on-publish — with minimal code and full override points.","directories":{},"sideEffects":["*.css"],"_nodeVersion":"24.15.0","dependencies":{"remark-gfm":"^4.0.1","react-markdown":"^10.1.0"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.0.0","tsup":"^8.3.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.7.0","@types/node":"^22.20.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"next":">=15","react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/next_0.1.2_1789054902318_0.4536567864812717","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@balladlabs/next","version":"0.1.3","keywords":["ballad","nextjs","blog","headless","content","app-router"],"license":"MIT","_id":"@balladlabs/next@0.1.3","maintainers":[{"name":"troygoode","email":"troygoode@gmail.com"}],"homepage":"https://www.balladlabs.com/docs/nextjs-blog","bugs":{"url":"https://github.com/BalladAI/next/issues"},"dist":{"shasum":"306f61855c82b48a5e0b71cd3f902c4c6b7898f9","tarball":"https://registry.npmjs.org/@balladlabs/next/-/next-0.1.3.tgz","fileCount":22,"integrity":"sha512-UknJTcUYK5lbcfCO7Dkfo7MpFBH8xbl6J+BAQbjbike3MYuOlmU1VVFjlE0pL7OE0dhSorB24e/WB1hkJLXmVA==","signatures":[{"sig":"MEQCIGAolzyrNIAkYJrpR//+L71MhSdWOI/jF2QbgEM8xnX4AiB8II7zYmQgjjZqZ33GDM1sxg22U+DN/Vwpn9mgo3KUJw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":125090},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./pages":{"types":"./dist/pages.d.ts","import":"./dist/pages.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js"},"./styles.css":"./dist/styles.css"},"gitHead":"dea2ce6fe776354e4cd0b7e0e0656183123e4c94","scripts":{"test":"vitest run","build":"tsup && cp src/styles.css dist/styles.css","verify":"npm run typecheck && npm test && npm run build","typecheck":"tsc --noEmit","prepublishOnly":"npm run verify"},"_npmUser":{"name":"troygoode","email":"troygoode@gmail.com"},"repository":{"url":"git+https://github.com/BalladAI/next.git","type":"git"},"_npmVersion":"12.0.2","description":"A Ballad-powered blog for Next.js: typed client for the content API, block renderer, SEO metadata, RSS, sitemap, and refresh-on-publish — with minimal code and full override points.","directories":{},"sideEffects":["*.css"],"_nodeVersion":"24.15.0","dependencies":{"remark-gfm":"^4.0.1","react-markdown":"^10.1.0"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.0.0","tsup":"^8.3.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.7.0","@types/node":"^22.20.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"next":">=15","react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/next_0.1.3_1789128149865_0.04870365402549659","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@balladlabs/next","version":"0.2.0","keywords":["ballad","nextjs","blog","headless","content","app-router"],"license":"MIT","_id":"@balladlabs/next@0.2.0","maintainers":[{"name":"troygoode","email":"troygoode@gmail.com"}],"homepage":"https://www.balladlabs.com/docs/nextjs-blog","bugs":{"url":"https://github.com/BalladAI/next/issues"},"dist":{"shasum":"14e572eed39536b8ddb2e8f281a2ab17bd9e2b32","tarball":"https://registry.npmjs.org/@balladlabs/next/-/next-0.2.0.tgz","fileCount":22,"integrity":"sha512-qkqsxA/OTmA9IExmoniw78a1hh35/j20f3UdPjRqF87e8mkuCUvWeqsijg4f1NJXfj2biLoPOoTAF2t/oFGpkg==","signatures":[{"sig":"MEYCIQD7vqRadd6fLJUQCm2VhH8gZWR026gee5jU7E5K666oWwIhANIOiq4gecHQsRDM1TpR11+NtnarP3HQwYNJ7pf1m88V","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIE76ZdfVguwLkld1GlQDzfK4XAzW2+KRyI4qSJbvnr5CAiEA6yRmlNOykTBdGy1b/TjCytVNKp+0rRJDKaHOMjfnxRc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":139950},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./pages":{"types":"./dist/pages.d.ts","import":"./dist/pages.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js"},"./styles.css":"./dist/styles.css"},"gitHead":"002ef7365752f1abb6822428ae74266442668e2b","scripts":{"test":"vitest run","build":"tsup && cp src/styles.css dist/styles.css","verify":"npm run typecheck && npm test && npm run build","typecheck":"tsc --noEmit","prepublishOnly":"npm run verify"},"_npmUser":{"name":"troygoode","email":"troygoode@gmail.com"},"repository":{"url":"git+https://github.com/BalladAI/next.git","type":"git"},"_npmVersion":"12.0.2","description":"A Ballad-powered blog for Next.js: typed client for the content API, block renderer, SEO metadata, RSS, sitemap, and refresh-on-publish — with minimal code and full override points.","directories":{},"sideEffects":["*.css"],"_nodeVersion":"24.15.0","dependencies":{"remark-gfm":"^4.0.1","react-markdown":"^10.1.0"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.0.0","tsup":"^8.3.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.7.0","@types/node":"^22.20.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"next":">=15","react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/next_0.2.0_1789670715383_0.13050724220832555","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"_id":"@balladlabs/next@0.3.0","bugs":{"url":"https://github.com/BalladAI/next/issues"},"dist":{"shasum":"4a34b49e333e2286aff9f725c9d8ae18eced8efb","tarball":"https://registry.npmjs.org/@balladlabs/next/-/next-0.3.0.tgz","fileCount":22,"integrity":"sha512-/zrZM6X2JM0dmpyhSednDMvisnBdNg8e2weWTBuVwlZ2S5a+1WQcUAXlEaafjWq0UIH2pJTg7gPwvnsyX/6NFA==","signatures":[{"sig":"MEUCIQD9dZ2QGMG2ty12fllDD+vgZRFNYu1aguIBEslw3m/85QIgJiX0NOLEzs5ZHzB4CK75c4M9uOeB/1LT6zZvCFee1w0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCmXoqYVbOpsCQOjWMKXbFcLy2BT1ii4gxGQPNXgi2mXgIhALbMG4hHugAny3D4XZUQEIuiHknb7Fx5OvmtLm0P1NnC"}],"unpackedSize":176615},"main":"./dist/index.js","name":"@balladlabs/next","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./pages":{"types":"./dist/pages.d.ts","import":"./dist/pages.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js"},"./styles.css":"./dist/styles.css"},"gitHead":"713ea5a94feaaa93d73646520ec8627acdf32045","license":"MIT","scripts":{"test":"vitest run","build":"tsup && cp src/styles.css dist/styles.css","verify":"npm run typecheck && npm test && npm run build","typecheck":"tsc --noEmit","prepublishOnly":"npm run verify"},"version":"0.3.0","_npmUser":{"name":"troygoode","email":"troygoode@gmail.com"},"homepage":"https://www.balladlabs.com/docs/nextjs-blog","keywords":["ballad","nextjs","blog","headless","content","app-router"],"repository":{"url":"git+https://github.com/BalladAI/next.git","type":"git"},"_npmVersion":"12.0.2","description":"A Ballad-powered blog for Next.js: typed client for the content API, block renderer, SEO metadata, RSS, sitemap, and refresh-on-publish — with minimal code and full override points.","directories":{},"maintainers":[{"name":"troygoode","email":"troygoode@gmail.com"}],"sideEffects":["*.css"],"_nodeVersion":"24.15.0","dependencies":{"remark-gfm":"^4.0.1","react-markdown":"^10.1.0"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.0.0","tsup":"^8.3.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.7.0","@types/node":"^22.20.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"next":">=15","react":">=18"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/next_0.3.0_1789746303299_0.48947422418174136"}}},"time":{"created":"2026-09-10T15:22:13.579Z","modified":"2026-09-18T15:45:03.588Z","0.1.0":"2026-09-10T15:22:14.065Z","0.1.1":"2026-09-10T15:26:51.081Z","0.1.2":"2026-09-10T15:41:42.576Z","0.1.3":"2026-09-11T12:02:29.997Z","0.2.0":"2026-09-17T18:45:15.470Z","0.3.0":"2026-09-18T15:45:03.396Z"},"bugs":{"url":"https://github.com/BalladAI/next/issues"},"license":"MIT","homepage":"https://www.balladlabs.com/docs/nextjs-blog","keywords":["ballad","nextjs","blog","headless","content","app-router"],"repository":{"url":"git+https://github.com/BalladAI/next.git","type":"git"},"description":"A Ballad-powered blog for Next.js: typed client for the content API, block renderer, SEO metadata, RSS, sitemap, and refresh-on-publish — with minimal code and full override points.","maintainers":[{"name":"troygoode","email":"troygoode@gmail.com"}],"readme":"# @balladlabs/next\n\nA Ballad-powered blog for Next.js, on your domain. Ballad writes and publishes the articles; this package fetches them, renders them, handles SEO, RSS, the sitemap, and refresh-on-publish — with a few lines of code, and a way to override every piece.\n\nWritten in TypeScript, shipped as JavaScript with types. App Router only.\n\n## Install\n\n```bash\nnpm install @balladlabs/next\n```\n\nTwo values from Ballad, under **Settings → Site**, in your environment:\n\n```bash\nBALLAD_API_KEY=blc_your_key_here          # Content API key\nBALLAD_REVALIDATE_SECRET=your-shared-secret   # for refresh on publish\nNEXT_PUBLIC_SITE_URL=https://your-site.com    # your origin, for permalinks\n```\n\n## The short version\n\nOne client, one call, five tiny files.\n\n```ts\n// lib/ballad.ts\nimport { createBallad } from \"@balladlabs/next\";\nimport { createBlogPages } from \"@balladlabs/next/pages\";\n\nexport const ballad = createBallad();            // reads BALLAD_* from the environment\nexport const blog = createBlogPages(ballad, {    // /blog, /blog/[slug], rss, sitemap, revalidate\n  basePath: \"/blog\",\n  publisher: { \"@type\": \"Organization\", name: \"Acme\", url: \"https://acme.com\" },\n});\n```\n\n```ts\n// app/blog/page.tsx\nimport { blog } from \"@/lib/ballad\";\nexport const generateMetadata = blog.index.generateMetadata;\nexport default blog.index.Page;\n```\n\n```ts\n// app/blog/[slug]/page.tsx\nimport { blog } from \"@/lib/ballad\";\nexport const generateStaticParams = blog.post.generateStaticParams;\nexport const generateMetadata = blog.post.generateMetadata;\nexport default blog.post.Page;\n```\n\n```ts\n// app/blog/rss.xml/route.ts\nimport { blog } from \"@/lib/ballad\";\nexport const GET = blog.rss.GET;\n```\n\n```ts\n// app/api/revalidate/route.ts\nimport { blog } from \"@/lib/ballad\";\nexport const POST = blog.revalidate.POST;\n```\n\n```ts\n// app/sitemap.ts\nimport { blog } from \"@/lib/ballad\";\nexport default async function sitemap() {\n  return [{ url: \"https://acme.com\" }, ...(await blog.sitemap())];\n}\n```\n\nOptionally, the default styles:\n\n```ts\nimport \"@balladlabs/next/styles.css\";\n```\n\nThen in Ballad, under Settings → Site, set your site URL and the same revalidate secret. New posts appear within seconds of approval, with no redeploy.\n\n## Make it yours\n\nEvery layer can be replaced without giving up the ones above it.\n\n**Swap a block renderer.** Keep the pages; change how one kind of block draws.\n\n```tsx\ncreateBlogPages(ballad, {\n  components: {\n    PullQuote: ({ block }) => <aside className=\"callout\">{block.payload.text}</aside>,\n    markdown: { a: MyLink, h2: MyHeading },   // elements inside prose\n  },\n});\n```\n\n**Draw the pages yourself.** Keep the data loading, metadata, static params, feed and revalidation; render from the loaded data.\n\n```tsx\nimport { Article, PostList } from \"@balladlabs/next/react\";\n\ncreateBlogPages(ballad, {\n  render: {\n    index: ({ collection, posts, hrefFor }) => (\n      <main>\n        <h1>{collection.name}</h1>\n        <PostList posts={posts} hrefFor={hrefFor} renderItem={(p, href) => <MyCard post={p} href={href} />} />\n      </main>\n    ),\n    post: ({ post, summary }) => (\n      <main>\n        <Article post={post} author={summary?.author} header={<MyHeader post={post} />}>\n          <Newsletter />\n        </Article>\n      </main>\n    ),\n  },\n  layout: (content) => <Shell>{content}</Shell>,\n});\n```\n\n**Or skip the page factories.** The client and the components stand on their own.\n\n```tsx\nimport { createBallad, postMetadata } from \"@balladlabs/next\";\nimport { Article, JsonLd } from \"@balladlabs/next/react\";\n\nconst ballad = createBallad();\nconst { items } = (await ballad.posts()) ?? { items: [] };   // every published post, newest first\nconst post = await ballad.post(slug);                         // blocks + SEO, or null\nexport const generateMetadata = async ({ params }) => postMetadata(await ballad.post((await params).slug), ballad);\n```\n\n## Working with blocks\n\n`ContentBlock` includes an unknown member (a block type Ballad adds later still parses), so `block.type === \"prose\"` alone can't narrow the payload. Use the guards:\n\n```ts\nimport { isProse, isHeroImage, knownBlocks, blockOfType } from \"@balladlabs/next\";\n\nfor (const block of post.blocks) {\n  if (isProse(block)) console.log(block.payload.markdown);\n}\nconst hero = post.blocks.map((b) => blockOfType(b, \"hero_image\")).find(Boolean);\n```\n\n### Block types\n\n| Type | Payload | Renders as |\n|---|---|---|\n| `prose` | `{ markdown }` | Markdown, raw HTML escaped |\n| `hero_image` | `{ url, alt, width, height, artUrl? }` | `<img>` |\n| `pull_quote` | `{ text }` | `<blockquote aria-hidden>`: a sentence of the article, so hidden from screen readers |\n| `stat_callout` | `{ value, label, sourceTitle?, sourceUrl? }` | `<figure>` with the figure, what it measures, and a `<cite>` to the source |\n| `key_takeaways` | `{ items: string[] }` | `<aside>` with a short list |\n| `faq` | `{ items: { q, a }[] }` | `<section>` of `<h3>`/`<p>` pairs, plus `FAQPage` JSON-LD |\n| `cta` | `{ text, url? }` | A closing line, linked when it has a `url` |\n| `code_embed` | `{ code, lang?, caption? }` | `<figure><pre><code>` |\n\nSince 0.2.0 Ballad may add a pull quote, a stat callout, key takeaways and an FAQ to an article after its prose is written. Each has a default renderer and a slot in `components` (`PullQuote`, `StatCallout`, `KeyTakeaways`, `Faq`), and `createBlogPages` emits `faqJsonLd(post)` beside the article's JSON-LD when the post has an FAQ. On 0.1.x those blocks arrive as unknown and render nothing.\n\nPrefer `knownBlocks(post.blocks)` over a cast when you render blocks yourself: a cast only silences the type error, while `knownBlocks` drops a block type Ballad adds later before it reaches your switch, so a new block type can never break your post page.\n\nTwo things the marketing site's own migration hit:\n\n- If your sitemap already lists the blog index, call `blog.sitemap({ index: false })` or it appears twice.\n- The index page's description falls back to the collection's tagline, then its theme. Set `index: { description }` in `createBlogPages` to keep a written one.\n\n## Landing pages\n\nBallad also writes landing pages: a page for one segment (`/for/technical-founders`),\none use case (`/use/launch-week`), one comparison (`/vs/acme`), one integration\n(`/integrations/slack`). They are collections of kind `pages`, one per type, with\nno index, no feed and no dates. Rather than a route per collection, they render\nthrough **one root catch-all** you add once:\n\n```ts\n// lib/ballad.ts\nimport { createSitePages } from \"@balladlabs/next/pages\";\nexport const pages = createSitePages(ballad, { publisher });\n\n// app/[...slug]/page.tsx\nimport { pages } from \"../../lib/ballad\";\nexport const revalidate = 300;\nexport const generateStaticParams = pages.generateStaticParams;\nexport const generateMetadata = pages.generateMetadata;\nexport default pages.Page;\n\n// app/api/revalidate/route.ts\nimport { revalidateRoutes } from \"@balladlabs/next/pages\";\nexport const { POST, GET } = revalidateRoutes([blog.revalidate, pages.revalidate]);\n\n// app/sitemap.ts\nreturn [...yourRoutes, ...(await blog.sitemap()), ...(await pages.sitemap())];\n```\n\nNext resolves your own routes first; the catch-all only sees paths nothing\nelse claims, and answers 404 unless the path is a live page in one of Ballad's\npage collections. Page collections created later render with no further\nwiring. A page marked to receive sent traffic gets a `noindex` robots tag and\nstays out of the sitemap. The JSON-LD is a `WebPage` (plus `FAQPage` when a\nsection is questions), with your publisher merged in.\n\n`GET /api/revalidate` answers with what the site serves (`{ pages, collections,\nversion }`). Ballad reads it before proposing a page, so it never proposes one\nyour site cannot render; until this is wired, Ballad's audit shows the install\nstep instead of \"Write it\". Pass `render.page` to draw the page yourself, and\n`components`, `layout`, `jsonLd: false`, `errors: \"empty\"` as for `createBlogPages`.\n\n## What's in the box\n\n| Import | What |\n| --- | --- |\n| `@balladlabs/next` | `createBallad`, `postMetadata`, `collectionMetadata`, `articleJsonLd`, `breadcrumbJsonLd`, `rssFeed`, `sitemapEntries`, `formatDate`, `readingTime`, block type guards, types |\n| `@balladlabs/next/react` | `Article`, `Blocks`, `PostList`, `PostCard`, `JsonLd`, the default block components |\n| `@balladlabs/next/pages` | `createBlogPages`, `createSitePages`, `createRevalidateHandler`, `revalidateRoutes` |\n| `@balladlabs/next/styles.css` | optional default styles, `ballad-*` classes |\n\n## How caching works\n\nEvery fetch is tagged (`ballad`, `ballad:collection:<slug>`, `ballad:post:<slug>`) and cached for five minutes by default. When Ballad publishes, it POSTs `{ secret, collection, slug }` to your revalidate route; the handler checks the secret in constant time and revalidates exactly those tags and the pages for that post, the index and the feed. Set `revalidate: 0` on the client to never cache, or `revalidatePaths: [\"/\"]` in `createBlogPages` if your home page lists posts.\n\n## Options\n\n```ts\ncreateBallad({\n  apiKey,         // BALLAD_API_KEY\n  baseUrl,        // BALLAD_API_URL, default https://app.balladlabs.com\n  collection,     // BALLAD_COLLECTION, default \"blog\"\n  siteUrl,        // NEXT_PUBLIC_SITE_URL / BALLAD_SITE_URL\n  basePath,       // \"/blog\"\n  revalidate,     // seconds, default 300; 0 = no-store\n});\n\ncreateBlogPages(ballad, {\n  basePath, collection, prefix,\n  components,           // block renderers + markdown elements\n  render: { index, post },\n  layout,\n  index: { title, description },\n  feed: { title, limit } | false,\n  publisher, jsonLd,\n  revalidatePaths, revalidateSecret,\n});\n```\n\n```ts\ncreateSitePages(ballad, {\n  components, prefix,\n  render: { page },\n  layout,\n  publisher, jsonLd,\n  revalidateSecret,\n  errors,\n});\n```\n\nUnconfigured (no key), the client is inert and the pages render their empty states, so builds and previews succeed before the key exists.\n\n**When the API fails** (a 5xx, a network error), the pages throw by default: a build fails loudly and your last good deploy stays live, and an ISR regeneration keeps the last good page rather than replacing it with an empty one. Pass `errors: \"empty\"` to `createBlogPages` if you'd rather render as if nothing were published. The RSS route answers 503 either way.\n\n## Requirements\n\nNext.js 15 or later on the App Router, React 18 or later. The package depends on `react-markdown` and `remark-gfm` to render prose; if your site already uses them, npm shares a compatible version. Content arrives as data, never as raw HTML: markdown renders to React nodes and anything that looks like markup in it is escaped.\n\nA post page calls `notFound()` only when Ballad answers 404 for the slug. An API failure during a build fails the build; during a regeneration Next keeps the last good page. A live article never turns into a 404 because of an outage.\n\n## Labelling posts\n\nEvery post and summary carries `shape`: `\"essay\"`, `\"comparison\"`, `\"guide\"`,\n`\"case_study\"` or `\"data\"` — the kind of piece, and the word a card can print\n(most posts are essays, so a label is worth showing only when it isn't one).\nThe older `tier` field (`\"cadence\"` / `\"signature\"`) is deprecated: Ballad\nretired that distinction, keeps sending the field for compatibility, and marks\nevery new post `\"cadence\"`. Don't render it.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}