diff --git a/app/lib/storefront.server.ts b/app/lib/storefront.server.ts index ffd9bfe..02cd509 100644 --- a/app/lib/storefront.server.ts +++ b/app/lib/storefront.server.ts @@ -5,6 +5,18 @@ import { num, offerSummary, moneyFormatter, pct } from "./offer-text"; import type { OfferWithRelations } from "./offers.server"; import { formatShopDate } from "./tz"; +// The storefront CSS/JS live in the theme app extension (loaded by the app embed). Proxy pages also carry +// them inline, so they still look right and "Add to cart" still works when the embed is switched off. +// bundle-offers.js guards against running twice; the CSS is idempotent. +import storefrontCss from "../../extensions/bundle-offers-theme/assets/bundle-offers.css?raw"; +import storefrontJs from "../../extensions/bundle-offers-theme/assets/bundle-offers.js?raw"; + +/** Inline assets for proxy pages (no Liquid delimiters in them: checked by engine/tests/storefront.test.js). */ +export function selfContained(html: string) { + return `${html} + +`; +} /** HTML-escape merchant text and neutralise Liquid delimiters so it can't run as Liquid. */ export function esc(s: string | null | undefined) { diff --git a/app/routes/app.templates.tsx b/app/routes/app.templates.tsx index 0b35b91..64b7562 100644 --- a/app/routes/app.templates.tsx +++ b/app/routes/app.templates.tsx @@ -40,11 +40,11 @@ export default function Templates() { {calendar.map((f) => ( - + {f.name} {f.date}{f.approximate ? " (approx.)" : ""} {f.weeksAway <= 0 ? "This week" : `In ${f.weeksAway} weeks`} - + ))} Plan your offers about 6 weeks ahead. Lunar dates are approximate: check them locally. diff --git a/app/routes/proxy.$handle.tsx b/app/routes/proxy.$handle.tsx index fac1825..172d07b 100644 --- a/app/routes/proxy.$handle.tsx +++ b/app/routes/proxy.$handle.tsx @@ -5,7 +5,7 @@ import { redirect } from "react-router"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { PROXY_CACHE, runningOffersForShop } from "../lib/proxy.server"; -import { bundlePageLiquid, notFoundLiquid, proxyBase } from "../lib/storefront.server"; +import { bundlePageLiquid, notFoundLiquid, proxyBase, selfContained } from "../lib/storefront.server"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const { liquid } = await authenticate.public.appProxy(request); @@ -18,7 +18,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { if (!data) return liquid(notFoundLiquid(base), { status: 404 }); const offer = data.running.find((o) => o.handle === handle); - if (offer) return liquid(bundlePageLiquid(offer, data.ctx, locale), { headers: { "Cache-Control": PROXY_CACHE } }); + if (offer) return liquid(selfContained(bundlePageLiquid(offer, data.ctx, locale)), { headers: { "Cache-Control": PROXY_CACHE } }); const renamed = await db.offer.findFirst({ where: { shopId: data.shop.id, oldHandles: { has: handle } }, select: { id: true, handle: true } }); if (renamed && data.running.some((o) => o.id === renamed.id)) { diff --git a/app/routes/proxy._index.tsx b/app/routes/proxy._index.tsx index e674988..6c3efc9 100644 --- a/app/routes/proxy._index.tsx +++ b/app/routes/proxy._index.tsx @@ -2,7 +2,7 @@ import type { LoaderFunctionArgs } from "react-router"; import { authenticate } from "../shopify.server"; import { PROXY_CACHE, runningOffersForShop } from "../lib/proxy.server"; -import { bundlesListLiquid, proxyBase } from "../lib/storefront.server"; +import { bundlesListLiquid, proxyBase, selfContained } from "../lib/storefront.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { liquid } = await authenticate.public.appProxy(request); @@ -12,5 +12,5 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const locale = request.headers.get("accept-language")?.split(",")[0]?.split("-")[0] || null; const data = await runningOffersForShop(shop, request.headers.get("x-shopify-country") || null); if (!data) return liquid(bundlesListLiquid([], { currency: "USD", timezone: "UTC", branding: false }, null, base)); - return liquid(bundlesListLiquid(data.running, data.ctx, locale, base), { headers: { "Cache-Control": PROXY_CACHE } }); + return liquid(selfContained(bundlesListLiquid(data.running, data.ctx, locale, base)), { headers: { "Cache-Control": PROXY_CACHE } }); }; diff --git a/brand/screenshots/listing/1-offers.png b/brand/screenshots/listing/1-offers.png new file mode 100644 index 0000000..94c7dd5 Binary files /dev/null and b/brand/screenshots/listing/1-offers.png differ diff --git a/brand/screenshots/listing/2-editor.png b/brand/screenshots/listing/2-editor.png new file mode 100644 index 0000000..8c3ad25 Binary files /dev/null and b/brand/screenshots/listing/2-editor.png differ diff --git a/brand/screenshots/listing/3-bundle-page.png b/brand/screenshots/listing/3-bundle-page.png new file mode 100644 index 0000000..54921b4 Binary files /dev/null and b/brand/screenshots/listing/3-bundle-page.png differ diff --git a/brand/screenshots/listing/4-checkout.png b/brand/screenshots/listing/4-checkout.png new file mode 100644 index 0000000..6b31186 Binary files /dev/null and b/brand/screenshots/listing/4-checkout.png differ diff --git a/brand/screenshots/listing/5-templates.png b/brand/screenshots/listing/5-templates.png new file mode 100644 index 0000000..2d727c7 Binary files /dev/null and b/brand/screenshots/listing/5-templates.png differ diff --git a/brand/screenshots/listing/6-home.png b/brand/screenshots/listing/6-home.png index 6f18258..9c7a875 100644 Binary files a/brand/screenshots/listing/6-home.png and b/brand/screenshots/listing/6-home.png differ diff --git a/brand/screenshots/listing/alt-text.txt b/brand/screenshots/listing/alt-text.txt index 09142d8..65b7cad 100644 --- a/brand/screenshots/listing/alt-text.txt +++ b/brand/screenshots/listing/alt-text.txt @@ -1 +1,6 @@ +1-offers.png: Offerlyst offers list showing running bundle offers with status badges and priority order +2-editor.png: Offer editor with bundle type, discount, products and a live example cart preview +3-bundle-page.png: Storefront bundle page with product list, bundle price and Add bundle to cart button +4-checkout.png: Shopify cart and checkout showing the bundle discount applied automatically +5-templates.png: Festival templates and calendar with ready-made bundle offers 6-home.png: Offerlyst home page with setup checklist and recent bundle results diff --git a/brand/screenshots/raw/1-offers.png b/brand/screenshots/raw/1-offers.png new file mode 100644 index 0000000..0b34197 Binary files /dev/null and b/brand/screenshots/raw/1-offers.png differ diff --git a/brand/screenshots/raw/2-editor.png b/brand/screenshots/raw/2-editor.png new file mode 100644 index 0000000..2aa2c81 Binary files /dev/null and b/brand/screenshots/raw/2-editor.png differ diff --git a/brand/screenshots/raw/3-bundle-page.png b/brand/screenshots/raw/3-bundle-page.png new file mode 100644 index 0000000..480fcbc Binary files /dev/null and b/brand/screenshots/raw/3-bundle-page.png differ diff --git a/brand/screenshots/raw/4-checkout.png b/brand/screenshots/raw/4-checkout.png new file mode 100644 index 0000000..bacabdb Binary files /dev/null and b/brand/screenshots/raw/4-checkout.png differ diff --git a/brand/screenshots/raw/5-templates.png b/brand/screenshots/raw/5-templates.png new file mode 100644 index 0000000..823e303 Binary files /dev/null and b/brand/screenshots/raw/5-templates.png differ diff --git a/brand/screenshots/raw/6-home.png b/brand/screenshots/raw/6-home.png index f3d373f..fe90275 100644 Binary files a/brand/screenshots/raw/6-home.png and b/brand/screenshots/raw/6-home.png differ diff --git a/engine/tests/storefront.test.js b/engine/tests/storefront.test.js index d2681b7..569e139 100644 --- a/engine/tests/storefront.test.js +++ b/engine/tests/storefront.test.js @@ -34,3 +34,15 @@ test('bundle pages render for every type without merchant-controlled Liquid', () expect(list).toContain('data-bo-money="20"'); if (out) writeFileSync(join(out, 'snippets', 'list.liquid'), list); }); + +test('inlined storefront CSS/JS contain no Liquid delimiters (proxy pages render them as Liquid)', async () => { + // vitest stubs CSS imports, so check the source files; the production build inlines them via ?raw + const { readFileSync } = await import('node:fs'); + for (const f of ['bundle-offers.css', 'bundle-offers.js']) { + const src = readFileSync(new URL(`../../extensions/bundle-offers-theme/assets/${f}`, import.meta.url), 'utf8'); + expect(src.length).toBeGreaterThan(1000); + expect(src).not.toMatch(/\{\{|\{%/); + } + const { selfContained } = await import('../../app/lib/storefront.server.ts'); + expect(selfContained('

x

')).toContain('window.BundleOffers'); +});