Skip to content

Astro Storefront

Use this guide when building the browser storefront. Start here after the plugin is registered and one product page can call createMika(Astro, { api }).

Mika’s default storefront path is Astro-native:

  • HTML forms use actions.mika.<namespace>.<action>.queryString as their action value.
  • Product pages use createMika(Astro, { api }).
  • The host app owns routes, content, layout, localization, auth policy, provider credentials, and deployment.
  • Copied components use Kumo-backed UI patterns but remain ordinary host files.
  • Mika is installed or linked as described in Install.
  • mikaPlugin({ entrypoint }) is registered in astro.config.mjs, and that entrypoint calls createMikaPlugin({ api }).
  • The host can provide fixture overrides or a real MikaApi implementation.
  • The app can run dynamic routes with output: "server" or per-route prerender = false.
  1. Copy the action files and product components.
  2. Install UI dependencies if you use the Kumo-backed templates.
  3. Register server.mika = createMikaActions({ api }).
  4. Render product purchase forms from createMika(Astro, { api }) sellables.
  5. Add host guards for rate limits, account policy, and feature gates.
  6. Verify that only catalog and stock reads are public plugin JSON routes.

Copy the action files and product components first:

src/actions/index.ts
src/actions/mika.ts
src/styles/kumo.css
src/lib/routes.ts
src/components/MikaKumoAppFrame.tsx
src/components/MikaKumoPage.astro
src/components/ProductPurchase.astro
src/components/ProductPurchaseSync.astro
src/components/AddToCartForm.astro
src/components/AddToCartFormSync.astro
src/components/BuyNowForm.astro
src/components/WishlistForm.astro
src/components/VariantOptionGroups.astro
src/components/VariantSelector.astro
src/components/StockBadge.astro
src/components/LowStockNotice.astro
src/components/UnavailableNotice.astro
src/components/ProductStructuredData.astro

Add cart, wishlist, checkout, account, downloads, and webhooks only when the host project needs those flows.

src/lib/routes.ts is included because BuyNowForm.astro uses template checkout defaults (/checkout/success and /checkout/cancel). If the host does not copy those checkout return pages yet, either copy them with the full storefront flow or pass host-owned successPath and cancelPath props when rendering BuyNowForm / ProductPurchase.

The copied components use Kumo components, Phosphor icons, and a React island for the app frame. Add these in the host app when copying the template UI:

Terminal window
npm install @cloudflare/kumo @phosphor-icons/react react react-dom @astrojs/react

Add react() to the host’s existing integrations array; keep emdash(...) and any other integrations already present:

import react from "@astrojs/react";
export default defineConfig({
integrations: [
react(),
// Keep the host's existing integrations here.
],
});

Copy src/styles/kumo.css or import it from your app stylesheet. The UI dependencies belong to the template presentation layer; Mika’s package contracts still remain provider- and layout-neutral.

MikaKumoPage.astro imports ../styles/kumo.css relative to src/components/, so the default destination is src/styles/kumo.css. If you move either file, update that import.

Copied forms use actions.mika.*.queryString, but that tree exists only once you register it. Copy src/actions/mika.ts (a thin re-export that keeps the factory versioned with Mika) and expose it as server.mika with the same host API your pages use:

src/actions/mika.ts
export {
createMikaActions,
type MikaActionName,
type MikaActions,
type MikaActionsOptions,
} from "@bnomei/emdash-mika/astro-actions";
src/actions/index.ts
import { createMikaActions } from "./mika";
import { api } from "../lib/mika-api";
export const server = { mika: createMikaActions({ api }) };

Pass the same host api object to createMikaActions({ api }), createMika(Astro, { api }), and the runtime plugin entrypoint so forms, pages, and plugin routes share one implementation.

Mika exposes the same operations two ways, and copied code picks by context:

  • Server reads / resolutionconst Mika = createMika(Astro, { api }) from @bnomei/emdash-mika/astro, in page or endpoint frontmatter (Mika.cart.get(), Mika.catalog.sellables(...)). Every call returns a result you branch on: result.ok ? result.data : result.error.
  • Browser mutations — HTML forms using action={actions.mika.<namespace>.<action>.queryString} with method="post", read back with the action client, for example Astro.getActionResult(actions.mika.cart.add).

There are no public browser JSON mutation routes. Only catalog.sellables and stock.availability are exposed as public plugin JSON.

Mika does not hard-code rate limits, bot checks, or account gates. Add them through the action guard, and keep Astro’s default security.checkOrigin for the CSRF baseline:

src/actions/index.ts
import { ActionError } from "astro:actions";
import { createMikaActions } from "./mika";
import { api } from "../lib/mika-api";
export const server = {
mika: createMikaActions({
api,
guard: async (ctx, action, input) => {
if (await isRateLimited(ctx)) {
throw new ActionError({ code: "TOO_MANY_REQUESTS", message: "Slow down." });
}
},
}),
};

Keep src/actions/mika.ts as the thin re-export from the copyable template file. Put host-specific policy in src/actions/index.ts, where the host creates the server.mika action tree:

src/actions/mika.ts
export {
createMikaActions,
type MikaActionName,
type MikaActions,
type MikaActionsOptions,
} from "@bnomei/emdash-mika/astro-actions";

For production, replace isRateLimited(ctx) with host code, for example:

async function isRateLimited(ctx: unknown): Promise<boolean> {
// Check the host rate-limit store, bot score, account state, or feature flag.
return false;
}

If a guard needs the operation name, use the action argument to branch:

guard: async (ctx, action, input) => {
if (action === "checkoutStart" && await checkoutBlocked(ctx, input)) {
throw new ActionError({ code: "FORBIDDEN", message: "Checkout is not available." });
},
},
  • actions.mika.cart.add.queryString exists and an add-to-cart form posts without an Action lookup error.
  • Astro.getActionResult(actions.mika.cart.add) can render success or failure feedback.
  • /_emdash/api/plugins/mika/catalog/sellables?collection=products&id=<product-id> returns a Mika result envelope for a real host content ref.
  • /_emdash/api/plugins/mika/sellables/availability?sellableId=<sellable-id> returns availability for a real sellable.
  • Cart, checkout, account, webhook, admin, and agent-tool mutations are not exposed as public plugin JSON routes.

Next: Cart, Wishlist, And Checkout covers buyer state and checkout forms. Revisit Product Authoring if sellable, price, stock, or JSON-LD mapping still feels unclear.

  • ../emdash-mika/src/templates/astro/examples/astro-storefront.md
  • ../emdash-mika/src/templates/astro/README.md
  • ../emdash-mika/src/templates/astro/actions/index.ts
  • ../emdash-mika/src/templates/astro/actions/mika.ts
  • ../emdash-mika/src/templates/astro/components/ProductPurchase.astro
  • ../emdash-mika/src/templates/astro/components/AddToCartForm.astro
  • ../emdash-mika/src/templates/astro/components/BuyNowForm.astro
  • ../emdash-mika/src/templates/astro/components/MikaKumoPage.astro
  • ../emdash-mika/src/templates/astro/lib/routes.ts
  • ../emdash-mika/src/astro-actions.ts