# Obrolo HTML-first Theme Reference for LLMs

Version: 2026-09-09

Canonical human documentation: https://obrolo.com/docs

This document is generated from the same typed reference inventory used by the public documentation and its parity tests. Exact identifiers are case-sensitive. Do not infer undocumented fields from ORM models or other ecommerce platforms.

## 1. Platform model and safety boundary

Obrolo themes contain bounded HTML and static Tailwind classes. The versioned server-side compiler validates the source and emits immutable Storefront V2 artifacts. Custom JavaScript and custom CSS are forbidden. Do not emit `script`, `style`, `iframe`, `object`, `embed`, `base`, external form actions, inline `style`, `srcdoc`, `on*` event attributes, unsafe URLs, or dynamic Tailwind class placeholders.

Commerce mutations must use documented `data-ob-action` forms. The compiler injects the same-origin endpoint, HTTP method, release identifier, redirect target, and session-bound action token. Never add or forge `ob_action_token`, `ob_release`, or action endpoint values.

## 2. Required page set

- `home`
- `collection`
- `product`
- `search`
- `content`
- `not-found`
- `cart`
- `checkout`

Every page source must be a complete document with doctype, one `html`, one `head`, one `body`, one `main`, and a viewport meta element. The `cart` and `checkout` pages are request-time private HTML. Other page types are publish-time artifacts; collection and search also use the query-aware public catalog renderer.

## 3. Custom editor inputs

Input types:

- `text`
- `textarea`
- `image`
- `url`
- `boolean`
- `number`
- `category`
- `products`

A placeholder without a dot, such as `{hero-title}`, creates an editor input. Its key must match `^[a-z][a-z0-9-]*$`. Type inference uses the binding target; use `data-ob-input-type` only when an explicit type is needed. Reusing one key with conflicting types fails compilation.

## 4. Shared sections and composition

Supported composition versions:

- `html-theme-composition-v0`
- `html-theme-composition-v1`

Fragment keys must match `^[a-z][a-z0-9-]{0,39}$`. A page includes a shared section with an explicitly closed element such as `<ob-include name="header"></ob-include>`. Includes may only be direct children of `body` or `main`, and a fragment may appear once per page. Fragment input keys should be namespaced, for example `header-promo-text`.

Fragment example:

```html
<header class="border-b border-stone-200 bg-white">
  <div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
    <a href="{global.routes.home}" class="font-black">{global.site.name}</a>
    <p class="text-sm text-stone-600">{header-promo-text}</p>
    <a href="{global.routes.cart}" class="font-medium">
      Sepet <span data-ob-cart-count></span>
    </a>
  </div>
</header>
```

Page include example:

```html
<!doctype html>
<html lang="{store.locale}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{page.seo.title}</title>
  </head>
  <body class="bg-white text-stone-950">
    <ob-include name="header"></ob-include>
    <main>
      <h1>{page.title}</h1>
    </main>
  </body>
</html>
```

## 5. Preview and internal navigation

- İç rotalarda global.routes.*, link.href, child.href, product.url, page.product.url ve platformun sağladığı diğer URL binding’lerini kullanın.
- Sabit bir iç rota gerekiyorsa /collections/all gibi aynı-origin ve / ile başlayan göreli URL yazın.
- Mağazanın production domain’ini HTML içine sabitlemeyin; farklı hosta giden bağlantılar preview token’ını korumaz.
- Preview runtime aynı hosta çözülen iç bağlantıları /__ob/html-theme/preview/{token}/... biçimine güvenli şekilde çevirir.
- Önizleme token’ı 30 dakika geçerlidir; süresi dolduğunda admin’den yeni önizleme başlatın.

Correct examples: `href="{global.routes.home}"`, `href="{link.href}"`, `href="{product.url}"`, and `href="/collections/all"`.

Incorrect example: a hard-coded merchant URL such as `href="https://merchant.example/collections/all"`.

## 6. Platform fields

Fields missing from the active route/view-model render as an empty string. Presence in this inventory does not guarantee a value on every page.

- `global.site.name` — text
- `global.site.title` — text
- `global.site.description` — text
- `global.site.logo` — image
- `global.site.logo.alt` — text
- `global.site.homeUrl` — url
- `global.footer.description` — text
- `global.footer.copyright` — text
- `global.routes.home` — url
- `global.routes.search` — url
- `global.routes.cart` — url
- `global.routes.account` — url
- `global.announcement.enabled` — boolean
- `store.name` — text
- `store.locale` — text
- `store.direction` — text
- `store.logo` — image
- `store.homeUrl` — url
- `theme.id` — text
- `theme.version` — text
- `page.title` — text
- `page.seoTitle` — text
- `page.seoDescription` — text
- `page.seo.title` — text
- `page.seo.description` — text
- `page.seo.canonicalUrl` — url
- `page.seo.robots` — text
- `page.hero.title` — text
- `page.body` — safe-html
- `page.message` — text
- `page.query` — text
- `page.homeLink.label` — text
- `page.homeLink.href` — url
- `page.catalogIndexUrl` — url
- `page.clearFiltersUrl` — url
- `page.selectedSort` — text
- `page.productPage.total` — number
- `page.productPage.page` — number
- `page.productPage.hasItems` — boolean
- `page.productPage.hasNoItems` — boolean
- `page.productPage.hasPrevious` — boolean
- `page.productPage.hasNext` — boolean
- `page.productPage.previousUrl` — url
- `page.productPage.nextUrl` — url
- `page.featuredProducts.hasItems` — boolean
- `page.featuredProducts.hasNoItems` — boolean
- `page.product.id` — text
- `page.product.slug` — text
- `page.product.url` — url
- `page.product.title` — text
- `page.product.descriptionHtml` — safe-html
- `page.product.image` — image
- `page.product.price.current` — text
- `page.product.price.compareAt` — text
- `page.product.inStock` — boolean
- `page.product.onSale` — boolean
- `page.product.defaultVariantId` — text
- `page.product.detailSummary.title` — text
- `page.product.detailSummary.copy` — text
- `page.product.hasDetailSummary` — boolean
- `page.product.hasNoDetailSummary` — boolean
- `page.product.hasDetailSummaryTitle` — boolean
- `page.product.hasNoDetailSummaryTitle` — boolean
- `page.product.hasDetailSummaryCopy` — boolean
- `page.product.hasNoDetailSummaryCopy` — boolean
- `page.product.hasDetailSpecs` — boolean
- `page.product.hasNoDetailSpecs` — boolean
- `page.product.hasDetailRows` — boolean
- `page.product.hasServiceItems` — boolean
- `page.product.badgeLabel` — text
- `page.product.detailTitle` — text
- `page.product.storyTitle` — text
- `page.product.faqTitle` — text
- `page.product.recommendationsTitle` — text
- `page.product.hasFeatures` — boolean
- `page.product.hasPolicyItems` — boolean
- `page.product.hasStoryBlocks` — boolean
- `page.product.hasSupportItems` — boolean
- `page.product.hasFaqs` — boolean
- `page.product.hasTestimonials` — boolean
- `page.product.hasPurchaseSignals` — boolean
- `page.product.hasComparison` — boolean
- `page.product.hasProof` — boolean
- `page.product.hasPrelude` — boolean
- `page.product.hasCampaign` — boolean
- `page.product.hasSpotlights` — boolean
- `page.product.hasSocial` — boolean
- `page.product.hasNewsletter` — boolean
- `page.product.prelude.title` — text
- `page.product.prelude.copy` — text
- `page.product.prelude.image` — image
- `page.product.prelude.hasImage` — boolean
- `page.product.proof.title` — text
- `page.product.proof.copy` — text
- `page.product.proof.disclaimer` — text
- `page.product.testimonials.title` — text
- `page.product.testimonials.subtitle` — text
- `page.product.campaign.title` — text
- `page.product.campaign.copy` — text
- `page.product.campaign.ctaLabel` — text
- `page.product.campaign.image` — image
- `page.product.campaign.hasImage` — boolean
- `page.product.comparison.title` — text
- `page.product.comparison.standardLabel` — text
- `page.product.comparison.productLabel` — text
- `page.product.newsletter.title` — text
- `page.product.newsletter.copy` — text
- `page.product.newsletter.placeholder` — text
- `page.product.newsletter.buttonLabel` — text
- `page.product.social.title` — text
- `page.product.vendor` — text
- `page.product.hasVendor` — boolean
- `page.product.hasNoVendor` — boolean
- `page.product.sku` — text
- `page.product.hasSku` — boolean
- `page.product.badgesLabel` — text
- `page.product.hasBadges` — boolean
- `page.product.hasNoBadges` — boolean
- `page.product.storyImage` — image
- `page.product.galleryCount` — number
- `page.product.hasMultipleImages` — boolean
- `page.product.sizeGuide` — image
- `page.product.hasSizeGuide` — boolean
- `page.product.colorOptionCount` — number
- `page.product.colorOptionsLabel` — text
- `page.product.sizeOptionCount` — number
- `page.product.sizeOptionsLabel` — text
- `page.product.outOfStock` — boolean
- `page.collection.id` — text
- `page.collection.title` — text
- `page.collection.descriptionHtml` — safe-html
- `page.collection.url` — url
- `product.id` — text
- `product.slug` — text
- `product.url` — url
- `product.title` — text
- `product.descriptionHtml` — safe-html
- `product.image` — image
- `product.price` — number
- `product.priceFormatted` — text
- `product.compareAtPrice` — number
- `product.compareAtPriceFormatted` — text
- `product.inStock` — boolean
- `product.onSale` — boolean
- `product.defaultVariantId` — text
- `product.hasImage` — boolean
- `product.hasNoImage` — boolean
- `product.vendor` — text
- `product.hasVendor` — boolean
- `product.hasNoVendor` — boolean
- `product.hasBadge` — boolean
- `product.primaryBadge` — text
- `product.outOfStock` — boolean
- `product.hasDiscount` — boolean
- `product.discountLabel` — text
- `media.src` — image
- `media.alt` — text
- `media.width` — number
- `media.height` — number
- `category.id` — text
- `category.title` — text
- `category.description` — text
- `category.url` — url
- `collection.id` — text
- `collection.title` — text
- `collection.description` — text
- `collection.url` — url
- `search.query` — text
- `search.title` — text
- `cart.id` — text
- `cart.itemCount` — number
- `cart.hasItems` — boolean
- `cart.hasNoItems` — boolean
- `cart.subtotalFormatted` — text
- `cart.shippingFormatted` — text
- `cart.totalFormatted` — text
- `customer.loggedIn` — boolean
- `customer.firstName` — text
- `checkout.legal.version` — text
- `checkout.legal.preInformationUrl` — url
- `checkout.legal.distanceSalesUrl` — url
- `checkout.legal.privacyUrl` — url
- `checkout.payment.method` — text
- `pagination.currentPage` — number
- `pagination.totalPages` — number
- `pagination.previousUrl` — url
- `pagination.nextUrl` — url
- `item.id` — text
- `item.lineKey` — text
- `item.title` — text
- `item.quantity` — number
- `item.image` — image
- `item.url` — url
- `item.priceFormatted` — text
- `item.totalFormatted` — text
- `item.selectionsLabel` — text
- `variant.id` — text
- `variant.title` — text
- `variant.priceFormatted` — text
- `variant.available` — boolean
- `link.label` — text
- `link.href` — url
- `link.title` — text
- `link.hasChildren` — boolean
- `link.isLeaf` — boolean
- `child.label` — text
- `child.href` — url
- `child.title` — text
- `child.hasChildren` — boolean
- `child.isLeaf` — boolean
- `grandchild.label` — text
- `grandchild.href` — url
- `grandchild.title` — text
- `announcement.text` — text
- `announcement.href` — url
- `announcement.hasLink` — boolean
- `filter.label` — text
- `filter.value` — text
- `filter.count` — number
- `filter.id` — text
- `filter.parameter` — text
- `filter.control` — text
- `filter.min` — number
- `filter.max` — number
- `filter.selectedMin` — number
- `filter.selectedMax` — number
- `filter.isRange` — boolean
- `filter.isColor` — boolean
- `filter.isOptions` — boolean
- `value.value` — text
- `value.label` — text
- `value.count` — number
- `value.selected` — boolean
- `value.selectedEnabled` — boolean
- `value.selectedDisabled` — boolean
- `value.unselectedEnabled` — boolean
- `value.unselectedDisabled` — boolean
- `value.disabled` — boolean
- `value.enabled` — boolean
- `option.id` — text
- `option.name` — text
- `option.label` — text
- `option.value` — text
- `option.selected` — boolean
- `option.unselected` — boolean
- `spec.label` — text
- `spec.value` — text
- `row.title` — text
- `row.copy` — text
- `row.image` — image
- `row.hasImage` — boolean
- `service.title` — text
- `service.copy` — text
- `feature.icon` — text
- `feature.title` — text
- `feature.copy` — text
- `policy.title` — text
- `policy.copy` — text
- `story.icon` — text
- `story.title` — text
- `story.copy` — text
- `story.image` — image
- `story.hasImage` — boolean
- `story.ctaLabel` — text
- `story.hasCta` — boolean
- `support.icon` — text
- `support.image` — image
- `support.hasImage` — boolean
- `support.title` — text
- `support.copy` — text
- `faq.question` — text
- `faq.answer` — text
- `testimonial.author` — text
- `testimonial.rating` — number
- `testimonial.copy` — text
- `testimonial.image` — image
- `testimonial.hasImage` — boolean
- `signal.icon` — text
- `signal.lead` — text
- `signal.highlight` — text
- `signal.tail` — text
- `comparison.label` — text
- `comparison.standard` — boolean
- `comparison.product` — boolean
- `stat.value` — text
- `stat.copy` — text
- `spotlight.eyebrow` — text
- `spotlight.title` — text
- `spotlight.copy` — text
- `spotlight.author` — text
- `spotlight.location` — text
- `spotlight.image` — image
- `spotlight.hasImage` — boolean
- `breadcrumb.label` — text
- `breadcrumb.href` — url
- `breadcrumb.current` — boolean
- `page.contactForm.submitLabel` — text
- `page.hasContactForm` — boolean
- `page.product.campaign.hasImageOnly` — boolean
- `page.product.campaign.hasVideo` — boolean
- `page.product.campaign.video` — image
- `page.product.faqImage` — image
- `page.product.featureComposition.eyebrow` — text
- `page.product.featureComposition.subtitle` — text
- `page.product.featureComposition.title` — text
- `page.product.hasFaqImage` — boolean
- `page.product.hasFeatureComposition` — boolean
- `page.product.hasMarqueeItems` — boolean
- `page.product.hasRitual` — boolean
- `page.product.hasShowcase` — boolean
- `page.product.hasShowcaseImages` — boolean
- `page.product.hasShowcaseVideos` — boolean
- `page.product.hasSocialChips` — boolean
- `page.product.hasSocialImages` — boolean
- `page.product.hasSocialVideos` — boolean
- `page.product.hasStandaloneFeatures` — boolean
- `page.product.prelude.ctaLabel` — text
- `page.product.ritual.background` — image
- `page.product.ritual.eyebrow` — text
- `page.product.showcase.title` — text
- `policy.hasLink` — boolean
- `policy.href` — url
- `policy.linkLabel` — text
- `product.hasColorSwatches` — boolean
- `spotlight.avatar` — image
- `spotlight.hasAvatar` — boolean
- `story.ctaHref` — url
- `story.hasImageOnly` — boolean
- `story.hasSecondaryImage` — boolean
- `story.hasVideo` — boolean
- `story.secondaryImage` — image
- `story.video` — image
- `swatch.hasImage` — boolean
- `swatch.hasNoImage` — boolean
- `swatch.image` — image
- `swatch.label` — text
- `swatch.value` — text
- `release.id` — text
- `release.themeId` — text
- `release.themeVersion` — text

## 7. Loop sources and required aliases

- `products` as `product`
- `featuredProducts` as `product`
- `category.products` as `product`
- `collection.products` as `product`
- `search.results` as `product`
- `product.colorSwatches` as `swatch`
- `product.gallery` as `media`
- `product.variants` as `variant`
- `page.relatedProducts` as `product`
- `page.filterSchema` as `filter`
- `filter.values` as `value`
- `page.sortOptions` as `option`
- `page.breadcrumbs` as `breadcrumb`
- `page.product.breadcrumbs` as `breadcrumb`
- `page.product.optionDefinitions` as `option`
- `page.product.detailSpecs` as `spec`
- `page.product.detailRows` as `row`
- `page.product.serviceItems` as `service`
- `page.product.features` as `feature`
- `page.product.policyItems` as `policy`
- `page.product.storyBlocks` as `story`
- `page.product.supportItems` as `support`
- `page.product.faqs` as `faq`
- `page.product.testimonials` as `testimonial`
- `page.product.purchaseSignals` as `signal`
- `page.product.comparisonRows` as `comparison`
- `page.product.proofStats` as `stat`
- `page.product.spotlights` as `spotlight`
- `page.product.socialImages` as `media`
- `page.product.socialVideos` as `media`
- `page.product.socialChips` as `announcement`
- `page.product.marqueeItems` as `announcement`
- `page.product.featureCompositionImages` as `media`
- `page.product.showcaseImages` as `media`
- `page.product.showcaseVideos` as `media`
- `page.product.showcaseItems` as `feature`
- `option.values` as `value`
- `cart.items` as `item`
- `page.productPage.items` as `product`
- `global.navigation.header` as `link`
- `global.navigation.footer` as `link`
- `global.announcement.messages` as `announcement`
- `link.children` as `child`
- `child.children` as `grandchild`

Maximum loop nesting depth: 3. Maximum rendered items per loop: 50.

## 8. Allowed data-ob attributes

- `data-ob-section`
- `data-ob-section-label`
- `data-ob-component`
- `data-ob-section-hidden`
- `data-ob-bind`
- `data-ob-if`
- `data-ob-each`
- `data-ob-navigation-items`
- `data-ob-source-overrides`
- `data-ob-as`
- `data-ob-action`
- `data-ob-sort`
- `data-ob-limit`
- `data-ob-category`
- `data-ob-product-ids`
- `data-ob-input-type`
- `data-ob-cart-count`
- `data-ob-add-to-cart`
- `data-ob-variant-price`
- `data-ob-cart-status`
- `data-ob-mega-menu`
- `data-ob-mega-trigger`
- `data-ob-mega-panel`
- `data-ob-product-gallery`
- `data-ob-gallery-track`
- `data-ob-gallery-slide`
- `data-ob-gallery-status`
- `data-ob-gallery-empty`
- `data-ob-gallery-previous`
- `data-ob-gallery-next`
- `data-ob-filter-form`
- `data-ob-catalog-index`
- `data-ob-collection-id`
- `data-ob-products`
- `data-ob-result-count`
- `data-ob-empty-state`
- `data-ob-filter-key`
- `data-ob-filter-value`
- `data-ob-filter-count`
- `data-ob-card-class`
- `data-ob-sort-control`
- `data-ob-quick-add`
- `data-ob-quick-add-trigger`
- `data-ob-quick-add-panel`
- `data-ob-quick-add-title`
- `data-ob-quick-add-options`
- `data-ob-quick-add-close`
- `data-ob-quick-add-status`
- `data-ob-responsive-filter`
- `data-ob-product-card-swatches`
- `data-ob-product-card-swatch`
- `data-ob-contact-form`
- `data-ob-form-status`

Unknown `data-ob-*` attributes fail compilation.

### Navigation and content source selections

The shared navigation editor stores a literal JSON array in `data-ob-navigation-items` on a `global.navigation.header` or `global.navigation.footer` loop. Each entry has `label`, `href`, and optional `children`. Limits are 3 levels, 50 entries per list, and 200 entries in total; labels and link destinations are validated. An empty array intentionally renders no links; removing the attribute restores the automatic menu. Placement inside another loop, a form, or a commerce action is rejected. A shared fragment's source changes affect every page that includes it.

The content source editor stores at most 32 literal selections per element in `data-ob-source-overrides`. A selection targets an existing authored binding slot: `target` is `text`, `href`, `src`, or `alt`; `bindingIndex` identifies its binding, and text slots also require `textIndex`. `mode: "manual"` supplies a safe literal `value`; `mode: "platform"` supplies an allowlisted, type-compatible `binding`. Only `page.product.image` accepts `imageIndex`, a one-based gallery index from 1 to 24; a missing image renders empty. The original binding remains in source and becomes active again when its override is removed. These are theme source choices, not catalog or locale-value mutations. Loop, form, commerce, and protected control boundaries cannot be overridden. Let the editor generate slot metadata for the selected element instead of copying indices between elements.

Both attributes are validated during server compilation and IR loading, resolved by the platform renderer, and omitted from the rendered HTML attributes. They do not enable custom scripts or an external runtime.

### Visual section metadata and component instances

HTML/Tailwind source remains authoritative. Visual operations patch selected source ranges, preserving unrelated HTML, comments, and formatting. The editor can reorder safe sections within the same parent and insert, duplicate, remove, or hide sections where supported. Loop, form, commerce, and shared-fragment boundaries restrict available operations; arbitrary elements are not universally movable. Changes must pass the server compiler and remain in the draft until publication.

These four attributes accept static values only on `section`, `article`, `div`, `header`, or `footer`:

- `data-ob-section="faq-main"`: stable section ID matching `^[a-z][a-z0-9-]{0,119}$`; unique across the effective page including shared fragments.
- `data-ob-section-label="Frequently asked questions"`: a non-empty trimmed label, at most 120 characters, without control characters or braces.
- `data-ob-component="htc-faq-editable-accordion@1"`: pinned component identity matching `^[a-z][a-z0-9-]{0,79}@[1-9][0-9]{0,5}$`. This is metadata for an inserted HTML/Tailwind copy, not a code loader or external runtime.
- `data-ob-section-hidden="true"`: omit the section from live rendering while keeping it accessible in the editor preview. Remove the attribute to show the section; `false` is invalid. A section containing forms, commerce actions, or shared includes cannot be hidden.

The label, component, and hidden attributes require `data-ob-section` on the same element. Existing source without metadata remains supported; the visual editor assigns a stable ID to a recognized safe section on its first mutation. Never author `data-ob-preview-*` attributes; the platform generates preview markers.

Ready-made FAQ and product-description designs are inserted as copies of a pinned published library version. Replacing a design preserves matching content fields only when their names and types agree, as well as the section identity, visibility, and color overrides. Duplication creates independent section, HTML, and input identities. Preserve platform product bindings when changing a data-driven product design.

### Theme palette and section color overrides

The palette uses `#RRGGBB` values for background, text, and accent. Store them as static Tailwind classes: `[--ob-color-background:#ffffff]`, `[--ob-color-text:#0f172a]`, and `[--ob-color-accent:#047857]`. Consume them using classes such as `bg-[var(--ob-color-background,#ffffff)]`, `text-[color:var(--ob-color-text,#0f172a)]`, and `text-[color:var(--ob-color-accent,#047857)]`.

The theme palette writes these variables to the body scope of all eight pages; shared fragments and sections inherit them unless overridden locally. A section palette overrides only that section's scope. Existing fixed-color descendant classes are not automatically converted: palette changes affect styles that consume these variables or inherit the resulting color. Resetting a section color to the theme removes the corresponding local variable and the editor's helper utility. Palette values cannot contain URLs or CSS fragments. This uses server-compiled static Tailwind classes, not raw CSS, style attributes, custom JavaScript, or dynamic class placeholders.

## 9. Static theme sorting versus visitor catalog sorting

Static compiler sort values for `data-ob-sort`:

- `best-selling`
- `newest`
- `oldest`
- `price-asc`
- `price-desc`
- `title-asc`
- `title-desc`

Visitor-controlled catalog query sort values:

- `relevance`
- `best_selling`
- `newest`
- `oldest`
- `price_asc`
- `price_desc`
- `title_asc`
- `title_desc`

Query parameters:

- `q` — Arama metni; en fazla 120 karakter.
- `sort` — Yayınlanmış allowlist içindeki sıralama anahtarı.
- `page` — 1 tabanlı sayfa; varsayılan 1.
- `pageSize` — Sayfa başına ürün; public gateway üst sınırı 48.
- `f.price.min` — Genel minimum fiyat; para biriminin ana biriminde.
- `f.price.max` — Genel maksimum fiyat; para biriminin ana biriminde.
- `filter.parameter` — Kategoriye yayınlanan dinamik parametre; ör. f.fiyat veya f.renk.

When `page.filterSchema` publishes a filter, `filter.parameter` is authoritative. For a range use `{filter.parameter}.min` and `{filter.parameter}.max`. Do not invent tenant-specific keys such as `f.renk`; render the published parameter.

Complete collection filter example:

```html
<!doctype html>
<html lang="{store.locale}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{page.seo.title}</title>
  </head>
  <body>
    <main>
      <h1>{page.collection.title}</h1>
      <form action="" method="get" data-ob-filter-form data-ob-catalog-index="{page.catalogIndexUrl}" data-ob-collection-id="{page.collection.id}">
        <span data-ob-result-count>{page.productPage.total} ürün</span>
        <fieldset data-ob-each="page.filterSchema" data-ob-as="filter">
          <legend>{filter.label}</legend>
          <div data-ob-if="filter.isRange">
            <label>Minimum <input type="number" name="{filter.parameter}.min" min="{filter.min}" max="{filter.max}" value="{filter.selectedMin}"></label>
            <label>Maksimum <input type="number" name="{filter.parameter}.max" min="{filter.min}" max="{filter.max}" value="{filter.selectedMax}"></label>
          </div>
          <label data-ob-if="filter.isOptions" data-ob-each="filter.values" data-ob-as="value">
            <input type="checkbox" name="{filter.parameter}" value="{value.value}" data-ob-filter-key="{filter.parameter}" data-ob-filter-value="{value.value}">
            {value.label} <span data-ob-filter-count>({value.count})</span>
          </label>
        </fieldset>
        <label>
          Sırala
          <select name="sort" data-ob-sort-control>
            <option data-ob-each="page.sortOptions" data-ob-as="option" data-ob-if="option.selected" value="{option.value}" selected>{option.label}</option>
            <option data-ob-each="page.sortOptions" data-ob-as="option" data-ob-if="option.unselected" value="{option.value}">{option.label}</option>
          </select>
        </label>
        <button type="submit">Uygula</button>
        <section data-ob-products data-ob-card-class="sf-product-card">
          <article class="sf-product-card" data-ob-each="products" data-ob-as="product">
          <a href="{product.url}"><h2>{product.title}</h2><span>{product.priceFormatted}</span></a>
          </article>
        </section>
        <section data-ob-empty-state hidden>Bu filtreye uygun ürün bulunamadı.</section>
      </form>
    </main>
  </body>
</html>
```

## 10. Commerce actions

| Action | Platform endpoint | Page | Required contract |
| --- | --- | --- | --- |
| `cart.add` | `/v1/storefront/actions/cart/add` | product | productSlug={page.product.slug} veya {product.slug}; quantity |
| `cart.update` | `/v1/storefront/actions/cart/update` | cart | lineKey={item.lineKey}; quantity={item.quantity} |
| `cart.remove` | `/v1/storefront/actions/cart/remove` | cart | lineKey={item.lineKey} |
| `checkout.start` | `/v1/storefront/actions/checkout/start` | checkout | İletişim, teslimat ve compiler tarafından zorunlu tutulan yasal onay alanları |

Do not add `method` or `action` to these forms. The compiler injects them.

## 11. Checkout field contract

| Input name | Requirement | Validation / source |
| --- | --- | --- |
| `contact.email` | Her zaman | Geçerli e-posta, en fazla 180 karakter |
| `contact.firstName` | Her zaman | 1-80 karakter |
| `contact.lastName` | Her zaman | 1-80 karakter |
| `contact.phone` | Her zaman | 6-30 karakter |
| `shipping.line1` | Her zaman | 2-180 karakter |
| `shipping.line2` | Opsiyonel | En fazla 180 karakter |
| `shipping.city` | Her zaman | 2-100 karakter |
| `shipping.region` | Her zaman | 2-100 karakter |
| `shipping.postal` | Opsiyonel | En fazla 30 karakter |
| `shipping.country` | Her zaman | 2-80 karakter; Türkiye için TR |
| `billing.sameAsShipping` | Opsiyonel | true, false, 1 veya 0; verilmezse true |
| `billing.firstName` | Koşullu | Farklı fatura adresinde dolu, en fazla 80 karakter |
| `billing.lastName` | Koşullu | Farklı fatura adresinde dolu, en fazla 80 karakter |
| `billing.phone` | Koşullu | Farklı fatura adresinde dolu, en fazla 30 karakter |
| `billing.line1` | Koşullu | Farklı fatura adresinde dolu, en fazla 180 karakter |
| `billing.line2` | Opsiyonel | En fazla 180 karakter |
| `billing.city` | Koşullu | Farklı fatura adresinde dolu, en fazla 100 karakter |
| `billing.region` | Koşullu | Farklı fatura adresinde dolu, en fazla 100 karakter |
| `billing.postal` | Opsiyonel | En fazla 30 karakter |
| `billing.country` | Koşullu | Farklı fatura adresinde dolu, en fazla 80 karakter |
| `shippingSelection.quoteId` | Birlikte opsiyonel | En fazla 200 karakter; güncel teklifin üst seviye quoteId değeri |
| `shippingSelection.serviceCode` | Birlikte opsiyonel | En fazla 120 karakter; seçilen services öğesinden |
| `shippingSelection.amount` | Birlikte opsiyonel | Negatif olmayan sayı; seçilen services öğesinden birebir |
| `payment.method` | Opsiyonel | {checkout.payment.method}; aktif sağlayıcıyla eşleşir |
| `legal.version` | Her zaman | {checkout.legal.version}; 4-80 karakter |
| `legal.privacyNoticeProvided` | Her zaman | required checkbox, value=true |
| `legal.preInfoAccepted` | Her zaman | required checkbox, value=true |
| `legal.distanceSalesAccepted` | Her zaman | required checkbox, value=true |

When `billing.sameAsShipping` is omitted, `true`, or `1`, shipping is used as billing. When it is `false` or `0`, first name, last name, phone, line1, city, region, and country are required. `line2` and `postal` remain optional.

Always bind `payment.method` to `{checkout.payment.method}`; do not hard-code a provider. Always bind `legal.version` to `{checkout.legal.version}`. All three legal consent controls must be required checkboxes with `value="true"`.

Complete supported checkout page:

```html
<!doctype html>
<html lang="{store.locale}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{page.seo.title}</title>
  </head>
  <body class="bg-white text-slate-950">
    <main class="mx-auto max-w-3xl px-4 py-10">
      <h1 class="text-3xl font-bold">Ödeme</h1>
      <form data-ob-action="checkout.start" class="mt-8 space-y-6">
        <fieldset class="grid gap-3 sm:grid-cols-2">
          <legend class="font-bold">İletişim</legend>
          <input type="email" name="contact.email" autocomplete="email" placeholder="E-posta" maxlength="180" required>
          <input type="text" name="contact.firstName" autocomplete="given-name" placeholder="Ad" maxlength="80" required>
          <input type="text" name="contact.lastName" autocomplete="family-name" placeholder="Soyad" maxlength="80" required>
          <input type="tel" name="contact.phone" autocomplete="tel" placeholder="Telefon" minlength="6" maxlength="30" required>
        </fieldset>

        <fieldset class="grid gap-3 sm:grid-cols-2">
          <legend class="font-bold">Teslimat adresi</legend>
          <input type="text" name="shipping.line1" autocomplete="address-line1" placeholder="Adres" maxlength="180" required>
          <input type="text" name="shipping.line2" autocomplete="address-line2" placeholder="Apartman, daire vb." maxlength="180">
          <input type="text" name="shipping.postal" autocomplete="postal-code" placeholder="Posta kodu" maxlength="30">
          <input type="text" name="shipping.city" autocomplete="address-level2" placeholder="Şehir" maxlength="100" required>
          <input type="text" name="shipping.region" autocomplete="address-level1" placeholder="İlçe / bölge" maxlength="100" required>
          <input type="text" name="shipping.country" autocomplete="country" value="TR" maxlength="80" required>
        </fieldset>

        <fieldset>
          <legend class="font-bold">Ödeme yöntemi</legend>
          <label class="mt-2 flex gap-2">
            <input type="radio" name="payment.method" value="{checkout.payment.method}" checked required>
            Güvenli kart ödemesi
          </label>
        </fieldset>

        <fieldset>
          <legend class="font-bold">Fatura adresi</legend>
          <label class="mt-2 flex gap-2">
            <input type="checkbox" name="billing.sameAsShipping" value="false" class="peer">
            Farklı bir fatura adresi kullan
          </label>
          <div class="mt-3 hidden gap-3 peer-checked:grid sm:grid-cols-2">
            <input type="text" name="billing.firstName" autocomplete="billing given-name" placeholder="Fatura adı" maxlength="80">
            <input type="text" name="billing.lastName" autocomplete="billing family-name" placeholder="Fatura soyadı" maxlength="80">
            <input type="tel" name="billing.phone" autocomplete="billing tel" placeholder="Fatura telefonu" maxlength="30">
            <input type="text" name="billing.line1" autocomplete="billing address-line1" placeholder="Fatura adresi" maxlength="180">
            <input type="text" name="billing.line2" autocomplete="billing address-line2" placeholder="Apartman, daire vb." maxlength="180">
            <input type="text" name="billing.postal" autocomplete="billing postal-code" placeholder="Posta kodu" maxlength="30">
            <input type="text" name="billing.city" autocomplete="billing address-level2" placeholder="Şehir" maxlength="100">
            <input type="text" name="billing.region" autocomplete="billing address-level1" placeholder="İlçe / bölge" maxlength="100">
            <input type="text" name="billing.country" autocomplete="billing country" value="TR" maxlength="80">
          </div>
        </fieldset>

        <input type="hidden" name="legal.version" value="{checkout.legal.version}">
        <label class="flex gap-2"><input type="checkbox" name="legal.privacyNoticeProvided" value="true" required> Gizlilik bildirimini okudum.</label>
        <label class="flex gap-2"><input type="checkbox" name="legal.preInfoAccepted" value="true" required> Ön bilgilendirme formunu kabul ediyorum.</label>
        <label class="flex gap-2"><input type="checkbox" name="legal.distanceSalesAccepted" value="true" required> Mesafeli satış sözleşmesini kabul ediyorum.</label>

        <button type="submit" class="w-full rounded-lg bg-blue-600 px-5 py-3 font-bold text-white">Şimdi öde</button>
      </form>
    </main>
  </body>
</html>
```

## 12. Shipping options API

Endpoint: `POST /v1/storefront/shipping/options`

The request is same-origin and uses the current cart session. Request body:

```json
{
  "shipping": {
    "line1": "Bağdat Caddesi No: 10",
    "line2": "Daire 4",
    "city": "İstanbul",
    "region": "Kadıköy",
    "postal": "34710",
    "country": "TR"
  }
}
```

Response body:

```json
{
  "item": {
    "quoteId": "quote_...",
    "expiresAt": "2026-09-04T12:30:00.000Z",
    "services": [
      {
        "serviceCode": "standard",
        "serviceName": "Standart",
        "carrierCode": "YURTICI",
        "carrierName": "Yurtiçi Kargo",
        "estimatedMinDays": 1,
        "estimatedMaxDays": 3,
        "amount": 0,
        "currency": "TRY",
        "isCheapest": true,
        "isFastest": true
      }
    ]
  }
}
```

To submit a choice, copy top-level `item.quoteId` to `shippingSelection.quoteId`, the selected service's `serviceCode` to `shippingSelection.serviceCode`, and that same service's numeric `amount` to `shippingSelection.amount`. Submit all three or none. Refresh the quote after `expiresAt`.

Important bounded-HTML limitation: custom theme JavaScript is forbidden and there is currently no `services[]` platform loop/binding. An HTML-first theme cannot fetch this endpoint and render interactive shipping cards on its own. Use the first-party built-in checkout surface for interactive shipping selection. In a bounded HTML form, include the three selection inputs only when the platform has provided a current quote; never hard-code or guess quote values.

## 13. Payment methods

| Value | Availability | Contract |
| --- | --- | --- |
| `PAYTR` | Canlı mağaza | Kart ödeme akışında /checkout/paytr sayfasına yönlendirir. |
| `MOCK` | Yönetilen demo | Yalnız platformun managed-demo checkout bağlamında yayınlanır. |
| `IYZICO` | Rezerve, kullanım dışı | Şemada geriye dönük uyumluluk için kabul edilir; aktif provider olmadığı sürece gönderilmemelidir. |
| `Havale/EFT` | Desteklenmiyor | checkout.start için geçerli bir payment.method değildir. |

The submitted method must equal the active tenant provider. PAYTR redirects to `/checkout/paytr` after successful checkout initialization. Bank transfer/EFT is not supported.

## 14. Checkout redirects and errors

Action outcomes return through the `ob_action` query parameter. Values are normalized to lowercase in the redirect URL.

- `checkout-started` — Sipariş başlatıldı; PAYTR dışındaki destekli akış checkout sayfasına ob_order ile döner.
- `action_token_invalid` — Form token’ı geçersiz veya oturumla eşleşmiyor.
- `action_release_inactive` — Form eski ya da aktif olmayan bir yayına ait.
- `checkout_consent_required` — Zorunlu yasal onaylardan biri verilmedi.
- `checkout_legal_documents_unavailable` — Checkout için yayınlanmış yasal dokümanlar hazır değil.
- `checkout_privacy_notice_unavailable` — Gizlilik bildirimi hazır değil.
- `checkout_legal_documents_changed` — Form açıldıktan sonra yasal doküman sürümü değişti.
- `shipping_quote_not_found` — Kargo teklif kimliği bulunamadı.
- `shipping_quote_expired` — Kargo teklifi sona erdi; seçenekler yeniden alınmalı.
- `shipping_service_not_found` — Seçilen servis teklifte yok.
- `shipping_selection_amount_mismatch` — Gönderilen tutar seçilen servis tutarıyla eşleşmiyor.
- `billing_address_required` — Farklı fatura adresinin zorunlu alanları eksik.
- `payment_method_unavailable` — Gönderilen ödeme yöntemi mağazanın aktif sağlayıcısıyla eşleşmiyor.

Themes may map only documented values to user-facing messages. Never render an arbitrary query value as trusted HTML.

## 15. Theme lifecycle and publication

| Status | Scope | Meaning |
| --- | --- | --- |
| `DRAFT` | Revision | Düzenlenebilir tema revizyonu. Kaynak ve alan değerleri yalnız bu durumda değiştirilebilir. |
| `PENDING` | Compile | Derleme kuyruğa alındı. Editör durum bilgisini yaklaşık 1,5 saniyede bir otomatik yeniler. |
| `RUNNING` | Compile | Compiler HTML, binding, composition ve Tailwind çıktısını doğruluyor. Sayfa yenilemek gerekmez. |
| `READY` | Compile | Tüm sayfalar, ortak bölümler, IR ve CSS hazır. Önizleme ve yayınlama yapılabilir. |
| `FAILED` | Compile | Derleme durdu. Editördeki satır/sütun diagnostic’lerini düzeltip yeniden kaydedin. |
| `FROZEN` | Revision | Yayın isteği alındı; revizyon immutable olur ve yeni düzenleme için yeni draft hazırlanır. |

While compile status is `PENDING` or `RUNNING`, the Admin editor automatically refetches status about every 1.5 seconds; users do not need to reload the page.

Publication requires: revision status `DRAFT`; compile status `READY`; all eight page keys present; compiled IR for every page; consistent shared fragments; a current compiled CSS artifact; an active publication target; and valid tenant-owned input values. Publishing freezes the revision and enqueues an immutable Storefront V2 publication.

## 16. Safe motion classes

- `animate-ob-marquee`
- `animate-ob-marquee-fast`
- `animate-ob-marquee-slow`
- `animate-ob-wave`
- `animate-ob-wave-fast`
- `animate-ob-wave-slow`
- `ob-motion-reverse`
- `ob-motion-paused`
- `hover:ob-motion-paused`
- `focus-within:ob-motion-paused`
- `peer-checked:ob-motion-paused`

These classes are supplied by the versioned compiler preset and stop automatically for `prefers-reduced-motion`.

## 17. Copyable recipes

### Dolu ve boş sepet görünümü

Sepet ve ödeme sayfalarında cart.hasItems, sepette en az bir satır olduğunda; cart.hasNoItems, sepet boşken true olur. data-ob-if ile ilgili bölümü gösterin. Taslak önizlemesinde sepet boştur; canlıda müşterinin oturumuna bağlı sepet kullanılır.

```html
<section data-ob-if="cart.hasItems">
  <p>Sepetinizde {cart.itemCount} ürün var.</p>
</section>
<section data-ob-if="cart.hasNoItems">
  <p>Sepetiniz henüz boş.</p>
  <a href="{global.routes.home}">Alışverişe başla</a>
</section>
```

Recipe ID: `cart-empty-state`

### Dinamik mega menü + native mobil menü

Header navigasyonunu DB'den okur. Masaüstünde platform runtime'ı; mobilde details/summary kullanır.

```html
<header class="relative border-b border-stone-200 bg-white">
  <nav class="mx-auto hidden max-w-7xl items-center gap-8 px-6 py-4 lg:flex" aria-label="Ana menü">
    <div data-ob-each="global.navigation.header" data-ob-as="link">
      <div data-ob-if="link.hasChildren" data-ob-mega-menu class="relative">
        <button type="button" data-ob-mega-trigger class="inline-flex items-center gap-2 py-2 font-medium">
          {link.label}<span aria-hidden="true">⌄</span>
        </button>
        <div data-ob-mega-panel class="absolute left-0 top-full z-40 min-w-64 rounded-xl border border-stone-200 bg-white p-3 shadow-xl">
          <a
            data-ob-each="link.children"
            data-ob-as="child"
            href="{child.href}"
            title="{child.title}"
            class="block rounded-lg px-3 py-2 hover:bg-stone-100"
          >{child.label}</a>
        </div>
      </div>
      <a data-ob-if="link.isLeaf" href="{link.href}" class="py-2 font-medium">{link.label}</a>
    </div>
  </nav>

  <nav class="space-y-2 px-4 py-3 lg:hidden" aria-label="Mobil menü">
    <div data-ob-each="global.navigation.header" data-ob-as="link">
      <details data-ob-if="link.hasChildren" class="rounded-lg border border-stone-200 px-3 py-2">
        <summary class="cursor-pointer font-medium">{link.label}</summary>
        <div class="mt-2 space-y-1 border-t border-stone-100 pt-2">
          <a
            data-ob-each="link.children"
            data-ob-as="child"
            href="{child.href}"
            class="block rounded-md px-2 py-2 hover:bg-stone-100"
          >{child.label}</a>
        </div>
      </details>
      <a data-ob-if="link.isLeaf" href="{link.href}" class="block rounded-lg px-3 py-2">{link.label}</a>
    </div>
  </nav>
</header>
```

Recipe ID: `dynamic-mega-menu`

### DB duyuru marquee'si + erişilebilir duraklatma

Altı mesaja kadar duyuruyu iki kez render ederek kesintisiz akış kurar; kullanıcı checkbox ile hareketi durdurabilir.

```html
<section data-ob-if="global.announcement.enabled" class="overflow-hidden bg-stone-950 px-4 py-3 text-white">
  <input id="announcement-pause" type="checkbox" class="peer ml-auto h-4 w-4">
  <label for="announcement-pause" class="ml-2 cursor-pointer text-xs">Akışı durdur</label>
  <div class="mt-2 flex w-max animate-ob-marquee peer-checked:ob-motion-paused hover:ob-motion-paused focus-within:ob-motion-paused">
    <div class="shrink-0 pr-10" data-ob-each="global.announcement.messages" data-ob-as="announcement">
      <span class="whitespace-nowrap">{announcement.text}</span>
    </div>
    <div aria-hidden="true" class="shrink-0 pr-10" data-ob-each="global.announcement.messages" data-ob-as="announcement">
      <span class="whitespace-nowrap">{announcement.text}</span>
    </div>
  </div>
</section>
```

Recipe ID: `announcement-marquee`

### Dört katmanlı hareketli footer dalgası

Inline SVG güvenli kalır; hareket her SVG'yi saran platform animasyonlu div üzerinde çalışır.

```html
<footer class="relative mt-24 bg-[#25140c] px-6 pb-12 pt-24 text-stone-100">
  <div aria-hidden="true" class="pointer-events-none absolute inset-x-0 -top-16 h-20 overflow-hidden">
    <div class="absolute -left-[90px] top-0 w-[calc(100%+180px)] animate-ob-wave-slow opacity-25">
      <svg viewBox="0 0 1440 120" class="block h-20 w-full" preserveAspectRatio="none"><path fill="#d6d3d1" d="M0 64C240 16 480 112 720 64s480-48 720 0v56H0Z"></path></svg>
    </div>
    <div class="absolute -left-[90px] top-2 w-[calc(100%+180px)] animate-ob-wave ob-motion-reverse opacity-35">
      <svg viewBox="0 0 1440 120" class="block h-20 w-full" preserveAspectRatio="none"><path fill="#a8a29e" d="M0 54c240 52 480-36 720 10s480 42 720-8v64H0Z"></path></svg>
    </div>
    <div class="absolute -left-[90px] top-5 w-[calc(100%+180px)] animate-ob-wave-fast opacity-55">
      <svg viewBox="0 0 1440 120" class="block h-20 w-full" preserveAspectRatio="none"><path fill="#78716c" d="M0 68c240-38 480 34 720-2s480-30 720 8v46H0Z"></path></svg>
    </div>
    <div class="absolute -left-[90px] top-8 w-[calc(100%+180px)] animate-ob-wave-slow ob-motion-reverse">
      <svg viewBox="0 0 1440 120" class="block h-20 w-full" preserveAspectRatio="none"><path fill="#25140c" d="M0 66c240 38 480-30 720 6s480 34 720-4v52H0Z"></path></svg>
    </div>
  </div>
  <div class="mx-auto max-w-7xl">
    <p class="text-sm text-stone-300">{global.footer.copyright}</p>
  </div>
</footer>
```

Recipe ID: `layered-footer-wave`

### Genel avantaj / kampanya şeridi

Özel editör alanlarıyla yönetilen üç mesajı, ikinci grubun erişilebilirlik ağacından gizlendiği yavaş bir şeritte tekrarlar.

```html
<section class="overflow-hidden border-y border-stone-200 bg-stone-50 py-4">
  <div class="flex w-max animate-ob-marquee-slow hover:ob-motion-paused">
    <div class="flex shrink-0 gap-12 pl-6 pr-12 font-medium">
      <span>{benefit-one}</span><span>{benefit-two}</span><span>{benefit-three}</span>
    </div>
    <div aria-hidden="true" class="flex shrink-0 gap-12 pl-6 pr-12 font-medium">
      <span>{benefit-one}</span><span>{benefit-two}</span><span>{benefit-three}</span>
    </div>
  </div>
</section>
```

Recipe ID: `benefits-marquee`

## 18. Compiler limits

- Source bytes: 262144
- Document nodes: 10000
- Bindings: 2000
- Loop depth: 3
- Loop items: 50
- Announcement messages: 6
- Navigation children: 32
- Shared fragments: 16
- Single fragment source bytes: 65536
- Total fragment source bytes: 524288
- Rendered output bytes: 8388608

## 19. Generation checklist for AI assistants

1. Select one documented page key and return one complete HTML document.
2. Use only exact field, loop, alias, attribute, action, and sort names from this document.
3. Keep all Tailwind classes static strings.
4. Use platform URL bindings for navigation and never hard-code a merchant host.
5. Include the required commerce form on product, cart, and checkout pages.
6. For checkout, bind legal and payment values to their platform fields and preserve conditional billing semantics.
7. Do not invent interactive shipping support in bounded HTML.
8. Do not emit JavaScript, CSS, plugins, external forms, unsafe URLs, or undocumented runtime behavior.
9. Treat compiler diagnostics as authoritative and revise the source until status is `READY`.
