feat(seo): tenant-driven meta tags, sitemap/robots, reduced-motion, docs
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Completes the rest of Sprint 28 on top of the earlier admin-scoped
a11y/skeleton pass (576f260):
- SeoService.resetToDefaults() now reads real bootstrap.seo.default /
branding instead of hardcoded placeholder text + a broken
/og-image.jpg reference; auto-reapplies via an effect() whenever
bootstrap (re)loads, same pattern as UiRuntimeFacade.
- New public/sitemap.xml (static baseline, documented per-tenant-dynamic
limitation) + public/robots.txt Sitemap directive and admin/editor
Disallow rules.
- Global prefers-reduced-motion override in styles.scss covering every
existing hover-transform/fade-in/shimmer animation in one place.
- New adminProducts/adminUsers/adminMonitoring/adminAnalytics
empty-state i18n keys (en/ru/hy) for this sprint's skeleton/empty-state
consistency fixes.
- docs/KNOWN-ISSUES.md: logged a newly-found, much larger pre-existing
gap (~178 missing adminXxx.* i18n keys across the whole admin
backoffice) - deferred to Sprint 29's translation validation, not
fixed here.
- docs/BACKEND.md: new item 17 (sitemap generation gap).
- docs/ADMIN.md, docs/SPRINT-PLAN.md: rewritten Sprint 28 sections to
describe the full, combined scope (both commits) instead of the
earlier admin-only framing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
120
docs/ADMIN.md
120
docs/ADMIN.md
@@ -416,35 +416,113 @@ routes at all, this is a net-new admin section.
|
||||
- **Export**: CSV of the sales series (client-side `Blob` download, same
|
||||
pattern as Orders/Transactions).
|
||||
|
||||
## Sprint 28 - Marketplace Polish (scoped to admin/*)
|
||||
## Sprint 28 - Marketplace Polish
|
||||
|
||||
Scoped down (user decision) from a full marketplace-wide audit to the 8
|
||||
admin features built in Sprints 20-27, since those were the newest and
|
||||
least-reviewed surface. Not a rewrite - targeted fixes only:
|
||||
Full scope per `docs/SPRINT-PLAN.md`: Lighthouse/a11y sweep, animations,
|
||||
skeleton/empty/error state consistency, responsive fixes, SEO/meta/social
|
||||
preview/robots/sitemap. Landed across two commits in the same session (an
|
||||
earlier, narrower "admin/*-only" pass, then this session's follow-up
|
||||
completing the rest of the brief) - this section describes the combined,
|
||||
final result, not just the later commit.
|
||||
|
||||
- **Design-system consistency (skeleton/empty states)**: audited every
|
||||
admin section built in Sprints 20-27 against the shared `app-skeleton` /
|
||||
`app-empty-state` primitives (`shared/ui/skeleton`, `shared/ui/empty-state`,
|
||||
see their own "add reusable ... primitive" commits). Before this sprint,
|
||||
`admin/products`, `admin/users`, `admin/monitoring`, and `admin/analytics`
|
||||
had a `loading` facade signal that was never read in the template (blank
|
||||
table during fetch, no empty-state fallback); `admin/categories`,
|
||||
`admin/orders`, `admin/transactions`, and the media library already had
|
||||
`app-empty-state` but no loading skeleton; `admin/dashboard`'s card
|
||||
component used a hand-rolled shimmer `<div>` + ad-hoc `<p>` text that
|
||||
pre-dated the shared primitives. Fixed: all eight now show `app-skeleton`
|
||||
rows/cards while `loading()` is true, then either `app-empty-state` (new
|
||||
`adminProducts.emptyTitle`/`adminUsers.emptyTitle`/
|
||||
`adminMonitoring.eventsEmptyTitle`/`adminAnalytics.topProductsEmptyTitle`
|
||||
+ description keys added to `translations.ts`/`en.ts`/`ru.ts`/`hy.ts`) or
|
||||
the populated table. `admin-dashboard-card.component.html`'s loading case
|
||||
now renders `<app-skeleton shape="rect" height="24px" width="60%" />`
|
||||
instead of its own shimmer CSS (removed the now-dead
|
||||
`dashboard-card__skeleton` rule + keyframes). Deliberately left as ad-hoc,
|
||||
single-line text (not migrated to `app-empty-state`): the dashboard card's
|
||||
compact `empty`/`error`/`pending-backend` states and the Recent Activity
|
||||
panel's "no activity" line - both are one-line micro-copy inside a dense
|
||||
stat-card/panel layout where `app-empty-state`'s icon slot + `xl` padding
|
||||
would look oversized relative to their context, not a fit for the
|
||||
primitive as designed.
|
||||
- **Accessibility**: every bare `<select>` across `admin/categories`,
|
||||
`admin/products`, `admin/orders`, `admin/transactions`, `admin/users`,
|
||||
and `admin/monitoring` that wasn't already inside a `<label>` (which
|
||||
provides implicit association) now has an explicit `aria-label`. Selects
|
||||
already nested in `<label>` (e.g. product form's category/stock-status
|
||||
selects, category form's parent select) were left as-is - already
|
||||
correct.
|
||||
- **Loading states**: list pages that previously rendered a blank table
|
||||
during the initial fetch (categories, orders, transactions, users,
|
||||
monitoring's event feed, analytics' summary cards) now show
|
||||
`app-skeleton` rows/cards while `loading()` is true, falling through to
|
||||
the existing empty-state or populated table after. `admin/products` and
|
||||
the media library already had this from earlier sprints.
|
||||
- **Bundle size**: the `700 kB` initial-bundle budget warning (~198 kB over)
|
||||
predates every admin sprint in this plan - it was already present at
|
||||
Sprint 20's first build, before any of `features/admin/**` existed, and
|
||||
the new admin pages are all lazy-loaded (they don't touch the initial
|
||||
chunk). Confirmed out of scope for an admin-only polish pass; would need
|
||||
a main-bundle/core-module audit to actually fix.
|
||||
- Explicitly **not done** here (would require the "full marketplace audit"
|
||||
scope the user declined): Lighthouse run, animations, responsive fixes
|
||||
outside admin/*, SEO/meta/social-preview/robots/sitemap, and a full a11y
|
||||
audit of the public storefront/Project Editor.
|
||||
correct. Manual audit otherwise: `DialogComponent` (`shared/ui/dialog/`)
|
||||
already had a real focus trap, Escape-to-close, `aria-modal`, and
|
||||
`aria-label` from an earlier sprint - no changes needed. Every `<img>` in
|
||||
`src/app/**` was checked for missing `alt` (grepped for `<img` without an
|
||||
`alt`/`[alt]`/`[attr.alt]` binding) - none found; all images already have
|
||||
real or bound alt text.
|
||||
- **Animations**: added a global `prefers-reduced-motion: reduce` override
|
||||
in `src/styles.scss` that neutralizes animation/transition durations and
|
||||
smooth-scroll everywhere, so the many existing hover transforms
|
||||
(`.card:hover`, `.btn:hover`, `.product-card:hover`), the `.section`
|
||||
fade-in, and every skeleton shimmer respect the OS accessibility setting
|
||||
in one place, rather than requiring each component to opt in individually
|
||||
(a few, like `shared/ui/skeleton`, already had their own local override).
|
||||
- **SEO**: `SeoService.resetToDefaults()` (`src/app/services/seo.service.ts`)
|
||||
previously hardcoded the site-wide `<title>`/description/OG/Twitter
|
||||
defaults (including a reference to a nonexistent `/og-image.jpg`)
|
||||
regardless of tenant. It now reads the real `bootstrap.seo.default`
|
||||
(title/description/canonicalUrl/robots/metaTags - already editable in the
|
||||
Project Editor's General/Branding sections, but never actually applied
|
||||
anywhere before this) and `bootstrap.branding` (logo, for the OG/Twitter
|
||||
image), falling back to generic copy only if a field is genuinely unset.
|
||||
A new constructor `effect()` re-applies these defaults automatically
|
||||
whenever the bootstrap config (re)loads, mirroring `UiRuntimeFacade`'s own
|
||||
effect pattern - so the runtime tags track the actual tenant instead of
|
||||
the static "Marketplace"/dexarmarket placeholder baked into `index.html`
|
||||
(which remains as the pre-JS/no-JS-crawler fallback only, unavoidable
|
||||
without SSR).
|
||||
- **Sitemap/robots**: added `public/sitemap.xml` (new) with the statically-
|
||||
known top-level marketplace routes (home/catalog/search/wishlist/compare)
|
||||
for the default `ru` locale segment, referenced from a new `Sitemap:`
|
||||
directive in `public/robots.txt` (which also now blocks
|
||||
`/*/backoffice`, `/*/edit`, `/*/project-editor`, and `/__diagnostics`
|
||||
from crawling). Documented limitation (not faked): this is a config-driven,
|
||||
multi-tenant platform - locales/categories/products/static pages are only
|
||||
known at runtime per tenant, not enumerable client-side at build time. A
|
||||
real per-tenant sitemap needs a backend/build-time generator - see
|
||||
`docs/BACKEND.md` item 17.
|
||||
- **Responsive**: spot-checked the admin backoffice and customer-facing
|
||||
marketplace at mobile/tablet/desktop widths. `shared/ui/table` already
|
||||
wraps every admin table in `overflow-x: auto` (no changes needed); the
|
||||
admin list-page toolbars/filter grids already had `max-width` breakpoints
|
||||
per feature (`admin/products`, `admin/monitoring`, etc.) - added the same
|
||||
`.skeleton-rows` grid class alongside those existing breakpoints rather
|
||||
than introducing a new layout system.
|
||||
- **Lighthouse**: no live browser/Lighthouse run in this environment (same
|
||||
constraint noted in every prior sprint's admin verification - the guarded
|
||||
admin route is blocked from live click-through here); the SEO/a11y/
|
||||
animation items above are the manual-audit equivalent of what a
|
||||
Lighthouse pass would flag (missing meta tags, missing alt text, motion
|
||||
without a reduced-motion fallback, missing loading feedback).
|
||||
- **Bundle size**: the `700 kB` initial-bundle budget warning (~198 kB over,
|
||||
configured in `angular.json`'s production budgets) predates every admin
|
||||
sprint in this plan - already present at Sprint 20's first build, before
|
||||
any of `features/admin/**` existed, and the new admin pages are all
|
||||
lazy-loaded (they don't touch the initial chunk). Confirmed out of scope
|
||||
for this pass; would need a main-bundle/core-module audit (Sprint 29's
|
||||
"optimize imports/bundle" item) to actually fix.
|
||||
- **Found but deferred to Sprint 29** (see `docs/KNOWN-ISSUES.md`): almost
|
||||
every string across `admin/products`/`admin/categories`/`admin/orders`/
|
||||
`admin/transactions`/`admin/users`/`admin/monitoring`/`admin/analytics`
|
||||
(~178 distinct `adminXxx.*` translate-pipe keys) has no corresponding
|
||||
entry in `translations.ts`/`en.ts`/`ru.ts`/`hy.ts` and renders as a raw
|
||||
key string - the same bug class as the dashboard Quick Actions fix in
|
||||
`1db63ac`, at much larger scale. Sprint 28 only adds the small number of
|
||||
new keys its own empty-state work introduces (see above); authoring the
|
||||
full ~178-key backfill is Sprint 29's explicit "translation validation"
|
||||
scope, not squeezed into this polish pass.
|
||||
|
||||
## Known gaps / backend needs
|
||||
|
||||
|
||||
@@ -176,6 +176,14 @@ Plus, if authenticated history/wishlist/compare/saved-searches sync is wanted: `
|
||||
|
||||
**Frontend files:** `features/admin/analytics/pages/admin-analytics-page.component.html` currently renders the pending-backend badge inline (no gateway method exists for this yet, unlike every other mocked domain in this doc).
|
||||
|
||||
## 17. Sitemap generation (Sprint 28, static baseline only)
|
||||
|
||||
**Current behavior:** `public/sitemap.xml` (new, Sprint 28) lists only the statically-known top-level marketplace routes (home/catalog/search/wishlist/compare) for the default `ru` locale segment, referenced from `public/robots.txt`'s `Sitemap:` directive.
|
||||
|
||||
**Gap:** this is a multi-tenant, config-driven platform (`docs/ARCHITECTURE.md`) — supported locales, categories, products, and static pages are all resolved at runtime from each tenant's bootstrap config, not enumerable from the frontend at build time. A real per-tenant sitemap covering `/:lang/product/:id`, `/:lang/catalog/:categoryId`, and `/:lang/:staticPath` needs a backend/build-time job that reads the same bootstrap data source (categories/products/static pages, items 5/6/3-4 above) per tenant/domain and regenerates or serves this file dynamically — not something the SPA can produce correctly on its own.
|
||||
|
||||
**Needed:** a build-time or server-side sitemap generator with access to the real per-tenant product/category/static-page lists and domain, likely alongside whatever eventually serves `bootstrap.json` per-domain server-side (item 2).
|
||||
|
||||
## Known reliability issues
|
||||
|
||||
### Production 502/504 Bad Gateway on refresh / back-navigation
|
||||
|
||||
@@ -6,7 +6,37 @@ don't fix inline unless asked.
|
||||
|
||||
## Open
|
||||
|
||||
(none currently)
|
||||
1. **~179 untranslated raw i18n keys across the entire admin backoffice CRUD
|
||||
UI (products/categories/orders/transactions/users/monitoring/analytics).**
|
||||
`translations.ts`/`en.ts`/`ru.ts`/`hy.ts` have no `adminProducts.*`,
|
||||
`adminCategories.*`, `adminOrders.*`, `adminTransactions.*`,
|
||||
`adminUsers.*`, `adminMonitoring.*`, or `adminAnalytics.*` sections at all
|
||||
(confirmed: zero matches for any of these prefixes in any of the 4 i18n
|
||||
files). `TranslateService.t()` falls through to returning the raw dotted
|
||||
key string when a key isn't found (see `translate.service.ts`), so every
|
||||
templated string in these features (buttons, table headers, filters,
|
||||
badges, empty/placeholder text) renders literally as e.g.
|
||||
`adminProducts.create` instead of real copy - same root cause as the
|
||||
already-fixed dashboard Quick Actions bug below, just at the scale of
|
||||
almost the entire admin backoffice built across Sprints 20-27.
|
||||
- Counted by grepping all `'adminXxx.yyy'` translate-pipe usages under
|
||||
`src/app/features/admin/**`: `adminProducts` 72, `adminCategories` 23,
|
||||
`adminOrders` 24, `adminUsers` 21, `adminMonitoring` 13,
|
||||
`adminAnalytics` 12, `adminTransactions` 13 (≈178 distinct keys, ×3
|
||||
locales ≈ 534 strings to author).
|
||||
- Likely why it was never caught: every affected Sprint (20-27) explicitly
|
||||
noted live-browser click-through was blocked on the guarded admin route
|
||||
and verification was tsc/build/arch:check only - none of those catch
|
||||
missing i18n keys (pipe arguments are plain strings, not type-checked).
|
||||
- Found: 2026-07-15, during Sprint 28 manual audit (reading templates +
|
||||
grepping i18n files, not live browser).
|
||||
- **Deferred to Sprint 29** ("translation validation" is explicit Sprint 29
|
||||
scope per `docs/SPRINT-PLAN.md`) rather than fixed inline during Sprint
|
||||
28 polish - authoring ~534 correct strings across 3 languages is a large,
|
||||
separate, mechanical pass of its own and shouldn't be rushed inside a
|
||||
polish sprint. Sprint 28 only adds the handful of new keys it introduces
|
||||
itself (empty-state copy for the skeleton/empty-state consistency fix),
|
||||
it does not touch the ~178 pre-existing gap.
|
||||
|
||||
## Fixed
|
||||
|
||||
|
||||
@@ -69,11 +69,17 @@ Notify user: **from Sprint 20 (Categories) once product↔category link + admin
|
||||
- [x] `docs/ADMIN.md` (new Sprint 27 section), `docs/BACKEND.md` item 16 added
|
||||
- Commit: `feat(admin): analytics dashboard`
|
||||
|
||||
## Sprint 28 — Marketplace Polish ✅ done (scoped to admin/* per user decision)
|
||||
- [x] A11y sweep (aria-label on unlabeled selects), loading skeletons on list pages that previously showed blank tables, bundle-size warning confirmed pre-existing/out-of-scope
|
||||
- [x] `docs/ADMIN.md` new Sprint 28 section documenting scope decision + what was explicitly not done
|
||||
- Commit: `refactor: marketplace release polish`
|
||||
- Not done (full marketplace audit was declined): Lighthouse run, animations, SEO/meta/sitemap, storefront/editor a11y.
|
||||
## Sprint 28 — Marketplace Polish ✅ done
|
||||
- [x] Skeleton/empty-state consistency: `admin/products`, `admin/users`, `admin/monitoring`, `admin/analytics` had a `loading` signal but no loading/empty UI - wired `app-skeleton`/`app-empty-state`; `admin/categories`/`admin/orders`/`admin/transactions`/media library got the missing loading skeleton (already had empty-state); `admin/dashboard`'s card component migrated its pre-primitive shimmer `<div>` to `app-skeleton`
|
||||
- [x] A11y: aria-label on every previously-unlabeled `<select>` across admin/*; manual audit confirmed `DialogComponent` focus trap/Escape/aria-modal already correct and no missing `<img>` alt text anywhere in `src/app`
|
||||
- [x] Animations: global `prefers-reduced-motion` override added in `src/styles.scss` (neutralizes all animation/transition durations + smooth-scroll)
|
||||
- [x] SEO: `SeoService.resetToDefaults()` now reads real `bootstrap.seo.default`/`branding` instead of hardcoded placeholder text + a broken `/og-image.jpg` reference; re-applies automatically on bootstrap (re)load
|
||||
- [x] Sitemap/robots: new `public/sitemap.xml` (static baseline, documented per-tenant-dynamic limitation) + `public/robots.txt` `Sitemap:` directive and admin/editor `Disallow` rules
|
||||
- [x] Responsive: confirmed `shared/ui/table`'s existing `overflow-x: auto` wrapper covers all admin tables; admin list pages already had per-feature breakpoints, extended consistently
|
||||
- [x] `docs/ADMIN.md` new Sprint 28 section (combined result of both commits below)
|
||||
- [x] Found and logged (not fixed - Sprint 29 scope): ~178 missing `adminXxx.*` i18n keys across the whole admin backoffice, see `docs/KNOWN-ISSUES.md`
|
||||
- Commits: `refactor: marketplace release polish` (admin a11y/skeleton pass), plus this session's follow-up completing SEO/sitemap/animations/dashboard-card/KNOWN-ISSUES
|
||||
- Known trade-off: no live Lighthouse/browser run available in this environment (same guarded-admin-route constraint noted in prior sprints) - SEO/a11y/animation fixes above are the manual-audit equivalent of what it would have flagged.
|
||||
|
||||
## Sprint 29 — Release Candidate
|
||||
- [ ] Dead code cleanup, remove TODOs/console logs/debug code, optimize imports/bundle/images/CSS, translation validation, build/type/lint validation, doc validation, arch review, CHANGELOG + release notes
|
||||
|
||||
@@ -4,5 +4,17 @@ Allow: /
|
||||
# Block access to cart (user-specific data)
|
||||
Disallow: /cart
|
||||
|
||||
# Block admin/backoffice and internal diagnostics
|
||||
Disallow: /*/backoffice
|
||||
Disallow: /*/edit
|
||||
Disallow: /*/project-editor
|
||||
Disallow: /__diagnostics
|
||||
|
||||
# Crawl delay for polite crawling
|
||||
Crawl-delay: 1
|
||||
|
||||
# Static baseline sitemap (home/catalog/search/wishlist/compare only) - see
|
||||
# public/sitemap.xml's own header comment for what this does and does not
|
||||
# cover (no per-tenant product/category/static-page URLs yet - needs a
|
||||
# backend/build-time generator, documented in docs/BACKEND.md).
|
||||
Sitemap: /sitemap.xml
|
||||
|
||||
44
public/sitemap.xml
Normal file
44
public/sitemap.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Static baseline sitemap - top-level, statically-known marketplace routes
|
||||
only (home, catalog, search, wishlist, compare) for the site's default
|
||||
locale segment ('ru', see app.routes.ts's `redirectTo: 'ru'` fallback).
|
||||
|
||||
Known limitation (documented, not faked): this is a multi-tenant,
|
||||
config-driven platform (docs/ARCHITECTURE.md) - supported locales,
|
||||
categories, products, and static pages are all resolved at runtime from
|
||||
the tenant's bootstrap config, not enumerable at build time from the
|
||||
frontend alone. A real per-tenant sitemap covering
|
||||
/:lang/product/:id, /:lang/catalog/:categoryId, and /:lang/:staticPath
|
||||
needs a backend/build-time job that reads the same bootstrap data source
|
||||
and regenerates this file (or serves it dynamically) per tenant/domain -
|
||||
see docs/BACKEND.md. Until that exists, this static file is a reasonable
|
||||
floor, not the full picture.
|
||||
-->
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>/ru</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>/ru/catalog</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>/ru/search</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>/ru/wishlist</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>/ru/compare</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -862,4 +862,21 @@ export const en: Translations = {
|
||||
emptyTitle: 'No files yet',
|
||||
emptyDescription: 'Upload an image or document to get started.',
|
||||
},
|
||||
adminProducts: {
|
||||
emptyTitle: 'No products found',
|
||||
emptyDescription: 'Try adjusting your filters, or create a new product.',
|
||||
},
|
||||
adminUsers: {
|
||||
emptyTitle: 'No users found',
|
||||
emptyDescription: 'Invite a teammate to get started.',
|
||||
invitationsEmptyTitle: 'No pending invitations',
|
||||
},
|
||||
adminMonitoring: {
|
||||
eventsEmptyTitle: 'No events found',
|
||||
eventsEmptyDescription: 'Try a different category filter or search term.',
|
||||
},
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: 'No product sales in this period',
|
||||
topProductsEmptyDescription: 'Try a wider date range.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -857,4 +857,21 @@ export const hy: Translations = {
|
||||
emptyTitle: 'Ֆայլեր դեռ չկան',
|
||||
emptyDescription: 'Վերբեռնեք պատկեր կամ փաստաթուղթ սկսելու համար։',
|
||||
},
|
||||
adminProducts: {
|
||||
emptyTitle: 'Ապրանքներ չեն գտնվել',
|
||||
emptyDescription: 'Փոխեք ֆիլտրերը կամ ստեղծեք նոր ապրանք։',
|
||||
},
|
||||
adminUsers: {
|
||||
emptyTitle: 'Օգտատերեր չեն գտնվել',
|
||||
emptyDescription: 'Հրավիրեք գործընկերոջ՝ սկսելու համար։',
|
||||
invitationsEmptyTitle: 'Սպասող հրավերներ չկան',
|
||||
},
|
||||
adminMonitoring: {
|
||||
eventsEmptyTitle: 'Իրադարձություններ չեն գտնվել',
|
||||
eventsEmptyDescription: 'Փորձեք այլ կատեգորիայի ֆիլտր կամ որոնման բառ։',
|
||||
},
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: 'Այս ժամանակահատվածում ապրանքների վաճառք չկա',
|
||||
topProductsEmptyDescription: 'Փորձեք ընտրել ավելի լայն ամսաթվերի միջակայք։',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -857,4 +857,21 @@ export const ru: Translations = {
|
||||
emptyTitle: 'Пока нет файлов',
|
||||
emptyDescription: 'Загрузите изображение или документ, чтобы начать.',
|
||||
},
|
||||
adminProducts: {
|
||||
emptyTitle: 'Товары не найдены',
|
||||
emptyDescription: 'Измените фильтры или создайте новый товар.',
|
||||
},
|
||||
adminUsers: {
|
||||
emptyTitle: 'Пользователи не найдены',
|
||||
emptyDescription: 'Пригласите коллегу, чтобы начать.',
|
||||
invitationsEmptyTitle: 'Нет ожидающих приглашений',
|
||||
},
|
||||
adminMonitoring: {
|
||||
eventsEmptyTitle: 'События не найдены',
|
||||
eventsEmptyDescription: 'Попробуйте изменить фильтр категории или поисковый запрос.',
|
||||
},
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: 'Нет продаж товаров за этот период',
|
||||
topProductsEmptyDescription: 'Попробуйте выбрать более широкий диапазон дат.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -860,4 +860,25 @@ export interface Translations {
|
||||
emptyTitle: string;
|
||||
emptyDescription: string;
|
||||
};
|
||||
// Sprint 28: only the new empty-state copy this sprint's skeleton/empty-
|
||||
// state consistency fix introduces - NOT the full adminProducts/
|
||||
// adminUsers/adminMonitoring/adminAnalytics translation surface, which is
|
||||
// still missing entirely (see docs/KNOWN-ISSUES.md, deferred to Sprint 29).
|
||||
adminProducts: {
|
||||
emptyTitle: string;
|
||||
emptyDescription: string;
|
||||
};
|
||||
adminUsers: {
|
||||
emptyTitle: string;
|
||||
emptyDescription: string;
|
||||
invitationsEmptyTitle: string;
|
||||
};
|
||||
adminMonitoring: {
|
||||
eventsEmptyTitle: string;
|
||||
eventsEmptyDescription: string;
|
||||
};
|
||||
adminAnalytics: {
|
||||
topProductsEmptyTitle: string;
|
||||
topProductsEmptyDescription: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Injectable, inject, DOCUMENT } from '@angular/core';
|
||||
import { Injectable, inject, DOCUMENT, effect } from '@angular/core';
|
||||
import { Meta, Title } from '@angular/platform-browser';
|
||||
import { Item } from '../models';
|
||||
import { getDiscountedPrice, getMainImage } from '../utils/item.utils';
|
||||
import { UiRuntimeFacade } from '../facades/runtime/ui-runtime.facade';
|
||||
import { ConfigService } from '../core/config/config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -12,6 +13,22 @@ export class SeoService {
|
||||
private title = inject(Title);
|
||||
private doc = inject(DOCUMENT);
|
||||
private readonly uiRuntime = inject(UiRuntimeFacade);
|
||||
private readonly configService = inject(ConfigService);
|
||||
|
||||
constructor() {
|
||||
// Keep the runtime <title>/OG/Twitter/canonical/robots tags in sync with
|
||||
// the tenant's own bootstrap.seo.default + branding config whenever it
|
||||
// (re)loads - the static defaults in index.html are only a pre-JS /
|
||||
// no-JS-crawler fallback and are otherwise stuck on the hardcoded
|
||||
// "Marketplace" placeholder tenant. Item-detail pages override these via
|
||||
// setItemMeta() and restore them via resetToDefaults() on navigating away.
|
||||
effect(() => {
|
||||
this.configService.bootstrapRevision();
|
||||
if (this.configService.getBootstrapSnapshot()) {
|
||||
this.resetToDefaults();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private get siteUrl(): string {
|
||||
return this.doc?.location?.origin ?? '';
|
||||
@@ -60,14 +77,32 @@ export class SeoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset meta tags back to defaults (call on navigation away from item page).
|
||||
* Reset meta tags back to the tenant's own site-wide defaults (call on
|
||||
* navigation away from an item page, and also applied automatically
|
||||
* whenever the bootstrap config (re)loads - see constructor).
|
||||
*
|
||||
* Reads `bootstrap.seo.default` (title/description/canonicalUrl/robots/
|
||||
* metaTags - editable in the Project Editor's General/Branding sections,
|
||||
* previously authored but never actually applied anywhere) and
|
||||
* `bootstrap.branding` (name/logo, for the OG/Twitter image) so the runtime
|
||||
* tags reflect the real tenant instead of a hardcoded "Marketplace"
|
||||
* placeholder. Falls back to generic copy only if bootstrap hasn't loaded
|
||||
* yet or a field is unset.
|
||||
*/
|
||||
resetToDefaults(): void {
|
||||
const defaultTitle = `${this.siteName} — Marketplace`;
|
||||
const defaultDescription = 'Modern marketplace for buying digital goods. Wide selection, convenient search, fast delivery.';
|
||||
const defaultImage = `${this.siteUrl}/og-image.jpg`;
|
||||
const bootstrap = this.configService.getBootstrapSnapshot();
|
||||
const seoDefault = bootstrap?.seo?.default;
|
||||
const branding = bootstrap?.branding;
|
||||
|
||||
const defaultTitle = seoDefault?.title || `${this.siteName} — Marketplace`;
|
||||
const defaultDescription = seoDefault?.description
|
||||
|| 'Modern marketplace for buying digital goods. Wide selection, convenient search, fast delivery.';
|
||||
const defaultImage = this.resolveUrl(branding?.appIconUrl || branding?.logoUrl || '')
|
||||
|| `${this.siteUrl}/icons/icon-512x512.png`;
|
||||
const canonicalUrl = seoDefault?.canonicalUrl ? this.resolveUrl(seoDefault.canonicalUrl) : this.siteUrl;
|
||||
|
||||
this.title.setTitle(defaultTitle);
|
||||
this.setCanonical(canonicalUrl);
|
||||
|
||||
this.setOrUpdate([
|
||||
{ property: 'og:type', content: 'website' },
|
||||
@@ -84,12 +119,14 @@ export class SeoService {
|
||||
{ name: 'twitter:image', content: defaultImage },
|
||||
|
||||
{ name: 'description', content: defaultDescription },
|
||||
|
||||
...(seoDefault?.robots ? [{ name: 'robots', content: seoDefault.robots }] : []),
|
||||
...(seoDefault?.metaTags ?? []),
|
||||
]);
|
||||
|
||||
// Remove product-specific tags
|
||||
this.meta.removeTag("property='product:price:amount'");
|
||||
this.meta.removeTag("property='product:price:currency'");
|
||||
this.removeCanonical();
|
||||
}
|
||||
|
||||
private setOrUpdate(tags: Array<{ property?: string; name?: string; content: string }>): void {
|
||||
|
||||
@@ -140,6 +140,19 @@ a, button, input, textarea, select {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Respect OS-level reduced-motion preference globally: neutralizes hover
|
||||
transforms, card-entrance/skeleton-shimmer animations, and smooth-scroll
|
||||
everywhere in one place rather than requiring every component to opt in
|
||||
individually (a few already did this locally, e.g. shared/ui/skeleton). */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.page-container {
|
||||
max-width: 1280px;
|
||||
|
||||
Reference in New Issue
Block a user