⚑ Cloudflare Workers Workshop

Add AI to any websitein 15 minutes

Deploy a static store, then wrap it with an AI chatbot Worker β€” no code changes to your origin.

2
Workers (Custom Domain + Route)
0
origin changes
0
lines to edit in Worker code
Free
plan supports the full demo

Architecture

⚑
Worker A
Single-file store (HTML inside)
β†’
πŸ€–
Worker B
Wraps A Β· injects AI chatbot
β†’
✨
Workers AI
Llama 3.1 8B Fast

Choose a scenario

Before you start: You need a free Cloudflare account. No credit card, no CLI, no server required.

How this demo binds Workers to your domain

  • Custom Domain β€” Worker is the origin. Used for Worker A (the store). Cloudflare auto-creates the DNS record + SSL cert.
  • Route β€” Worker runs in front of an existing origin. Used for Worker B (the chatbot wrapper). Intercepts traffic and modifies it.
  • .workers.dev β€” Auto-generated test URL. Useful for smoke-testing Worker A in isolation; cannot be the target of a Route, so the chatbot demo requires the Custom Domain path above.
0

Connect a Custom Hostname (required first)

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.

1 Already have a domain on Cloudflare? Verify the zone shows Active in dash β†’ Websites. Skip to Step 1.
2 No domain yet? Either register one via Cloudflare Registrar (cheapest TLDs, no markup) or add an existing domain via dash β†’ Add a domain. Activation takes ~5 minutes once nameservers are updated at your existing registrar.
3 Free plan is fine. No credit card needed for the demo.
4 Decide on the demo subdomain now (e.g. store.yourdomain.com) β€” you'll use it in Step 1 (Custom Domain for Worker A) and Step 2 (Route for Worker B).
⚠️ Why not .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.
1

Create Worker A and bind it to your Custom Domain

Worker A serves the static HTML store. Bind it as a Custom Domain so it becomes the origin for store.yourdomain.com.

1 Go to dash.cloudflare.com β†’ Workers & Pages in the left sidebar
2 Click Create application β†’ Create Worker
3 Name it my-demo-store β†’ click Deploy
4 Click Edit code β€” the online editor opens
5 Delete the default code, then paste Worker A code below (click Copy first)
6 Click Save and deploy
7 Bind the Custom Domain: Worker β†’ Settings β†’ Domains & Routes β†’ Add β†’ Custom Domain β†’ enter store.yourdomain.com β†’ Add Custom Domain. Cloudflare creates the DNS record and SSL cert (~30 s).
8 Verify Worker A is live at https://store.yourdomain.com β€” you should see the store page.
Worker A β€” paste into editor
⬇ Download
// 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 &nbsp;Β·&nbsp; <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 &amp; Armchairs</a>
      <a href="#">Bedroom</a>
      <a href="#">Storage</a>
      <a href="#">Desks &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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">&copy; 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
2

Create Worker B and bind it as a Route

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.

βœ… No code edits. Worker B uses X-Worker-Bypass to avoid fetch loops, and same-zone fetch() from Route β†’ Custom Domain works without a service binding.
1 Workers & Pages β†’ Create application β†’ Create Worker
2 Name it my-demo-chatbot β†’ Deploy
3 Edit code β†’ delete default code β†’ paste Worker B code below
4 No code edits needed β€” paste as-is. Hostname is determined by the Route binding in the next steps.
5 Save and deploy
6 Bind the Route: Worker β†’ Settings β†’ Domains & Routes β†’ Add β†’ Route β†’ Pattern: store.yourdomain.com/* β†’ Zone: yourdomain.com β†’ Add Route
7 Open https://store.yourdomain.com β€” Worker B intercepts, fetches Worker A, injects the chatbot widget. πŸŽ‰
Worker B β€” paste into editor
⬇ Download
/**
 * 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">&#x1F4AC;</button>',
  '<div id="cf-win">',
  '<div id="cf-hd"><div id="cf-dot"></div>Store AI &#x26A1; 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">&#x2192;</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' }
    })
  }
}
⚠️ Enable Workers AI binding: After deploying Worker B, go to its Settings β†’ Bindings β†’ Add β†’ choose Workers AI β†’ variable name: AI β†’ Save. Then redeploy. Free tier: 10,000 neurons/day.
πŸŽ‰ Done! Open 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.

Under the hood

What Worker B does

πŸ”Œ
Creates POST /api/chat
A new endpoint that didn't exist on your origin. Calls Llama 3.1 via Workers AI.
πŸ”
Uses HTMLRewriter
Appends the chat widget to <body> on every HTML response. Server: zero changes.
✨
Runs Workers AI
Llama 3.1 8B Fast on Cloudflare's network. Free: 10,000 neurons/day.

Go further

Add more features

See all 14 demos live

Every Cloudflare product demoed in context β€” HΓ–MSTYLE furniture store and STREAMVAULT streaming.