On the shop front
Blocks in four slots, sections a merchant places anywhere, and components the shop draws in its own look.
An app can say something on a shop's pages in two ways. A block answers in one of four named slots, wherever the theme draws that slot. A section is one your manifest offers, which a merchant places on any page from the page editor.
Both are asked for on the server while the page is drawn, so a shopper, a search engine and a shopping agent all read the same words, and none of them waits for a script. Both are signed, both answer in HTML or in components, and both are dropped quietly when your server is slow or wrong: the page is drawn without them, exactly as it was before your app was installed.
Blocks
[[extensions.storefront]]
block = "storefront.product.below_description"
url = "https://sizes.example.com/blocks/product"
format = "components"| Slot | Where it is drawn | In context |
|---|---|---|
storefront.product.above_add_to_cart | Above a product's add to basket | productId, handle |
storefront.product.below_description | Under a product's description | productId, handle |
storefront.collection.header | Under a category's heading | collectionId, handle |
storefront.cart.summary | Beside the basket's totals | nothing |
Storeware posts { "block", "storeId", "context" } to your url, signed as the kind block with the installation's secret, and with the header x-storeware-block. Verify it before you answer:
import { verifyBlockRequest } from '@storeware/plugin-sdk/blocks';
const rawBody = await request.text();
const { block, storeId, context } = await verifyBlockRequest({
secret: installation.secret,
rawBody,
signatureHeader: request.headers.get('storeware-signature'),
});context came from a shopper's browser, so treat it as input rather than as a fact.
The limits
| Limit | Value |
|---|---|
| Time to answer | 700 ms |
| Size of an answer | 64 KB |
| Redirects | Never followed. A redirect is a block that is not drawn. |
| A status other than 2xx | Not drawn |
Every block carries a line under it naming your app, so a shopper can tell the shop did not write it.
HTML or components
format in the manifest says what you answer with: html, the default for a block, or components, the default for a section. An answer whose content type is application/vnd.storeware.components+json is read as components whatever the manifest says, so you can move a block over without a new release.
HTML is reduced to an allow list: text, headings, lists, tables, links and quotes. No script, style, class, image, form or frame survives, and every link that opens a new window is made safe. Whatever the list removes is gone; HTML that is emptied is not drawn.
Components are a list of nodes that Storeware draws itself, in the shop's own type, colours and spacing, with the classes its own bands use. Your block then looks native on every theme without shipping a single style.
{
"components": [
{ "type": "heading", "text": "How it fits" },
{
"type": "table",
"head": ["Size", "Chest"],
"rows": [
["S", "88 cm"],
["M", "96 cm"]
]
}
]
}Build and check an answer in your own code with components() from @storeware/plugin-sdk/components, which throws in development on a tree the shop would refuse. parseComponents is the same check Storeware runs: a tree it accepts is a tree the shop draws, and a tree it refuses is dropped whole, never drawn in part.
An answer holds at most 200 nodes, children included, nested at most four deep.
The components
heading
{ "type": "heading", "text": "How it fits", "level": 3 }level is 2, 3 or 4, and 3 when you leave it out.
text
{ "type": "text", "text": "Runs a half size small.", "emphasis": "subdued" }emphasis is strong or subdued.
list
{ "type": "list", "items": ["Hand wash", "Dry flat"], "ordered": false }table
{ "type": "table", "head": ["Size", "Waist"], "rows": [["M", "80 cm"]] }head is drawn as column headings. Up to 8 columns and 50 rows.
rating
{ "type": "rating", "value": 4.6, "count": 128, "label": "128 reviews" }Drawn in the shop's own stars, with the number said in words for a screen reader. value is 0 to 5.
badge
{ "type": "badge", "text": "Bestseller", "tone": "success" }tone is neutral, info, success, warning or critical, drawn in the shop's colours.
image
{
"type": "image",
"src": "https://cdn.mystoreware.com/apps/size-chart.png",
"alt": "A size chart",
"width": 800,
"height": 600
}Only from cdn.mystoreware.com. Put a picture there with POST /api/v1/app/assets, which needs no scope, and use the address it answers with. An image is always loaded lazily, so it never holds up the shop's own pictures. Give width and height so the page does not move when it arrives.
link
{ "type": "link", "text": "The full guide", "href": "https://sizes.example.com/guide" }An href is an https address or a path on the shop, starting with /. A link off the shop gives the page it opens no hold on the shop's window.
button_link
{ "type": "button_link", "text": "Find my size", "href": "/apps/sizes/quiz" }Drawn as the shop's own button. Pair it with an app proxy page to take a shopper somewhere of yours on the shop's own address.
accordion
{
"type": "accordion",
"items": [{ "title": "Care", "children": [{ "type": "text", "text": "Wash cold." }] }]
}Drawn as the shop's own folds, which open without script.
notice
{ "type": "notice", "title": "Made to order", "text": "Ships in two weeks.", "tone": "info" }stack
{
"type": "stack",
"direction": "row",
"children": [
{ "type": "badge", "text": "Organic" },
{ "type": "badge", "text": "Fair trade" }
]
}Groups nodes, one under another (column, the default) or side by side (row).
Sections in the page editor
A block is drawn wherever the theme put its slot. A section goes wherever the merchant wants it: they add the band A section from an app on any page, and choose one of the sections their installed apps offer.
[[extensions.sections]]
handle = "size_chart"
name = "Size chart"
description = "A chart of this product's sizes."
url = "https://sizes.example.com/sections"A manifest offers up to ten. handle is lower case letters, digits and underscores; name and description are what the merchant is shown in the editor. format is components unless you say html.
When the page is drawn, Storeware posts { "section", "storeId", "context" } to the section's url, with the header x-storeware-section, signed as the kind block and held to the same limits. On a product page context names the product, and on a category page the collection.
import { verifySectionRequest } from '@storeware/plugin-sdk/blocks';
const { section, context } = await verifySectionRequest({
secret: installation.secret,
rawBody: await request.text(),
signatureHeader: request.headers.get('storeware-signature'),
});To serve blocks and sections from one URL, call verifyBlockRequest and look for section in what it returns.
Sections are read from the release the shop has approved. A section a new release adds is offered once the merchant approves it, and a band whose section is gone (the app was removed, or a release dropped it) draws nothing on the page and says so in the editor. A theme listed for sale cannot carry an app's section, because its content comes from your server every time the page is drawn.