Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162// Shop API, 25.12.04// Fetch products from Shopify for the products carousel// Credentials stored in MongoDB to avoid Netlify's 4KB env var limit// Now with Redis caching (10 min TTL) to avoid Shopify rate limits
import { respond } from "../../backend/http.mjs";import { connect } from "../../backend/database.mjs";import { getOrCompute, CACHE_KEYS, CACHE_TTLS } from "../../backend/cache.mjs";
// Collection ID for "Home page" (frontpage)const HOME_COLLECTION_ID = 369657512117;
// Cache credentials in memory for warm function invocationslet cachedCredentials = null;
async function getShopifyCredentials() { if (cachedCredentials) return cachedCredentials; const { db } = await connect(); const secrets = await db.collection("secrets").findOne({ _id: "shopify" }); if (!secrets) { throw new Error("Shopify credentials not found in database"); } cachedCredentials = { domain: secrets.storeDomain, token: secrets.adminAccessToken, }; return cachedCredentials;}
export async function handler(event, context) { // Handle CORS preflight if (event.httpMethod === "OPTIONS") { return respond(200, {}); }
if (event.httpMethod !== "GET") { return respond(405, { message: "Method Not Allowed" }); }
try { // Check if client wants fresh data (bypass cache) const bypassCache = event.queryStringParameters?.fresh === 'true'; if (bypassCache) { console.log("🛒 Shop: bypassing cache (fresh=true)"); return respond(200, await computeShopProducts()); } // Use cached shop data (10 min TTL) const shopData = await getOrCompute( CACHE_KEYS.SHOP, computeShopProducts, CACHE_TTLS.SHOP ); return respond(200, shopData); } catch (error) { console.error(`❌ Shop API error:`, error); return respond(500, { message: error.message || String(error) }); }}
// Extracted shop computation for cachingasync function computeShopProducts() { const { domain: SHOPIFY_STORE_DOMAIN, token: SHOPIFY_ADMIN_ACCESS_TOKEN } = await getShopifyCredentials(); // Get products from the Home page collection const productsRes = await fetch( `https://${SHOPIFY_STORE_DOMAIN}/admin/api/2024-10/collections/${HOME_COLLECTION_ID}/products.json?limit=50`, { headers: { "X-Shopify-Access-Token": SHOPIFY_ADMIN_ACCESS_TOKEN, }, } );
if (!productsRes.ok) { throw new Error(`Shopify API error: ${productsRes.status}`); }
const productsData = await productsRes.json();
// Process products into a simplified format for the carousel const products = await Promise.all( productsData.products.map(async (p) => { // Get inventory for this product const variantsRes = await fetch( `https://${SHOPIFY_STORE_DOMAIN}/admin/api/2024-10/products/${p.id}/variants.json`, { headers: { "X-Shopify-Access-Token": SHOPIFY_ADMIN_ACCESS_TOKEN, }, } ); const variantsData = await variantsRes.json(); const totalInventory = variantsData.variants.reduce( (sum, v) => sum + (v.inventory_quantity || 0), 0 );
// Get ALL image URLs (not just the first) const images = p.images?.map(img => img.src) || []; const imageUrl = images[0] || null;
// Get price from first variant const price = variantsData.variants?.[0]?.price || "0.00"; // Strip HTML tags from description for plain text, preserve line breaks const descriptionHtml = p.body_html || ''; const description = descriptionHtml .replace(/<br\s*\/?>/gi, '\n') // Convert <br> to newlines .replace(/<\/p>\s*<p[^>]*>/gi, '\n\n') // Convert </p><p> to double newlines .replace(/<\/?(p|div|h[1-6])[^>]*>/gi, '\n') // Block elements to newlines .replace(/<[^>]*>/g, '') // Remove remaining HTML tags .replace(/ /g, ' ') // Replace HTML entities .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/[ \t]+/g, ' ') // Collapse horizontal whitespace only .replace(/\n\s*\n\s*\n/g, '\n\n') // Max 2 consecutive newlines .trim();
return { id: p.id, handle: p.handle, title: p.title, vendor: p.vendor, productType: p.product_type, price: `$${parseFloat(price).toFixed(0)} USD`, priceRaw: parseFloat(price), imageUrl, images, // All product images description, // Plain text description with newlines descriptionHtml, // Raw HTML description shopUrl: `https://shop.aesthetic.computer/products/${p.handle}`, inventory: totalInventory, available: totalInventory > 0, tags: p.tags ? p.tags.split(", ") : [], createdAt: p.created_at, updatedAt: p.updated_at, }; }) );
// Separate available and sold products const available = products.filter((p) => p.available); const sold = products.filter((p) => !p.available);
return { products, available, sold, total: products.length, lastUpdated: new Date().toISOString(), };}