Deploy a static store, then wrap it with an AI chatbot Worker β no code changes to your origin.
Point R2's built-in Sippy migration at your existing S3 bucket. Every object is served from R2 the moment it's first requested β copied transparently in the background. No migration Worker to write.
Architecture
Architecture
No Worker in this path at all β R2's native GetObject does the fallback-and-copy itself once Sippy is enabled on the bucket.
Choose a scenario
How this demo binds Workers to your domain
Both Workers bind to a hostname on a Cloudflare-managed zone. This must exist before you deploy either Worker. Pick a subdomain you'll use for the demo, e.g. store.yourdomain.com.
store.yourdomain.com) β you'll use it in Step 1 (Custom Domain for Worker A) and Step 2 (Route for Worker B). .workers.dev? Routes only fire on Cloudflare-managed zones. Worker B injects the chatbot via a Route; without a Custom Hostname, Worker B never intercepts traffic and the demo will not work.
Worker A serves the static HTML store. Bind it as a Custom Domain so it becomes the origin for store.yourdomain.com.
my-demo-store β click Deploy store.yourdomain.com β Add Custom Domain. Cloudflare creates the DNS record and SSL cert (~30 s). https://store.yourdomain.com β you should see the store page. // worker-a.js β Single-file static store (HTML embedded)
// β
Paste this ENTIRE file into the Worker editor β no other files needed
// β
Deploy Worker A β then deploy Worker B on the same domain with a route
const STORE_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>HΓMSTYLE β Scandinavian Furniture</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#f5f5f5;color:#111}
.topbar{background:#111;color:#fff;text-align:center;padding:8px;font-size:12px}
.topbar span{color:#FFDA1A;font-weight:700}
header{background:#0058A3;position:sticky;top:0;z-index:100}
.hdr{max-width:1200px;margin:0 auto;padding:0 24px;height:68px;
display:flex;align-items:center;gap:20px}
.logo{font-size:24px;font-weight:900;color:#fff}
.logo span{color:#FFDA1A}
.search{flex:1;display:flex;max-width:480px}
.search input{flex:1;border:none;padding:10px 14px;font-size:14px;
border-radius:4px 0 0 4px;outline:none}
.search button{background:#FFDA1A;border:none;padding:0 14px;
border-radius:0 4px 4px 0;cursor:pointer}
.hdr-right{margin-left:auto;display:flex;gap:16px}
.hdr-link{color:#b3d1f0;font-size:12px;text-decoration:none;
text-align:center;cursor:pointer}
.hdr-link .i{font-size:18px;display:block}
.catnav{background:#003e7e}
.catnav-inner{max-width:1200px;margin:0 auto;display:flex;overflow-x:auto}
.catnav a{color:#b3d1f0;text-decoration:none;padding:10px 16px;
font-size:13px;white-space:nowrap}
.catnav a:hover,.catnav a.act{background:#0058A3;color:#fff}
.hero{background:#FFDA1A;padding:0}
.hero-inner{max-width:1200px;margin:0 auto;padding:48px 40px;
display:flex;align-items:center;justify-content:space-between;gap:24px}
.hero h1{font-size:42px;font-weight:900;color:#111;line-height:1.05;margin-bottom:14px}
.hero p{font-size:15px;color:#333;margin-bottom:24px;max-width:420px}
.hero-tag{font-size:11px;font-weight:800;color:#0058A3;
text-transform:uppercase;letter-spacing:1.5px;margin-bottom:10px}
.btn-p{background:#0058A3;color:#fff;border:none;padding:14px 28px;
font-size:15px;font-weight:800;border-radius:4px;cursor:pointer}
.btn-s{background:transparent;color:#0058A3;border:2px solid #0058A3;
padding:13px 28px;font-size:15px;font-weight:800;border-radius:4px;cursor:pointer}
.hero-vis{display:flex;flex-wrap:wrap;gap:10px;width:240px}
.hero-vis div{background:rgba(255,255,255,.6);border-radius:8px;
width:105px;height:105px;display:flex;align-items:center;
justify-content:center;font-size:50px}
.promo{max-width:1200px;margin:24px auto;padding:0 24px;
display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
.promo-c{border-radius:8px;padding:18px;display:flex;align-items:center;gap:14px}
.promo-c h3{font-size:13px;font-weight:800;margin-bottom:3px}
.promo-c p{font-size:12px;color:#555;line-height:1.4}
.section{max-width:1200px;margin:0 auto;padding:28px 24px}
.sec-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.sec-h h2{font-size:24px;font-weight:900}
.sec-h a{color:#0058A3;font-size:13px;font-weight:700;text-decoration:none}
.rooms{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px}
.room{background:#fff;border:1px solid #e5e7eb;border-radius:8px;
overflow:hidden;cursor:pointer;text-align:center;transition:transform .15s}
.room:hover{transform:translateY(-2px);border-color:#0058A3}
.room-img{background:#f0f4f8;height:100px;display:flex;
align-items:center;justify-content:center;font-size:48px}
.room-name{font-size:12px;font-weight:800;padding:8px 6px 10px}
.products{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:18px}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:6px;
overflow:hidden;cursor:pointer;position:relative;transition:box-shadow .15s}
.card:hover{box-shadow:0 4px 20px rgba(0,88,163,.15);border-color:#0058A3}
.card-img{aspect-ratio:1;background:#f5f5f5;display:flex;
align-items:center;justify-content:center;font-size:76px;position:relative}
.badge{position:absolute;top:10px;left:10px;background:#FFDA1A;color:#111;
font-size:10px;font-weight:900;padding:2px 8px;border-radius:3px}
.badge.sale{background:#cc0000;color:#fff}
.card-body{padding:14px}
.card-cat{font-size:11px;color:#767676;text-transform:uppercase;
letter-spacing:.5px;margin-bottom:3px}
.card-name{font-size:15px;font-weight:900;color:#111;margin-bottom:3px}
.card-desc{font-size:12px;color:#767676;margin-bottom:8px;line-height:1.4}
.stars{color:#FFDA1A;font-size:12px}
.rating{font-size:11px;color:#767676;margin-bottom:10px}
.card-ft{display:flex;align-items:center;justify-content:space-between}
.price{font-size:20px;font-weight:900;color:#0058A3}
.was{font-size:11px;color:#cc0000;font-weight:700}
.add{background:#0058A3;color:#fff;border:none;padding:9px 16px;
font-size:12px;font-weight:800;border-radius:4px;cursor:pointer}
.inspire{background:linear-gradient(135deg,#003e7e,#0058A3);
border-radius:12px;padding:44px 40px;color:#fff;
display:flex;align-items:center;justify-content:space-between;gap:20px}
.inspire h2{font-size:26px;font-weight:900;margin-bottom:10px}
.inspire p{font-size:14px;color:rgba(255,255,255,.8);
max-width:360px;line-height:1.6;margin-bottom:18px}
.ir{display:flex;gap:10px;flex-wrap:wrap}
.ir div{background:rgba(255,255,255,.15);border-radius:6px;
padding:10px 16px;font-size:13px;font-weight:700;cursor:pointer}
.reviews{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}
.rv{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:18px}
.rv-h{display:flex;justify-content:space-between;margin-bottom:10px}
.rv-name{font-weight:700;font-size:13px}
.rv-prod{font-size:11px;color:#767676;margin-bottom:6px}
.rv-text{font-size:12px;color:#333;line-height:1.6}
.support{background:#fff8e1;border:1px solid #fde68a;border-radius:8px;
padding:18px 22px;margin:0 24px 24px;font-size:13px;
color:#92400e;display:flex;align-items:center;gap:14px}
footer{background:#111;color:#aaa;padding:36px 24px}
.fi{max-width:1200px;margin:0 auto;
display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:28px}
footer h4{color:#fff;font-size:12px;font-weight:800;margin-bottom:10px;
text-transform:uppercase;letter-spacing:.5px}
footer a{display:block;color:#888;text-decoration:none;font-size:12px;margin-bottom:5px}
footer a:hover{color:#fff}
.fb{max-width:1200px;margin:20px auto 0;padding-top:20px;
border-top:1px solid #333;font-size:11px;color:#555;text-align:center}
@media(max-width:768px){
.hero-inner{flex-direction:column;padding:28px 20px}
.hero h1{font-size:28px}
.hero-vis,.ir{display:none}
.promo{grid-template-columns:1fr}
.inspire{flex-direction:column}
}
</style>
</head>
<body>
<div class="topbar">π Free delivery over ΰΈΏ2,500 Β· <span>SAVE15</span> β 15% off first order</div>
<header>
<div class="hdr">
<div class="logo">HΓM<span>STYLE</span></div>
<div class="search">
<input placeholder="Search products, rooms, brandsβ¦">
<button>π</button>
</div>
<div class="hdr-right">
<a class="hdr-link"><span class="i">π€</span>Wishlist</a>
<a class="hdr-link"><span class="i">π€</span>Account</a>
<a class="hdr-link"><span class="i">π</span>Cart</a>
</div>
</div>
</header>
<div class="catnav">
<div class="catnav-inner">
<a href="#" class="act">All Products</a>
<a href="#">Sofas & Armchairs</a>
<a href="#">Bedroom</a>
<a href="#">Storage</a>
<a href="#">Desks & Office</a>
<a href="#">Dining</a>
<a href="#">Lighting</a>
<a href="#">Living Room</a>
<a href="#" style="color:#FFDA1A;font-weight:700">π· Sale</a>
</div>
</div>
<div class="hero">
<div class="hero-inner">
<div>
<div class="hero-tag">Summer 2026 Collection</div>
<h1>Designed for<br>the way you live.</h1>
<p>Scandinavian-inspired furniture β functional, lasting, and affordable. Free delivery over ΰΈΏ2,500.</p>
<div style="display:flex;gap:12px;flex-wrap:wrap">
<button class="btn-p">Shop All Products</button>
<button class="btn-s">Room Inspiration</button>
</div>
</div>
<div class="hero-vis">
<div>πͺ</div><div>ποΈ</div><div>π</div><div>π₯οΈ</div>
</div>
</div>
</div>
<div class="promo">
<div class="promo-c" style="background:#eff6ff"><div style="font-size:32px">π</div><div><h3>Free Delivery</h3><p>On orders over ΰΈΏ2,500. Bangkok & metro area.</p></div></div>
<div class="promo-c" style="background:#f0fdf4"><div style="font-size:32px">π</div><div><h3>365-Day Returns</h3><p>Return any product within a year, hassle-free.</p></div></div>
<div class="promo-c" style="background:#fff7ed"><div style="font-size:32px">π </div><div><h3>Assembly Service</h3><p>Professional home assembly from ΰΈΏ299.</p></div></div>
</div>
<div class="section">
<div class="sec-h"><h2>Shop by Room</h2><a href="#">View all β</a></div>
<div class="rooms">
<div class="room"><div class="room-img">ποΈ</div><div class="room-name">Living Room</div></div>
<div class="room"><div class="room-img">ποΈ</div><div class="room-name">Bedroom</div></div>
<div class="room"><div class="room-img">π₯οΈ</div><div class="room-name">Home Office</div></div>
<div class="room"><div class="room-img">π½οΈ</div><div class="room-name">Dining Room</div></div>
<div class="room"><div class="room-img">π¦</div><div class="room-name">Storage</div></div>
<div class="room"><div class="room-img">π‘</div><div class="room-name">Lighting</div></div>
</div>
</div>
<div class="section" style="padding-top:0">
<div class="sec-h"><h2>Bestsellers This Month</h2><a href="#">See all β</a></div>
<div class="products">
<div class="card"><div class="card-img">πͺ<span class="badge">Best Seller</span></div><div class="card-body"><div class="card-cat">Sofas & Armchairs</div><div class="card-name">POΓNG Armchair</div><div class="card-desc">Birch veneer, cushion included. Layer upon layer of comfort.</div><div class="rating"><span class="stars">β
β
β
β
β
</span> 4.7 (18,432 reviews)</div><div class="card-ft"><div class="price">ΰΈΏ6,990</div><button class="add">Add to Cart</button></div></div></div>
<div class="card"><div class="card-img">π</div><div class="card-body"><div class="card-cat">Storage & Shelving</div><div class="card-name">BILLY Bookcase</div><div class="card-desc">Most versatile bookcase. Adjustable shelves, easy assembly.</div><div class="rating"><span class="stars">β
β
β
β
β
</span> 4.8 (42,100 reviews)</div><div class="card-ft"><div class="price">ΰΈΏ3,490</div><button class="add">Add to Cart</button></div></div></div>
<div class="card"><div class="card-img">ποΈ</div><div class="card-body"><div class="card-cat">Sofas & Armchairs</div><div class="card-name">KIVIK 3-seat Sofa</div><div class="card-desc">Deep, generous seating. Washable covers in 10 colours.</div><div class="rating"><span class="stars">β
β
β
β
β</span> 4.4 (9,876 reviews)</div><div class="card-ft"><div class="price">ΰΈΏ31,490</div><button class="add">Add to Cart</button></div></div></div>
<div class="card"><div class="card-img">π₯οΈ<span class="badge">New</span></div><div class="card-body"><div class="card-cat">Desks & Office</div><div class="card-name">BEKANT Sit/Stand Desk</div><div class="card-desc">Electric height adjustment. Silent motor, memory positions.</div><div class="rating"><span class="stars">β
β
β
β
β</span> 4.5 (7,203 reviews)</div><div class="card-ft"><div class="price">ΰΈΏ14,990</div><button class="add">Add to Cart</button></div></div></div>
<div class="card"><div class="card-img">π¦</div><div class="card-body"><div class="card-cat">Storage & Shelving</div><div class="card-name">KALLAX Shelf Unit</div><div class="card-desc">Cubes of possibility. Mix open and closed storage.</div><div class="rating"><span class="stars">β
β
β
β
β
</span> 4.9 (31,200 reviews)</div><div class="card-ft"><div class="price">ΰΈΏ2,990</div><button class="add">Add to Cart</button></div></div></div>
<div class="card"><div class="card-img">πΊ</div><div class="card-body"><div class="card-cat">Desks & Office</div><div class="card-name">MARKUS Office Chair</div><div class="card-desc">Ergonomic mesh back, lumbar support. 8-hour comfort.</div><div class="rating"><span class="stars">β
β
β
β
β</span> 4.5 (5,432 reviews)</div><div class="card-ft"><div class="price">ΰΈΏ11,990</div><button class="add">Add to Cart</button></div></div></div>
<div class="card"><div class="card-img">πͺ</div><div class="card-body"><div class="card-cat">Sofas & Armchairs</div><div class="card-name">STRANDMON Wing Chair</div><div class="card-desc">High back for excellent neck support. Great for reading.</div><div class="rating"><span class="stars">β
β
β
β
β
</span> 4.6 (11,203 reviews)</div><div class="card-ft"><div class="price">ΰΈΏ10,490</div><button class="add">Add to Cart</button></div></div></div>
<div class="card"><div class="card-img">ποΈ<span class="badge sale">Sale</span></div><div class="card-body"><div class="card-cat">Bedroom</div><div class="card-name">HEMNES Bed Frame</div><div class="card-desc">Solid pine, traditional style. Works with all IKEA mattresses.</div><div class="rating"><span class="stars">β
β
β
β
β</span> 4.3 (8,921 reviews)</div><div class="card-ft"><div><div class="price">ΰΈΏ10,990</div><div class="was">Was ΰΈΏ13,490</div></div><button class="add">Add to Cart</button></div></div></div>
</div>
</div>
<div class="section" style="padding-top:0">
<div class="inspire">
<div>
<h2>Create your perfect space</h2>
<p>Browse room inspiration guides β mix and match furniture to build the home you've always imagined. Free interior design consultation available.</p>
<button class="btn-p">Get Inspired</button>
</div>
<div class="ir">
<div>ποΈ Living Room</div><div>ποΈ Bedroom</div>
<div>π₯οΈ Home Office</div><div>π½οΈ Dining</div>
</div>
</div>
</div>
<div class="section" style="padding-top:0">
<div class="sec-h"><h2>What Customers Say</h2><a href="#">All reviews β</a></div>
<div class="reviews">
<div class="rv"><div class="rv-h"><div><div class="rv-name">Natchaya K. <span class="stars">β
β
β
β
β
</span></div><div class="rv-prod">POΓNG Armchair Β· Feb 2024</div></div><div style="color:#22c55e;font-size:11px;font-weight:700">β Verified</div></div><div class="rv-text">"I've had my POΓNG for 12 years and it still looks perfect. Just bought a second one. Unbeatable quality for the price."</div></div>
<div class="rv"><div class="rv-h"><div><div class="rv-name">Tanawat P. <span class="stars">β
β
β
β
β</span></div><div class="rv-prod">KALLAX Shelf Unit Β· Jan 2024</div></div><div style="color:#22c55e;font-size:11px;font-weight:700">β Verified</div></div><div class="rv-text">"I have 8 KALLAX units across my apartment in different configurations. Added inserts to half for closed storage. Incredibly versatile."</div></div>
<div class="rv"><div class="rv-h"><div><div class="rv-name">Siriporn T. <span class="stars">β
β
β
β
β
</span></div><div class="rv-prod">MARKUS Chair Β· Feb 2024</div></div><div style="color:#22c55e;font-size:11px;font-weight:700">β Verified</div></div><div class="rv-text">"MARKUS is genuinely comfortable for 8-hour work days. The lumbar support is excellent. Assembly took an hour but worth it."</div></div>
</div>
</div>
<div class="support">
<div style="font-size:28px">β</div>
<div>
<strong>Have a question?</strong> Email <strong>support@homstyle.com</strong> Β·
<span style="color:#c0392b;font-weight:700">β οΈ Response time: 3β5 business days</span> Β·
Phone: 02-123-4567 (MonβFri 9amβ6pm)
<!-- NO AI CHATBOT β deploy Worker B to add instant AI support -->
</div>
</div>
<footer>
<div class="fi">
<div><div style="font-size:20px;font-weight:900;color:#fff;margin-bottom:8px">HΓM<span style="color:#FFDA1A">STYLE</span></div><p style="font-size:12px;line-height:1.6">Scandinavian-inspired furniture for modern living.</p></div>
<div><h4>Products</h4><a href="#">Sofas & Armchairs</a><a href="#">Bedroom</a><a href="#">Storage</a><a href="#">Office</a><a href="#">Dining</a></div>
<div><h4>Service</h4><a href="#">Order Status</a><a href="#">Returns</a><a href="#">Assembly</a><a href="#">Store Locator</a></div>
<div><h4>About</h4><a href="#">Our Story</a><a href="#">Sustainability</a><a href="#">Careers</a></div>
</div>
<div class="fb">© 2026 HΓMSTYLE Co., Ltd. Β· Demo site for Cloudflare Workers workshops.</div>
</footer>
</body>
</html>`
// Request handler
export default {
async fetch(request) {
const url = new URL(request.url)
if (!url.pathname.includes('.') || url.pathname.endsWith('.html')) {
return new Response(STORE_HTML, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'public, max-age=300',
}
})
}
return new Response('Not found', { status: 404, headers: { 'Content-Type': 'text/plain' } })
}
}
// What this demonstrates:
// β
Worker serves a full IKEA-style store globally (300+ Cloudflare PoPs)
// β
No origin server needed β the Worker IS the origin
// β
Zero cold starts, <1ms boot time
// β
Free: 100,000 requests/day on the free tier
// β
Worker B wraps this Worker via a route β no origin changes needed Worker B wraps Worker A and injects the AI chatbot. Bind it as a Route on the same hostname β Cloudflare runs Worker B before requests reach Worker A.
X-Worker-Bypass to avoid fetch loops, and same-zone fetch() from Route β Custom Domain works without a service binding. my-demo-chatbot β Deploy store.yourdomain.com/* β Zone: yourdomain.com β Add Route https://store.yourdomain.com β Worker B intercepts, fetches Worker A, injects the chatbot widget. π /**
* Worker B: AI chatbot
*
* How it reads products:
* 1. Fetches HTML from Worker A
* 2. Parses product names + prices from the HTML response (server-side)
* 3. Injects window.__cfProducts = "..." into the page as a JS variable
* 4. Also injects the chat widget into <body>
* 5. /api/chat receives window.__cfProducts from the browser and uses it
* to build the system prompt dynamically β no hardcoding needed
*
* Change Worker A's products β Worker B reads the new list on the next request.
*
* β οΈ Required: Add Workers AI binding in dashboard BEFORE deploying
* Settings β Variables β AI Bindings β Add binding β name: AI
*/
// Worker code is hostname-agnostic β bind it to your Cloudflare zone via
// wrangler.toml [[routes]] block (or the dashboard Settings β Routes).
// Loop prevention: Worker B adds X-Worker-Bypass: 1 when fetching origin.
// On re-entry the Worker sees that header and passes straight through,
// so Cloudflare routes to the real Worker A without re-triggering this code.
// ββ Chat widget HTML βββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Rules: no backticks, no single-quotes inside single-quoted strings,
// all event handlers via addEventListener in the <script> block
const CHAT_WIDGET = [
'<style>',
'#cf-btn{position:fixed;bottom:24px;right:24px;width:54px;height:54px;',
'border-radius:50%;background:#0058A3;color:#fff;border:none;font-size:22px;',
'cursor:pointer;box-shadow:0 4px 20px rgba(0,88,163,.5);z-index:9999}',
'#cf-win{display:none;position:fixed;bottom:88px;right:24px;width:320px;',
'height:420px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;',
'box-shadow:0 8px 40px rgba(0,0,0,.15);z-index:9999;flex-direction:column;overflow:hidden}',
'#cf-win.open{display:flex}',
'#cf-hd{background:#0058A3;color:#fff;padding:14px 16px;font-weight:800;font-size:14px;display:flex;align-items:center;gap:8px}',
'#cf-dot{width:8px;height:8px;border-radius:50%;background:#FFDA1A;animation:cfp 2s infinite}',
'@keyframes cfp{0%,100%{opacity:1}50%{opacity:.4}}',
'#cf-ms{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px}',
'.m{max-width:85%;padding:8px 12px;border-radius:10px;font-size:13px;line-height:1.5}',
'.m.bot{background:#eff6ff;color:#1e3a5f;align-self:flex-start}',
'.m.usr{background:#0058A3;color:#fff;align-self:flex-end}',
'#cf-row{padding:10px;border-top:1px solid #f0f0f0;display:flex;gap:8px}',
'#cf-in{flex:1;border:1px solid #d1d5db;border-radius:8px;padding:8px 12px;font-size:13px;outline:none}',
'#cf-sb{background:#FFDA1A;color:#111;border:none;padding:8px 14px;border-radius:8px;font-weight:800;cursor:pointer}',
'</style>',
// HTML β no onclick attributes (all events wired in <script> block below)
'<button id="cf-btn">💬</button>',
'<div id="cf-win">',
'<div id="cf-hd"><div id="cf-dot"></div>Store AI ⚡ Workers AI</div>',
'<div id="cf-ms"><div class="m bot">Hi! Ask me anything about our products.</div></div>',
'<div id="cf-row">',
'<input id="cf-in" placeholder="Ask about any product...">',
'<button id="cf-sb">→</button>',
'</div></div>',
// Script β addEventListener instead of onclick so no quote escaping needed
'<script>',
'(function(){',
' var btn=document.getElementById("cf-btn");',
' var win=document.getElementById("cf-win");',
' var inp=document.getElementById("cf-in");',
' var sb =document.getElementById("cf-sb");',
' var ms =document.getElementById("cf-ms");',
' btn.addEventListener("click",function(){win.classList.toggle("open");});',
' sb.addEventListener("click",cfS);',
' inp.addEventListener("keydown",function(e){if(e.key==="Enter")cfS();});',
' function add(s,r){',
' var d=document.createElement("div");',
' d.className="m "+r; d.textContent=s;',
' ms.appendChild(d); ms.scrollTop=ms.scrollHeight; return d;',
' }',
// window.__cfProducts is injected server-side by Worker B before this script runs
' async function cfS(){',
' var t=inp.value.trim(); if(!t)return; inp.value="";',
' add(t,"usr"); var p=add("...","bot");',
' try{',
' var res=await fetch("/api/chat",{',
' method:"POST",',
' headers:{"Content-Type":"application/json"},',
' body:JSON.stringify({message:t,products:window.__cfProducts||""})});',
' var d=await res.json();',
' p.textContent=d.response||(d.error||"Try again.");',
' }catch(e){p.textContent="Connection error.";}',
' ms.scrollTop=ms.scrollHeight;',
' }',
'})()',
'</script>'
].join("")
// ββ Server-side product extraction from HTML response βββββββββββββββββββββ
// Worker B reads the HTML from Worker A and extracts product data
// before serving it to the browser. No DOM scraping needed.
function extractProducts(html) {
var names = []
var prices = []
// Extract all .card-name text values
var nm = html.match(/class="card-name"[^>]*>([^<]+)</g) || []
nm.forEach(function(s){ var v = s.replace(/^[^>]+>/, '').replace(/<$/, '').trim(); if(v) names.push(v) })
// Extract all .price / .card-price text values
var pr = html.match(/class="(?:price|card-price)"[^>]*>([^<]+)</g) || []
pr.forEach(function(s){ var v = s.replace(/^[^>]+>/, '').replace(/<$/, '').trim(); if(v) prices.push(v) })
var results = []
var len = Math.min(names.length, prices.length)
for (var i = 0; i < len; i++) {
results.push(names[i] + ' (' + prices[i] + ')')
}
return results.join('; ')
}
export default {
async fetch(request, env) {
const url = new URL(request.url)
// ββ Bypass pass-through βββββββββββββββββββββββββββββββββββββββββββββββββ
// Worker B fetched origin with this header to avoid an infinite loop.
// Pass straight through β Cloudflare routes to Worker A.
if (request.headers.get('X-Worker-Bypass') === '1') {
return fetch(request)
}
if (request.method === 'OPTIONS') {
return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'Content-Type' } })
}
// ββ POST /api/chat ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (url.pathname === '/api/chat' && request.method === 'POST') {
try {
if (!env.AI) {
return Response.json(
{ response: 'Workers AI binding missing. Go to dashboard: Worker Settings β Variables β AI Bindings β Add β name: AI', error: 'no_ai_binding' },
{ status: 503, headers: { 'Access-Control-Allow-Origin': '*' } }
)
}
const { message, products } = await request.json()
// System prompt built from products extracted by Worker B server-side
const systemPrompt = products
? 'You are a helpful store assistant. The store sells: ' + products + '. Answer in 2-3 sentences. Be friendly.'
: 'You are a helpful store assistant. Answer in 2-3 sentences.'
const result = await env.AI.run('@cf/zai-org/glm-4.7-flash', {
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: message }
],
max_tokens: 150
})
return Response.json(
{ response: result.response },
{ headers: { 'Access-Control-Allow-Origin': '*' } }
)
} catch (err) {
return Response.json(
{ response: 'Error: ' + err.message, error: err.message },
{ status: 500, headers: { 'Access-Control-Allow-Origin': '*' } }
)
}
}
// ββ Fetch HTML from origin using bypass header (no infinite loop) βββββββ
const originReq = new Request(request, {
headers: { ...Object.fromEntries(request.headers), 'X-Worker-Bypass': '1' },
})
const res = await fetch(originReq)
if (!res.headers.get('Content-Type')?.includes('text/html')) return res
// Read HTML as text so we can:
// 1. Extract product data server-side (no DOM scraping in browser)
// 2. Inject window.__cfProducts = "..." into the page
// 3. Inject the chat widget
const html = await res.text()
const products = extractProducts(html)
// Inject product data as a JS variable + chat widget before </body>
const injection =
'<script>window.__cfProducts = "' + products.replace(/"/g, '\"') + '";</script>' +
CHAT_WIDGET
const modified = html.replace('</body>', injection + '</body>')
return new Response(modified, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
})
}
} AI β Save. Then redeploy. Free tier: 10,000 neurons/day.
https://store.yourdomain.com β Worker B intercepts, Worker A renders the store, and the chat widget is injected. Ask "What's a good sofa under ΰΈΏ20,000?" β Llama 3.1 8B Fast answers instantly.
How Sippy connects your two clouds β no Worker involved
GetObject, R2 checks itself first. If the object isn't there yet, R2 fetches it from S3, returns it, and copies it into R2 β automatically, with zero custom code.A minimal bucket with a few sample images is enough to prove the migration works β this is deliberately kept small.
my-sippy-source) in any region. {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket*", "s3:GetObject*"],
"Resource": [
"arn:aws:s3:::my-sippy-source",
"arn:aws:s3:::my-sippy-source/*"
]
}
]
} This is the migration destination β where objects will actually live once Sippy copies them over.
Go to dash.cloudflare.com β left sidebar: Storage & databases β R2 β Create bucket
Name it (e.g. my-images) β Create bucket
Generate an R2 API token: R2 Overview β Manage R2 API Tokens β Create API Token β give it Object Read & Write permission β Create API Token. Save the Access Key ID and Secret Access Key β Sippy needs these too, alongside your AWS credentials, since it writes into R2 on your behalf.
R2's Public bucket feature exposes the bucket directly to the internet over your own domain. There is no Worker anywhere in this scenario.
Open your bucket β Settings tab β Custom Domains β Add
Enter images.yourdomain.com β Continue β review the DNS record β Connect Domain
Status starts as Initializing and flips to Active within a few minutes.
One form. No code, no deploy step. This is the whole point of Sippy.
Open your bucket β Settings tab β scroll to On Demand Migration β Enable
Select Amazon S3 as the provider
Enter your S3 bucket name, region, and the AWS Access Key ID / Secret Access Key from Step 0
Enter the R2 API token Access Key ID / Secret Access Key from Step 1
Enable. That's it β no deploy, no Worker, no wrangler command required for this path.
Real Sippy responses don't carry a distinguishing header (there's no X-Served-By to check) β verify via the dashboard instead.
Visit https://images.yourdomain.com/<one-of-your-sample-image-keys> β it loads, proxied live from S3 on this first request.
Check R2 β your bucket β Objects β the object now exists in R2, copied automatically by that one request.
Check R2 β your bucket β Metrics tab β "Requests served by Sippy" and "Data migrated by Sippy" tick up as you load more objects.
Load the same image again β this time it's served directly from R2, not S3. Zero egress cost on this and every subsequent request for that object.
Under the hood
Under the hood
Go further
Every Cloudflare product demoed in context β HΓMSTYLE furniture store and STREAMVAULT streaming.