A static ecommerce site built with Eleventy and deployed to Cloudflare Pages, using Stripe for payment processing and a Cloudflare Worker for checkout and fulfillment. Printful is integrated via API for print-on-demand fulfillment.
Live site: https://hmc-cycling.org
- Static site generator: Eleventy (11ty) v3
- Hosting: Cloudflare Pages (free tier)
- Payments: Stripe (2.9% + 30¢ per transaction, no monthly fee)
- Backend: Cloudflare Worker
- Idempotency: Cloudflare KV
- Fulfillment: Printful API (Manual Order / API store)
- Source control: GitHub (nopolabs/HMC)
| Service | Cost |
|---|---|
| Cloudflare Pages | $0 |
| Cloudflare Workers | $0 (free tier) |
| Cloudflare KV | $0 (free tier) |
| Stripe | 2.9% + 30¢ per transaction |
| Domain | ~$1 amortized |
| Total fixed cost | ~$0/month |
hmc/
├── src/
│ ├── _includes/
│ │ └── layout.njk # Shared HTML layout (nav, footer, cart drawer + JS)
│ ├── _data/
│ │ ├── products.json # Generated product catalog — do not edit directly
│ │ └── site.json # Site-wide flags (e.g. preview mode)
│ ├── images/ # Product photos
│ ├── styles.css # Site styles
│ ├── index.njk # Home page — product cards with Add to Cart
│ ├── about.njk # About page
│ ├── contact.njk # Contact page
│ └── success.njk # Order confirmation page (clears cart)
├── worker/
│ ├── src/
│ │ ├── index.js # Cloudflare Worker — checkout and webhook handlers
│ │ └── products.js # Generated product catalog for Worker — do not edit directly
│ ├── wrangler.json # Worker configuration
│ ├── .dev.vars # Local secrets (never commit — in .gitignore)
│ └── package.json
├── products-config.json # Source of truth for product definitions
├── sync-products.js # Syncs products from Printful → products.json + products.js
├── eleventy.config.cjs # Eleventy config (input: src/, output: _site/)
├── package.json
└── .gitignore
- Customer browses products and clicks Add to Cart (size must be selected)
- Cart state is stored in
localStorageand shown in a cart drawer - Customer clicks Checkout in the cart drawer
- Browser POSTs cart items to
POST /checkouton the Worker - Worker builds Stripe line items and creates a Checkout Session, returns the Stripe URL
- Browser redirects customer to Stripe's hosted payment page
- Customer completes payment on Stripe; Stripe sends a receipt email automatically
- Stripe fires a
checkout.session.completedwebhook toPOST /webhook - Worker validates the Stripe webhook signature
- Worker checks Cloudflare KV for idempotency (prevents duplicate orders on retries)
- Worker creates and confirms a Printful order via the Printful API
- Customer is redirected to
/success, which clears the cart
Shipping is US-only, calculated per order at checkout:
- First item: $4.75
- Each additional item: +$2.20
This matches Printful's actual shipping rates for t-shirts. The amount is calculated in the Worker and passed to Stripe when the session is created.
src/_data/site.json has a preview flag. When true:
- Products can be added to the cart normally
- The Checkout button in the cart is disabled and shows "Coming soon"
Useful for sharing the site for feedback before going live.
# Eleventy site
npm install
npm start # dev server at http://localhost:8080
# Worker
cd worker
npm install
npm run dev # Worker at http://localhost:8787Create worker/.dev.vars (never commit this file):
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
PRINTFUL_API_KEY=...
Install the Stripe CLI and run:
stripe listen --forward-to http://localhost:8787/webhookThis forwards Stripe webhook events to your local Worker and prints the STRIPE_WEBHOOK_SECRET to use in .dev.vars.
products-config.json is the single source of truth. Never edit src/_data/products.json or worker/src/products.js directly — they are generated files.
To add or update a product, edit products-config.json then run:
npm run syncThis fetches current variant and sizing data from Printful and regenerates both output files.
To list available products in your Printful store:
node sync-products.js --listDeployment is automatic — push to main on GitHub and Cloudflare Pages builds and deploys.
- Build command:
npm run build - Build output directory:
_site
Deploy worker first, then push frontend changes.
cd worker
npm run deployWorker URL: https://hmc-worker.danrevel.workers.dev
Worker routes: hmc-cycling.org/checkout* and hmc-cycling.org/webhook*
cd worker
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_WEBHOOK_SECRET
npx wrangler secret put PRINTFUL_API_KEY- Store type: Manual Order / API (not Squarespace or Shopify)
- Store ID:
17828143 - Products must be created and synced in this store
- API token must have order read/write permissions scoped to this store
Stripe retries webhooks on failure. To prevent duplicate Printful orders, each processed Stripe session ID is stored in Cloudflare KV (ORDERS namespace) with a 30-day TTL. Subsequent webhook retries for the same session are ignored.
Receipt emails are sent automatically by Stripe after payment. Enable in the Stripe Dashboard under Settings → Business → Customer emails → Successful payments.