> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Theme Blocks

> Add custom blocks to merchant storefronts

# Theme Block Extensions

Theme blocks are Liquid-based UI components that merchants can add to their storefront themes. They appear in the theme editor alongside native blocks, allowing merchants to position and configure them visually.

## How Theme Blocks Work

1. Your app installs block files (`.aqua` template + `.schema.json` schema)
2. Blocks appear in the merchant's theme editor under "App Blocks"
3. Merchants drag blocks into sections and configure settings
4. LaunchMyStore renders your block using the Liquid template engine

## Block File Structure

Each theme block requires two files:

```
extensions/{domainSlug}/{appHandle}/blocks/
├── product-reviews.aqua          # Liquid template
└── product-reviews.schema.json   # Settings schema
```

## Creating a Theme Block

### 1. Write the Liquid Template

Theme blocks use `.aqua` files with standard Liquid syntax:

```liquid theme={null}
{% comment %} product-reviews.aqua {% endcomment %}

<div class="app-reviews-widget" data-product-id="{{ product.id }}">
  {% if block.settings.show_title %}
    <h3 class="reviews-title">{{ block.settings.title }}</h3>
  {% endif %}
  
  <div class="reviews-summary">
    {% render 'review-stars', rating: product.metafields.reviews.average %}
    <span class="review-count">
      {{ product.metafields.reviews.count }} reviews
    </span>
  </div>
  
  <div class="reviews-list" id="reviews-{{ product.id }}">
    {% for review in product.metafields.reviews.items limit: block.settings.limit %}
      <div class="review-item">
        <div class="review-header">
          <strong>{{ review.author }}</strong>
          {% render 'review-stars', rating: review.rating %}
        </div>
        <p class="review-body">{{ review.body }}</p>
      </div>
    {% endfor %}
  </div>
  
  {% if block.settings.enable_pagination %}
    <button class="load-more" data-page="2">Load More Reviews</button>
  {% endif %}
</div>

<style>
  .app-reviews-widget {
    padding: {{ block.settings.padding }}px;
    background: {{ block.settings.background_color }};
  }
  .reviews-title {
    color: {{ block.settings.title_color }};
    font-size: {{ block.settings.title_size }}px;
  }
</style>

{# Per-app CSS/JS is shipped via the manifest's stylesheetUrl/scriptUrl
   fields — there is no `extension_asset_url` filter. The host injects
   them automatically when the block renders. See "Per-app CSS/JS" below. #}
```

### 2. Define the Schema

The schema controls what settings appear in the theme editor:

```json theme={null}
{
  "name": "Product Reviews",
  "target": "product",
  "settings": [
    {
      "type": "header",
      "content": "Content"
    },
    {
      "type": "text",
      "id": "title",
      "label": "Section Title",
      "default": "Customer Reviews"
    },
    {
      "type": "checkbox",
      "id": "show_title",
      "label": "Show Title",
      "default": true
    },
    {
      "type": "range",
      "id": "limit",
      "label": "Reviews to Show",
      "min": 3,
      "max": 20,
      "step": 1,
      "default": 5
    },
    {
      "type": "checkbox",
      "id": "enable_pagination",
      "label": "Enable Load More",
      "default": true
    },
    {
      "type": "header",
      "content": "Styling"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background Color",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "title_color",
      "label": "Title Color",
      "default": "#333333"
    },
    {
      "type": "range",
      "id": "title_size",
      "label": "Title Size",
      "min": 14,
      "max": 36,
      "step": 2,
      "default": 24,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding",
      "label": "Padding",
      "min": 0,
      "max": 60,
      "step": 5,
      "default": 20,
      "unit": "px"
    },
    {
      "type": "header",
      "content": "Advanced"
    },
    {
      "type": "checkbox",
      "id": "enable_lazy_load",
      "label": "Lazy Load Reviews",
      "default": false,
      "info": "Load reviews via JavaScript for better performance"
    }
  ]
}
```

## Top-level Schema Fields

The schema JSON accepts the following top-level keys:

| Field          | Type                           | Description                                                                                                                                       |
| -------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`         | string (required)              | Display name in the theme editor block picker.                                                                                                    |
| `target`       | string \| string\[] (required) | Page-type slot where the block can be inserted. See [Available Targets](#available-targets).                                                      |
| `tag`          | string                         | HTML wrapper element for the block (default `section`).                                                                                           |
| `class`        | string                         | Class name applied to the wrapper element. Useful for shared per-app CSS hooks.                                                                   |
| `settings`     | object\[]                      | Block-level settings shown in the theme editor. See [Setting Types](#setting-types).                                                              |
| `blocks`       | object\[]                      | Nested block templates the merchant can add inside this block — same `type` / `name` / `settings` shape as a section's `blocks` array.            |
| `max_blocks`   | integer                        | Hard cap on how many nested `blocks` a merchant can add (default 50, configurable via `MAX_BLOCKS_PER_SECTION`).                                  |
| `presets`      | object\[]                      | Pre-configured combinations of settings + nested blocks shown as quick-add options in the picker. Each preset has `{ name, settings?, blocks? }`. |
| `color_scheme` | string                         | Default color-scheme handle to apply (e.g. `"scheme-1"`). Honoured by themes that wire schemes through their `t:section.color_scheme` plumbing.   |

```json theme={null}
{
  "name": "Trust Badge Row",
  "tag": "div",
  "class": "plinth-trust-row",
  "target": ["product", "cart"],
  "settings": [
    { "type": "range", "id": "columns", "label": "Columns", "min": 2, "max": 6, "default": 4 }
  ],
  "blocks": [
    {
      "type": "badge",
      "name": "Trust Badge",
      "settings": [
        { "type": "text", "id": "icon", "label": "Icon", "default": "truck" },
        { "type": "text", "id": "label", "label": "Label", "default": "Free shipping" }
      ]
    }
  ],
  "max_blocks": 6,
  "presets": [
    { "name": "Default", "blocks": [{ "type": "badge" }, { "type": "badge" }, { "type": "badge" }] }
  ]
}
```

## Available Targets

The `target` field determines which page types your block can be added to:

| Target       | Description      | Available Objects                 |
| ------------ | ---------------- | --------------------------------- |
| `index`      | Homepage         | `shop`, `collections`, `articles` |
| `product`    | Product pages    | `product`, `shop`, `collection`   |
| `collection` | Collection pages | `collection`, `products`, `shop`  |
| `cart`       | Cart page        | `cart`, `shop`                    |
| `article`    | Blog articles    | `article`, `blog`, `shop`         |
| `page`       | Custom pages     | `page`, `shop`                    |
| `blog`       | Blog listing     | `blog`, `articles`, `shop`        |
| `search`     | Search results   | `search`, `results`, `shop`       |

<Info>
  You can target multiple page types by specifying an array: `"target": ["product", "collection"]`
</Info>

## Setting Types

### Basic Inputs

```json theme={null}
{
  "type": "text",
  "id": "heading",
  "label": "Heading Text",
  "default": "Welcome",
  "placeholder": "Enter heading..."
}
```

```json theme={null}
{
  "type": "textarea",
  "id": "description",
  "label": "Description",
  "default": "",
  "placeholder": "Enter description..."
}
```

```json theme={null}
{
  "type": "number",
  "id": "columns",
  "label": "Number of Columns",
  "default": 3
}
```

### Selection Inputs

```json theme={null}
{
  "type": "select",
  "id": "layout",
  "label": "Layout Style",
  "options": [
    { "value": "grid", "label": "Grid" },
    { "value": "list", "label": "List" },
    { "value": "carousel", "label": "Carousel" }
  ],
  "default": "grid"
}
```

```json theme={null}
{
  "type": "radio",
  "id": "alignment",
  "label": "Text Alignment",
  "options": [
    { "value": "left", "label": "Left" },
    { "value": "center", "label": "Center" },
    { "value": "right", "label": "Right" }
  ],
  "default": "left"
}
```

### Range & Toggle

```json theme={null}
{
  "type": "range",
  "id": "opacity",
  "label": "Opacity",
  "min": 0,
  "max": 100,
  "step": 5,
  "default": 100,
  "unit": "%"
}
```

```json theme={null}
{
  "type": "checkbox",
  "id": "show_badge",
  "label": "Show Badge",
  "default": true,
  "info": "Display a badge on featured items"
}
```

### Visual Pickers

```json theme={null}
{
  "type": "color",
  "id": "accent_color",
  "label": "Accent Color",
  "default": "#007bff"
}
```

```json theme={null}
{
  "type": "color_background",
  "id": "background",
  "label": "Background",
  "default": "linear-gradient(180deg, #ffffff 0%, #f5f5f5 100%)"
}
```

```json theme={null}
{
  "type": "image_picker",
  "id": "banner_image",
  "label": "Banner Image"
}
```

### Resource Pickers

```json theme={null}
{
  "type": "product",
  "id": "featured_product",
  "label": "Featured Product"
}
```

```json theme={null}
{
  "type": "collection",
  "id": "source_collection",
  "label": "Source Collection"
}
```

```json theme={null}
{
  "type": "url",
  "id": "link",
  "label": "Link URL"
}
```

## Installing Theme Blocks

Blocks are deployed by the install pipeline when your app is installed on a
store. `POST /api/apps/install-extensions` is a **server-to-server**
endpoint gated by the platform's internal API key (no CORS) — the backend
calls it during install with inline block content; it is not called
directly from a browser with a merchant access token. (The only
browser-accepted variant is a `fromCatalog: true` catalog copy, which
requires the merchant to own the target store.)

```javascript theme={null}
const response = await fetch('https://store.launchmystore.io/api/apps/install-extensions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Internal-API-Key': INTERNAL_API_KEY
  },
  body: JSON.stringify({
    domainSlug: 'merchant-store',
    appHandle: 'my-reviews-app',
    extensions: {
      blocks: [
        {
          handle: 'product-reviews',
          template: `{% comment %}Your Liquid template{% endcomment %}...`,
          schema: {
            name: 'Product Reviews',
            target: 'product',
            settings: [/* ... */]
          }
        }
      ],
      snippets: [
        {
          handle: 'review-stars',
          template: `{% for i in (1..5) %}...{% endfor %}`
        }
      ],
      assets: [
        {
          filename: 'reviews.js',
          url: 'https://my-app.example.com/dist/reviews.js'
        },
        {
          filename: 'reviews.css',
          url: 'https://my-app.example.com/dist/reviews.css'
        }
      ]
    }
  })
});
```

## Per-app CSS/JS

There is **no `extension_asset_url` filter**. Apps ship CSS/JS in one of
two ways:

### 1. Inline `<style>` / `<script>` in the `.aqua` template

For small, dynamic CSS that depends on `block.settings.*`, inline a
`<style>` tag inside the template (as shown in the example above).
This is the recommended approach for block-scoped styles.

### 2. Bundled assets (per-merchant disk copy)

Files in `extensions.assets[]` are downloaded and stored under
`extensions/{domainSlug}/{appHandle}/assets/`. They're publicly
served at the same path, so reference them as absolute URLs:

```liquid theme={null}
<link rel="stylesheet" href="/extensions/{{ __domainSlug }}/my-reviews-app/assets/reviews.css">
```

`__domainSlug` is the merchant's store identifier and is the same path
segment the host uses when copying your bundled assets onto disk.

### 3. App embeds (page-wide scripts)

For scripts that should load on every page (analytics, chat widgets,
review widgets that scan the page), use an **app embed** instead of a
block. See [App Embeds](/extensions/app-embeds) — embeds support
`scriptSrc`, `inlineHtml`, and `stylesheetUrl` and are injected by the
host into the rendered HTML.

<Note>
  Block manifests accept `stylesheetUrl` and `scriptUrl` fields, but the
  host **does not currently inject them** when the block renders. If your
  block needs external CSS/JS, declare it as an app embed (option 3) or
  inline it in the `.aqua` template (option 1).
</Note>

### Theme assets (different from extension assets)

For files **inside the merchant's theme** (the theme's `assets/` folder),
use the standard Liquid filters:

* `{{ 'foo.css' | asset_url }}` — theme asset CDN URL
* `{{ 'foo.png' | asset_img_url }}` — theme image asset
* `{{ 'foo.svg' | inline_asset_content }}` — inline SVG content
* `{{ 'foo.pdf' | file_url }}` — theme file CDN URL

## Snippets

Create reusable components as snippets:

```liquid theme={null}
{% comment %} snippets/review-stars.aqua {% endcomment %}

<div class="star-rating" aria-label="{{ rating }} out of 5 stars">
  {% assign full_stars = rating | floor %}
  {% assign half_star = rating | modulo: 1 | round %}
  {% assign empty_stars = 5 | minus: full_stars | minus: half_star %}
  
  {% for i in (1..full_stars) %}
    <span class="star star--full">★</span>
  {% endfor %}
  
  {% if half_star == 1 %}
    <span class="star star--half">★</span>
  {% endif %}
  
  {% for i in (1..empty_stars) %}
    <span class="star star--empty">☆</span>
  {% endfor %}
</div>
```

Then render the snippet from your block:

```liquid theme={null}
{% render 'review-stars', rating: 4.5 %}
```

## Accessing Data

### Block Settings

Access settings via `block.settings`:

```liquid theme={null}
{{ block.settings.title }}
{{ block.settings.show_title }}
{{ block.settings.background_color }}
```

### Global Objects

Standard Liquid theme global objects are available:

```liquid theme={null}
{{ shop.name }}
{{ product.title }}
{{ customer.email }}
{{ cart.item_count }}
```

### Metafields

Access your app's metafields:

```liquid theme={null}
{{ product.metafields.my_app.rating }}
{{ shop.metafields.my_app.settings | json }}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Keep templates performant">
    Avoid complex logic in Liquid. Pre-compute values in your backend and store them in metafields.
  </Accordion>

  <Accordion title="Use semantic HTML">
    Use proper heading levels, ARIA labels, and semantic elements for accessibility.
  </Accordion>

  <Accordion title="Scope your CSS">
    Prefix class names with your app handle to avoid style conflicts with the theme.
  </Accordion>

  <Accordion title="Lazy load JavaScript">
    Use `defer` or dynamic imports for JavaScript to avoid blocking page render.
  </Accordion>

  <Accordion title="Support theme settings">
    Respect the merchant's theme colors and fonts where possible. Use CSS custom properties.
  </Accordion>

  <Accordion title="Test across themes">
    Test your blocks with multiple themes to ensure compatibility with different layouts.
  </Accordion>
</AccordionGroup>

## Example: Featured Products Block

A complete example showing a featured products carousel:

<CodeGroup>
  ```liquid featured-products.aqua theme={null}
  <div class="app-featured-products" data-app="{{ block.settings.app_id }}">
    <div class="featured-header">
      {% if block.settings.show_title %}
        <h2 class="featured-title">{{ block.settings.title }}</h2>
      {% endif %}
      {% if block.settings.show_subtitle %}
        <p class="featured-subtitle">{{ block.settings.subtitle }}</p>
      {% endif %}
    </div>
    
    <div class="featured-grid" style="--columns: {{ block.settings.columns }}">
      {% assign collection = collections[block.settings.collection] %}
      {% for product in collection.products limit: block.settings.limit %}
        <div class="featured-product">
          <a href="{{ product.url }}">
            <img 
              src="{{ product.featured_image | image_url: width: 400 }}" 
              alt="{{ product.title }}"
              loading="lazy"
            >
            <h3>{{ product.title }}</h3>
            <p class="price">{{ product.price | money }}</p>
          </a>
          <button class="add-to-cart" data-variant="{{ product.first_available_variant.id }}">
            Add to Cart
          </button>
        </div>
      {% endfor %}
    </div>
  </div>

  <style>
    .app-featured-products {
      padding: {{ block.settings.padding }}px 0;
    }
    .featured-grid {
      display: grid;
      grid-template-columns: repeat(var(--columns), 1fr);
      gap: 20px;
    }
    @media (max-width: 768px) {
      .featured-grid {
        grid-template-columns: repeat(2, 1fr);
      }
    }
  </style>
  ```

  ```json featured-products.schema.json theme={null}
  {
    "name": "Featured Products",
    "target": ["index", "collection", "product"],
    "settings": [
      {
        "type": "text",
        "id": "title",
        "label": "Title",
        "default": "Featured Products"
      },
      {
        "type": "checkbox",
        "id": "show_title",
        "label": "Show Title",
        "default": true
      },
      {
        "type": "text",
        "id": "subtitle",
        "label": "Subtitle",
        "default": ""
      },
      {
        "type": "checkbox",
        "id": "show_subtitle",
        "label": "Show Subtitle",
        "default": false
      },
      {
        "type": "collection",
        "id": "collection",
        "label": "Collection"
      },
      {
        "type": "range",
        "id": "limit",
        "label": "Products to Show",
        "min": 2,
        "max": 12,
        "step": 1,
        "default": 4
      },
      {
        "type": "range",
        "id": "columns",
        "label": "Columns",
        "min": 2,
        "max": 6,
        "step": 1,
        "default": 4
      },
      {
        "type": "range",
        "id": "padding",
        "label": "Vertical Padding",
        "min": 0,
        "max": 100,
        "step": 10,
        "default": 40,
        "unit": "px"
      }
    ]
  }
  ```
</CodeGroup>
