P-001 – Dynamic Square catalog and hosted checkout
P-001 – Dynamic Square catalog and hosted checkout
Purpose / Big Picture
Hermeticus needs the placeholder /books/ page replaced with a real, buyer-facing catalog that pulls live inventory from Square and lets visitors buy one or more books through Square-hosted checkout. The website remains a static Jekyll site on GitHub Pages. A repo-owned Cloudflare Worker provides the dynamic layer: it reads Square catalog and inventory data, caches the public catalog response at Cloudflare, validates cart requests server-side, and creates Square payment links so card data never touches this repository or the browser code.
V1 is intentionally narrow:
- show live books from Square on
pages/books.md - include name, price, customer-facing description, category, stock-aware purchase controls, and multiple images
- support a simple multi-item cart
- send buyers to a Square-hosted checkout page
- keep all secrets out of git and out of the browser
Context and Orientation
Repository facts as of 2026-04-22:
- The public site origin is
https://hermeticus.org, fromCNAME. - The current catalog page is a placeholder at
pages/books.md. - Jekyll serves
assets/js/main.json every page from_layouts/default.html. - Sass entrypoint is
assets/css/main.scss, which currently imports_sass/_tokens.scss,_sass/_base.scss,_sass/_layout.scss, and_sass/_components.scss.
External system facts used by this plan:
- Square Catalog items always contain at least one variation. V1 treats a book as publishable only when it has exactly one sellable variation. Multiple variations are out of scope for now and will be skipped.
- Square
ListCatalogreturns up to 100 objects per page and uses cursors. The worker must paginate. - Square exposes category names on
CATEGORYobjects and item image URLs onIMAGEobjects. - Square Checkout
CreatePaymentLinkcan create a Square-hosted checkout page for an order containing multiple line items. - Square Payment Links currently advertise no monthly fee on Square’s public pricing page; transaction fees still apply.
- Cloudflare Workers support secrets and cacheable responses at the edge.
V1 publishing rule:
- A book appears on the website only if it is active in Square, has exactly one variation, has a non-zero price, and has tracked inventory above zero at the configured Square location.
- Archiving the item or reducing inventory to zero removes it from the website after cache expiry or refresh.
Scope
- Add a repo-owned Cloudflare Worker project at
integrations/square-catalog-worker/. - Implement
GET /catalogon the worker to return a minimal JSON array of public book data. - Cache the
GET /catalogresponse at Cloudflare so the Square APIs are not called on every page view. - Implement
POST /checkouton the worker to validate a submitted cart against fresh Square data and create a Square-hosted payment link for one-or-more books. - Replace the placeholder content in
pages/books.mdwith a catalog mount point, explanatory copy, and no-JavaScript fallback text. - Extend
assets/js/main.jsto render the catalog, manage a lightweight cart, and redirect buyers to Square checkout. - Add catalog and cart styling in the existing Sass structure.
- Add operator documentation for configuration, deployment, and rollback.
- Add a Docker-based local preview workflow so the site can be served without relying on host Ruby setup.
- Update durable docs for the new architecture and decisions.
Non-goals
- No custom backend other than the Cloudflare Worker.
- No card-entry UI on
hermeticus.org. - No user accounts, saved carts, wishlists, search indexing, or faceted filtering.
- No support for Square items with multiple sellable variations.
- No modifiers, subscriptions, shipping calculation, tax customization, discount codes, or custom attributes.
- No attempt to make the static site work as a storefront without JavaScript. With JavaScript disabled, the books page remains informational and directs users to contact the shop.
Interfaces and Dependencies
integrations/square-catalog-worker/src/index.jsPublic worker entrypoint with two routes:GET /catalogPOST /checkout
integrations/square-catalog-worker/wrangler.tomlWorker config with no secrets committed.integrations/square-catalog-worker/package.jsonLocal tooling for tests and deploy commands.integrations/square-catalog-worker/test/*.test.jsWorker-focused tests for filtering, serialization, CORS, and checkout validation._includes/square-catalog.htmlHTML shell for the catalog UI.assets/js/main.jsClient-side rendering and cart logic.assets/css/main.scssSass entrypoint that must import any new partial used for catalog styles._sass/_components.scssExisting component layer; preferred place for catalog and cart styles unless the code becomes too large.pages/books.mdPublic catalog page.docs/architecture.mdStable description of the Jekyll-to-Worker-to-Square flow.docs/decisions.mdDurable rationale for hosted checkout, inventory-gated publishing, and cached public JSON.docs/square-catalog.mdRunbook for deployment, secrets, and recovery.
Worker configuration, all repo-visible and non-secret unless marked secret:
ALLOWED_ORIGINSComma-separated list. V1 must includehttps://hermeticus.org. Local preview originhttp://127.0.0.1:4000may be added for development.SQUARE_ENVproductionorsandbox.SQUARE_LOCATION_IDSquare location to use for inventory checks and checkout creation.CATALOG_TTL_SECONDSEdge cache TTL forGET /catalog. V1 target:300.SQUARE_ACCESS_TOKENSecret. Never committed.SQUARE_VERSIONPinned API version string used in outbound Square requests.
Public catalog JSON shape from GET /catalog:
- Top-level value: array
- Item keys:
i– Square item IDv– Square variation IDn– item namep– price in minor unitsd– customer-facing description or empty stringc– category name or empty stringm– array of image URLsq– integer quantity available atSQUARE_LOCATION_ID
Checkout request and response shape:
POST /checkoutRequest body:{ "items": [{ "v": "<variation-id>", "q": <integer> }, ...] }- Response body on success:
{ "u": "<square-checkout-url>" }
Plan of Work
- Rewrite the plan into a concrete v1 architecture and move it to
plans/active/. - Scaffold the worker in
integrations/square-catalog-worker/with repo-owned config and tests. - Implement Square client code for:
- paginated catalog fetch of
ITEM,CATEGORY,IMAGE - inventory count retrieval for publishable variation IDs
- creation of a hosted payment link for a validated cart
- paginated catalog fetch of
- Implement edge caching for the public catalog response with a short TTL and explicit cache headers.
- Replace the
/books/placeholder page with a real catalog shell and cart UI. - Validate the full flow locally and through worker tests. Deploy the worker, then point the site at the deployed worker URL.
- Update progress and durable docs as concrete outcomes land.
Concrete Steps
All repo commands run from /Users/q/src/hermeticus unless noted.
- Create the worker project:
mkdir -p integrations/square-catalog-worker/src integrations/square-catalog-worker/test
- Add worker files:
integrations/square-catalog-worker/package.jsonintegrations/square-catalog-worker/wrangler.tomlintegrations/square-catalog-worker/src/index.jsintegrations/square-catalog-worker/test/catalog-worker.test.js
- Install worker dependencies:
npm install --prefix integrations/square-catalog-worker
- Run worker tests:
npm test --prefix integrations/square-catalog-worker
- Update site files:
- create
_includes/square-catalog.html - replace
pages/books.md - update
assets/js/main.js - update
assets/css/main.scssif a new Sass partial is added - update
_sass/_components.scssor add_sass/_catalog.scss
- Build Jekyll:
bundle exec jekyll build
- Configure Cloudflare secrets and vars before deploy:
npx wrangler secret put SQUARE_ACCESS_TOKEN --config integrations/square-catalog-worker/wrangler.toml
npx wrangler deploy --config integrations/square-catalog-worker/wrangler.toml
- Validate the deployed worker:
curl -H 'Origin: https://hermeticus.org' '<worker-url>/catalog'
curl -X POST '<worker-url>/checkout' \
-H 'Content-Type: application/json' \
-H 'Origin: https://hermeticus.org' \
--data '{"items":[{"v":"<variation-id>","q":1}]}'
- Set the site-side worker URL in the page or include configuration and push the Jekyll changes to
main. - Add
Dockerfileanddocker-compose.ymlfor local Jekyll preview with the project mounted into the container and host-accessible ports.
Validation and Acceptance
Acceptance is met only when all of the following are true:
npm test --prefix integrations/square-catalog-workerpasses.bundle exec jekyll buildsucceeds with no warnings introduced by this work.GET /catalogresponds200toOrigin: https://hermeticus.organd returns a JSON array using only the documented short keys.GET /catalogomits items that are archived, zero-priced, have zero inventory, or have more than one variation.GET /catalogincludes category names and all associated item image URLs for publishable items.- Repeated
GET /catalogrequests hit Cloudflare cache according to headers and do not call Square on every request. POST /checkoutrejects malformed carts, empty carts, duplicate variation IDs, quantities below 1, and quantities above current stock.POST /checkoutcreates a Square-hosted checkout URL for a valid multi-item cart.- The
/books/page loads the catalog, renders image galleries, price, description, category, and stock-aware purchase controls. - A buyer can add more than one distinct book to the cart and reach a Square-hosted checkout page.
- With JavaScript disabled,
/books/still presents meaningful explanatory content and contact information instead of broken UI. - Reducing a listed item’s inventory to zero or archiving it in Square removes it from the public page after cache expiry or refresh.
Idempotence and Recovery
- Re-running
npx wrangler deploy --config integrations/square-catalog-worker/wrangler.tomlupdates the worker in place without exposing secrets. - Rotating the Square token requires only re-running
wrangler secret put; no code changes are needed. - If the deployed worker breaks the site:
- set the books page back to static explanatory content and push, or
- redeploy the previous known-good worker version from the last good commit
- If the worker cannot reach Square,
GET /catalogshould fail closed with a clear error response and the browser should render a friendly unavailable state. - If
POST /checkoutcannot validate live inventory, it must return an error and avoid creating a stale payment link. - If local preview needs worker access, add
http://127.0.0.1:4000toALLOWED_ORIGINSfor development only.
Progress
- Moved
P-001fromplans/queue/toplans/active/. - Reviewed the original plan against repo state and current external docs.
- Chose the v1 architecture: cached public catalog endpoint plus server-validated multi-item hosted checkout.
- Scaffold
integrations/square-catalog-worker/. - Implement worker routes, filtering, caching, and checkout creation.
- Replace the
/books/placeholder with the dynamic catalog UI. - Run worker tests and
bundle exec jekyll build. - Deploy the worker and wire the final worker URL into the site.
- Update
plans/progress.md,docs/architecture.md,docs/decisions.md, anddocs/square-catalog.md. - Add Docker-based local preview files and usage notes.
- Commit and push to
main.
Surprises & Discoveries
- 2026-04-22: The placeholder catalog page already exists at
pages/books.md, so v1 should replace that page directly instead of introducing a second product page. - 2026-04-22: The shared
pagelayout wraps content in.prose, which caps the catalog UI at40rem; the books page needs a full-width body while keeping its intro copy readable. - 2026-04-22:
CNAMEalready fixes the production origin ashttps://hermeticus.org, so the production CORS allowlist value is known and should not remain an open question. - 2026-04-22: Square
ListCatalogpaginates at 100 objects per page, so the worker must iterate cursors even though current catalog size will likely be much smaller. - 2026-04-22: Square Checkout supports hosted checkout for an order with multiple line items, which makes a lightweight cart feasible without exposing payment credentials in the browser.
- 2026-04-22:
npm installinitially failed because the default npm cache contains root-owned files on this machine. Using a temp cache directory worked without changing global state. - 2026-04-22:
bundle exec jekyll buildis currently blocked in this environment because the installed Ruby is2.6.10, while the currentgithub-pagesdependency set now resolves gems that require Ruby>= 3.0. - 2026-04-22: Running the GitHub Pages Jekyll stack directly on a modern host Ruby surfaced several compatibility issues, so a containerized local preview flow is useful even after the site itself works in production.
- 2026-04-22: The repo already had
.bundle/configpointing atvendor/bundle, so the Docker setup needs its ownBUNDLE_APP_CONFIGand bundle volume to avoid rewriting repo-local Bundler settings. - 2026-04-22: For this repo’s local preview workflow, it is better to bake gems into the Docker image and rebuild on
GemfileorGemfile.lockchanges than to runbundle installon every container start. - 2026-04-22: After updating to the current GitHub Pages gemset,
jekyll serve --livereloadin Docker still crashes in Jekyll’s LiveReload WebSocket layer withHTTP::Parser::Error, even though the site build succeeds.
Decision Log
- 2026-04-22: V1 uses Square-hosted checkout created by the worker instead of embedding payment fields on the website. Reason: This is the simplest secure path, keeps card handling off the site, and supports multi-item orders.
- 2026-04-22: V1 requires exactly one sellable variation per published book and skips items with multiple variations. Reason: The project explicitly does not want user-facing variations yet, and filtering them out avoids ambiguous pricing and inventory rules.
- 2026-04-22: V1 publishes only items with tracked inventory above zero at one configured Square location. Reason: Inventory-gated publishing is the simplest reliable way to avoid advertising sold items.
- 2026-04-22: The worker returns a compressed public schema with short keys. Reason: The catalog is read often and changes infrequently; compact responses reduce bandwidth without hiding meaning in the codebase.
- 2026-04-22:
GET /catalogis cached, butPOST /checkoutvalidates against fresh Square data. Reason: Browsing should be cheap and fast, while checkout should prioritize correctness over cache hits. - 2026-04-22: Local preview should run in Docker with the repo bind-mounted into the container. Reason: That keeps Ruby and gem churn off the host machine while still giving a reproducible preview environment for Jekyll work.
- 2026-04-22: The Docker image should contain Ruby gems, while the bind mount should contain only source files from the host repo. Reason: This matches the intended workflow better, avoids repeated startup installs, and keeps dependency ownership inside the container boundary.
- 2026-04-22: The Docker preview should prefer a stable
jekyll serveprocess over built-in LiveReload. Reason: A working local server is more important than automatic refresh, and the current LiveReload path is crashing after the site has already built successfully.
Outcomes & Retrospective
- In progress. Worker tests pass,
bundle exec jekyll buildsucceeds inside the Dockerized Ruby environment, the deployed catalog works on the live site, the repo now has a stable Docker-based local preview path, and the home/about content reflects the shop visitors actually describe. Final plan closure still depends on user review and explicit confirmation.
Change Log
- 2026-04-22: Activated the plan, removed contradictions, chose the v1 architecture, and rewrote the plan as an execution-ready document.
- 2026-04-22: Updated the books page and catalog UI follow-up scope to remove the stale loading banner, break the catalog out of the prose column, and restyle the cart as a distinct sidebar.
- 2026-04-22: Reverted the empty-cart hiding and mobile cart reordering follow-up after it behaved incorrectly in the live layout.
- 2026-04-22: Replaced placeholder home-page and about-page copy with review-based content, and tightened the home hero copy to better match the shop.