Stefan Petanovski — Personal Site & Merch Store
11/4/2025

Overview
We built a bespoke site for guitarist Stefan Petanovski that blends a fast storefront (physical CDs and merch) with a flexible CMS for pages, albums, and services. A dedicated admin app manages catalog, content, SEO, and orders end‑to‑end.

Architecture
- Monorepo apps:
web/(storefront),admin/, andserver/(API) - API: Node.js/Express + MongoDB (Mongoose)
- Payments: Stripe Payment Intents + webhook order finalization + receipt email
- Media: image uploads via server with Sharp processing
[web] → React + Vite + Tailwind, react-helmet-async for SEO
[admin] → React + Vite admin with catalog/content/SEO tools
[server] → Express API (auth, catalog, cart, checkout, pages, sitemaps, uploads)
models: Product, Order, Album, Collection, Page, Service, ShippingZone, Coupon, User

Features
- Storefront: albums and merch with variants (size/color/format), stock control, cart/checkout
- CMS: create/edit pages, service pages, about, testimonials, home hero
- Shipping: zones and methods; rates included in order totals
- Coupons: campaign codes and per‑order capture
- Orders: status timeline, email receipts, Stripe webhook sync

Catalog & Albums
- Products: variant attributes (size/color/format), per‑variant SKU, stock, featured image
- Collections: group releases or merch capsules for curated browsing
- Albums: dedicated detail pages with track lists and artwork; merch items can cross‑link
Advanced Implementation
Server‑side cart validation and Payment Intent
// apps/server/src/routes/checkout.js (excerpt)
async function validateCart(lineItems) {
const results = [];
for (const item of lineItems) {
const product = await Product.findById(item.productId).lean();
if (!product) throw Object.assign(new Error('Product not found'), { status: 400 });
const variant = product.variants[item.variantIndex];
if (!variant) throw Object.assign(new Error('Variant not found'), { status: 400 });
if (variant.stock < item.qty) throw Object.assign(new Error('Insufficient stock'), { status: 400 });
const unitPrice = variant.salePrice || variant.price;
const attrs = variant.attributes || {};
const parts = [];
if (attrs.size) parts.push(attrs.size);
if (attrs.color) parts.push(attrs.color);
if (attrs.format) parts.push(attrs.format);
const variantLabel = parts.join(' / ');
results.push({ title: product.title, sku: variant.sku, unitPrice, total: unitPrice * item.qty, variantLabel });
}
return results;
}

// apps/server/src/routes/checkout.js (excerpt)
router.post('/payment-intent', authenticate, async (req, res, next) => {
const { email, currency = 'usd', lineItems, shipping } = req.body;
const validated = await validateCart(lineItems);
const subtotal = validated.reduce((s, li) => s + li.total, 0);
const shippingAmount = Math.round((shipping && shipping.rate) ? shipping.rate * 100 : 0);
const amount = Math.round(subtotal * 100) + shippingAmount;
const pi = await stripe.paymentIntents.create({ amount, currency, receipt_email: email, automatic_payment_methods: { enabled: true } });
const order = await Order.create({ number: `ORD-${Date.now()}`, email, lineItems: lineItems.map((it, i) => ({ ...it, ...validated[i] })), totals: { subtotal, shipping: shipping?.rate || 0, grandTotal: subtotal + (shipping?.rate || 0), currency }, status: 'pending', stripePaymentIntentId: pi.id });
res.json({ clientSecret: pi.client_secret, orderId: order._id });
});
Stripe webhook → order finalization + email receipt
// apps/server/src/routes/checkout.js (excerpt)
if (event.type === 'payment_intent.succeeded') {
const pi = event.data.object;
const order = await Order.findOne({ stripePaymentIntentId: pi.id });
if (order) {
order.status = 'paid';
order.timeline.push({ status: 'paid', note: 'Stripe webhook' });
await order.save();
// nodemailer: themed receipt with line items and totals
}
}
SEO & Branding
- SEO: storefront uses
react-helmet-async(titles, descriptions, canonicals, OG/Twitter); shared builders output JSON‑LD for products/albums/breadcrumbs; API exposes sitemaps - Brand: dark, high‑contrast aesthetic aligned with metal; cyan glow accents on key surfaces and email headers
- Admin SEO tools: per‑page/per‑product fields for title/description/social image
// apps/web/src/components/SEO.jsx (excerpt)
<Helmet>
<title>{title}</title>
<link rel="canonical" href={url} />
{tags.map(t => t.name ? <meta name={t.name} content={t.content} /> : <meta property={t.property} content={t.content} />)}
{jsonld && (Array.isArray(jsonld) ? jsonld : [jsonld]).map((obj, i) => (
<script key={i} type="application/ld+json">{JSON.stringify(obj)}</script>
))}
</Helmet>
// shared/lib/index.js (excerpt)
export function productJsonLd({ name, description, images = [], offers = [] }) {
return { '@context':'https://schema.org', '@type':'Product', name, description, image: images, brand:{ '@type':'Brand', name:'Stefan Petanovski' }, offers }
}

Performance & Ops
- API hardening: Helmet, CORS, rate limiting; structured logging with Pino
- Media: Sharp for image processing; uploads stored locally and organized by date
- Data model: variants on products (attributes: size/color/format), stock per variant
Admin Workflows
- Pages: create/update publishable CMS pages (home hero, about, services)
- Catalog: products, albums, collections with images and variants
- Orders: status timeline with paid/shipped updates; automatic receipts via Nodemailer
- Shipping: zones/methods configuration reflected in checkout totals
Referral Tracking
- Middleware captures referrer and UTM parameters into an order referral snapshot for simple campaign attributions across checkout
// apps/server/src/middleware/referral.js (excerpt)
export function referralCaptureMiddleware(req,res,next){
const referrer = req.get('referer') || req.get('referrer');
const utm = { source:req.query.utm_source, medium:req.query.utm_medium, campaign:req.query.utm_campaign };
req._referralContext = { referrer, utm, path: req.originalUrl };
next();
}
Accessibility
- Keyboard‑navigable forms and controls; focus states; semantic headings
- High contrast on dark theme; readable body copy and touch‑target sizes
What made it work
- A clear separation of CMS content and catalog made updates fast
- Robust server‑validated checkout kept orders reliable and stock accurate
- Admin gave the artist full control over merch, content, and SEO without developer support
Deliverables
- Artist website with albums, products, services, and CMS pages
- Cart + checkout (Stripe) with order emails
- Admin: products, collections, pages, shipping, coupons, customers
- SEO controls and sitemaps