From 173ceb80818a6caa5546a1036bc3bb1de142f37f Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 16 Jul 2026 00:19:41 +0400 Subject: [PATCH] feat(seo): tenant-driven meta tags, sitemap/robots, reduced-motion, docs 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 --- docs/ADMIN.md | 120 ++++++++++++++++++++++++++------ docs/BACKEND.md | 8 +++ docs/KNOWN-ISSUES.md | 32 ++++++++- docs/SPRINT-PLAN.md | 16 +++-- public/robots.txt | 12 ++++ public/sitemap.xml | 44 ++++++++++++ src/app/i18n/en.ts | 17 +++++ src/app/i18n/hy.ts | 17 +++++ src/app/i18n/ru.ts | 17 +++++ src/app/i18n/translations.ts | 21 ++++++ src/app/services/seo.service.ts | 49 +++++++++++-- src/styles.scss | 13 ++++ 12 files changed, 333 insertions(+), 33 deletions(-) create mode 100644 public/sitemap.xml diff --git a/docs/ADMIN.md b/docs/ADMIN.md index d3328b8..3c8d345 100644 --- a/docs/ADMIN.md +++ b/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 `
` + ad-hoc `

` 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 `` + 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 `` across admin/*; manual audit confirmed `DialogComponent` focus trap/Escape/aria-modal already correct and no missing `` 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 diff --git a/public/robots.txt b/public/robots.txt index 0018433..92113ae 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -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 diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..c9bb63c --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,44 @@ + + + + + /ru + daily + 1.0 + + + /ru/catalog + daily + 0.9 + + + /ru/search + weekly + 0.5 + + + /ru/wishlist + monthly + 0.3 + + + /ru/compare + monthly + 0.3 + + diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index b394cc4..dcae278 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -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.', + }, }; diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index fd28137..58e62d9 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -857,4 +857,21 @@ export const hy: Translations = { emptyTitle: 'Ֆայլեր դեռ չկան', emptyDescription: 'Վերբեռնեք պատկեր կամ փաստաթուղթ սկսելու համար։', }, + adminProducts: { + emptyTitle: 'Ապրանքներ չեն գտնվել', + emptyDescription: 'Փոխեք ֆիլտրերը կամ ստեղծեք նոր ապրանք։', + }, + adminUsers: { + emptyTitle: 'Օգտատերեր չեն գտնվել', + emptyDescription: 'Հրավիրեք գործընկերոջ՝ սկսելու համար։', + invitationsEmptyTitle: 'Սպասող հրավերներ չկան', + }, + adminMonitoring: { + eventsEmptyTitle: 'Իրադարձություններ չեն գտնվել', + eventsEmptyDescription: 'Փորձեք այլ կատեգորիայի ֆիլտր կամ որոնման բառ։', + }, + adminAnalytics: { + topProductsEmptyTitle: 'Այս ժամանակահատվածում ապրանքների վաճառք չկա', + topProductsEmptyDescription: 'Փորձեք ընտրել ավելի լայն ամսաթվերի միջակայք։', + }, }; diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index b9166af..198e60a 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -857,4 +857,21 @@ export const ru: Translations = { emptyTitle: 'Пока нет файлов', emptyDescription: 'Загрузите изображение или документ, чтобы начать.', }, + adminProducts: { + emptyTitle: 'Товары не найдены', + emptyDescription: 'Измените фильтры или создайте новый товар.', + }, + adminUsers: { + emptyTitle: 'Пользователи не найдены', + emptyDescription: 'Пригласите коллегу, чтобы начать.', + invitationsEmptyTitle: 'Нет ожидающих приглашений', + }, + adminMonitoring: { + eventsEmptyTitle: 'События не найдены', + eventsEmptyDescription: 'Попробуйте изменить фильтр категории или поисковый запрос.', + }, + adminAnalytics: { + topProductsEmptyTitle: 'Нет продаж товаров за этот период', + topProductsEmptyDescription: 'Попробуйте выбрать более широкий диапазон дат.', + }, }; diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index d41701b..92b1b38 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -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; + }; } diff --git a/src/app/services/seo.service.ts b/src/app/services/seo.service.ts index 75743bb..6aa4446 100644 --- a/src/app/services/seo.service.ts +++ b/src/app/services/seo.service.ts @@ -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 /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 { diff --git a/src/styles.scss b/src/styles.scss index 2c3bdc9..bd0910c 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -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;