@spindlecommerce/plugin-channable
v1.0.0
Published
Channable for SpindleCommerce: a product feed that Channable imports and forwards to marketplaces and ad channels, with an item for each variant, priced and worded as the channel's storefront shows it.
Maintainers
Readme
Channable, a SpindleCommerce plugin
A product feed for Channable, which forwards your products to the marketplaces and ad channels you connect there. The feed has an item for each variant, worded and priced as the channel's storefront shows it to a guest. It is written while the catalogue is read, a page of 100 products at a time, so it never sits in memory whole, and Vercel's 4.5 MB cap on responses doesn't apply to it.
Setup
Add the plugin to your SpindleCommerce installation and deploy:
pnpm add @spindlecommerce/plugin-channable// ecom.config.ts import { channable } from "@spindlecommerce/plugin-channable"; export default defineConfig({ plugins: [email(), stripe(), channable()] });It needs plugin API 1.1 (the catalogue service), so pnpm refuses it in an installation whose platform is older.
Settings › Plugins › Channable. Enter a feed token: a long random string, such as the output of
openssl rand -hex 32. On a server, theCHANNABLE_FEED_TOKENenvironment variable can supply it instead. Until a token is set, the feed answers 503.Check the links. Items link to
{storefront}/products/{slug}, where{storefront}is the channel's storefront under Settings › Storefronts.- If the storefront puts some locales under a prefix (
/de/products/…), fill in Locale prefixes. - If its product pages live elsewhere, change Product URL (see Settings).
- If the storefront puts some locales under a prefix (
Add the feed in Channable. In the Channable project, go to Setup › Import, click + Choose import method and pick JSON. Then fill in:
- Name: for example "SpindleCommerce eu, en-GB, GBP".
- JSON file url: the feed URL for the project's channel, locale and currency, with the token (see Feed URL examples).
- Authentication: none, since the token is in the URL. Alternatively, pick
Authentication header, enter the token and leave
token=out of the URL. Channable then sends the token in anx-api-keyheader, which keeps it out of URLs and logs (Importing a password protected feed). - Encoding: UTF-8, or Automatically detect.
Click Start import and check the mapping. Channable matches most fields to its own, and the
attr_…fields become fields of their own. Then click Save & Import.Schedule. Channable imports once a day by default; some plans allow up to 24 times a day, under Settings › Schedule settings (Change the frequency of data updates). The feed is built when it's requested, so each import gets the current prices and stock.
A feed covers one channel in one locale and currency. Use a Channable project for each combination you sell through, each importing its own URL.
Settings
Feed token (secret, required,
CHANNABLE_FEED_TOKEN). Requests without it answer 401. To revoke a URL, change the token, then update the import in Channable.Product URL (default
{storefront}/products/{slug}): each item'slink. Its placeholders:| Placeholder | Value | |---|---| |
{storefront}| The channel's storefront (Settings › Storefronts), followed by the locale's prefix. | |{slug}| The product's slug in the feed's locale, URL-encoded. | |{locale}| The feed's locale, such asde-DE. | |{language}| Its language, such asde. |For a storefront with a domain per language, for example:
https://{language}.shop.example.com/p/{slug}.Locale prefixes (such as
de-DE=/de, fr-FR=/fr): the path the storefront puts before each locale's pages, added to{storefront}.- A language alone (
de=/de) covers all of its locales. - Locales that aren't listed have no prefix.
- A language alone (
Brand attribute (
brand), Color attribute (color) and Size attribute (size): the codes of the attributes that hold them. For each, the variant's option comes first, then the product's attribute.
Feed URL examples
The feed is at /plugins/channable/feed.json on the API's public URL, the one whose /admin is the
admin. The examples use https://shop-api.example.com.
| URL | Feed |
|---|---|
| https://shop-api.example.com/plugins/channable/feed.json?token=TOKEN | The default channel (DEFAULT_CHANNEL, else the first active one), in its default locale and currency. |
| …/feed.json?token=TOKEN&channel=eu | The eu channel, in its default locale and currency. |
| …/feed.json?token=TOKEN&channel=eu&locale=de-DE¤cy=EUR | The eu channel in German, in euros. |
| …/feed.json?token=TOKEN&channel=eu&locale=en-GB¤cy=GBP | The eu channel in British English, in pounds. |
| …/feed.json?channel=eu&locale=fr-FR, with the token in an x-api-key header | Channable's Authentication header option. |
The query parameters:
channel: a channel's code. An unknown or inactive one answers 404.locale: the locale of texts and links. If the channel lacks it, the feed uses the same language, then the channel's default.currency: the currency of prices. If the channel lacks it, the feed uses the channel's default currency. Each item'scurrencysays which was used.
The answers:
- 200 with the feed;
- 401 without the token;
- 404 for an unknown channel;
- 409 when the links need a storefront the channel lacks;
- 503 while the plugin is disabled or has no token.
To look at a feed:
curl -s "https://shop-api.example.com/plugins/channable/feed.json?token=TOKEN&channel=eu" | head -c 2000Field mapping
The feed has an item for each variant with a price in the feed's currency. The field names follow Channable's list in Requirements for import files, so Channable maps most of them by itself.
| Field | Value |
|---|---|
| id | The variant's SKU. |
| item_group_id | The product's id, shared by its variants. |
| title | The variant's name: the product's name and its options, such as "Oxford Shirt – Blue / M". |
| description | The product's description as plain text, with a line per paragraph or list item. Without one, its SEO description. |
| link | The Product URL setting, filled in for the product. |
| image_link | The first image: the variant's own images come first, then the product's images that belong to no variant. |
| additional_image_link | The rest of those images, separated by commas. Videos are left out. |
| price | In major units, as a number with the currency's decimals: 59.00, or 5900 in yen. When the variant is reduced, this is the price before the reduction (its compare-at price). |
| sale_price | The reduced price, or null when the variant isn't reduced. |
| currency | The prices' currency, such as GBP. |
| availability | in stock or out of stock: whether the variant can be bought now. |
| stock | The salable quantity in the channel's stock. Null when the variant doesn't track inventory. |
| brand | The Brand attribute's value. |
| gtin | The variant's barcode (GTIN, EAN or UPC). |
| color | The Color attribute's value. |
| size | The Size attribute's value. |
| product_type | The product's first category as a breadcrumb, such as Men > Men's Shirts. Without a category, its product type. |
| categories | All of its categories as breadcrumbs, separated by \|. |
| weight_grams | The variant's weight in grams. Null when it has none. |
| visible_in_listings | False for a product kept out of the storefront's listings and search, which is reachable by its URL only. A Channable rule can leave such products out. |
| attr_<code> | Every attribute of the product, and the variant's options, by code: attr_material, attr_size and so on. |
- Texts are in the feed's locale.
- Prices are what a guest pays for one unit in the feed's currency, including tax when the channel's prices include it, as the storefront shows them.
- Missing values are empty strings for texts and null for numbers.
Format choice
Channable's imports can't follow pages, so the feed is a single JSON array, streamed while the catalogue is read.
- What Channable imports.
- Products come from a webshop integration or from a data file at a URL (Import your product data).
- A data file is XML, CSV, TXT, JSON or a Google Sheet. It holds up to 10 million items and 10 GB, and XML and JSON must be flat (Requirements for import files).
- Each import reads one URL (JSON). Nothing documents following a next-page link.
- Several imports can be combined (Combine multiple imports), but each is a fixed URL added by hand, and a project has at most 50 by default. A growing catalogue would outgrow them.
- Vercel's cap.
- A Vercel Function's response body is capped at 4.5 MB (Vercel Functions limits). A feed reaches that at a few thousand items: an item with a 700-character description takes about 1.3 KB.
- Streamed responses aren't held to that cap (How do I bypass the 4.5MB body size limit of Vercel Functions). What bounds them is the function's maximum duration (Max duration).
- So the route reads the catalogue 100 products at a time, following the catalogue service's
nextcursor, and sends each page's items before it reads the next page.
- JSON rather than CSV. CSV is Channable's fastest import
(CSV),
and the smallest, but:
- its header must name every column before the first row. The
attr_<code>columns are only known once the whole catalogue has been read, so a CSV would need a second pass, doubling the time the feed takes; - a stream cut short, by the time limit or a database error, leaves a JSON array unclosed, so it can't pass for a whole catalogue. A CSV cut short is still a valid CSV, and its missing rows would go unnoticed.
- its header must name every column before the first row. The
- JSON rather than XML. Both carry each item's own fields, but JSON is smaller, and attribute codes needn't be valid XML names.
- Flat items. Channable takes flat JSON only, so lists such as
additional_image_linkare comma-separated strings, as Channable's field list describes them. - Prices without a currency. Channable's field list shows no currency inside
priceorsale_price, so the feed doesn't add a "59.00 GBP" form. Prices are numbers, andcurrencynames their currency.
Limitations
- Orders. Importing marketplace orders from Channable isn't supported yet. For a platform Channable has no integration with, its order connection goes through the Channable API, which the platform's backend must implement (How to set up the Channable API for an order connection). This plugin only sends products.
- Size. The whole feed must be sent within the function's maximum duration: 300 seconds in
SpindleCommerce's Vercel build. Bytes aren't capped, but time is.
- How many products fit depends on the database. As a rough guide, 500 products with 4 variants each (2,000 items, 2.7 MB) took 0.2 seconds on a laptop with the in-memory test database. A hosted database adds its round trips to each page of 100 products.
- Each feed logs how long it took ("Served the Channable feed", with
ms), so compare that with the limit as the catalogue grows. - A feed cut off by the limit, or by a failure partway, ends without its closing
], so it can't be read as a whole catalogue. - Channable's own limits are 10 million items and 10 GB per file.
- Variants with no price in the feed's currency, nor one the platform can convert to it, are left out.
- Prices are a guest's. Customer-group prices, quantity prices and cart promotions aren't in the feed.
- No shipping costs, condition or delivery times. Add them with Channable rules if your channels need them.
Develop
pnpm install
pnpm test
pnpm buildThe tests mount the plugin's route on a Hono app, over a fake catalogue. The plugin API it builds on is @spindlecommerce/plugin-sdk 1.1.
Release
Each version is staged on npm and goes live when a maintainer approves it.
- Raise
versioninpackage.jsonand insrc/index.ts. A test checks they match. - Commit, then tag and push: for example
git tag v1.0.1 && git push origin main v1.0.1. - The Release workflow tests, builds and stages the version, and creates the GitHub release.
- Approve it on npmjs.com, under Staged Packages, with 2FA.
The workflow publishes with npm's trusted publishing, so it needs no token. Before its first release, a maintainer links the package to the workflow:
npm trust github @spindlecommerce/plugin-channable --repo spindlecommerce/plugin-channable --file release.yml --allow-stage-publishRun it just before tagging: npm drops a link that isn't used within 48 hours.
License
MIT
