Skip to content

Powered by Grav CMS + Helios Theme

Shortcodes

Six blocks of the storefront, writable into any page's Markdown. A featured grid on the home page, an add-to-cart button in a blog post, a product's gallery inside a landing page — none of which needs a theme change, a template override, or Twig turned on in content.

MARKDOWN
## This month's picks

[kahunacart_products featured limit="4" title="This month's picks"]

Before you begin

  • KahunaCart installed and enabled, with at least one published product.
  • The Shortcode Core plugin installed and enabled.

That is the whole setup. KahunaCart registers its blocks on an event Shortcode Core fires, so on a site without it nothing here exists and the storefront is unchanged. There is no setting to switch on.

Why not just use Twig in the page

You can put {{ kahunacart_products(4) }} in a page's content, and on Grav 1.7 people did. On Grav 2.0 it means opening security.twig_content and adding every function you call to the security.twig_sandbox allowlist, site-wide, for every page on the site — a large permission granted so that four products can appear on the home page.

A shortcode needs none of it. The page stays Markdown, the person writing it needs no Twig, and the site's security settings stay where they are.

The Twig functions are still the right tool in a theme, where they run on every request and can do things a shortcode must not.

The blocks

Shortcode What it draws
[kahunacart_products] A grid of the catalog, narrowed however you ask
[kahunacart_sections] The store's own sectioned listing, as configured in catalog.sections
[kahunacart_product] One product, as the card the grid draws
[kahunacart_buy] One product's add-to-cart form, with its option picker
[kahunacart_gallery] One product's carousel — pictures, clips and embeds
[kahunacart_categories] The category chips, as a way into the shop

Every one of them renders through the same partial the storefront's own pages render through, so a theme that has overridden a partial sees its override inside a shortcode too, with no extra work and nothing to keep in step. A block that has nothing to draw — a product that is a draft, a slug nobody has, a store with no categories — draws nothing at all, heading included, rather than an empty box or a broken card.

[kahunacart_products]

Attribute Value Default
tag A tag slug, or several separated by commas Every tag
category A category slug, or several separated by commas. Each includes its subcategories Every category
products Exactly these slugs, in exactly this order —
exclude Slugs or ids to leave out, separated by commas —
featured true or false — only featured, or only unfeatured Either
featured_first true or false The store's catalog.featured_first
sort newest, oldest, manual, title, price_asc, price_desc The store's catalog.sort
limit How many, up to 500 12
offset How many to skip 0
title A heading above the block No heading
layout grid, hero or compact grid
columns How many across — up to 6 for grid, 4 for compact, 2 for hero 3, 2, 2

The default is twelve, not the Twig function's fifty. A block on a page is a handful of cards; the listing at your shop's base route is where the whole catalog lives. Twelve divides by two, three, four and six, so the default fills whatever column count you ask for.

Facets combine the way the storefront's own filters do — and across, or within:

MARKDOWN
[kahunacart_products category="gear" tag="add-on, bundle" limit="6"]

is "in gear, and tagged either add-on or bundle". An attribute left out is a facet that is not applied at all.

featured can be written bare, which means true:

MARKDOWN
[kahunacart_products featured limit="4" layout="hero"]

A mistyped attribute draws the shop rather than an empty block. limit="eight" falls back to twelve, layout="heroic" falls back to the grid, sort="cheapest" falls back to the store's own order, and featured="sometimes" is read as "not written down" rather than as false. A typo in a page every visitor loads should cost a block that is not quite what you meant, not a page that is wrong or missing.

A tag or category naming nothing is dropped, not refused, which is the same rule the storefront's own filters follow: a block that names a tag somebody has since renamed widens to the shop rather than going blank. That is deliberate, and it is the one case where a typo is invisible — if a block is showing more than you expected, check the tag slug first. products is the opposite, because naming exact products and getting none of them means something different: an unknown slug there contributes nothing, and a block that finds none of the products it named draws nothing at all.

The three layouts are the ones the sectioned listing draws, described on Storefront. A block with a title gets a heading with an id on it, so #kahunacart-section-this-months-picks is a link you can write down; two blocks sharing a title still get separate ids.

[kahunacart_sections]

The store's catalog.sections config, drawn as the listing draws it, on a page of your own. No attributes: this is the merchant's editorial layout rendered where they want it, and a shortcode that started overriding the blocks would be [kahunacart_products] with extra steps. Nothing at all on a store that has declared no sections.

All three name one product, and both spellings work:

MARKDOWN
[kahunacart_product slug="reef-runner-tee"]
[kahunacart_buy="reef-runner-tee"]

[kahunacart_product] draws the card the grid draws, linking to the product's page. It takes title, layout and columns as well, so a single product can be a hero block.

[kahunacart_buy] draws the add-to-cart form — the option widgets, the variant select the form actually posts, the price and the sale badge. The option picker script is loaded for you, so swatches resolve here exactly as they do on the product page. On a member-only product the viewer is not entitled to buy, it draws the sign-in notice in place of the form, because the cart would refuse it.

[kahunacart_gallery] draws the product's carousel. Embeds stay click-to-load: nothing third-party is in the page until a visitor presses play.

[kahunacart_categories]

The category chips, for a page that wants a way into the shop — an About page, a landing page, a post. Nothing at all on a store with no categories.

In the editor

Editor Pro users get full shortcode support built in. All six appear in its shortcode dialog under KahunaCart, so you fill in a form instead of remembering an attribute name. Nothing to switch on, and nothing changes on a site without it.

Shortcodes can be typed by hand in any editor, so none of this is required. It is there for the people who already run Editor Pro.

If you do use it, use 2.0.14 or later. The product picker offers its options as names rather than slugs, and Editor Pro's insert dialog could not read that shape until 2.0.14 — on 2.0.13 the dialog fails to open when a KahunaCart block is chosen, with config.options.map is not a function in the console. That is a limitation of the dialog and nothing else: the shortcodes on the page are unaffected either way.

The single-product blocks get a real picker. [kahunacart_product], [kahunacart_buy] and [kahunacart_gallery] list your published products by name, so nobody has to go and look up a slug. Choosing one is required — a block naming no product draws nothing at all, and a form that let one be inserted empty would be a way to publish an invisible block. Unlisted products are in the list too — they are exactly the ones you embed by hand — and drafts are not, since a draft renders nothing wherever it is named. Past 200 products the dropdown gives up and becomes a text field with your shop's address in the hint, because a <select> is not a catalog.

tag and category stay text fields on purpose. Both take several slugs separated by commas and a dropdown holds one value. Offering one would be worse than not: opening the form on a hand-written tag="add-on, bundle" would fall back to the first option, and saving would write that back — turning a two-tag block into the whole shop with nothing on screen to say so. The placeholder lists your own slugs instead.

The written shortcode stays minimal. Editor Pro leaves out any attribute still at its declared default, so a grid inserted with the defaults is [kahunacart_products] rather than a line of attributes all saying what would have happened anyway. sort and columns declare no default at all, because theirs is whatever your catalog.sort and the chosen layout say — their blank option means "leave it to the shop" and writes nothing.

A block collapses to its name and the two or three things that tell it apart — the heading, the product, the tag — so a page with four grids on it can be read without opening each one.

Showing a shortcode without running it

Shortcode Core runs a shortcode wherever it finds one, and a fenced code block is no exception — by the time it looks, the fence is already markup. A page that documents a shortcode therefore renders it instead of showing it. Turn the processing off for that one page in its frontmatter:

YAML
shortcode-core:
    active: false

This page does exactly that, which is why the examples above are examples.

What must not be a shortcode, and why

Rendered page content is cached. A shortcode is processed once and its HTML is served to everybody until the page changes.

For a catalog block that is a win: a home page with a featured grid on it stops asking the commerce database anything on all but the first request. It is why the set here is catalog-only. Anything that differs per visitor would freeze at whatever the first visitor saw — a cart count stuck on somebody else's number, or a members-only product resolved for a signed-in member and then shown to everybody. Those stay Twig functions in a theme, where they run on every request:

TWIG
{{ kahunacart_cart_count() }}

The buy button is the exception, and it is handled for you. [kahunacart_buy] draws a form carrying a CSRF nonce that belongs to one visitor's session, and a cached nonce is not a page looking slightly old — it is an add-to-cart button that works for whoever warmed the cache and answers everybody else with "Session expired". So a page carrying [kahunacart_buy] is rendered on every request: KahunaCart reads the page's Markdown before Grav decides, and switches that page's content cache off. You do not have to set anything, and it costs only the pages that actually have a buy button on them — every other block has no nonce in it and stays cached.

A price list shown to some shoppers is handled for you too. When a price list is switched on under Show as a discount in the catalog and its rules are about the customer (their group, email, email domain or order count), [kahunacart_products], [kahunacart_sections], [kahunacart_product] and [kahunacart_categories] would print the price of whoever rendered the page first. So a page carrying one is rendered on every request, however many currencies you sell in. A page that showed a signed-in customer one of those prices also goes out private, no-cache, so a CDN doesn't hand a member's copy to a guest. A store with no such list keeps its caching.

Stock, prices and sales are on the same clock as the page. A cached grid shows the prices that were true when the page was last rendered, which for a home page is usually fine and for a price that changes hourly is not. Grav's page cache clears when the page changes; a store running timed sale campaigns should either put those blocks on a page with caching off, or keep them in the theme.

Styling

The blocks carry the storefront's own classes and are painted by the storefront's own stylesheet, which is added to any page that uses one — so a grid on the home page looks like a grid on the listing. The --kahunacart-* custom properties on Theming apply here exactly as they do there, and builtin_css: false leaves these pages unstyled too, for a theme that paints the storefront itself.

The stylesheet and the scripts are registered through Shortcode Core's asset store rather than added to the page directly. That is what makes them survive the page cache: the shortcode itself does not run again on a cached page, but what it asked for is remembered and put back on every request.