"field_contract": "nested-v1" trong config/theme.json. Field hệ thống giữ ở cấp gốc (item.title, term.name); field tùy chỉnh đọc qua item.fields.{key}, term.fields.{key}. Các field hệ thống như categories vẫn ở cấp gốc dù lưu dạng quan hệ. Section settings, block settings và configs không đổi. Bản release chưa chuyển đổi vẫn dùng mô hình cũ; thay đổi draft chỉ lên runtime sau khi publish.Object products
Lấy sản phẩm trực tiếp trong template từ resource product của store/channel liên kết website. Không fetch catalog API, không cần khách đăng nhập để xem catalog công khai.
get — danh sách
{% set products = products.get(limit=12) %}
{% for product in products %}
<a href="/products/{{ product.slug|urlencode }}">{{ product.name }}</a>
{% for variant in product.variants %}
{{ variant.title }} — {{ variant.price.amount }} {{ variant.price.currency }}
{% endfor %}
{% endfor %}get() mặc định 8 dòng, tối đa 100 (hoặc thấp hơn theo runtime security limit). Trả mảng, không có dữ liệu trả []. Biến products ở vế trái che object sau lệnh set; nếu cần gọi tiếp các hàm, dùng tên items hoặc listing.
find, first, count, exists
{% set product = products.find(slug=slug) %}
{% if not product %}{% abort 404 %}{% endif %}
{{ product.name }}
{% set item = products.find(id="UUID-san-pham") %}
{% set newest = products.first() %}
{% set total = products.count() %}
{% set available = products.exists(q="áo") %}Trong pages/products/[slug].html, slug là biến route. find cần đúng một id hoặc slug; không thấy trả null, nhiều kết quả cùng slug báo lỗi. first trả dòng đầu hoặc null. count trả số nguyên, exists trả boolean. Không tự chọn ngẫu nhiên khi slug trùng.
paginate — phân trang SQL
{% set listing = products.paginate(limit=12, page_key="page") %}
{% for product in listing.items %}
<a href="/products/{{ product.slug|urlencode }}">{{ product.name }}</a>
{% endfor %}
{% if listing.pagination.has_previous %}
<a href="{{ listing.pagination.previous_url }}">Trước</a>
{% endif %}
Trang {{ listing.pagination.page }} / {{ listing.pagination.total_pages }}
{% if listing.pagination.has_next %}
<a href="{{ listing.pagination.next_url }}">Sau</a>
{% endif %}Tên đúng là paginate, không phải pageinate. SQL COUNT/LIMIT/OFFSET, không tải toàn bộ catalog rồi cắt bằng JavaScript. items và records là cùng danh sách. pagination gồm page, per_page, total, total_pages, has_previous, has_next; previous_url/next_url chỉ có khi phù hợp và giữ query khác. Trang vượt cuối được đưa về trang cuối như entries; danh sách rỗng page=1, total_pages=0.
Tham số
| Key | Quy tắc |
|---|---|
| limit | 1–100, mặc định 8; first chỉ lấy 1, find kiểm tra khớp duy nhất. |
| id / slug | Lọc chính xác; find bắt buộc một trong hai. Slug tối đa 200 bytes, theo locale hiện tại. |
| q | Tìm name/slug theo locale, tối đa 200 bytes. Nếu không truyền, các hàm trừ find lấy ?q= từ URL. |
| order | published_at desc mặc định; hỗ trợ published_at, updated_at, created_at, id với asc/desc. Có id làm thứ tự phụ ổn định. |
| page_key | Chỉ paginate; mặc định page, đọc số trang từ query URL. |
Không nhận type, status, term, store_id, workspace_id, channel_id hoặc tên cột tùy ý. Bộ hàm giống entries nhưng không hứa hỗ trợ mọi bộ lọc CMS. Content type key=product với resource=post không thuộc object này.
Kiểu giá trị trả về
Product: id, name, slug, locale, short_description, description, resource="product", variants, media, terms. Tất cả key public dùng snake_case. description giữ dữ liệu nội dung gốc, không tự đổi key hay render raw HTML. Chưa flatten custom fields trong contract này.
- variants luôn là mảng: id, title, sku, requires_shipping, price. price=null khi không có giá; nếu có: amount, currency, compare_at_amount.
- media luôn là mảng: id, url, name, role, position. Chỉ public media.
- terms luôn là mảng: id, name, slug, taxonomy, field.
- Tiền là integer minor unit: VND đồng, USD cent; không mặc định chia 100. Giá là tham khảo, quote/checkout kiểm tra lại.
Phạm vi và bảo vệ dữ liệu
Scope do runtime lấy từ website: workspace → store/channel active, capability commerce.catalog.read. Chỉ published/public, chưa archive và khả dụng trong channel; quy tắc dùng chung cho list/find/count. Không đọc sản phẩm toàn workspace. Website chưa liên kết store/channel hoặc thiếu quyền sẽ báo lỗi cấu hình, không âm thầm trả dữ liệu khác.
Đọc catalog không cần account session. Khi bấm thêm giỏ vẫn cần đăng nhập, CSRF và API /_post/cart/get, /_post/cart/update. Dùng variant.id, không product.id.
Thay thế API catalog cũ
Đã gỡ GET /_account/catalog và GET /_account/catalog/products/:slug. Không còn hướng dẫn fetch catalog từ browser. Chuyển danh sách sang products.get/paginate, chi tiết sang products.find; tìm kiếm bằng form GET và phân trang bằng link. Các thao tác ghi dùng namespace /_post/; xem Website POST routes.
Xem API thao tác tài khoản & đơn hàng và hướng dẫn dựng trang.
Field tùy chỉnh của sản phẩm
product.fields.{key} chứa field tùy chỉnh đang bật trong content type của store sở hữu. Chỉ hydrate sản phẩm đã được sale channel cho phép đọc; không mở quyền đọc các sản phẩm khác. Các field hệ thống, variants, media và terms giữ nguyên. Field media tùy chỉnh trả mảng UUID.