Architecture
ArtLove365 follows the nemo12.com architecture: one npm-workspaces monorepo, everything on Cloudflare, deployed by GitHub Actions on every push to main.
| Host | Workspace | Runtime |
|---|---|---|
artlove365.com | apps/web | Vite + React 19 + Tailwind v4 + shadcn/ui + motion, served by a Worker with static assets |
api.artlove365.com | workers/api | Hono + @hono/zod-openapi Worker; D1 (artlove365-platform), R2 (artlove365-uploads), Workers AI, Email Service, daily cron |
docs.artlove365.com | apps/docs | VitePress, content from docs/ |
packages/shared holds what both sides must agree on: the locale list, UI translations for en, es, fr, de, ja, vi, the price table, fact-based page text (describe.ts) and shared types. The API recomputes every price from that table and never trusts a price sent by the browser.
Modules (API)
| Module | Purpose | Doc |
|---|---|---|
catalog | artworks, artists, taxa — paginated, published only | API |
seo | sitemap data, daily publisher, IndexNow | SEO pipeline |
account | Google sign-in, sessions, favorites | Accounts |
orders, uploads, payments | ordering, photos/sketches, Stripe | Payments |
email | Cloudflare Email Service, 6-language templates |
i18n
- URLs carry the language:
/en/gallery,/ja/artworks/the-starry-night. /redirects (302) to the best language fromAccept-Language.- UI strings:
packages/shared/src/locales/*.ts. A missing key fails typecheck; a test checks all six files have identical keys. - Catalog text (titles, bios, descriptions) is stored per row as JSON
{"en": …, "vi": …}in D1, falling back to English.
Order flow
- Reproduction:
POST /v1/orderswith anartworkSlug. Custom:POST /v1/uploads(raw image, ≤ 20 MB, magic-byte checked, stored privately in R2), thenPOST /v1/orderswith theuploadId. - If
STRIPE_SECRET_KEYis set, the API creates a Stripe Checkout session and the browser is sent there. Stripe calls/v1/payments/stripe-webhookand the order moves topaid. - Studio staff move orders through
sketching → painting → drying → shipped → deliveredvia the admin endpoints; each change is recorded inorder_events, which the tracking page shows.