# Design ## Context See `proposal.md` — Why. The home page renders four local arrays from `~/mock/data` (`pages/index.vue`): `MOCK_BANNERS` and `MOCK_PROMOS` are `{ image, url }`, `MOCK_QUICK_LINKS` adds a bilingual `label` and an inline SVG `glyph`, and each floor's advert art is picked by floor index from `/mock/floor-adv-1..6.svg`. Those assets already exist under `apps/mall/public/mock/`. The backend has no content concept: the only shared tables are identity, catalog and orders (`apps/api/migrations/0001..0005`). The admin console already manages shops, users and currencies through `/api/admin/*` with `platform_admin` gating (`apps/api/src/routes/admin.rs`). ## Goals / Non-Goals **Goals:** - The home page renders every element from the backend, with no local content arrays left in `pages/index.vue`. - Content is editable by a platform admin through the API, not only by SQL. - The flip is visually invisible: the seeded content reproduces today's page exactly. **Non-Goals:** - No image upload or CDN; rows hold URLs and the seed points at the existing static assets. - No scheduling, targeting or A/B testing. - No content for the seckill, collective or integral pages, nor for stores, brands, coupons, favourites or addresses. ## Decisions **1. Four typed tables, not one JSONB content table.** `banners`, `promos`, `quick_links` and `floor_adverts` each get explicit columns, so Postgres enforces the shape and `sqlx::query_as` maps rows without hand-written validation. *Alternative:* a single `content_blocks(kind, payload JSONB)` table is fewer lines, but every read then needs a runtime payload check, which is precisely the `any`-shaped weakness the repo's TS and Rust rules avoid. **2. Replace a whole kind at a time instead of per-entry CRUD.** `PUT /api/admin/content/{kind}` takes the ordered list, validates it, and rewrites that kind in one transaction, reindexing positions from array order. *Alternatives:* twelve REST routes for four small lists is a lot of surface, and per-entry CRUD still needs a separate reorder endpoint to express order. Bulk replace makes reordering, insertion and deletion one operation, which is how the content is actually edited. *Consequence to state plainly:* submitting an empty list clears that kind. That is intended (it is how a block is removed), and the endpoint returns the stored list so a caller can confirm what it now holds. **3. Floor adverts are an ordered pool, not rows keyed to categories.** The page assigns them to floors in order and wraps, matching today's `index % 6` behaviour. *Alternative:* `floor_adverts.category_id` would let an editor target one specific floor. That is a nicer model, but it changes what the page does today and would leave floors without adverts whenever the tree grows; the pool keeps the current behaviour and the schema smaller. **4. The quick-link glyph is stored with the row.** It is a short SVG path string. Keeping it in content means a new quick link renders without a frontend deploy, which is the point of moving this into the database at all. **5. A migration seeds content from the assets already in use.** `banner-1..3`, `promo-1..3` and `floor-adv-1..6` become rows, so the page is byte-identical after the flip and the change can be verified as a visual no-op. ## Risks / Trade-offs - [An empty submission silently clears a content kind] → intended (decision 2); the naive admin UI should confirm, and the response echoes the stored list. - [Content rows point at frontend asset paths] → temporary coupling, called out in the non-goals; a real upload capability replaces the URLs later without touching the schema. - [Four extra queries on every home render] → negligible at this size; if it ever matters, the four reads collapse into one query. - [The home page loses its content if the content API is down] → the page renders the remaining blocks rather than failing (a spec scenario), and the fixed-data rollback still works because the mock client keeps serving the same four lists. ## Migration Plan 1. Ship the migration and the public read; nothing is reading it yet. 2. Flip `pages/index.vue` to the content API and drop the three mock arrays in the same commit as the admin surface, so content is never live-but-uneditable. 3. Verify the page is visually unchanged, then verify rollback with the fixed-data adapter. 4. Rollback: revert `pages/index.vue`. The content tables are additive and harmless if left in place.