↑↓ move↵ open⌘K toggle

    templates/page.resources.json

    Resources.

    The tools I build for my own workflow, the snippets I paste into every store, and the extensions I'd install on a new laptop first. Free to use.

    assets/tools/*

    Tools I built.

    Each one started as something I was tired of doing by hand.

    tools/hmac.js

    Webhook HMAC checker.

    The first thing to check when a webhook endpoint answers 401. Runs on Web Crypto in your browser; the secret never leaves this page.

    Computed signature

    –

    Paste the raw body and your app secret

    Most mismatches come from verifying a body that was already parsed and re-serialized. Read the raw text first, verify, then parse.

    snippets/*.liquid

    Liquid I paste into every store.

    Small, boring and right: money through filters, no layout shift, settings instead of hard-coded values.

    Price per serving

    snippets/price-per-serving.liquid

    Renders

    Shoppers compare cost per use. Servings come from a product metafield.

    {%- liquid
      assign variant = product.selected_or_first_available_variant
      assign servings = product.metafields.custom.servings.value | default: 1
      assign per = variant.price | divided_by: servings
    -%}
    <span class="per-serving">{{ per | money }} / serving</span>

    Savings in dollars

    snippets/savings.liquid

    Renders

    “Save $18” often reads bigger than “Save 15%”, and it’s a classic test.

    {%- if variant.compare_at_price > variant.price -%}
      {%- assign saved = variant.compare_at_price | minus: variant.price -%}
      <span class="badge">Save {{ saved | money_without_trailing_zeros }}</span>
    {%- endif -%}

    Free-shipping gap

    snippets/shipping-gap.liquid

    Renders

    The number that makes shoppers add one more item. Threshold from theme settings.

    {%- liquid
      assign threshold = settings.free_shipping_threshold | times: 100
      assign gap = threshold | minus: cart.total_price
    -%}
    {%- if gap > 0 -%}
      <p>You're {{ gap | money }} away from free shipping</p>
    {%- else -%}
      <p>Free shipping unlocked</p>
    {%- endif -%}

    Responsive product image

    snippets/product-image.liquid

    Renders

    Right size on every screen and no layout shift: width, height and srcset from Shopify’s CDN.

    {{
      image
      | image_url: width: 1800
      | image_tag:
        widths: '360, 540, 720, 960, 1200, 1800',
        sizes: '(min-width: 990px) 50vw, 100vw',
        loading: 'lazy',
        alt: image.alt
    }}

    scripts/capture.mjs

    How the screenshots are made.

    Full-page screenshots of modern stores come out half blank: sections fade in as you scroll, so they never paint. This captures one screen at a time instead.

    Never painted
    Full-page screenshot
    Alpha Lion homepage captured screen by screen
    Screen by screen, stitched
    • Waits for images before each screen
    • Sticky headers appear once
    • Popups and chat blocked
    • Opens the cart drawer by itself
    async function scrollCapture(page, maxCss) {
      let { vh, vw, total, dpr } = await page.evaluate((max) => {
        document.querySelectorAll('img[loading="lazy"]').forEach((img) => { img.loading = 'eager'; });
        // Sticky elements flow normally while we stitch: a sticky header then shows once at the
        // top, and a sticky section (whole heroes on some Webflow pages) isn't lost
        for (const el of document.querySelectorAll('body *')) {
          if (getComputedStyle(el).position === 'sticky') el.setAttribute('data-capture-unstick', '');
        }
        const style = document.createElement('style');
        style.textContent = 'html{scroll-behavior:auto!important}[data-capture-stuck]{visibility:hidden!important}[data-capture-unstick]{position:relative!important;top:auto!important}';
        document.head.appendChild(style);
        return { vh: innerHeight, vw: innerWidth, total: Math.min(document.documentElement.scrollHeight, max), dpr: devicePixelRatio };
      }, maxCss);
      const shots = [];
      let y = 0;
      for (let i = 0; i < 30; i++) {
        await page.evaluate((top) => window.scrollTo(0, top), y);
        await page.waitForTimeout(500);
        // Wait (briefly) for images now in view to finish loading
        await page.evaluate(() => Promise.race([
          Promise.all([...document.images].filter((img) => { const r = img.getBoundingClientRect(); return r.bottom > 0 && r.top < innerHeight && !img.complete; })
            .map((img) => new Promise((res) => { img.addEventListener('load', res, { once: true }); img.addEventListener('error', res, { once: true }); }))),
          new Promise((res) => setTimeout(res, 2000)),
        ]));
        const actual = await page.evaluate(() => Math.round(window.scrollY));
        if (shots.length) {
          if (actual === shots[shots.length - 1].y) break; // scrolling is locked or we reached the end
          await page.evaluate(() => {
            for (const el of document.querySelectorAll('body *')) {
              // Only bars fixed to the top (headers, announcement bars): they'd repeat on every screen
              const r = el.getBoundingClientRect();
              if (getComputedStyle(el).position === 'fixed' && r.top < 160 && r.height < 240) el.setAttribute('data-capture-stuck', '');
            }
          });
        }
        shots.push({ buf: await page.screenshot(), y: actual });
        // Pages that render sections as you scroll grow while we go, so re-measure every step
        total = await page.evaluate((max) => Math.min(document.documentElement.scrollHeight, max), maxCss);
        if (actual + vh >= total - 2) break;
        y = actual + vh;
      }
      await page.evaluate(() => {
        document.querySelectorAll('[data-capture-stuck],[data-capture-unstick]').forEach((el) => { el.removeAttribute('data-capture-stuck'); el.removeAttribute('data-capture-unstick'); });
        window.scrollTo(0, 0);
      });
      const height = Math.min(total, shots[shots.length - 1].y + vh);
      return sharp({ create: { width: Math.round(vw * dpr), height: Math.round(height * dpr), channels: 3, background: '#ffffff' } })
        .composite(shots.map((s) => ({ input: s.buf, left: 0, top: Math.round(s.y * dpr) })))
        .png().toBuffer();
    }

    config/toolbox.json

    Toolbox.

    Installed on day one. Most are free, and every one saves time on every project.

    sections/contact.liquid

    Let's buildthe next one.

    Hiring for a senior Shopify role, need a storefront rebuilt, an app built or a workflow automated? Send a line.