shop-content-site
Build a NON-ecommerce content website on this multi-tenant content platform via the shop MCP — real-estate listings, restaurant menus, agency portfolios, brochure/affiliate/blog sites. Covers the commerce_enabled toggle (hides ecom admin menus + 404s ecom storefront routes), the storefront `content` Liquid Drop + data-driven url_pattern router + pagination, reusing the built-in blog/pages/menus, making content list/detail pages customizer-editable via .json section templates. Also the recipe for turning a Claude Design (claude.ai/design) HTML/CSS handoff into such a site. Use when building a site that is NOT a shop, or implementing a design handoff as a content site. NOT for a shop with a cart — use shop-builder for that.
Build a website that is not a shop — content/brochure/listing/blog — on this platform. The catalog/cart is turned OFF; the site is driven by the built-in blog/pages/menus plus a custom content type (the Strapi-like engine) for any structured listing.
Read
shop-core-rulesfirst. This skill assumes §A (confirm plan), §B (extract spec), §C (images), §D (verify-on-draft), §E (links), §F (design contract), §G (palette), §H (NO-HARDCODE), §I (durability). It does NOT repeat them. The content-type data model (field types,url_pattern, records, filter/sort grammar, content-type-vs-blocks decision) lives inshop-create-content-type— model the data there, render it here.
Cart-like flows (ordering / multi-item booking / quotes): the base ships WebPick (localStorage picklist,
data-pick-*hooks in theme.js) — items collect client-side, the final step is an is_form submission to the merchant inbox. Use for food ordering, room/tour booking requests, quote lists; never a fake checkout. Recipe: shop-build-section "Cart-like flows".
The non-negotiables (what done-right means)
- It is NOT a shop.
set_commerce_enabled false(writesstores.settings.commerce_enabled=false). That 404s/cart/products/*/checkout/collections/search/account(ECOM_ROUTE_KINDSinrender-theme.ts, gated onpublicShop.commerce_enabled) and hides Orders/Products/Marketing in the admin sidebar (AppSidebar.tsx,commerceOnlyitems). Astore_type='web'store is still an ecom shop until this flag is false. Content themes inheritweb-base, which is now a ROOT theme (parent_theme_id NULL) that OWNS its owntheme.css/theme.js/locales(copied from the ecom base at seed time) — so they keep interactivity + the customizer/page-builder preview listener WITHOUT inheriting the ecom base's templates/sections (the code editor no longer lists product/cart/checkout/collection as inherited files). - Every page is a
.jsonsection template, never a bare.liquid. Atemplates/<x>.liquiddoes NOT appear in the customizer and is shadowed by the basetemplates/<x>.json(JSON wins). Shiptemplates/<x>.json→{ "order":[...], "sections":{...} }. Home =templates/index.json. (§F: the section file must exist before the template references it.) - Every section has a full
{% schema %}(§F) — empty schema renders but is dead in the customizer. Wire every visible string/image/link/label tosection.settings.* | default: '...'. - Reuse the platform's features; don't reinvent in store_pages:
- Long-form articles / guides / news → the blog (
create_blog_post,store_blog_*) →/blogs,/blogs/:slug. - Structured listings (houses, dishes, projects, events, gear reviews…) → a custom content type (
shop-create-content-type) →/<x>/:slug. - Navigation → the header/footer section's nav link blocks ('link' items + nested 'dropdown'/'column'; there is no menu tool), rendered by the recursive
{% render 'nav-menu' %}snippet — §H. Author content-appropriate links, each pointing at a REAL built page. - Genuinely static prose (about/contact/legal) → pages (
create_page/publish_page). - Theme look (hero, colors, fonts) → theme customizer sections + §G palette.
- Long-form articles / guides / news → the blog (
The model: commerce is a capability toggle, not a theme
store_type (pos|web) means physical-POS vs online — a web store is an online SHOP by default. A content site is distinguished only by store.settings.commerce_enabled = false (default true/undefined, so existing shops are untouched). Exposed to the storefront via publicShop.commerce_enabled. The create-store wizard's StepBasics offers "Online shop / Content website"; via the MCP just call set_commerce_enabled false.
Storefront wiring: the content Drop + router + pagination
contentDrop (liquid/content-drop.ts) is on EVERY page. Lazy:{{ content.houses }}/{% for h in content.house %}fetches that type's published records on first access, cached per request, 0 queries if unused. Key = type code OR singular.- Data-driven router (
routing/content-routes.ts): when the built-in route table misses, the path is matched against each type's editableurl_pattern(/properties/:slug); the list URL is the pattern prefix. No hardcoded prefixes. Single-segment detail only: the matcher is^<prefix>/([^/]+)$, so a detail slug is ONE path segment — nested/multi-segment patterns (/properties/:city/:slug) do NOT match. The router is fixed code; the ONLY per-type config isurl_pattern(set on the content type) — there is no per-store route file to author. - List vs detail context (
build-page-data.ts): list page →records(current page, filtered/sorted) +pagination+ the records under type-code/singular +total/total_all+search/sort/filters+content_type; detail →record+ singular alias. Records are flattened ({ id, title, slug, status, url, ...fieldValues });gallery/multiselectcoerced to arrays ({% for img in house.gallery %}loops),imageis a string, numbers are floats. - Filter/search/sort/pagination is server-side via URL query params (
?q=,?<field>=,?sort=,?per_page=,?page=) — build the list section as a plain<form method="get">, NOT client JS. Full grammar + copy-paste faceted-list snippet:shop-create-content-type("Filter, search, sort & pagination"). - Templates resolve
templates/<type.template>(detail) /<type.template>-list(list); the renderer probes.jsonfirst (customizer-editable) then.liquid, then a genericcontent(-list).liquidfallback.contentis a reserved Liquid key — list/detail expose records underrecords/type-code/singular, NOTcontent.
Customizer integration
TEMPLATE_OPTIONSentries taggedcommerceOnlyare hidden whencommerce_enabled === false(the editor store query selectssettings).- Content-type pages are added dynamically: fetch
contentTypes→ per type a list option (<template>-list, iframe = url_pattern prefix) + a detail option (<template>, iframe = prefix +/+sample_slug). They appear in the dropdown only once their.jsontemplate exists — skip the template and the route 404s with nothing to edit.
Theme customizer vs. Page Builder — which editor edits what (READ; #1 confusion on multi-page sites)
Two separate editors. Mixing them up makes you think bespoke pages "disappeared":
- Theme customizer + code editor edit SHARED theme files ONLY:
templates/index.json(home), the singletemplates/page.json(the "Static page" template — shared by every plain page),blog/article/404, plus per-content-type<code>-list.json/<code>.json. The router has no per-page template:resolveRoute→pickTemplate("page")→ onetemplates/page.jsonfor ALL pages. So you cannot give each marketing page its own theme-customizer entry —templates/page.<suffix>.jsonis never resolved. (Verified infrontend/modules/storefront/lib/routing/resolve-route.ts.) - Bespoke one-off pages (Platform, Pricing, Book-a-demo…) built via
updatePage content_mode:"sections" + sections_dataare page-builder content stored on the page record, edited in the visual Page Builder: Admin → Content → Pages → (page) → Edit =/admin/page-builder/<storeId>/<pageId>— the SAME drag/section-settings UI as the homepage customizer, just per page. They render fromsections_data(draft) /published_sections_data(live), NOT a theme template, so they do not appear in the Theme-customizer template dropdown. This is by design, not a structure bug — there is no "more standard" liquid structure that surfaces them in the theme customizer. (updatePagewrites the DRAFTsections_data; you MUSTpublish_pageto copy it topublished_sections_data, thenclear_storefront_cache.) - Decision rule + tell the user up front: repeating listings → a content TYPE (customizer-editable
-list/detail templates, surfaced automatically). Bespoke landing pages → a CMS page (content_mode:"sections", edited in the Page Builder). Only the home + content-type templates are Theme-customizer editable; every other page is edited in the Page Builder.
Build order (MCP-live)
- Plan + confirm (§A) — state up front it's content-only (
set_commerce_enabled false), the content type(s)+fields (propose from the design pershop-create-content-type§0–§1, user chooses), sample records, sections, pages, menus, palette (§B/§G). set_commerce_enabled false.- Palette/type from the design (§G):
set_global_settings. - Data: content type(s) + a few published records with re-hosted images (§C) —
shop-create-content-type. Built-in blog posts / pages as needed. - Sections:
build_sectionper design block — list/detail sections that READcontent.<type>/recordsand bind each element to a real field ({{ house.price | money }},{% for img in house.gallery %}). A dual-mode grid:{% assign items = records | default: content[section.settings.source] %}(filteredrecordson a list page, the Drop on home). Render a pager whenpagination.total_pages > 1. Seeshop-build-section. The list AND detail templates are first-class design surfaces (core §K) — design them as richly as the homepage (list = styled card grid + filters, detail = gallery + laid-out fields + CTA), never a bare unstyled{% for %}stack. Same for the built-in blog (/blogslist +articletemplate): decompose into real sections, don't ship the sparse default. A half-finished listing/blog page is the #1 looks-thin/unfinished failure.- Reuse the web-base section library FIRST — don't re-author what already ships.
web-baseprovides ready-made, customizer-editable, token-wired content sections you PLACE (viaset_section_settings/ a.jsontemplate) and restyle, instead of building from scratch: chromeweb-header/web-footer; contentweb-hero,web-rich-text,web-feature-grid,web-cta,web-gallery,web-testimonials,web-faq(native<details>accordion),web-stats,web-logos,web-feature-split; plusweb-form(binds to anis_formcontent type),web-page,web-blog-list,web-article. Each has the canonical Style group + presets (so it shows in the "Add section" picker). Onlybuild_sectiona NEW type when the design needs something these don't cover (e.g. a data-bound listing readingcontent.<type>/records).
- Reuse the web-base section library FIRST — don't re-author what already ships.
- Templates:
set_section_settings/.jsontemplates —templates/<code>-list.json(list) +templates/<code>.json(detail) +templates/index.json(home). - Pages & chrome — build EVERY page, none blank. Static/marketing pages (about, services, contact, legal):
create_pageWITH abody(prose HTML styled to the design) —create_pagewith no body = a BLANK page (the #1 missing-page cause); for a section-designed page build it SECTION-BY-SECTION (core §K):create_pageFIRST (empty/skeleton, so it exists + opens without a 404), thenupdate_page_sections { id, section_id, type, settings, position }once per section in order (the incremental twin ofset_section_settings— merges per-field, preview fills in section-by-section), instead of dumping a wholesections_datavia oneupdatePage. All plain pages share ONEtemplates/page.json, so per-page bespoke layout comes from the body (or model the page as a content-type record).publish_pageeach.create_blog_postif the design has a blog. Nav + header/footer: author the nav links in the header/footer section'slink_listblock (linking pages/content URLs), then RESTYLE THE EXISTINGweb-header/web-footersection (get_theme_file→upsert_theme_file) — do NOT bake a new homepage section with a hard-coded<nav>/<a>menu. Header nav loops thelink_listblock, logo fromshop.logo_url(core §H "Header & footer = layout chrome" + logo); a hard-coded header = a §D fail. - Verify on the draft (§D — every page incl. list+detail; confirm
/cart/products/x/checkoutcorrectly 404) →publish_draft_theme→clear_storefront_cache.
Completeness checklist (where builds come up short)
- Content type(s) created with every field the design needs (modeled from the design).
- Real published records (draft = invisible), each with re-hosted images (no blank cards, no hotlinks — §C).
-
templates/<code>-list.jsonrenders the grid;templates/<code>.jsonrenders one record (incl. image/gallery) — opening a record URL must NOT 404/empty. - Both pages + every section customizer-editable (
.jsonreferencing full-{% schema %}sections); they appear in the customizer dropdown. - Nav (built-in menu, a resolving handle) links the list page(s); about/contact pages; blog if the design has one.
-
set_commerce_enabled false→ ecom routes 404, ecom admin menus hidden. - Verify live (§D/§E):
list_content_recordshas data; the list URL AND a record URL render with images; every link 200 (check body).
If the input is a Claude Design (claude.ai/design) handoff
A design handoff is HTML/CSS you must turn into Liquid that looks native to this platform. Everything above still applies; this is just the ingestion + the "make it durable" recipe.
Fetch + read the handoff
The design URL (api.anthropic.com/v1/design/h/<id>) returns a gzip→tar bundle: curl -o b.gz <url>; gunzip b.gz; tar -xf b. Read README.md, the chats, then the primary file + its imports. Pull the design's images and re-host them (§C — feed the URLs through upload_image, or download + docker cp <img> crm-go-backend:/app/storage/<slug>/).
Learn the codebase's Liquid FIRST (don't invent conventions)
Before authoring, read real examples via the MCP — copy the conventions, don't guess:
list_theme_files { path_prefix: "sections/", include_inherited: true }thenget_theme_fileon 2–3 sections closest to the design's blocks — wrapper/marker (data-section-v2), container/spacing, snippet reuse ({% render 'product-card' %}),| money, block loops, the{% schema %}shape.get_theme_file { path: "layout/theme.liquid" }for the globalsettings.*tokens (§G).
Map the content model (the key decision)
Map each repeating thing to a platform feature: long-form-with-body → blog posts; structured records (price/rating/specs/address) → a content type (url_pattern /<x>/:slug); one-off narrative → pages; taxonomy → blog categories or a content-type dropdown field. (Classify per shop-create-content-type — don't model menus/pages/blog as content types.)
Preserve core features when restyling chrome
Overriding header/footer/search/cart/account = RESTYLING, not rebuilding. Read the original (get_theme_file) and KEEP its functional bits (quick search/autocomplete, cart drawer, account, wishlist, mobile nav, locale/currency switchers) — a handoff visual rarely shows the search dropdown or cart drawer, but those are platform CORE. Only drop a feature the design explicitly excludes. Verify (§D) they still work.
Handoff gotchas (each cost a round)
- Template-name collision → ecom page. If the content type's
Templateor a fixed route name (product/article/blog) matches a basetemplates/<name>.json, the base wins. Pick a non-colliding stem (reviewnotproduct), or override the fixed one by shipping your owntemplates/article.json→ your sections. .liquidtemplate ≠ customizer page → convert to.json+ section.- Empty schema = uneditable → real settings on every section.
- Theme assets serve at
/assets/...; design photos in the storage volume at/storage/.... - Test via
<slug>.lvh.me:3000, NOTlocalhost:3000/storefront/...(locale middleware 307s).
Definition of done
<slug>.lvh.me:3000renders home + every page styled (§D screenshot-verify).- Every link works (§E).
/cart/products/x/checkout/account→ 404 (correct — commerce off). - Customizer lists home, blog, article, the content list/detail, and static pages — each with editable settings; no ecom template options.
- Content type + records under Admin → Content; blog under Content → Blog; nav under Content → Menus.
