@peristyle/grocery-cart-widget
v0.6.1
Published
Chat, recipe search, and one-click ingredient ordering (Kroger/Walmart via Peristyle) for EmDash-powered food blogs
Downloads
866
Maintainers
Readme
@peristyle/grocery-cart-widget
An EmDash CMS plugin that puts a grocery concierge on every page of a food blog: readers chat, search the blog's recipes, and order the ingredients at Kroger or Walmart — powered by the Peristyle Grocery Cart API.
Iframe architecture (0.5.0): the plugin injects a ~1.4KB embed script that mounts a cross-origin <iframe> pointed at the Peristyle-served /widget page. Everything sensitive — the reader's pk_ key, chat (including pantry-aware AI replies for signed-in readers), match data — lives inside the iframe on the Peristyle origin, where host-page scripts, analytics, and ads cannot reach it (the same isolation model claude.ai uses for artifacts and MCP-app widgets). The widget page is CSP-hardened: inline scripts hashed, connect-src 'self', and frame-ancestors locked to the creator's verified domains when a widget token is present. The blog's brand colors are read from its CSS custom properties and passed through sanitized URL params, so the widget still matches the site. The embed script is the only code that runs in the page, which makes any future CMS integration (e.g. WordPress) trivial.
After changing widget UI code, run pnpm build && pnpm run sync:api to refresh the bundle the API serves (peristyle-grocery-list/src/peristyle_grocery_cart/assets/widget.js).
What readers get
- A floating Grocery chat launcher on every page
- Chat — Peristyle's concierge, grounded on the blog's recipe index (never invents recipes). With a widget token configured, search and chat are scoped to this blog's recipes and the assistant speaks as this blog's concierge. AI vs. deterministic-search mode is decided server-side by Peristyle — no LLM key in the plugin.
- Recipe cards with a Shop button per result
- Kroger — one-time account connect (OAuth via Peristyle; the reader's
pk_…key lives only in their browser, layered across localStorage → sessionStorage → memory so sign-in survives privacy modes as long as the browser allows), ingredient → product matching, server-side cart add - A compact Sign in / ● Signed in chip in the panel header — always-visible status, tap to sign in anytime mid-session (it joins an already-open sign-in card rather than duplicating it) or, when signed in, to see status and sign out. Lives in the existing header bar, so chat space is untouched, including on mobile
- Walmart — no sign-in; matching plus an add-to-cart checkout link
- A reviewable match list before anything is added, in one of two tiers:
- Simple list (default) — untick what you have, see prices, add
- Full review — the Peristyle Grocery Cart MCP app experience, ported to the blog: product photos, sale/stock badges, quantity steppers (minus at 1 removes a line), alternative-product swaps, a full-recipe reference section with one-tap add-back, a running estimated total, a proper cart confirmation panel (including an "Already added" state when the server dedupes a repeat add), and — for readers who opted into the Peristyle pantry — a one-tap post-checkout purchase confirmation that keeps their pantry current
Install
pnpm add @peristyle/grocery-cart-widget// astro.config.mjs
import emdash from "emdash/astro";
import { groceryCartWidgetPlugin } from "@peristyle/grocery-cart-widget";
export default defineConfig({
integrations: [
emdash({
plugins: [groceryCartWidgetPlugin()], // or sandboxed: [groceryCartWidgetPlugin()]
}),
],
});The widget is inlined into pages via the page:fragments hook. Configure under Plugins → Grocery List in the admin:
| Setting | Default | Notes |
| ----------------------- | -------------------------- | ---------------------------------------------------------------------- |
| Show the grocery widget | on | |
| Peristyle widget token | — | wt_… from your Peristyle creator dashboard (GET /v1/creator/widget-token); scopes search + chat to your recipes and attributes widget traffic to you |
| Default store | Kroger | Readers can switch in the widget |
| Cart review experience | Simple list | "Full review" enables the MCP-app-style match review + pantry loop |
| Peristyle API base URL | https://api.peristyle.io | Point at a staging API if needed |
For matching to work end-to-end, the blog's recipes should be indexed in Peristyle (see peristyle.io partner onboarding).
Capabilities & privacy
- Declares only
hooks.page-fragments:register— the plugin makes no server-side network calls - The reader's browser calls the Peristyle API directly: match routes return the full normalized shape (suggested product + alternatives, regular/promo prices, stock, pantry-staple flags); cart-add items carry optional
description/ingredient_name/pricecontext so pantry confirmations and order-value analytics stay readable upstream - The reader's Kroger key is issued by the Peristyle connect flow directly to their browser and sent per-request as a bearer; the plugin stores no reader credentials and no LLM keys anywhere
Development
pnpm install
pnpm build # builds the widget IIFE first, then the plugin (see tsup configs)
pnpm typecheck
pnpm test # drives the built IIFE in jsdom against a stubbed Peristyle API, both tiersThe widget (src/widget.ts) is compiled to dist/widget.iife.js and inlined as a string into the sandbox entry — no CDN request, always the same version as the installed package.
Server-side counterparts live in the peristyle-grocery-list repo: /v1/concierge/chat (chat orchestration), widget tokens (/v1/creator/widget-token), creator-scoped /v1/recipes/search, and CORS for browser-direct calls.
Releasing
- Bump
"version"inpackage.json(single source of truth; the build injects it into the descriptor). npm publish(runs typecheck + fresh build + tests viaprepublishOnly).- Commit, tag, push; bump consumers (e.g.
food-blog-base).
