docs: Storefront audit for market.com/seller.market.com compatibility
Audit only, no code changed - facts gathered by reading current source (routes, containers, header/footer, SeoService), not assumed. Covers Homepage, Categories, Products, Search, Favorites, Cart, Checkout, Reviews, SEO, Breadcrumbs, Header, Footer. Core finding: tenant resolution is already entirely backend-side by request Host (ADR-001) - the frontend just consumes whatever bootstrap comes back for whatever hostname it's running on. A seller subdomain is architecturally closer to already working than any part of the Backoffice audit found; the real gaps are all about whether the *data* rendered carries a seller-aware value, not about routing/ hosting. Key findings: - Canonical URLs already correct today - SeoService.siteUrl derives from location.origin dynamically, not hardcoded. Nothing to change. - Header/Footer/SEO branding all read through one shared facade (UiRuntimeFacade.reloadFromBootstrap()) - a single future injection point that would cascade to all three for free, rather than three separate fixes. - SeoService.setItemMeta() (per-product OG/canonical tags) is defined but never called anywhere in the codebase today - a pre-existing dead hook, unrelated to seller-scoping but blocking any future per-product/per-seller SEO work until wired. - No dedicated breadcrumb component/service exists anywhere in the storefront - the only breadcrumb logic in the app is one local signal in catalog-container.component.ts. - Checkout is not a separate route - it's an inline popup flow in cart.component.ts, with no multi-vendor/multi-seller cart concept at all. This is where Checkout Modes and Unified/Split Orders (both marked Future in Seller-Management.md) would actually need to land. - Structured data (JSON-LD) and sitemap generation don't exist for anyone today, marketplace or seller - net-new work either way, not seller-specific gaps. - One pre-existing, unrelated issue noted in passing: og:locale is hardcoded 'ru_RU' in SeoService - flagged, not fixed (out of scope). Linked from docs/architecture/foundation/README.md alongside the other Seller Management docs.
This commit is contained in:
@@ -55,6 +55,7 @@ It is a platform runtime that must support unlimited tenants from one Angular ap
|
|||||||
- [Seller-Management-Domain-Models.md](Seller-Management-Domain-Models.md) — typed models, optional sellerId fields
|
- [Seller-Management-Domain-Models.md](Seller-Management-Domain-Models.md) — typed models, optional sellerId fields
|
||||||
- [Seller-Management-UX-Review.md](Seller-Management-UX-Review.md) — UX/accessibility review of the Phase 1 UI
|
- [Seller-Management-UX-Review.md](Seller-Management-UX-Review.md) — UX/accessibility review of the Phase 1 UI
|
||||||
- [Seller-Management-Backoffice-Readiness-Audit.md](Seller-Management-Backoffice-Readiness-Audit.md) — per-module scoping/permissions readiness audit
|
- [Seller-Management-Backoffice-Readiness-Audit.md](Seller-Management-Backoffice-Readiness-Audit.md) — per-module scoping/permissions readiness audit
|
||||||
|
- [Seller-Management-Storefront-Audit.md](Seller-Management-Storefront-Audit.md) — `market.com`/`seller.market.com` storefront readiness audit
|
||||||
|
|
||||||
### Engineering Rule Documents
|
### Engineering Rule Documents
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Seller Management — Storefront Audit (`market.com` / `seller.market.com`)
|
||||||
|
|
||||||
|
Audit only, no code changed. Every fact below was gathered by reading
|
||||||
|
current source on `feature/seller-management-foundation`, not assumed.
|
||||||
|
Companion to [Seller-Management.md](Seller-Management.md) §6 (Seller
|
||||||
|
Storefronts, Seller Branding — both marked Future there) and
|
||||||
|
[Seller-Management-Backoffice-Readiness-Audit.md](Seller-Management-Backoffice-Readiness-Audit.md)
|
||||||
|
(the equivalent admin-side audit).
|
||||||
|
|
||||||
|
## The core question: does the architecture already support a seller subdomain?
|
||||||
|
|
||||||
|
**Yes, at the resolution layer — no frontend change needed there.** Tenant
|
||||||
|
resolution is entirely backend-side by request Host (ADR-001): the frontend
|
||||||
|
calls `GET /bootstrap` and renders whatever comes back, with no client-side
|
||||||
|
knowledge of what hostname it's running on beyond what it reads from
|
||||||
|
`window.location`. If a backend ever resolves `seller.market.com` to a
|
||||||
|
seller-scoped bootstrap response, the frontend's fetch-and-render pipeline
|
||||||
|
doesn't need to know that happened — it already just consumes
|
||||||
|
`BootstrapConfig`. **This is the single most important finding of this
|
||||||
|
audit**: the hard problem is not "can the frontend handle a second
|
||||||
|
hostname" (it already architecturally can, by design), it's "does the
|
||||||
|
*data* the frontend renders (branding, breadcrumbs, SEO, contact info)
|
||||||
|
carry a seller-aware value to render instead of the marketplace-wide one."
|
||||||
|
That's a bootstrap-response and per-surface question, audited area by area
|
||||||
|
below — not a routing or hosting question.
|
||||||
|
|
||||||
|
## Global structure
|
||||||
|
|
||||||
|
Every storefront route sits under one `:lang` prefix (`app.routes.ts`) —
|
||||||
|
`/${lang}/catalog/:id`, `/${lang}/product/:id`, `/${lang}/wishlist`,
|
||||||
|
`/${lang}/cart`, `/${lang}/search`. No seller/tenant segment exists in the
|
||||||
|
route tree today, and none needs to for the subdomain approach — the
|
||||||
|
hostname carries the seller scope, not a path segment. `/search` is not a
|
||||||
|
distinct page — it lazy-loads the same `CatalogContainerComponent` as
|
||||||
|
`/catalog`. **Checkout is not a separate route or component** — it's a
|
||||||
|
payment-popup flow inline inside `cart.component.ts`
|
||||||
|
(`features/website/checkout/` and `features/website/cart/` are empty
|
||||||
|
placeholder folders, `.gitkeep` only).
|
||||||
|
|
||||||
|
## Area by area
|
||||||
|
|
||||||
|
### Homepage — Ready structurally
|
||||||
|
`pages/home/home.component.ts` is a thin wrapper: gets a page-render model
|
||||||
|
from `WebsiteRuntimeFacade.getPageRenderModelForUrl()` and renders it. No
|
||||||
|
branding/URL logic of its own, nothing hardcoded. **Future:** a seller-scoped
|
||||||
|
home would need `WebsiteRuntimeFacade`'s page-model resolution to become
|
||||||
|
seller-aware — out of scope of this file, belongs to whichever facade
|
||||||
|
resolves the page model once a seller bootstrap concept exists.
|
||||||
|
|
||||||
|
### Categories / Search — single injection point identified
|
||||||
|
`features/website/catalog/containers/catalog-container.component.ts`
|
||||||
|
(shared by both `/catalog` and `/search`) builds its own breadcrumb via
|
||||||
|
`CategoryFacade.getBreadcrumb()` → `CategoryTreeUtils.getBreadcrumb()` — pure
|
||||||
|
category-parent-chain traversal, no marketplace-root assumption baked into
|
||||||
|
the algorithm itself, but no seller dimension either. Reads
|
||||||
|
`catalogConfig`/`userExperienceConfig` straight from the bootstrap snapshot
|
||||||
|
(marketplace-wide today). **Future — Seller breadcrumbs:** since **no
|
||||||
|
dedicated breadcrumb component or service exists anywhere in the
|
||||||
|
storefront** (confirmed repo-wide — this is the only breadcrumb logic that
|
||||||
|
exists at all), a future "Seller X > Category > Product" trail has exactly
|
||||||
|
one call site to touch: this component's `breadcrumb` signal build.
|
||||||
|
|
||||||
|
### Products — reviews live here too, one dead SEO hook found
|
||||||
|
`features/website/product/containers/product-details-container.component.ts`.
|
||||||
|
Product URLs and share links (`shareProduct()`) are built from
|
||||||
|
`window.location.origin` dynamically, never hardcoded. Reviews and Q&A
|
||||||
|
render inside this same container (`ReviewListComponent`,
|
||||||
|
`QuestionListComponent`) — **there is no separate Reviews page/route to
|
||||||
|
audit independently.** **Real finding, not seller-specific but directly
|
||||||
|
relevant:** `SeoService.setItemMeta(item)` — the method that would set
|
||||||
|
per-product OG/canonical tags — is defined but **never called anywhere in
|
||||||
|
the codebase**. Product pages today get only the site-wide default meta
|
||||||
|
tags, not per-product ones. **Future — Seller SEO on product pages**
|
||||||
|
depends on first wiring this already-existing but currently dead hook, not
|
||||||
|
on inventing a new one.
|
||||||
|
|
||||||
|
### Favorites / Wishlist — Ready, no change needed today
|
||||||
|
`features/website/user-experience/wishlist/containers/wishlist-page.component.ts`.
|
||||||
|
Thin list view, no branding, no URL construction, no SEO. **Future:** if
|
||||||
|
sellers want their own "favorited from my store" filtering, that's a filter
|
||||||
|
on the already-prepared `Item.sellerId?` field (`Seller-Management-Domain-Models.md`)
|
||||||
|
applied at read time — no structural change to this page.
|
||||||
|
|
||||||
|
### Cart / Checkout — one literal string worth flagging
|
||||||
|
`pages/cart/cart.component.ts`. Checkout logic (`openPaymentPopup`,
|
||||||
|
`createPayment`, status polling) lives entirely in this one file — there is
|
||||||
|
no separate checkout page. `getPaymentDescription()` falls back, in order:
|
||||||
|
`bootstrap.branding.brandName` → `TenantResolverService.getHostname()`
|
||||||
|
(non-localhost) → the literal string `'Покупка на Маркетплейсе'`
|
||||||
|
("Purchase on Marketplace") for the payment-provider's description field.
|
||||||
|
This is a generic last-resort fallback, not a hardcoded brand name, but it
|
||||||
|
is single-tenant-framed. `recordOrder()` posts to `apiService.createOrder`
|
||||||
|
with no explicit marketplace/seller field — backend infers via Host
|
||||||
|
(ADR-001), same pattern as everywhere else. **Future — this is where
|
||||||
|
Checkout Modes and Unified/Split Orders (both marked Future, undecided, in
|
||||||
|
`Seller-Management.md` §6) would actually land**: today one cart always
|
||||||
|
produces one order via one popup flow; a cart containing items from
|
||||||
|
multiple sellers has no defined behavior here at all yet.
|
||||||
|
|
||||||
|
### Header — clean, single injection point for branding
|
||||||
|
`components/header/header.component.ts`. `brandName` →
|
||||||
|
`UiRuntimeFacade.marketplaceDisplayName()`; `logo` →
|
||||||
|
`UiRuntimeFacade.logoUrl()`. Both fully dynamic, sourced from
|
||||||
|
`bootstrap.branding` via `UiRuntimeFacade.reloadFromBootstrap()`
|
||||||
|
(`facades/runtime/ui-runtime.facade.ts`). No hardcoded name/logo anywhere.
|
||||||
|
`homeUrl` is `/${lang}` (root-relative — correct as-is for a seller
|
||||||
|
subdomain, since the subdomain itself carries the scope, not a path
|
||||||
|
segment). **Future — Seller logo / Seller banner / Seller branding**: this
|
||||||
|
facade's `reloadFromBootstrap()` method is the **single highest-leverage
|
||||||
|
place to add a seller-branding override** — if `bootstrap.seller?.branding`
|
||||||
|
(the already-typed but unused `SellerBranding`, `Seller-Management-Domain-Models.md`)
|
||||||
|
is ever populated, this one method could prefer it over
|
||||||
|
`bootstrap.branding` before setting facade state, and Header/Footer would
|
||||||
|
pick it up automatically with zero changes of their own, since they already
|
||||||
|
read exclusively through this facade.
|
||||||
|
|
||||||
|
### Footer — same source as Header, plus contact info
|
||||||
|
`components/footer/footer.component.ts`. `brandName` →
|
||||||
|
`uiRuntime.marketplaceName()`, `contactEmail` → `uiRuntime.contactEmail()` —
|
||||||
|
identical facade source as Header. Footer link groups/payment icons come
|
||||||
|
from `FooterResolverService.resolveFooterModelFromBootstrap()`, entirely
|
||||||
|
bootstrap-driven, nothing hardcoded. **Future — Seller contact page**: no
|
||||||
|
such page or concept exists today; the footer currently only ever surfaces
|
||||||
|
one marketplace-wide contact email/address. A seller contact page would be
|
||||||
|
net-new routed content, not an extension of the footer's existing contact
|
||||||
|
surfacing — the footer would, at most, link to it once it exists.
|
||||||
|
|
||||||
|
### SEO — canonical URLs already correct; structured data and sitemap are 100% net-new
|
||||||
|
`services/seo.service.ts` is the sole SEO surface in the app — confirmed no
|
||||||
|
sitemap generator, no robots.txt handler, and no JSON-LD/structured-data
|
||||||
|
code exists anywhere in the repository.
|
||||||
|
|
||||||
|
- **Canonical URLs — Ready today, no change needed.** `siteUrl` is derived
|
||||||
|
from `this.doc?.location?.origin` (actual browser location), not
|
||||||
|
hardcoded — a page served from `seller.market.com` already gets a
|
||||||
|
correct `seller.market.com` canonical URL with zero code changes. This is
|
||||||
|
the one area in the whole audit that needs nothing further.
|
||||||
|
- **Seller branding in meta tags** — `siteName` getter reads
|
||||||
|
`this.uiRuntime.marketplaceDisplayName() || 'Marketplace'`; the
|
||||||
|
`resetToDefaults()` effect reads `bootstrap.seo.default` +
|
||||||
|
`bootstrap.branding` (including `og:image` from
|
||||||
|
`branding.socialImageUrl`/`logoUrl`). **Future:** the same single
|
||||||
|
injection point as Header/Footer above (`UiRuntimeFacade`) — once that
|
||||||
|
facade can prefer seller branding, `SeoService` inherits it automatically
|
||||||
|
without its own changes, since it already reads through the same facade.
|
||||||
|
- **Seller structured data (JSON-LD)** — **does not exist for anything
|
||||||
|
today**, marketplace or seller. This is entirely Future/net-new work, not
|
||||||
|
an extension of an existing pattern.
|
||||||
|
- **Seller sitemap** — no client-side sitemap code exists at all; dynamic
|
||||||
|
sitemap generation is already flagged in `BACKEND.md` as a
|
||||||
|
server-side-only remaining-work item with no frontend action. A
|
||||||
|
per-seller sitemap is the same story: entirely a backend concern.
|
||||||
|
- **Pre-existing, unrelated to seller-scoping, worth flagging anyway**:
|
||||||
|
`og:locale` is hardcoded to `'ru_RU'` in both `setItemMeta()` and
|
||||||
|
`resetToDefaults()` — a real gap for multi-locale SEO generally, not
|
||||||
|
something to fix as part of Seller Management, but adjacent enough to
|
||||||
|
note here since it lives in the same service this audit reviewed closely.
|
||||||
|
|
||||||
|
### Breadcrumbs — no shared component, one call site
|
||||||
|
Already covered under Categories/Search above — repeating for completeness
|
||||||
|
since the mission listed it separately: there is no dedicated breadcrumb
|
||||||
|
component or service anywhere in the storefront. The only breadcrumb logic
|
||||||
|
in the entire codebase is `catalog-container.component.ts`'s local signal,
|
||||||
|
built from `CategoryFacade.getBreadcrumb()`.
|
||||||
|
|
||||||
|
## Summary — future changes only (nothing here is implemented)
|
||||||
|
|
||||||
|
| Area | Current state | Future seller-scoped change |
|
||||||
|
|---|---|---|
|
||||||
|
| Homepage | Ready — thin page-model wrapper | Depends on `WebsiteRuntimeFacade` becoming seller-aware |
|
||||||
|
| Categories / Search | One breadcrumb call site, no seller dimension | Extend `CategoryFacade.getBreadcrumb()` call in `catalog-container.component.ts` |
|
||||||
|
| Products | URLs already dynamic; `setItemMeta()` exists but is dead code | Wire the existing (currently unused) per-page SEO hook before adding seller data to it |
|
||||||
|
| Reviews | Lives inside Product detail, no separate page | No independent seller-scoping work — follows Products |
|
||||||
|
| Favorites | Ready — no branding/URL logic | Optional future filter on already-prepared `Item.sellerId?` |
|
||||||
|
| Cart / Checkout | One inline flow, one popup, no seller/multi-vendor concept | Where Checkout Modes + Unified/Split Orders (both Future in `Seller-Management.md`) would land |
|
||||||
|
| Header | Fully dynamic via `UiRuntimeFacade` | **Highest-leverage single injection point**: prefer `bootstrap.seller?.branding` in `reloadFromBootstrap()` |
|
||||||
|
| Footer | Same facade source as Header | Inherits the Header fix automatically; Seller contact page is net-new routed content |
|
||||||
|
| SEO — canonical URLs | **Already correct**, derived from `location.origin` | None needed |
|
||||||
|
| SEO — branding in meta tags | Reads through `UiRuntimeFacade` | Inherits the Header/Footer fix automatically |
|
||||||
|
| SEO — structured data (JSON-LD) | Does not exist for anything today | 100% net-new, not an extension |
|
||||||
|
| SEO — sitemap | No client-side code at all; backend-only concern (`BACKEND.md`) | No frontend action, ever |
|
||||||
|
| Breadcrumbs | One ad hoc signal, no shared component | Same single call site as Categories/Search |
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The storefront's existing discipline — everything reads through
|
||||||
|
`UiRuntimeFacade`/`ConfigService`/bootstrap, nothing hardcodes marketplace
|
||||||
|
identity, canonical URLs derive from actual browser location — means a
|
||||||
|
`seller.market.com` subdomain is **structurally closer to already working
|
||||||
|
than any other part of this audit found**. The entire future-work surface
|
||||||
|
collapses to two real gaps: (1) `UiRuntimeFacade.reloadFromBootstrap()`
|
||||||
|
needs to prefer seller branding when present (one method, cascades to
|
||||||
|
Header/Footer/SEO for free), and (2) Cart/Checkout's single-seller-per-order
|
||||||
|
assumption needs the Unified-vs-Split-Orders decision from
|
||||||
|
`Seller-Management.md` before multi-vendor carts can be handled at all.
|
||||||
|
Structured data and sitemap are not seller-specific gaps — they're simply
|
||||||
|
unbuilt for anyone today.
|
||||||
Reference in New Issue
Block a user