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');
+});