CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Jekyll 3.9 site for serin-labs.com — marketing, docs, blog, and storefront markup for the Serin CN105 smart controller for Mitsubishi mini-splits. Static output only: no server-side code lives here (the contact API and analytics run on the VPS, in the separate openbin-deploy repo).
Repos and deployment
The directory is named serin-labs.github.io but origin is the private repo Serin-Labs/serin-site. The public remote (Serin-Labs/serin-labs.github.io) is a CNAME-only 301 redirect stub that must stay public — never push site source to it.
Production is serin-labs.com, served by Caddy file_server on a VPS. .github/workflows/deploy-vps.yml runs on push to main: build → commerce tests → restricted rsync of _site/ → live smoke test of POST /api/contact (it fills the honeypot, so nothing is delivered). A broken contact endpoint fails the deploy.
redirect_to in _config.yml is a migration toggle: empty serves content normally; setting it turns every page into a redirect.
Commands
bundle is not on the default PATH on this machine:
export PATH="/home/akifb/.local/share/gem/ruby/3.3.0/bin:$PATH"
bundle exec jekyll build # ~2s; a csv deprecation and a faraday-retry notice always print — not failures
Tests are plain Node scripts using assert — no runner, no package.json (and the repo must keep none; put jsdom harnesses and npm installs in the session scratchpad, never here). Run one with node tests/<name>.test.js.
- Need a fresh
jekyll buildfirst (they read_site/):check-build.js,render-prices.test.js,faq-match.test.js,compatibility-data.test.js - Load
assets/js/*.jsdirectly viarequire(), no build needed: all the rest (model-checker,buy-link,gallery*,flash-*,parse-label)
The CI gate is: node tests/buy-link.test.js && node tests/check-build.js && node tests/render-prices.test.js && node tests/faq-match.test.js. CHECK_BUILD_STRICT=1 promotes check-build warnings to failures.
For browser verification, serve the built site over HTTP (python3 -m http.server -d _site) — the Playwright MCP Chrome blocks file:// URLs. Screenshots given a relative filename land in the repo working directory; delete them before committing.
Architecture
Drift-as-build-failure is the test philosophy. The same fact is deliberately stated in two places (data file + JS module, visible copy + JSON-LD), and a test makes disagreement fail the build:
_data/store.ymlis the single source of truth for commerce: products, selectors (radio/checkbox), one Shopify variant ID per selector combination, whole-dollar integer prices,on_etsyflags. Rendered by_includes/buy.html,offer-terms.html,product-jsonld.html; guarded byrender-pricesandcheck-build._data/compatibility.yml(the on-page record list) andassets/js/model-checker.js(the checker's verdicts) must agree —compatibility-data.test.js.- FAQ JSON-LD must match the visible FAQ copy word-for-word (Google treats mismatch as cloaking) —
faq-match.test.js.
Page JS (assets/js/) consists of small dependency-free modules wrapped in IIFEs that attach to window in a browser and module.exports under Node — that dual export is what lets tests require() them. Keep logic (decisions, classification, formatting) in these modules and out of inline render()/DOM code so it stays testable; flash-console.js is the reference example. Third-party JS is vendored in assets/js/vendor/ (never SRI-pin jsdelivr /+esm bundles — their hashes drift).
Blog: posts in _posts/, permalink /blog/:title.html. Jekyll's default future: false applies and there is no cron, so future-dated posts publish only when someone re-runs the deploy workflow on or after their date — this manual drip is deliberate. The backlog, evidence rules, and the house style contract (SmartHomeScene register) live in docs/content-backlog.md; read it before writing or editing a post.
Analytics: everything goes through window.serinTrack(name, props) defined in _layouts/default.html (guards a missing tracker, defers early calls). The Umami script loads first-party from /t/ — a Caddy proxy that exists only on serin-labs.com, so events are untestable on any other host. Event catalog: docs/analytics-events.md.
docs/, tests/, tools/, and the design_handoff_* dirs are excluded from the Jekyll build. temp/ holds full-res image masters (gitignored); tools/build-product-images.py derives the published assets/ images from them.
Content rules
- HomeKit copy framing: sell a generic controller; the HomeKit-compatible firmware is a free, open-source thing you flash. Never imply the device ships with it pre-installed, and never claim the firmware is © Serin Labs (it builds on esp-homekit-sdk and GPL-3.0 SwiCago code — see
licenses.html). - Blog posts are evidence-heavy: no invented measurements, quotes, or benchmarks. A post that needs an untaken measurement gets the measurement or gets cut.
- Prices in
store.ymlare whole-dollar integers; if cents are ever needed, add aprice_displaystring rather than formatting floats in Liquid. - The site title is "Serin Labs" (never "Serin") — SEO entity resolution depends on one spelling everywhere.
Concurrent sessions
Multiple Claude sessions sometimes work this same worktree at once. "File modified since read" notices are usually real concurrent edits. Before staging a shared file, check git diff ownership and commit only your own hunks (rebuild HEAD + your edits via git hash-object -w + git update-index --cacheinfo if needed); re-run the relevant tests against what is actually staged, not just the worktree.