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

# Theme Structure

> The folders a theme is made of, which template renders which URL, and how custom pages work.

## Folders

A theme has a fixed set of folders. Only these are readable and writable by the CLI, the code editor, and the visual editor:

| Path | Contents |
| - | - |
| `layouts/` | Full HTML documents. Almost always just `master.njk`. |
| `templates/` | One file per page type. The page body, without the `<html>` wrapper. |
| `components/` | Sections a merchant can add and configure in the visual editor. |
| `snippets/` | Reusable partials you call from anywhere. Not visible in the editor. |
| `assets/` | CSS, JS, images, fonts. Served publicly. |
| Theme root | `schema.json`, `settings.json`, `settings.default.json`, `tailwind.config.js` |

Subfolders inside these are not supported. `assets/img/logo.png` will not sync, keep asset files flat in `assets/`.

### Root files

| File | Purpose |
| - | - |
| `schema.json` | Declares which settings and components exist. Generates the visual editor UI. Never reaches your templates. |
| `settings.json` | The merchant's values plus the component composition of every page. This is what your templates read. |
| `settings.default.json` | A pristine copy of the theme's shipped `settings.json`, kept for reference. Not read at render time. |
| `tailwind.config.js` | Tailwind configuration. Its presence is what makes the CLI build your CSS. |

## The four kinds of file

### Layout

A layout is the complete HTML document. It receives the rendered template as `templateContent` and is responsible for the `<head>`, the global components (navbar, footer, and so on), and the script tags.

```njk layouts/master.njk theme={null}
<!DOCTYPE html>
<html lang="en">
  <head>
    {% render_snippet "meta-tags.njk" %}
    <link href="{{ 'built.css' | assetUrl }}" rel="stylesheet"/>
  </head>
  <body class="sa-tpl-{{ templateName }}">
    {% render_component "navbar" %}
    {{ templateContent | safe }}
    {% render_component "footer" %}
  </body>
</html>
```

The layout used for a page comes from `settings.json` (`templates.<key>.layout`), so a theme can ship more than one, but in practice `master` covers everything.

### Template

Templates hold the page body only. Most of them are nothing but the component loop, because the actual content is composed by the merchant in the visual editor:

```njk templates/shop.njk theme={null}
<div class="components">
  {% for componentId in components_order %}
    {% render_component componentId %}
  {% endfor %}
</div>
```

Both `components_order` and the component definitions come from `settings.json` for the current template.

### Component

A component is one addable section. **The filename must match the component type key in `schema.json`**, so `components/hero.njk` pairs with a `hero` entry in `schema.components`.

Components read their own configuration from `properties` and their instance ID from `componentId`:

```njk components/text-block.njk theme={null}
<section id="{{ componentId }}" class="component sa-text-block py-section">
  <div class="wrap">
    {% if properties.title %}
      <h2 class="sa-section-title">{{ properties.title | renderString }}</h2>
    {% endif %}
    {% if properties.text %}
      <div class="sa-text-block-body">{{ properties.text | renderString }}</div>
    {% endif %}
  </div>
</section>
```

<Note>
  A component does not inherit variables set by whatever rendered it. It receives its own `properties`, the page data, and the globals. See [scope rules](/developers/nunjucks#scope-rules).
</Note>

### Snippet

A snippet is a reusable partial that takes keyword arguments. Snippets are not visible in the editor and have no schema entry.

```njk theme={null}
{% render_snippet "product-card.njk", product=product, card_style='flat' %}
```

The `.njk` extension is required in the snippet name. Document a snippet's arguments in a comment at the top, the way the official themes do:

```njk snippets/section-header.njk theme={null}
{#
  Shared section header.
  Args: header_title, header_headline (eyebrow), header_subtitle,
        header_align ('left'|'center'|'right', default 'center')
#}
```

## Required files

For a page to render, three things must exist:

1. An entry for the template key in `settings.json` under `templates`, with a `layout` value.
2. The layout that entry names, in `layouts/`.
3. The template file itself, in `templates/`.

Missing components and snippets do not break the page. They render the literal text `Component not found` or `Snippet not found` in place, which is usually your first clue that a filename and a schema key disagree.

## Templates and routes

Routing is not filename-based. SellAuth resolves each URL to a template name, then renders `templates/<name>.njk`.

| URL | Template |
| - | - |
| `/` | `shop` |
| `/cart` | `cart` |
| `/products` | `products` |
| `/products/{categoryPath}` | `products` |
| `/product/{productId}` | `product` |
| `/feedback` | `feedback` |
| `/status` | `status` |
| `/faq` | `faq` |
| `/terms-of-service` | `legal-page` |
| `/privacy-policy` | `legal-page` |
| `/refund-policy` | `legal-page` |
| `/shipping-policy` | `legal-page` |
| `/cookie-policy` | `legal-page` |
| `/impressum` | `legal-page` |
| `/right-of-withdrawal` | `legal-page` |
| `/blog` | `blog` |
| `/blog/{path}` | `blog-post` |
| `/customer/dashboard` | `customer-dashboard` |
| `/customer/invoices` | `customer-invoices` |
| `/customer/subscriptions` | `customer-subscriptions` |
| `/customer/tickets` | `customer-tickets` |
| `/customer/tickets/{ticketId}` | `customer-ticket` |
| `/customer/balance` | `customer-balance` |
| `/customer/affiliate` | `customer-affiliate` |
| `/customer/reseller` | `customer-reseller` |
| `/maintenance` | `maintenance` |
| Any other path | `custom-page` |

`/terms` redirects to `/terms-of-service`. The current template name is always available as `templateName`, which is how layouts special-case pages:

```njk theme={null}
{% if templateName != 'maintenance' %}
  {% render_component "footer" %}
{% endif %}
```

### Legal pages

The seven [legal pages](/guides/legal-pages) share one route pattern and all render through `templates/legal-page.njk`, with `legal_page` telling the template which one it is. See [Template Variables](/developers/theme-variables#storefront) for its shape. A legal page without content returns 404, unless a custom page uses that slug.

<Note>
  A theme copy without a `legal-page` entry in `settings.json` (every shop copy made before the template shipped, and community themes) keeps rendering terms, privacy and refunds through the older `terms`, `privacy-policy` and `refund-policy` templates, and returns 404 for the four newer pages until the merchant updates the theme. Updating removes the three older templates from the copy. If you maintain a custom theme, add `templates/legal-page.njk` and its `settings.json` entry yourself.
</Note>

### Cookie consent

Every official layout renders `snippets/cookie-consent.njk` just before the `js.cookie` and Alpine script tags, so `window.saConsent` exists before the theme script and before `snippets/script-integrations.njk`. The snippet renders nothing unless `shop.settings.cookie_consent.enabled` is true and the page is not the builder or a preview. Scripts that must wait for consent are written as `<script type="text/plain" data-consent="functional|analytics|marketing">`; the runtime turns them into live scripts when the category is allowed. The theme script checks `window.saConsent.has('marketing')` before storing an affiliate code and `has('analytics')` before storing UTM attribution, and subscribes with `onChange` so a later acceptance still captures them. Any link to `#cookie-settings` reopens the preferences. See the [Cookie Consent guide](/guides/cookie-consent) for the merchant-facing behaviour.

## Custom pages

Merchants can create extra pages with their own slug and their own component composition. All of them render through the single file `templates/custom-page.njk`.

Each custom page gets its own key in `settings.json` (`custom-page-1712345678901`) holding its `name`, `slug`, `layout`, and components. At render time:

* `templateName` is the settings key, for example `custom-page-1712345678901`.
* `custom_page` holds `key`, `name`, and `path`.

So branch on `custom_page.path` if a specific page needs special markup:

```njk templates/custom-page.njk theme={null}
{% if custom_page.path == 'about-us' %}
  {# markup unique to the About Us page #}
{% endif %}

<div class="components">
  {% for componentId in components_order %}
    {% render_component componentId %}
  {% endfor %}
</div>
```

Slugs must match `^[a-z0-9-]+$` and cannot collide with a built-in route. These slugs are reserved:

```txt theme={null}
cart, products, product, terms, terms-of-service, privacy-policy,
refund-policy, shipping-policy, cookie-policy, impressum,
right-of-withdrawal, feedback, status, faq, contact, blog, customer,
trust-profile, checkout-link
```

<Note>
  Custom pages require a qualifying plan. On plans without them, custom page URLs return 404.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.