Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- `sveltekit/` — SvelteKit 2 + Svelte 5 (runes) port with server `load`
- `astro/` — Astro 6 port with `@astrojs/node` SSR and frontmatter data fetching
- `solid-start/` — SolidStart v1 port with `query` + `createAsync` and signal-driven product page state
- `marko-run/` — Marko 6 + `@marko/run` port with route-local handlers and streamed layout data
- `nuxt/` — Nuxt 4 port with server middleware and Vue pages
- `nuxt-binding/` — Nuxt 4 port using Hydrogen's Vue binding layer

Expand Down
5 changes: 5 additions & 0 deletions examples/marko-run/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist
.marko-run
.env
.env.*
!.env.example
13 changes: 13 additions & 0 deletions examples/marko-run/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Hydrogen Marko Run example

A proof-of-concept Hydrogen storefront built with Marko 6 and `@marko/run`.

## Running

From the repo root:

```sh
pnpm --filter @shopify/hydrogen-example-marko-run dev
```

The example uses `@marko/run` route files under `src/routes`, root middleware for Hydrogen request context and cart routes, and route-local `+handler.ts` files to fetch Storefront API data before rendering Marko pages.
28 changes: 28 additions & 0 deletions examples/marko-run/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@shopify/hydrogen-example-marko-run",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "marko-run",
"build": "marko-run build",
"start": "node --enable-source-maps ./dist/index.mjs",
"preview": "marko-run preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@shopify/hydrogen": "workspace:*",
"marko": "^6.1.23"
},
"devDependencies": {
"@marko/run": "^0.11.0",
"@tailwindcss/vite": "^4.2.2",
"gql.tada": "^1.9.2",
"tailwindcss": "^4.2.2",
"typescript": "catalog:",
"vite": "^8.0.7"
},
"engines": {
"node": ">=24"
}
}
1 change: 1 addition & 0 deletions examples/marko-run/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions examples/marko-run/src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
@import "tailwindcss";

@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--color-paper: #f4f4f2;
}

html {
-webkit-font-smoothing: antialiased;
}

dialog#cart-drawer {
position: fixed;
inset-block: 0;
right: 0;
left: auto;
margin: 0;
width: 100%;
max-width: 36rem;
height: 100dvh;
max-height: none;
border: 0;
padding: 0;
background: white;
color: black;
transform: translateX(100%);
transition:
transform 250ms cubic-bezier(0.22, 1, 0.36, 1),
overlay 250ms allow-discrete,
display 250ms allow-discrete;
}

dialog#cart-drawer[open] {
transform: translateX(0);
}

@starting-style {
dialog#cart-drawer[open] {
transform: translateX(100%);
}
}

dialog#cart-drawer::backdrop {
background: rgb(0 0 0 / 0);
transition:
background-color 250ms ease-out,
overlay 250ms allow-discrete,
display 250ms allow-discrete;
}

dialog#cart-drawer[open]::backdrop {
background: rgb(0 0 0 / 0.3);
}

@starting-style {
dialog#cart-drawer[open]::backdrop {
background: rgb(0 0 0 / 0);
}
}

@media (prefers-reduced-motion: reduce) {
dialog#cart-drawer,
dialog#cart-drawer::backdrop {
transition: none;
}
}

body:has(dialog#cart-drawer[open]) {
overflow: hidden;
}

body {
font-family: var(--font-sans);
}
38 changes: 38 additions & 0 deletions examples/marko-run/src/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { analyticsConsent, analyticsShop } from "@shared/config";
import {
AnalyticsEvent,
createStorefrontAnalytics,
type StorefrontAnalytics,
} from "@shopify/hydrogen";

export { analyticsShop };

let bus: StorefrontAnalytics | null = null;

export function getAnalytics(): StorefrontAnalytics | null {
if (typeof window === "undefined") return null;
if (bus) return bus;

bus = createStorefrontAnalytics({
shop: analyticsShop,
consent: analyticsConsent,
});

const events = [
AnalyticsEvent.PAGE_VIEWED,
AnalyticsEvent.PRODUCT_VIEWED,
AnalyticsEvent.COLLECTION_VIEWED,
AnalyticsEvent.CART_VIEWED,
AnalyticsEvent.SEARCH_VIEWED,
];

for (const event of events) {
bus.subscribe(event, (payload) => {
console.log(`[analytics] ${event}`, payload);
});
}

return bus;
}

export { AnalyticsEvent };
41 changes: 41 additions & 0 deletions examples/marko-run/src/lib/cart.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
configureCartEndpoint,
createCartStore,
type CartData,
type CartState,
type CartStore,
} from "@shopify/hydrogen";

export const CART_ENDPOINT = "/api/cart";

let cartStore: CartStore | null = null;
let connected = false;

export type ClientCartState = CartState<CartData>;

export function getCartStore(): CartStore {
if (!cartStore) cartStore = createCartStore();
return cartStore;
}

export function connectCartStore(): CartStore {
const store = getCartStore();
if (connected || typeof window === "undefined") return store;

configureCartEndpoint(CART_ENDPOINT);
store.connect();
store.fetch().catch(() => {});
connected = true;
return store;
}

export function subscribeCartState(listener: (state: ClientCartState) => void): () => void {
const store = connectCartStore();
listener(store.getState() as ClientCartState);
return store.subscribe(() => listener(store.getState() as ClientCartState));
}

export async function handleCartFormSubmit(event: SubmitEvent): Promise<void> {
event.preventDefault();
await connectCartStore().handleFormSubmit(event);
}
64 changes: 64 additions & 0 deletions examples/marko-run/src/lib/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { AvailableFilter, ProductFilter } from "@shopify/hydrogen";
import {
getFilterRemovalUrl,
getSortByValue,
isFilterInputActive,
serializeCollectionParams,
} from "@shopify/hydrogen";

export const SORT_OPTIONS = [
{ label: "Featured", value: getSortByValue("COLLECTION_DEFAULT", false) },
{ label: "Best selling", value: getSortByValue("BEST_SELLING", false) },
{ label: "Alphabetically, A-Z", value: getSortByValue("TITLE", false) },
{ label: "Alphabetically, Z-A", value: getSortByValue("TITLE", true) },
{ label: "Price, low to high", value: getSortByValue("PRICE", false) },
{ label: "Price, high to low", value: getSortByValue("PRICE", true) },
{ label: "Date, old to new", value: getSortByValue("CREATED", false) },
{ label: "Date, new to old", value: getSortByValue("CREATED", true) },
];

export function filterInputToParamEntries(input: string): Array<{ name: string; value: string }> {
let filter: ProductFilter;
try {
filter = JSON.parse(input) as ProductFilter;
} catch {
return [];
}

return Array.from(
serializeCollectionParams({ filters: [filter], sortKey: undefined, reverse: false }),
([name, value]) => ({ name, value }),
);
}

export function describeFilter(filter: ProductFilter): string {
if (filter.tag) return filter.tag;
if (filter.productType) return filter.productType;
if (filter.productVendor) return filter.productVendor;
if (filter.available != null) return filter.available ? "In stock" : "Out of stock";
if (filter.variantOption) return `${filter.variantOption.name}: ${filter.variantOption.value}`;
if (filter.price) {
const { min, max } = filter.price;
if (min != null && max == null) return `$${min}+`;
if (max != null && min == null) return `Up to $${max}`;
if (min != null && max != null) return `$${min} - $${max}`;
}
return "Filter";
}

export function filterRemovalHref(
collectionPath: string,
currentParams: URLSearchParams,
filter: ProductFilter,
): string {
const removal = getFilterRemovalUrl(currentParams, filter);
return removal === "?" ? collectionPath : `${collectionPath}${removal}`;
}

export function visibleValues(filter: AvailableFilter) {
return filter.values.filter((value) => value.count > 0);
}

export function isFilterActive(filters: ProductFilter[], input: string): boolean {
return isFilterInputActive(filters, input);
}
5 changes: 5 additions & 0 deletions examples/marko-run/src/lib/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const dateFormatter = new Intl.DateTimeFormat("en-US", { dateStyle: "long" });

export function formatDate(value: string): string {
return dateFormatter.format(new Date(value));
}
Loading
Loading