Privacy: privacy: { consentMode, privacyUrl, builtInAnalytics, submissionRetentionDays }.
consentMode:off,eu(EU and UK visitors) orall. When on, the tags below wait for Accept. A page with thecookie-consentblock asks through it, so design or translate that block for a custom banner; otherwise visitors get a plain English one. A footer link withdata-cookie-settingslets them change their answer. Scripts you write yourself must checklocalStorage["ph-cookie-consent"].privacyUrl: the site's privacy page, linked from the plain banner.builtInAnalytics:falseremoves PageHub's own analytics.submissionRetentionDays:0,30,90,180or365. Older form submissions are deleted nightly. Ask the owner first: they can't be recovered.
Providers (rendered as real tags on published pages):
googleAnalytics: GA4 idG-XXXXXXXXXX(Admin → Data Streams).googleTagManager:GTM-XXXXXXX. With GTM, configure tags inside GTM and pass only the container id here.googleSearchConsole: the verification content value, not the whole meta tag.metaPixel: Meta Pixel id.googleAds:AW-XXXXXXXXXX. Shares the gtag.js loader with GA4.
Per-action events: data-track="ga4:<event>" on a link or button reports it. GA4 and Meta wire automatically once their ids are set.
Anything else (chat widgets, A/B testing, CRMs, custom scripts): site.json inject head / footer — third-party snippets only. Per-page head code: a tag in that page file's <head>.
Stripe
stripe_connect returns a single-use onboarding URL while setup is unfinished. Only the site owner can complete it: hand them the URL, then call stripe_connect again once they're done to confirm ready. set_checkout and the stripe_* lookups need that ready connection.
Once Stripe is ready, the site's Stripe products sync into a products collection on their own (already sellable — no set_checkout needed). The first sync runs in the background, so if list_collections doesn't show it yet, or products changed in Stripe since, call refresh_products (returns the product count).
set_checkout makes a collection sellable: data-cart="add" buttons on its rows then charge through Stripe. Without priceIdField each row charges its numeric price field in minor units (2500 = $25.00), so the collection needs that number field; with priceIdField each row holds a Stripe price id. remove: true stops selling; the rows stay.
Orders and bookings: list_orders (paid, refunds, booking, balance, deposit hold, agreement, shipping) and get_order. update_order ships, refunds, approves a held order, charges a stay's balance, places / keeps / releases a deposit hold, unbooks dates (never refunds) or resends a link. It moves the owner's money: only for an order and amount they asked about, with confirmAmount set to what they agreed.
Build a shop
Fastest: create_site({ template: "modern-store" }) (apparel: search, facet filters, variants) or "moss-and-fern" (plant shop + a care-guide collection that ships with it), then stripe_connect. The owner's own products fill the shop. To build one, these are the parts:
- Shop page: a URL-driven listing, not a bare loop.
{% assign list = query["products?limit=12&in=q,category,sort,page,minPrice,maxPrice"] %}and the identical key inquery_meta[...](totalCount,totalPages,page) give this visitor's rows and pager;{% for item in list %}draws the cards. The filters are a<form method="get" data-autosubmit="300ms">with inputs namedqandsort(newest,price_asc,price_desc,alpha) and category chips as links?category=Tees(names fromproduct_categories; size / colour counts fromproduct_facets, selected as?facet.color=red, so addfacet.*toin=). Start from thelist-url-filtersblock and theconnector-product-gridcard (itsdata-cart-itemJSON is what the cart reads). To filter without a reload give the grid anidanddata-refetch="q,category,sort,page". Full syntax: get_style_reference({ topic: "liquid" }), "Lists from the URL". - Filter on product row fields (
categories,tags,subcategory,optionNamesCsv), never Stripe metadata — the sync doesn't copy metadata, so a metadata filter matches nothing. - Product page: page
pathPattern: ":handle", the row matched byrequest.params.handle. Add to Cart is adata-cart="add"button;data-cart="open"opens the cart,data-cart-countshows the count. - Order confirmed page and the account page (magic-link sign-in, profile and order history from
customer): start from the library blocksorder-confirmed,checkout-cancelled-bannerandcustomer-account. Customer accounts need a paid plan. - Shipping rates, countries and Stripe Tax are the owner's call, set on the dashboard's Shop page — ask, don't invent rates.
- Test on the preview (the
previewUrlthat create_site, duplicate_site and get_import_status return, orscreenshot_site: the draft with its scripts running) and again after publishing. Never build a preview link by hand: previews live on per-site hosts.
Form inbox and webhooks
list_form_submissions is the inbox (each submission's webhook delivery: delivered, retrying or FAILED). A {% form webhook: 'https://…' %} URL goes live on publish; list_form_webhooks lists them with the endpoint key set_form_webhook_secret takes (rotate: true shows the new secret once; set stores the receiver's). After the receiver is fixed, resend_form_webhook replays a submission with the same submissionId. Owners only. Body, signature and retries: the liquid topic, Forms.
Site emails
After update_site_email, run preview_site_email: an email with errors sends the default instead, and the preview is where those errors show.