- TypeScript 81.8%
- JavaScript 13.9%
- Dockerfile 1.8%
- Liquid 1.3%
- Shell 0.7%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| app | ||
| deploy | ||
| extensions/hover-swap | ||
| prisma | ||
| public | ||
| scripts | ||
| storefront/src | ||
| tests | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .eslintignore | ||
| .eslintrc.cjs | ||
| .gitattributes | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| DEPLOYMENT.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| env.d.ts | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
| shopify.app.toml | ||
| shopify.web.toml | ||
| TASK.md | ||
| THIRD_PARTY_LICENSES.md | ||
| tsconfig.json | ||
| VALIDATION.md | ||
| vite.config.ts | ||
| vitest.config.ts | ||
Hover Swap
Hover Swap displays the next product photo when a mouse or trackpad hovers over a product-card image, and restores the original view on leave. It works on collection/search grids and recommendation cards, including cards on a product page. The main PDP gallery is excluded. Touch-only devices perform no application product/image preparation.
Architecture
- Official Shopify React Router template architecture, React, App Bridge and Polaris web components. Authentication/token exchange and webhook HMAC verification use
@shopify/shopify-app-react-router. - Prisma stores Shopify sessions only. Local SQLite and production PostgreSQL have separate schemas/migrations with the same Session fields, including expiring offline token support.
- Theme App Extension App Embed serves assets through Shopify CDN. Enable/disable, Fade/Instant and 0–1000 ms duration are configured in Theme Editor. Shopify loads the schema JavaScript asset non-blockingly.
- Framework-free TypeScript storefront, compiled by esbuild. No product synchronization, Storefront API token, image proxy or page-view backend requests. No Admin API product/theme scopes are required.
- Authenticated uninstall, scopes-update and mandatory privacy webhooks. No shopper data is collected. Shop redaction/uninstall removes sessions; repeated deliveries are safe.
The initial scaffold came from Shopify's official React Router template, with demo routes removed. Platform references: extension configuration and editor deep links, Ajax Product API, and Polaris web components.
Project structure
app/ Embedded admin, auth, webhook routes, health
extensions/hover-swap/ Liquid embed, locales, generated CDN assets
storefront/src/ Detection, cache, loading, rendering, observers
prisma/schema.prisma Local SQLite sessions
prisma/postgresql/ Production schema and PostgreSQL migrations
scripts/ Storefront build, fixture server, consistency checks
tests/ Unit and Chromium storefront tests
deploy/ VPS reverse proxy and deployment helper
Dockerfile, docker-compose.yml
Requirements and Windows local setup
Node.js 24, npm, Git, a Shopify Dev Dashboard organization, a development app and development store with an Online Store theme. Docker is only needed for production-container testing. Work in this repository; commands below use PowerShell.
npm ci
Copy-Item .env.example .env
npm run setup
npm run build:storefront
setup generates Prisma Client and applies SQLite migrations. DATABASE_URL=file:dev.sqlite resolves relative to prisma/schema.prisma. Do not commit .env, databases or tokens.
For a local backend smoke test, placeholder API key/secret from .env.example are sufficient; they cannot authenticate a Shopify store:
npm run dev:local
# http://localhost:8012 and http://localhost:8012/health
Ports 8012 (backend), 8013 (fixture) and 64999 (local Vite HMR) are separate from Bagira 8000 and Ygen 8001. Check availability before starting. In production, Compose publishes only loopback 8012. Use HOVER_SWAP_PORT to change the host port.
Connect a Shopify development app/store
Shopify CLI is installed as a project dev dependency; run it with npm run shopify -- ... or npx shopify .... This repository intentionally ships a zero placeholder client_id and example.com URLs. They must be linked before Shopify development or release:
- Sign in at Dev Dashboard, create/select an organization and development app, and create/select a development store. An account without an organization causes
Cannot find a valid organizationin CLI. - Run
npm run config:linkand select that app. Review changes toshopify.app.toml; preserve the empty access scopes and webhook subscriptions. - Set
SHOPIFY_API_KEY,SHOPIFY_API_SECRETand development URL in your ignored.envfrom that app's credentials.npm run shopify -- app env showcan display environment values locally; never paste secrets into tickets or Git. - Run
npm run dev -- --store your-development-store.myshopify.com. CLI provides a public tunnel, adjusts development URLs and previews the extension. Follow its install link for the development store. - Open Hover Swap in Shopify admin. Click Enable in Theme Editor (documented
activateAppId=API_KEY/hover-swapdeep link), enable the embed and Save. Verify the switch in Theme Editor; admin reports installation, not a guessed activation state. - Add two or more photos to a test product and hover its card on a collection page. Check PDP gallery and PDP recommendations separately.
For storefront changes, keep npm run watch:storefront running in a second terminal while npm run dev watches the extension assets. No manual theme edits are needed. Do not install in a live store or release the extension without approval.
Detection and safe fallback
Discovery starts from same-origin product links, including localized and collection-scoped routes. CardDetector combines known card shortcuts with structural scoring (semantic card/item names, repeated siblings, grid/list/search/recommendation context). Mixed-product ancestors and navigation/cart/gallery containers are rejected. ThemeAdapter is a small extension point for future compatibility improvements; generic detection remains the fallback.
ImageResolver searches the card media wrapper for a distinct, non-icon/swatches secondary image. It preloads a decorative overlay using that URL, preserving theme-owned DOM and image sources. If unavailable, ProductCache fetches window.Shopify.routes.root + products/HANDLE.js, shares pending promises and caches the second image or safe null result. At most four Ajax requests run concurrently, with an eight-second timeout. Failed products/images and one-photo products are skipped.
VisibilityObserver starts preparation within 300 px of the viewport. With no IntersectionObserver, preparation waits for first hover. HoverRenderer adds an absolutely positioned, pointer-transparent, empty-alt/aria-hidden overlay, follows image dimensions with ResizeObserver and respects reduced motion. CardRegistry prevents duplicate overlays and releases removed/replaced cards. MutationWatcher batches added subtrees and href changes instead of rescanning the document. Shopify section-load events are supported. Destroy/reinitialization removes owned overlays and listeners.
Conservative detection deliberately skips ambiguous structures, oversized hero media, swatches, mixed-product containers and media wrappers with unrelated text/controls. Unknown themes cannot be guaranteed compatible without store testing. Native theme hover effects remain theme-owned; test their interaction in the selected theme. No paid-plan/billing behavior is included in this free MVP; future billing can be isolated behind authenticated backend routes.
Add ?hswap_debug=1 (or &hswap_debug=1) to a storefront URL for [HoverSwap] console diagnostics. Production logging is silent by default.
Commands and validation
npm run check # config consistency, typecheck, lint, format, unit tests, build
npx playwright install chromium
npm run test:browser # automatic fixture server on 127.0.0.1:8013
npm run preview:storefront # inspect offline fixture with colored image placeholders on port 8013
npm run shopify -- app config validate
npm run shopify -- app build # local Theme Check and extension packaging
npm run build builds both storefront CDN assets and React Router server/client. Commit the generated extension assets; no source maps or framework dependencies are sent to the storefront. Initial minified storefront JavaScript is approximately 9.98 KB / 3.90 KB gzip, CSS 0.26 KB / 0.19 KB gzip; see VALIDATION.md for measured results.
Unit tests cover URL/image validation, generic detection, PDP exclusion, request deduplication/concurrency, safe errors and visibility gating. Chromium tests cover actual rendering, card geometry, clicks, original source preservation, dynamic cards, gallery/recommendation distinction, lazy Ajax and touch-only startup. Fixtures simulate Shopify responses; real OAuth, store installation and theme compatibility require a linked development store.
Environment
| Variable | Purpose |
|---|---|
SHOPIFY_API_KEY |
App client ID, also used for App Bridge/editor link |
SHOPIFY_API_SECRET |
Shopify app secret used by the official auth framework |
SHOPIFY_APP_URL |
Public tunnel URL in development; HTTPS app origin in production |
DATABASE_URL |
SQLite locally; PostgreSQL in production |
NODE_ENV |
development or production |
PORT |
Node server port, default deployment port 8012 |
POSTGRES_PASSWORD |
Compose database password, production only |
HOVER_SWAP_PORT |
Compose loopback host port, default 8012 |
There is no custom session-cookie signing implementation, so no invented SESSION_SECRET is needed. Shopify manages auth through its official library. Keep scopes in app TOML; this MVP requests none. Server logs contain event, level, shop/topic and timestamp, never webhook bodies or tokens. /health checks database connectivity and returns only ok or unavailable.
Production
The multi-stage Node 24 Docker image uses PostgreSQL Prisma Client, runs migrations before starting, and runs as an unprivileged user. Compose contains only app and database, with a private database network/volume. Host Nginx terminates HTTPS and proxies to the loopback app port. See DEPLOYMENT.md for VPS preparation, releases, backups and rollback. shopify app deploy releases app configuration/extensions; it does not host this backend. No external deployment is performed by this task.