The merge creates the storefront-content capability (two requirements, five scenarios) and updates the mall's home-page requirement so its banner, promotion, quick-link and floor advert content comes from the content API. openspec validate --all --strict stays green at 12 passed / 0 failed.
55 lines
4.4 KiB
Markdown
55 lines
4.4 KiB
Markdown
# 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.
|