Skip to content

Architecture ​

ArtLove365 follows the nemo12.com architecture: one npm-workspaces monorepo, everything on Cloudflare, deployed by GitHub Actions on every push to main.

HostWorkspaceRuntime
artlove365.comapps/webVite + React 19 + Tailwind v4 + shadcn/ui + motion, served by a Worker with static assets
api.artlove365.comworkers/apiHono + @hono/zod-openapi Worker; D1 (artlove365-platform), R2 (artlove365-uploads), Workers AI, Email Service, daily cron
docs.artlove365.comapps/docsVitePress, 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) ​

ModulePurposeDoc
catalogartworks, artists, taxa — paginated, published onlyAPI
seositemap data, daily publisher, IndexNowSEO pipeline
accountGoogle sign-in, sessions, favoritesAccounts
orders, uploads, paymentsordering, photos/sketches, StripePayments
emailCloudflare Email Service, 6-language templatesEmail

i18n ​

  • URLs carry the language: /en/gallery, /ja/artworks/the-starry-night.
  • / redirects (302) to the best language from Accept-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 ​

  1. Reproduction: POST /v1/orders with an artworkSlug. Custom: POST /v1/uploads (raw image, ≤ 20 MB, magic-byte checked, stored privately in R2), then POST /v1/orders with the uploadId.
  2. If STRIPE_SECRET_KEY is set, the API creates a Stripe Checkout session and the browser is sent there. Stripe calls /v1/payments/stripe-webhook and the order moves to paid.
  3. Studio staff move orders through sketching → painting → drying → shipped → delivered via the admin endpoints; each change is recorded in order_events, which the tracking page shows.