Skip to content

About

Website for the Updraft Pottery Studio

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

Updraft Pottery Studio website

Static site (plain HTML/CSS/JS) plus a small AWS Lambda that talks to Square.

site/      source: HTML templates, CSS, JS (+ js/test/ unit tests), data/*.json, images/ (full-size masters, never uploaded)
scripts/   build.mjs produces dist/; publish, deploy-api, put-token, serve (local preview)
dist/      generated; this is what gets uploaded to S3
backend/   Lambda: catalog, tax quote, checkout, signup (Square Catalog, Orders, Payments, Customers APIs) + test/
e2e/       browser tests (Playwright) against dist/ with the API mocked
deploy/    CloudFormation for hosting, stage settings, IAM policies; see deploy/README.md

Build

npm ci
SITE_URL=https://yourdomain.com npm run build
npm run preview

The build:

  • makes 400 / 800px AVIF and 400 / 800 / 1600px WebP copies of each photo (cached in .cache/). The grid uses 400 or 800 depending on screen (AVIF where the browser supports it); 1600 loads only when a photo is opened.
  • puts a content hash in every image file name, so a replaced photo gets a new URL automatically.
  • renders the galleries into the pages from site/data/*.json, so pages need no data requests.
  • adds canonical, Open Graph and icon tags to every page, plus robots.txt, site.webmanifest and (with SITE_URL set) sitemap.xml.
  • bundles, minifies and content-hashes the JS and CSS, and strips all comments from HTML/CSS/JS.
  • the Square payment SDK loads only once something is in the cart.

Adding your work

  1. Put the photo (JPG, metadata stripped) in site/images/work/ with the next number, e.g. 11.jpg.
  2. Add an entry to site/data/work.json:
    { "image": "images/work/11.jpg", "title": "Ash-glaze mug", "description": "Wheel thrown stoneware.", "alt": "Speckled grey mug on a wooden table" }
  3. npm run build and deploy.

Selling

Products come straight from your Square catalog. Add items in Square Dashboard > Items & services, with a price and a photo. They appear on the shop within a minute. Shop prices and tax are always calculated by Square on the server.

Shipping is priced per piece, from the delivery ZIP: one double-boxed parcel per piece, at the published label price for its billed weight and zone from Tacoma, rounded up to whole dollars (backend/src/shipping.mjs). UPS Ground carries everything it can; USPS Ground Advantage takes PO boxes, military addresses, Alaska, Hawaii and the territories. Each item needs a Ship weight (lb) custom attribute in Square: the billed weight of the packed box (the larger of actual and dimensional weight, from the pack-and-ship planner). An item without one can be picked up but not shipped. Rates and zones live in backend/src/shipping-rates.json; update it when UPS or USPS change prices (USPS holiday prices end 2027-01-17; the UPS fuel surcharge changes weekly).

Email signup

The "First look at new work" form posts to /api/subscribe, which adds the address to your Square customer directory (existing customers are matched by email, not duplicated). To keep website signups separate, they also go into a "Website newsletter" customer group: run node scripts/newsletter-group.mjs (--stage staging for staging), which finds or creates the group and prints its id, put that id in the stage's newsletterGroupId in deploy/stages.json, and run npm run deploy:api. An id from the wrong Square account (sandbox vs production) makes every signup fail. Send emails to that group with Square Marketing. Check Square's email-marketing consent rules before your first campaign.

Abuse limits: each running API instance allows 5 signup attempts per IP per 10 minutes and ignores repeat signups of the same address within an hour.

Concurrency limits per environment

Each Lambda function can only run so many copies at once (reserved concurrency). Past the limit, Lambda answers 429 instead of running. The limits are set per stage in deploy/stages.json and applied by npm run deploy:api:

Function Setting prod staging When it's reached
Api (catalog, quote, signup, share pages) apiConcurrency 5 2 Busy pages get a 429; the photo resizer retries its catalog check.
Checkout fixed in backend/template.yaml 1 1 On purpose: one order at a time, so a one-of-a-kind piece can't sell twice. Buyers queue and retry.
Images (photo resizer) imagesConcurrency 5 5 The first visit after new photos asks for more resizes at once; the page retries each photo twice, then shows Square's full-size original. npm run warm-photos makes every copy ahead of time.

The account needs 10 unreserved on top of all of these (both stages together: 19 reserved now). A new AWS account with a low Lambda concurrency quota can't reserve any; deploy with apiConcurrency: 0 there, which turns off every reservation.

Setup

  1. Square app: developer.squareup.com > create an application. Note the Application ID, the Access Token (sandbox first) and your Location ID.
  2. Deploy the API: put the stage's public Square settings (squareAppId, squareLocationId, squareEnv) in deploy/stages.json. Put the access token in backend/.env (git-ignored) and run npm run put-token, which stores it in SSM Parameter Store. Then npm run deploy:api and npm run publish. Add -- --stage staging to any of these for the staging site. Details are in deploy/README.md.
  3. Site hosting: S3 bucket (private) + CloudFront with Origin Access Control, default root object index.html, custom error response 404 -> /error.html, and an ACM certificate for your domain. Turn on Compress objects automatically (gzip/Brotli) in the CloudFront cache behavior. Upload the content-hashed folders with long caching and everything else (HTML, favicon, manifest, robots, sitemap) with short caching:
    aws s3 sync dist/ s3://YOUR-BUCKET --delete --exclude "*" --include "assets/*" --include "img/*" --include "full/*" --cache-control "public,max-age=31536000,immutable"
    aws s3 sync dist/ s3://YOUR-BUCKET --delete --exclude "assets/*" --exclude "img/*" --exclude "full/*" --cache-control "public,max-age=300"
    
    Security headers: attach a CloudFront response headers policy to the default behavior with HSTS (max-age=63072000; includeSubDomains), X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, frame options DENY, and a Content-Security-Policy along these lines (add your API origin to connect-src if apiBase is not same-origin):
    default-src 'self'; img-src 'self' data: https://*.squarecdn.com https://items-images-production.s3.us-west-2.amazonaws.com https://items-images-sandbox.s3.us-west-2.amazonaws.com;
    script-src 'self' https://*.squarecdn.com https://js.squareup.com https://js.squareupsandbox.com;
    style-src 'self' 'unsafe-inline' https://*.squarecdn.com; font-src 'self' https://*.squarecdn.com https://d1g145x70srn7h.cloudfront.net;
    connect-src 'self' https://*.squareup.com https://*.squareupsandbox.com https://*.squarecdn.com https://o160250.ingest.sentry.io;
    frame-src https://*.squarecdn.com https://*.squareup.com https://*.squareupsandbox.com https://*.cardinalcommerce.com;
    base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'
    
    Check the hosts against Square's current Web Payments SDK CSP docs, and roll it out as Content-Security-Policy-Report-Only first, then switch once a sandbox checkout works with no violations.
  4. Go live: test the whole flow on staging first, with Square's test card 4111 1111 1111 1111. In deploy/stages.json, set prod's squareEnv to "production" with the production application ID, location ID and newsletter group ID. Load the pieces into the production catalog with node scripts/load-items.mjs <batch.json> --go (it also creates the "Ship weight (lb)" attribute). Then, together, store the token with npm run put-token and run npm run deploy:api and npm run publish: the live functions read the new token on their next start, so storing it before the deploy breaks the shop until the deploy finishes. For a production stage, each script asks for the token with hidden input (nothing shows, and it never enters a command or the shell history); it never reads backend/.env, which holds the sandbox token. Try the prompt first with node scripts/square-client.mjs --try-prompt.

Local preview and checks

npm run build && npm run preview   # http://localhost:4173
npm run preview -- --api https://staging.updraftpotterystudio.com   # same, with the shop's data from staging (sandbox)
npm run lint                       # ESLint
npm run typecheck                  # TypeScript over the JS (JSDoc types)
npm run format:check               # Prettier (npm run format to fix)
npm test                           # unit tests: site/js/test and backend/test
npm run test:e2e                   # browser tests in Chromium, Firefox and WebKit, incl. axe accessibility checks (after a build)
npm run test:lighthouse            # Lighthouse budgets on the phone preset (after a build; set CHROME_PATH if Chrome isn't installed)

CI (.github/workflows/ci.yml) runs all of these, plus cfn-lint on the CloudFormation templates, on every push to master and on pull requests. It caches encoded photos (named by a hash of their inputs) and Playwright's browsers between runs. CodeQL scans the code, and Dependabot opens weekly grouped dependency updates. Deploys run from Actions → Deploy (deploy.yml).

Checkout load test

Every piece is one of a kind, so two buyers must never both pay for it. Checkout runs one order at a time and re-checks stock after authorising the card (see backend/template.yaml). npm run load-test proves it on a deployed sandbox stage: it sends many simultaneous buyers through CloudFront, exactly as the site does, at a piece with stock 1.

npm run load-test -- --stage staging --buyers 12 --item "test 2" --verify --restock

--verify asks Square how many payments completed or were left authorised, and --restock puts the piece back to stock 1. Both need the sandbox token. The script refuses to run against a production stage.

Result on staging, 2026-10-07, 12 buyers at once:

Check Result
Buyers charged 1
Other buyers told "sold, not charged" 11 of 11
Stock shown afterwards 0
Square payments completed / authorisations left open 1 / 0
Time until the last buyer heard "sold" 53 s (queued behind one-at-a-time checkout)

About

Website for the Updraft Pottery Studio

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages