Installing the widgets
The widgets in @meisei/widgets render ratings, reviews, wishlists and the loyalty
program on any storefront. If your storefront is a React app, the same widgets are React
components you can import and compose: see React widgets. Every
endpoint they call is in the Widget API reference.
The script tag
Drop one script tag on the page and mark up the elements you want filled in:
data-meisei-base-url is your store's own address with /api on the end. The snippet the
dashboard hands you on
Settings → Widget installation already
carries it, and Settings → General prints it
under Address. No other address serves your store's data.
The value of each product attribute is the product slug. To name the product by its ID on
your store instead, add data-meisei-product-id beside it. With several sites that share an
ID, also put data-meisei-site on the script tag so Meisei knows which one you mean:
An ID keeps working when the product's slug changes. The Shopify app's theme blocks send the Shopify product ID this way, so renaming a handle never disconnects a product's reviews.
Optional script tag attributes:
The review and rating widgets read public data, so the publishable key is all they need. The wishlist widgets need a signed-in shopper for their real work and keep a guest's saves in the browser until then. The loyalty widget renders for everybody and gains the member's own state once signed in.
The script loads only the widgets your page uses, from the same address the script came
from, so a page with star ratings alone downloads none of the review list or the loyalty
panel. It adds the widgets' stylesheet too, so the page needs no <link> of its own, and it
leaves a stylesheet alone if the page already links one. Load the script from the address the
dashboard gives you rather than a copy on your own server, since a copy can't find the files it
loads. If your site sets a Content-Security-Policy, allow that address in script-src and
style-src.
The properties you can set
Everything the widgets draw resolves through these CSS custom properties. The appearance editor sets them, and you can set any of them from your own stylesheet.
Two more are derived rather than chosen, and are set from custom CSS only:
--meisei-control-radius rounds buttons and inputs on their own (follows
--meisei-radius), and --meisei-floating-surface is the opaque ground under the loyalty
panel and the wishlist dialog.
How the theme reaches your storefront
The embed script asks for the stylesheet in the same request as everything else the page's
widgets need, and puts it in the page before any widget draws, so the page never flashes
default colors. The stylesheet is also its own URL, for a page that links it from <head>:
That URL is cached for five minutes. The appearance page also shows a versioned URL, which is
immutable until you next change something. Put the version on the script tag as
data-meisei-theme-version and the embed links that URL instead, which the browser can
cache indefinitely, or link the versioned URL from your own <head> if you load the embed
script late.
One request per page
However many widgets a page has, the embed script asks Meisei for their first view in one
request, GET /widgets/boot, and every star rating on the page shares
one read inside it. For a shopper who is not signed in, that request is the same for everyone
looking at the page, so it is answered from the edge cache. After the first view each widget
asks its own endpoint, for the next page of reviews or a different sort. The React widgets do
the same for every widget inside one MeiseiProvider.
Custom CSS rules
The class names below are a supported API and will not be renamed without notice. Anything
else in the markup is internal. A modifier is always a -- suffix on its block
(.meisei-button--primary, .meisei-loyalty-tier--current).
Custom CSS is sanitized on save, and the sanitized version is what is stored:
- Every rule is scoped under
.meisei. A rule forbodyor:rootis kept but can never match. - Only
@media,@supportsand@containersurvive.@import,@font-faceand the rest are removed. Load a webfont from your own theme and point--meisei-fontat it. url()may only carry adata:URI. A declaration with any otherurl()is dropped.- Nested rules are dropped. Write each selector out in full.
- 20,000 characters is the limit. Past that the save is refused.
CSS the assistant writes goes through the same sanitizer and replaces the custom CSS in full rather than appending to it. Every version is kept under Earlier versions, so you can put back what it replaced.
Reviews widget options
The write form asks for a rating, title, review, name and email, up to five photos or videos, and any review questions. A signed-in shopper's email is filled in and locked, and their review is marked as a verified buyer when they have an order for the product that wasn't canceled. A guest's review never is. When the store only accepts storefront reviews from verified buyers, a guest is asked to sign in and a signed-in shopper with no order is told why, in place of the form, so sign shoppers in (see Identifying the customer) if you turn that on. Turn the form off to collect reviews only from the requests you send:
Helpful votes need no sign-in: the widget keeps a random token in local storage so a visitor can change their vote rather than vote twice. Votes are anonymous and only published reviews accept them.
Each review also carries a small Report link beside the votes. It opens a dialog with five
reasons (looks fake or not from a real customer, offensive or abusive, shares someone's
private information, not about this product, something else) and an optional note of up to
1,000 characters, and sends them to POST /widgets/reviews/{id}/reports. A report flags the
review for the store's staff and never unpublishes it; reporting the same review again from
the same IP address replaces the earlier report. With React, pass your own onReport to
ProductReviews or leave it out to hide the link.
Review questions
Questions set up on Settings → Features → Reviews appear in three places: in the write form (scales as a row of steps, multiple choice as radios or checkboxes, required ones block sending until answered), above the list as a summary of published answers (a scale as a track with a marker at the average, a multiple choice as one bar per option; shares can exceed 100% where several options are allowed), and on each review card as that reviewer's answers. A question nobody has answered is left out. Archiving keeps existing answers; hiding a question or option removes it from the storefront while the answers stay in reports.
To keep the answers on the cards but drop the summary above the list:
Review summaries
Once a product has at least five published reviews, the reviews widget shows a generated "What reviewers say" summary with up to five themes, refreshed at most once a day per product. It is kept out of the structured data. Regenerate, hide or revert it from the product page in the dashboard, or turn the feature off with "Summarize what reviewers say" on Settings → Features → Reviews. See Review summaries.
Instant answers
With instant answers on, the reviews widget shows a
"Looking for specific info?" box. The answer is generated from the product's published
reviews, its description as Meisei has it, and what the page says: the widget reads the
page title, its meta descriptions and any schema.org Product the page already carries.
Two attributes shape that:
Changing the notes regenerates stored answers the next time each question is asked. On Shopify, the Product reviews block passes the product's title and description as the page context and has a Notes for instant answers setting for the rest.
Reviews carousel
Store-wide is the default; each card then names and links to its product when Meisei knows the URL. A product slug narrows it to one product.
Cards are ordered newest first unless data-meisei-sort says otherwise, and slide with the
arrow buttons, by dragging, or with the arrow keys once focused. Nothing advances on its own.
A carousel set to highest or given a lowest rating shows a selection of your reviews, so
label it as one ("Some of our favorite reviews") rather than as what customers think overall.
The carousel emits no structured data. On Shopify it is the Reviews carousel block, with
the same settings (its Order offers newest or highest rated first) plus an "Only this
product" checkbox.
Media grid
Tiles are ordered newest review first and a review's own photos stay together, so a page can come back a little over the number asked for. "Load more photos" appends the next page. As with the carousel, a lowest rating makes the grid a selection, and its heading should say so. Store-wide, the lightbox carries a "View product" link. Arrow keys walk the grid and Escape closes it. The grid emits no structured data. On Shopify it is the Review photos and videos block.
Structured data
Both review widgets can emit schema.org JSON-LD, which produces the star
snippets in Google results: a Product node with an AggregateRating, and from the reviews
widget the individual Review records too.
The generated instant answer is never part of the markup.
Override either with data-meisei-structured-data (or ="false"). In React the same
controls are the structuredData and productUrl props on <ProductRatingWidget> and
<ProductReviewsWidget>.
Placement rules:
- Put the marked-up widget on the product's own page. Markup on a home page or blog post is ignored or flagged in Search Console.
- One marked-up widget per page. With both widgets on a product page, let the reviews widget emit it.
- Leave it off in collection and search grids.
- If your theme already publishes an
aggregateRatingfrom another review app, turn Meisei's off rather than shipping two contradictory ratings.
The product URL comes from <link rel="canonical">, falling back to the current path without
query or fragment; set data-meisei-product-url when neither is right (a quick-view drawer,
for example). Nothing is emitted until the product has one published review. Only published
reviews are included, reviewers appear as first name and last initial ("Anonymous" if
unnamed), and reviews loaded through "Load more" are added as they appear.
Verify with Google's Rich Results Test using the URL tab: the markup only exists after the page's JavaScript runs.
Identifying the customer
Anything that belongs to one shopper (wishlists, loyalty balance) needs the storefront to prove who is signed in with a signature made on your server. Add three attributes to the script tag:
All three go together. For a signed-out shopper leave all three off: the wishlist widgets keep guest lists in the browser that are claimed on sign-in (see Before signing in), and the loyalty widget shows the signed-out catalog. A page with some but not all of them is refused by the wishlist widgets and treated as signed out by the loyalty widget. The reviews widget sends the signature too, which is how a review is matched to the shopper's orders.
The signing recipe
- Take the email exactly as sent in
data-meisei-customer-email, including its case. - Take the current Unix time in seconds, as a string.
- Concatenate the two, in that order, with nothing between them.
- HMAC-SHA256 the result, keyed with your widget signing secret, as lowercase hex.
A signature is good for 24 hours; sign on every page render anyway. The secret is under Settings → API keys in the widget signing secret section. There is one per organization, shared by every site. Keep it on your server and out of theme files, JavaScript bundles and version control: anyone holding it can sign as any of your customers.
Rotate secret in the same section takes effect immediately with no grace period: every storefront still signing with the old value fails until the new one is deployed. Rotations are recorded in the audit trail.
With the Python client,
sign_customer(email, signing_secret=secret) does the same.
Test vector: secret wsk_demo_signing_secret, email test@example.com, timestamp
1735689600 gives 48eb876e883be4490d05d06ea8b9fe870b481c1098cc7ef88553998690a1dd31.
On Shopify there is nothing to do. The Meisei embed asks Meisei for the signature of whoever is signed in, on your store's own domain, and the secret never leaves Meisei. Do not sign in Liquid: the secret would have to live in a theme setting or metafield, both readable by anyone who can edit your theme. See Widgets in your theme.
Every widget call carrying a customer answers 401 with one of:
Calling the widget API from a server
A storefront that renders on the server (Next.js, Remix, a custom app) can call the same
endpoints with a secret key instead of the publishable key. Send it as a bearer token and
leave apiPublishableKey off; the key names your organization, and the response is the same:
Prefer this over the publishable key on a server. The publishable key is rate limited per address, so one server calling for every shopper spends a single shopper's allowance; a secret key draws on your organization's API allowance instead.
To read one shopper's wishlists or loyalty balance, ask Meisei for their signature rather than keeping the widget signing secret in your own code:
The answer is the email, timestamp and signature trio above, good for 24 hours. Pass all
three on the widget request as usual. The key needs the customers:write permission, because
holding the trio is the power to act as that shopper.
Keep secret keys on your server. A secret key in browser JavaScript can read and write everything your organization has.
From Ruby, the meisei gem does all of this for you, signing included. See
Ruby client.
Wishlists
A save button for a product page, the list manager for an account page, and the same manager behind a button that opens it in a sheet from the right edge of the page. All three need the customer attributes above. See Wishlists for sharing, purchase tracking and the Shopify markup.
The last one is the banner a product page opens with when somebody arrives from a shared
wishlist link (?meiseiWishlist=...&meiseiItem=...). It needs no customer attributes,
reads the link's parameters itself, and renders nothing at all on an ordinary visit, so it
is safe to place on every product page. In React it is <WishlistBannerWidget productSlug="..." />.
Loyalty
Without a customer the panel shows the catalog: tiers, ways to earn, offers and what points buy. With the customer attributes it adds the member's balance, tier progress, progress toward any punch card rule and held rewards with their codes. Joining, claiming and canceling a reward need a signed-in shopper.
Which sections appear is decided in the dashboard, always in the order tiers, ways to earn, offers, rewards. Ways to earn and offers follow the Sections switches on Settings → Features → Loyalty; tiers appears whenever the program has any; rewards appears once the program's points are named. A switched-off section's rules are never sent to the browser.
Check-ins. Loading a page earns a shopper nothing on its own. To reward a store visit or a check-in, have your own systems report it as a custom event and give the rule a limit of once a day. See Custom loyalty events.
Points on the product page.
The price is read once when the badge mounts, so re-render it on a variant change (or use the
React component's price prop). A member reads "Earn 250 points with this purchase." with
their tier multiplier applied; everyone else reads "Join to earn 250 points with this
purchase." The badge renders nothing when there is no program, no live purchase rule, a paused
program, no named points, or a price too small to earn; product-restricted rules are left out.
On a page with the inline embed, the badge links to it. On Shopify it is the Points on
product page block.
Links from loyalty emails go to your storefront: the Loyalty page section on
Settings → URLs & domains takes a loyalty
page URL per site, defaulting to the site's base URL. Every link carries #meisei-rewards; the
launcher opens its panel on arrival and the inline embed scrolls into view.
Every endpoint behind these embeds is in the Widget API reference.
AI agents
With data-meisei-agent-tools="true" on the script tag (agentTools on MeiseiProvider
in React), the widgets register WebMCP tools
that an agent in the shopper's browser can call. A browser without WebMCP gets nothing, and
the tools are registered on document.modelContext, or on navigator.modelContext in the
earlier drafts. What a merchant sees is on AI agents on your storefront.
Each tool is registered once per page, however many widgets offer it. The product tools take
a productSlug, listed as an enum of the products the page's widgets show and optional
when there is only one. They read with the same publishable key and signed shopper as the
widgets, so an agent can see nothing the shopper couldn't.