Build your first shop template
This walkthrough builds First shop: a small responsive shopfront with a product search, availability labels and links to Bottle’s product pages. It uses no libraries, server code or API keys.
You need a text editor and access to Shop → Design → Look in a Bottle business with its shop enabled. Use a test shop while learning. You do not need access to Bottle’s source repository or AWS.
Download the finished walkthrough ZIP if you want to try it first. The download is built from the four code blocks below, and tested against Bottle’s upload checker.
1. Create four files
Section titled “1. Create four files”Create a folder called first-shop with these files:
first-shop/ bottle.template.json index.html style.css shop.jsDo not add a package manager, React project, PHP files or build configuration. These are finished browser files. If you use a build tool in your own project, upload its compatible final output instead of the source project.
2. Tell Bottle what to load
Section titled “2. Tell Bottle what to load”Save this as bottle.template.json:
{ "format": "bottle-template", "schemaVersion": 1, "name": "First shop", "description": "A simple, responsive catalogue with search and availability labels.", "pages": { "home": "index.html", "products": "index.html", "category": "index.html" }, "styles": ["style.css"], "scripts": ["shop.js"]}The same HTML serves the homepage, catalogue and categories. JavaScript will change the title for the current page. We deliberately leave out page, so custom content pages retain Bottle’s existing sections and layout. Product details, accounts, basket and checkout are always Bottle-owned.
3. Build the page structure
Section titled “3. Build the page structure”Save this as index.html:
<!doctype html><html lang="en-GB"> <head> <title>First shop</title> </head> <body> <a href="#catalogue" class="skip-link">Skip to products</a> <header class="site-header"> <a href="/" data-shop-link class="brand" id="shop-name">Our shop</a> <nav aria-label="Shop"> <a href="/products" data-shop-link>Products</a> <a href="/basket" data-shop-link>Basket</a> <a href="/account/sign-in" data-shop-link>Sign in</a> </nav> </header> <main id="catalogue"> <section class="intro" aria-labelledby="page-title"> <p class="eyebrow">Our products</p> <h1 id="page-title">Choose your gas and accessories</h1> <p>View a product to check its options and add it to your basket.</p> </section> <section aria-label="Product catalogue"> <div class="search-box"> <label for="product-search">Search products</label> <input type="search" id="product-search" placeholder="Product name or description" /> </div> <p id="catalogue-limit" hidden> More products are available. Choose a category to narrow your search. </p> <div id="categories" class="categories" aria-label="Product categories"></div> <p id="result-count" role="status" aria-live="polite"></p> <div id="products" class="product-grid" aria-busy="true"></div> <p id="empty" hidden>No products match your search. Try another name.</p> </section> </main> <footer>Delivery options and the final total are shown during checkout.</footer> </body></html>Use normal headings, links, labels and buttons. The search input has type="search", which Bottle supports. Do not add <meta>, <script>, <style>, <link> or form tags; Bottle provides the document setup and installs the manifest’s CSS and JavaScript.
The data-shop-link attributes are ours, not a special Bottle language. They help our JavaScript identify which links should call Bottle.navigate.
4. Make it responsive
Section titled “4. Make it responsive”Save this as style.css:
:root { color-scheme: light; --ink: #142d28; --muted: #475c56; --paper: #f6f5ef; --line: #c7d4ca; --accent: #14563c;}* { box-sizing: border-box;}body { margin: 0; background: var(--paper); color: var(--ink); font: 1rem/1.6 system-ui, sans-serif;}a { color: var(--accent);}a:focus-visible,input:focus-visible { outline: 3px solid #14563c; outline-offset: 4px;}.skip-link { position: absolute; left: 1rem; top: -5rem;}.skip-link:focus { top: 1rem; background: white; padding: 0.5rem;}.site-header,main,footer { max-width: 76rem; margin: auto; padding: 1.5rem;}.site-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem;}.brand { font-size: 1.4rem; font-weight: 800; text-decoration: none;}nav { display: flex; flex-wrap: wrap; gap: 1rem;}nav a,.categories a { padding: 0.5rem;}.intro { padding: 2rem 0;}.eyebrow { font-weight: 700; color: var(--accent);}h1 { font-size: clamp(2rem, 6vw, 3.5rem); line-height: 1.15; max-width: 18ch;}.search-box { display: grid; gap: 0.5rem; max-width: 30rem;}label { font-weight: 700;}input { width: 100%; padding: 0.8rem; border: 1px solid var(--line); border-radius: 0.6rem; font: inherit;}.categories { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1rem 0;}.product-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1.25rem;}.product-card { min-width: 0; background: white; padding: 1.25rem; border: 1px solid var(--line); border-radius: 1rem;}.product-card img { display: block; width: 100%; height: 12rem; object-fit: contain;}.product-card h2 { font-size: 1.25rem;}.product-card p { color: var(--muted);}.price { font-size: 1.5rem; font-weight: 800;}.product-card .price { color: var(--ink);}.status { font-weight: 700;}.product-link { display: inline-block; padding: 0.6rem 0; font-weight: 700;}[hidden] { display: none !important;}@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; }}The grid stacks naturally on small screens. Product pictures use object-fit: contain so cylinders and accessories are not cropped or stretched. Text labels communicate availability without relying on colour. Change the colours and spacing to your own brand; there are no Yard or Flame constraints.
5. Render real products
Section titled “5. Render real products”Save this as shop.js:
Bottle.ready(function (data) { 'use strict'; var grid = document.getElementById('products'); var search = document.getElementById('product-search'); var money = new Intl.NumberFormat('en-GB', { style: 'currency', currency: data.shop.currency }); var labels = { in_stock: 'In stock', low: 'Low stock', out: 'Out of stock', backorder: 'Available to order', }; document.getElementById('shop-name').textContent = data.shop.name; if (data.page.type !== 'home') document.getElementById('page-title').textContent = data.page.title; document.getElementById('catalogue-limit').hidden = !data.catalogueTruncated;
function text(tag, value, className) { var node = document.createElement(tag); node.textContent = value; if (className) node.className = className; return node; }
data.categories.forEach(function (category) { var link = text('a', category.name); link.href = category.path; link.setAttribute('data-shop-link', ''); if (category.slug === data.page.categorySlug) link.setAttribute('aria-current', 'page'); document.getElementById('categories').append(link); });
function render() { var query = search.value.trim().toLocaleLowerCase('en-GB'); var matching = data.products.filter(function (product) { return (product.name + ' ' + (product.description || '')) .toLocaleLowerCase('en-GB') .includes(query); }); grid.replaceChildren(); matching.forEach(function (product) { var card = document.createElement('article'); card.className = 'product-card'; var picture = product.images[0]; if (picture) { var image = document.createElement('img'); image.src = picture.src; image.alt = picture.alt; image.width = picture.width; image.height = picture.height; image.loading = 'lazy'; card.append(image); } card.append(text('h2', product.name)); card.append(text('p', money.format(product.pricePence / 100), 'price')); card.append(text('p', labels[product.availability], 'status')); if (product.description) card.append(text('p', product.description)); var link = text('a', 'View product', 'product-link'); link.href = product.path; link.setAttribute('data-shop-link', ''); card.append(link); grid.append(card); }); document.getElementById('result-count').textContent = matching.length + (matching.length === 1 ? ' product' : ' products'); document.getElementById('empty').hidden = matching.length > 0; grid.setAttribute('aria-busy', 'false'); }
document.addEventListener('click', function (event) { var link = event.target instanceof Element ? event.target.closest('a[data-shop-link]') : null; if (!link) return; event.preventDefault(); Bottle.navigate(link.getAttribute('href')); }); search.addEventListener('input', render); render();});Bottle.ready supplies this shop’s public catalogue after the interface is connected. Render inside its callback—not before it. Prices are integer pence, so divide by 100 for pounds. Missing descriptions and pictures are normal; the example handles both.
Each product links to its supplied path. Do not construct paths from names, use IDs to call the API, or implement your own checkout. A product page handles quantity, cylinder swaps, cylinder charges and the actual basket operation. The price shown here is the public product price, not a quote including delivery or extra cylinder charges.
6. ZIP and upload
Section titled “6. ZIP and upload”Select the four files inside first-shop, and compress them. Do not compress the enclosing folder. Opening the ZIP should show bottle.template.json immediately.
Open Shop → Design → Look → Browse templates → Upload. Drop the ZIP into the upload area. You should see the name First shop and three supported layouts. Choose Use in draft.
Opening index.html directly from your computer is not a full test: Bottle’s interface and live data only exist inside the shop runtime. A local file can show the basic structure, but it cannot supply the products. Use the draft preview for a real test.
7. Check the draft
Section titled “7. Check the draft”Before publishing:
- Check the homepage,
/productsand a category containing published products. - Search for an existing product, then a name that matches nothing; clear the search again.
- Open a product and use Bottle’s basket flow. Check that basket and sign-in links work.
- Check narrow phone widths, long product names, no picture and no products.
- Use the keyboard to reach search and links, and check that focus is visible.
- Check the browser console for unexpected JavaScript errors. See troubleshooting if anything fails.
Testing checkout can create real orders or payments. Do that on a test shop with test payment settings, not a customer’s live shop.
8. Publish or undo
Section titled “8. Publish or undo”Publish through the shop editor when you are happy. Applying the ZIP alone never changes the live shop. To undo unpublished work, choose Discard changes. To roll back a published design, apply a saved template to the draft, preview it and publish again.
Your existing products, customers, prices, orders and content sections remain stored. Next, explore the recipes or the more detailed Harbour example.