{"_id":"@alecjessen/ab-cms","_rev":"5-eb476c33ae95415dc0deba3672d83eba","name":"@alecjessen/ab-cms","dist-tags":{"latest":"0.8.0"},"versions":{"0.3.2":{"name":"@alecjessen/ab-cms","version":"0.3.2","license":"UNLICENSED","_id":"@alecjessen/ab-cms@0.3.2","maintainers":[{"name":"alecjessen","email":"alecjessen@gmail.com"}],"homepage":"https://github.com/alecjessen/ab-cms-sdk#readme","bugs":{"url":"https://github.com/alecjessen/ab-cms-sdk/issues"},"bin":{"ab-cms":"bin/ab-cms.mjs"},"dist":{"shasum":"50838f6cb0dc7de30c2d4b0ce57e1e12d73946be","tarball":"https://registry.npmjs.org/@alecjessen/ab-cms/-/ab-cms-0.3.2.tgz","fileCount":16,"integrity":"sha512-36ENmWzLLYKB4JqqvD576M/tWVJzsEJxwdkAE5hCwhdmAEMOfXeYfyLkqfkuxHo9gWcPwLEtj91N3GAse2wa4Q==","signatures":[{"sig":"MEUCIQCUlig6ktWVm4aFV1fuTZ2jI3/fhtk6zfljUC6v7yCffwIgVdTU03TTPjBxnhAz386U081dFICgMtZBsUtCXayVlnA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":34293},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"1e252b7348e82aee1ab98b7212489499556d0fbe","scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","check":"tsc --noEmit","prepare":"npm run build"},"_npmUser":{"name":"alecjessen","email":"alecjessen@gmail.com"},"repository":{"url":"git+https://github.com/alecjessen/ab-cms-sdk.git","type":"git"},"_npmVersion":"11.17.0","description":"AB Dev Panel site SDK: manifest, typed content reader, sync CLI","directories":{},"_nodeVersion":"22.16.0","dependencies":{"zod":"^3.25.0","jiti":"^2.4.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"^5.8.0","@types/node":"^22"},"_npmOperationalInternal":{"tmp":"tmp/ab-cms_0.3.2_1783492779348_0.8387173286705898","host":"s3://npm-registry-packages-npm-production"}},"0.3.3":{"name":"@alecjessen/ab-cms","version":"0.3.3","license":"MIT","_id":"@alecjessen/ab-cms@0.3.3","maintainers":[{"name":"alecjessen","email":"alecjessen@gmail.com"}],"homepage":"https://github.com/alecjessen/ab-cms-sdk#readme","bugs":{"url":"https://github.com/alecjessen/ab-cms-sdk/issues"},"bin":{"ab-cms":"bin/ab-cms.mjs"},"dist":{"shasum":"4286b9c8268055c4b9ed4aff42bbca0d7a706505","tarball":"https://registry.npmjs.org/@alecjessen/ab-cms/-/ab-cms-0.3.3.tgz","fileCount":18,"integrity":"sha512-bIc1/7MUd+IL2rMEq8WPExaJY9gXavqYvNQ3islAyNyf9rp6GFJOinghHD7Ozd4RLHyZ4rRqaLNHpD/wew29EQ==","signatures":[{"sig":"MEUCIQDrDgwxSQjnHWHiu0/K3TK2gHWTM3hn1uiSnNN2rPOK4wIgYZ8HhYhnIVtInA3hVzUpuvsyRwqUctdPuidjD9RpT/A=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37910},"main":"./dist/index.js","type":"module","_from":"file:alecjessen-ab-cms-0.3.3.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","check":"tsc --noEmit","prepare":"npm run build"},"_npmUser":{"name":"alecjessen","email":"alecjessen@gmail.com"},"_resolved":"/home/alecj/Desktop/work/ab-cms-sdk/alecjessen-ab-cms-0.3.3.tgz","_integrity":"sha512-bIc1/7MUd+IL2rMEq8WPExaJY9gXavqYvNQ3islAyNyf9rp6GFJOinghHD7Ozd4RLHyZ4rRqaLNHpD/wew29EQ==","repository":{"url":"git+https://github.com/alecjessen/ab-cms-sdk.git","type":"git"},"_npmVersion":"11.17.0","description":"AB Dev Panel site SDK: manifest, typed content reader, sync CLI","directories":{},"_nodeVersion":"22.16.0","dependencies":{"zod":"^3.25.0","jiti":"^2.4.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"^5.8.0","@types/node":"^22"},"_npmOperationalInternal":{"tmp":"tmp/ab-cms_0.3.3_1783493436003_0.05743211694313244","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@alecjessen/ab-cms","version":"0.5.0","license":"MIT","_id":"@alecjessen/ab-cms@0.5.0","maintainers":[{"name":"alecjessen","email":"alecjessen@gmail.com"}],"homepage":"https://github.com/alecjessen/ab-cms-sdk#readme","bugs":{"url":"https://github.com/alecjessen/ab-cms-sdk/issues"},"bin":{"ab-cms":"bin/ab-cms.mjs"},"dist":{"shasum":"adf67c7a097314963e18fa74301816d1e076b7c3","tarball":"https://registry.npmjs.org/@alecjessen/ab-cms/-/ab-cms-0.5.0.tgz","fileCount":20,"integrity":"sha512-+S60AO/puuRkmWnCVlTTnLH7Xu5V5jAgPjxi9C7fgWuCEgEg7TvpjI7OFuO2M6PFcZxJDJk4VPOeZmWoT3dVIQ==","signatures":[{"sig":"MEYCIQCG2LuIlDGe9mGlcjM6XU0BXynzN/Noxp8cDhTE9ERljgIhAMcmG5h0bd13NKhehLMNn4NSUKVMlta+uz8bQQ3mGsg1","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":52646},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"d70f7ae5318755a69ee4f93c986c487d9cd51729","scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","check":"tsc --noEmit","prepare":"npm run build"},"_npmUser":{"name":"alecjessen","email":"alecjessen@gmail.com"},"repository":{"url":"git+https://github.com/alecjessen/ab-cms-sdk.git","type":"git"},"_npmVersion":"11.17.0","description":"AB Dev Panel site SDK: manifest, typed content reader, sync CLI","directories":{},"_nodeVersion":"22.16.0","dependencies":{"zod":"^3.25.0","jiti":"^2.4.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"^5.8.0","@types/node":"^22"},"_npmOperationalInternal":{"tmp":"tmp/ab-cms_0.5.0_1788299967203_0.5671457840989802","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@alecjessen/ab-cms","version":"0.6.0","license":"MIT","_id":"@alecjessen/ab-cms@0.6.0","maintainers":[{"name":"alecjessen","email":"alecjessen@gmail.com"}],"homepage":"https://github.com/alecjessen/ab-cms-sdk#readme","bugs":{"url":"https://github.com/alecjessen/ab-cms-sdk/issues"},"bin":{"ab-cms":"bin/ab-cms.mjs"},"dist":{"shasum":"4a597e4ca7fba0347f3ed410fb5a663dca923d56","tarball":"https://registry.npmjs.org/@alecjessen/ab-cms/-/ab-cms-0.6.0.tgz","fileCount":22,"integrity":"sha512-3qaDnuBHup7dmKPJ/rFqm46XsxZEn55rcdfVHDHU7TD56Ij667orhvT9k9wGg0sjXnkT9miPASjGhADZNAjXIA==","signatures":[{"sig":"MEUCIFdbBh6JHawzuKeNF4c0WAzIT1x6E9ID0XnqAlpiiUj5AiEAlgQaawf0OHdcxydTZMGDhxBAC9paFoe9ly8IjTSDS18=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDl1eigkr7OtCmljalHxKMaNM8ySsTndxKPgDIPMiUlzwIgGAUs4hwc7Tn2HUInGVYqVyb3pqPTyA1gBjM21gPAQd4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":57940},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b6de42108593ef00f702b313bddb61b1116624c2","scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","check":"tsc --noEmit","prepare":"npm run build"},"_npmUser":{"name":"alecjessen","email":"alecjessen@gmail.com"},"repository":{"url":"git+https://github.com/alecjessen/ab-cms-sdk.git","type":"git"},"_npmVersion":"11.17.0","description":"AB Dev Panel site SDK: manifest, typed content reader, sync CLI","directories":{},"_nodeVersion":"22.16.0","dependencies":{"zod":"^3.25.0","jiti":"^2.4.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"^5.8.0","@types/node":"^22"},"_npmOperationalInternal":{"tmp":"tmp/ab-cms_0.6.0_1790038992333_0.09125348706687575","host":"s3://npm-registry-packages-npm-production"}},"0.8.0":{"_id":"@alecjessen/ab-cms@0.8.0","bin":{"ab-cms":"bin/ab-cms.mjs"},"bugs":{"url":"https://github.com/alecjessen/ab-cms-sdk/issues"},"dist":{"shasum":"d5e68193e44de12c1d4ef99164508d0b6c93cbfe","tarball":"https://registry.npmjs.org/@alecjessen/ab-cms/-/ab-cms-0.8.0.tgz","fileCount":26,"integrity":"sha512-2UbrT2Th004+4pw51rQDRH/wftKmVY4qNKataT7jmz48TvXfi39S4Rxp79fiepFIiiBCuYa3CXE49FMOhI8VXw==","signatures":[{"sig":"MEYCIQChSL8ouOqscjAHMJFccj3v+QxUYIbcw0GgRgwDUjO/awIhANIe8cXvW/GgBfw76psUhmDsKZb4xe2Xz0xwWlu2AYtF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFUzbBHlyoCGISIcbWeZupoaxPO6bWP1hpZvABSOdiPHAiEAj6e+3HtWZg0F8vuyop94ddA8XAuWOKj7SnuCEogpst8="}],"unpackedSize":73645},"main":"./dist/index.js","name":"@alecjessen/ab-cms","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"d9af280509ddc76216766da3215bc88e9e065313","license":"MIT","scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","check":"tsc --noEmit","prepare":"npm run build"},"version":"0.8.0","_npmUser":{"name":"alecjessen","email":"alecjessen@gmail.com"},"homepage":"https://github.com/alecjessen/ab-cms-sdk#readme","repository":{"url":"git+https://github.com/alecjessen/ab-cms-sdk.git","type":"git"},"_npmVersion":"11.17.0","description":"AB Dev Panel site SDK: manifest, typed content reader, sync CLI","directories":{},"maintainers":[{"name":"alecjessen","email":"alecjessen@gmail.com"}],"_nodeVersion":"22.16.0","dependencies":{"zod":"^3.25.0","jiti":"^2.4.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"^5.8.0","@types/node":"^22"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ab-cms_0.8.0_1790719219683_0.5664033062680109"}}},"time":{"created":"2026-07-08T06:39:39.195Z","modified":"2026-09-29T22:00:19.963Z","0.3.2":"2026-07-08T06:39:39.479Z","0.3.3":"2026-07-08T06:50:36.145Z","0.5.0":"2026-09-01T21:59:27.359Z","0.6.0":"2026-09-22T01:03:12.420Z","0.8.0":"2026-09-29T22:00:19.765Z"},"bugs":{"url":"https://github.com/alecjessen/ab-cms-sdk/issues"},"license":"MIT","homepage":"https://github.com/alecjessen/ab-cms-sdk#readme","repository":{"url":"git+https://github.com/alecjessen/ab-cms-sdk.git","type":"git"},"description":"AB Dev Panel site SDK: manifest, typed content reader, sync CLI","maintainers":[{"name":"alecjessen","email":"alecjessen@gmail.com"}],"readme":"# @alecjessen/ab-cms\n\nSite SDK for the [AB Dev Panel](https://www.abdevpanel.com) CMS: declare your\nsite's structure in a typed manifest, read CMS-managed content in your pages,\nand sync structural changes with one command.\n\n```bash\nnpm install @alecjessen/ab-cms\n```\n\n## How it fits together\n\n- **The manifest owns structure.** `cms.manifest.ts` in your repo declares\n  pages, sections, and fields.\n- **The CMS owns content.** Editors change copy and images in the AB Dev\n  Panel; your site fetches that content at render time.\n- **Seeds run once.** The `seed` blocks in the manifest fill a section the\n  first time it syncs — after that, the CMS content is the source of truth\n  and drifts from the manifest by design.\n\n## Declare your site\n\n```ts\n// cms.manifest.ts\nimport { defineSite } from \"@alecjessen/ab-cms\";\n\nexport default defineSite({\n  alias: \"my_site\",\n  pages: [\n    {\n      slug: \"/\",\n      title: \"Home\",\n      sections: [\n        {\n          key: \"hero\",\n          title: \"Hero\",\n          kind: \"singleton\",\n          fields: {\n            heading: { label: \"Heading\", type: \"text\" },\n            subheading: { label: \"Subheading\", type: \"text\" },\n          },\n          seed: [{ name: \"Hero\", values: { heading: \"Welcome\" } }],\n        },\n      ],\n    },\n  ],\n});\n```\n\n## Read content in your pages\n\n```tsx\nimport { fetchPublicSiteData, readSiteContent, cmsItemProps } from \"@alecjessen/ab-cms\";\nimport manifest from \"../cms.manifest\";\n\n// fetchPublicSiteData takes the site's CMS id (a UUID from your AB Dev Team\n// contact), NOT the manifest alias. It returns { website }, so unwrap it before\n// passing to readSiteContent — otherwise every section silently reads as seed.\nconst data = await fetchPublicSiteData(process.env.WEBSITE_ID!);\nconst content = readSiteContent(manifest, data.website);\n\ncontent.hero.heading; // typed field access\n\n// Collection sections are arrays; spread cmsItemProps(item) onto each item's\n// wrapper for live-preview click-to-edit.\ncontent.showcase.map((item) => (\n  <div key={item._cms.id} {...cmsItemProps(item)}>\n    {item.name}\n  </div>\n));\n```\n\n`fetchPublicSiteData` targets the AB Dev Panel by default; override with the\n`AB_CMS_URL` environment variable or `{ baseUrl }`.\n\n## Sync structural changes\n\n```bash\nnpx ab-cms sync --dry-run   # show what would change\nnpx ab-cms sync             # apply schema changes; seed only brand-new sections\n```\n\nRequires `AB_CMS_API_KEY` in the environment (ask your AB Dev Team contact).\nThe dry-run reports existing content as `skipped: seed` and CMS-only content\nas orphans — both are normal, not errors.\n\n## Commerce (Lite Commerce stores)\n\nFor sites whose store is enabled in the panel. **Server-only** — the API key\nis a secret; `createCommerce` throws if constructed in the browser.\n\n```ts\nimport { createCommerce } from \"@alecjessen/ab-cms\";\n\nconst commerce = createCommerce({ apiKey: process.env.COMMERCE_API_KEY! });\n\nconst { store, products } = await commerce.listProducts();\nconst { product } = await commerce.getProduct(\"demo-tee\");\nconst result = await commerce.createCheckout({\n  items: [{ variantId: product!.variants[0].id, quantity: 1 }],\n  successUrl: \"https://your-site.com/thanks\",\n  cancelUrl: \"https://your-site.com/cart\",\n});\nif (\"url\" in result) redirect(result.url); // Stripe Checkout\nelse showIssues(result.issues);           // sold_out | invalid_items | commerce_paused\n```\n\n`store.checkoutAvailable` says whether buying works right now — the catalog\nkeeps serving while a store is paused, so sites can choose to hide the shop\nor show it with purchasing disabled. `store.shippingCents` and\n`store.allowedCountries` mirror what checkout will charge and accept. Ask\nyour AB Dev Team contact for the `COMMERCE_API_KEY`.\n\nCart pages validate against the live catalog (no stock is held):\n\n```ts\nconst { lines, issues, subtotalCents } = await commerce.previewCart(cartItems);\n```\n\nSuccess pages look the order up by Stripe's `{CHECKOUT_SESSION_ID}`\ntemplate value (add it to your `successUrl` as a query param):\n\n```ts\nconst { order } = await commerce.getOrderStatus(searchParams.session_id);\n// order: { status, fulfilled, trackingCarrier, items, totalCents, … } | null\n```\n\n## Inquiries (contact forms)\n\nFor sites with a contact/booking form. **Server-only** — the API key is a\nsecret; `createInquiries` throws if constructed in the browser.\n\n```ts\nimport { createInquiries } from \"@alecjessen/ab-cms\";\n\nconst inquiries = createInquiries({ apiKey: process.env.INQUIRY_API_KEY! });\n\nconst result = await inquiries.submit({\n  form: \"contact\",                      // lower-kebab, ≤ 40 chars; default \"contact\"\n  name, email, phone, message,          // name + message required; email or phone required for \"contact\"\n  fields: { company, subject },         // optional extras, shown as label/value\n  rating: 5,                            // review forms only: a whole number 1–5\n  page: \"/contact\",                     // optional, where it was sent from\n  honeypot: String(formData.get(\"website\") ?? \"\"),   // optional; non-empty = bot\n  meta: { ip, userAgent },              // optional; from the request headers\n});\n// { ok: true, id }\n// { ok: false, error: \"invalid\", issues: Record<string, string> }\n// { ok: false, error: \"disabled\" | \"rate_limited\" | \"unavailable\" }\n```\n\nThe site owns its success and error states — `submit` never throws for an\nexpected outcome. On `invalid`, show `result.issues` next to the fields; on\n`rate_limited`, say \"try again in a few minutes\"; on `disabled` (the client\nturned inquiries off in the panel) or `unavailable` (panel unreachable, a\nbad key, or any other unexpected response — already logged via\n`console.error`), fall back to a mailto/phone link rather than a dead form.\nAsk your AB Dev Team contact for the `INQUIRY_API_KEY` (`abik_…`).\n\n## Bookings (appointments)\n\nFor sites whose business takes appointments. **Server-only** — the API key\nis a secret; `createBookings` throws if constructed in the browser. Every\nmethod returns a typed result and never throws for an expected outcome.\n\n```ts\nimport { createBookings } from \"@alecjessen/ab-cms\";\n\nconst bookings = createBookings({ apiKey: process.env.BOOKING_API_KEY! });\n\nconst services = await bookings.listServices();\nif (!services.ok) return fallback();                                  // \"disabled\" | \"unavailable\" → show the phone number\nconst { timezone, requirePhone } = services;                          // services.services: BookingService[]; requirePhone → mark the phone field required\n\nconst days = await bookings.getAvailability({ serviceId, from: \"2026-06-11\", to: \"2026-06-17\" });\nif (!days.ok) return days.error === \"invalid\" ? badRequest(days.issues) : fallback();\n// days.days: [{ date: \"2026-06-11\", slots: [\"2026-06-11T16:00:00.000Z\", …] }]\n\nconst result = await bookings.book({\n  serviceId, start: slot,                                              // one of the returned slots, verbatim\n  customer: { name, email, phone },                                    // email required; phone if the site requires it\n  note, fields: { \"Intake · Area\": \"left arm\" }, page: \"/book\",\n  honeypot: String(formData.get(\"website\") ?? \"\"), meta: { ip, userAgent },\n});\nif (!result.ok) { /* \"invalid\" (result.issues) | \"slot_taken\" | \"too_many_active\" | \"rate_limited\" | \"unknown_service\" | \"disabled\" | \"unavailable\" */ }\nelse { /* result.booking: BookingView — its manageUrl is what the customer's emails link to */ }\nawait bookings.requestTime({ serviceId, text: \"any weekday after 3\", customer });   // when no slot fits\n```\n\nRender slots in the business's zone with no library:\n`new Intl.DateTimeFormat(\"en-US\", { timeZone: timezone, weekday: \"short\", month: \"short\", day: \"numeric\", hour: \"numeric\", minute: \"2-digit\" }).format(new Date(slot))`.\n\nOn `slot_taken`, re-fetch the day and ask for another time; on `disabled` or\n`unavailable`, show the phone number rather than a dead picker. Customers\nmanage a booking from the `manageUrl` in their emails (the panel hosts that\npage); a site that wants its own can set a manage URL in the panel and call\n`getBooking(token)`, `cancel`, `reschedule`, and `respond` itself. Such a page\nshould pass back the `version` it rendered (`cancel(token, { version: booking.version })`)\nso a stale page can't act on a booking that changed since it loaded.\n\n## Reviews\n\nFor sites with a review form. Submit reviews through `createInquiries` with\n`form: \"review\"` and a `rating` (1–5). When the client turns on review\nmoderation in the panel, each review waits in their inbox until they approve\nit; approved reviews then arrive with your content as `website.reviews`\n(newest first, at most 50) and refresh on the same revalidate ping as any\ncontent edit.\n\n```ts\nimport { readReviews, type CMSReview } from \"@alecjessen/ab-cms\";\n\n// lib/cms.ts — through the same cached fetch as the content\nexport async function getReviews(): Promise<CMSReview[]> {\n  const websiteId = process.env.WEBSITE_ID;\n  if (!websiteId) return [];\n  try {\n    return readReviews((await getPublicSiteData(websiteId)).website);\n  } catch (err) {\n    console.error(\"[cms] reviews fetch failed:\", err);\n    return [];\n  }\n}\n```\n\nEach review is `{ id, name, rating, text, createdAt }`: `name` is the display\nname the business chose (never the reviewer's email or full name), `rating`\nmay be `null`, `createdAt` is ISO 8601 UTC. `readReviews` never throws: seed\nmode, reviews switched off, or an older panel all give `[]` — render nothing\n(or a code-owned fallback) for an empty list, and never invent reviews.\nDon't emit `aggregateRating` structured data from these: self-served reviews\naren't eligible for review stars in search.\n\n## Live preview\n\n`initCmsPreview()` wires a site to the panel's live preview iframe (section\nscrolling, click-to-edit). Call it once client-side; it no-ops outside the\npreview.\n\n## License\n\nMIT © Alexander Jessen\n","readmeFilename":"README.md"}