@snabcentr/client-core 4.8.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import * as i1 from 'rxjs';
2
2
  import { of, startWith as startWith$1, Subject, ReplaySubject, partition, combineLatest, map as map$1, debounceTime, switchMap, tap, filter, shareReplay, distinctUntilChanged, BehaviorSubject, materialize, dematerialize, merge, distinctUntilKeyChanged, skip, first, catchError as catchError$1, throwError, expand, takeWhile, toArray, EMPTY, take, defaultIfEmpty, concatMap, finalize, takeUntil, timeout, interval, share, scan } from 'rxjs';
3
3
  import { map, startWith, catchError } from 'rxjs/operators';
4
4
  import { parseISO, parse, isValid, format } from 'date-fns';
5
- import { isString, isNil, isObject, isDate, isMatch, isArray, get, isNumber, isBoolean } from 'lodash-es';
5
+ import { isString, isNil, isObject, isDate, isMatch, isArray, omit, get, isNumber, isBoolean } from 'lodash-es';
6
6
  import * as i0 from '@angular/core';
7
7
  import { InjectionToken, inject, PLATFORM_ID, Inject, Optional, Injectable, EventEmitter, DestroyRef, LOCALE_ID, Pipe, RESPONSE_INIT, RendererFactory2 } from '@angular/core';
8
8
  import { WA_LOCAL_STORAGE, WA_USER_AGENT, WA_WINDOW } from '@ng-web-apis/common';
@@ -49,12 +49,14 @@ const ScLoadingRequestState = { status: ScRequestStateStatusEnum.Loading };
49
49
  const ScSuccessRequestState = (requestData) => ({ status: ScRequestStateStatusEnum.Success, data: requestData });
50
50
  /**
51
51
  * Состояние ошибки.
52
+ *
53
+ * @param error Причина ошибки (например, HttpErrorResponse).
52
54
  */
53
- const ScErrorRequestState = { status: ScRequestStateStatusEnum.Error };
55
+ const ScErrorRequestState = (error) => ({ status: ScRequestStateStatusEnum.Error, error });
54
56
  /**
55
57
  * Преобразует поток данных в поток с состояниями запроса.
56
58
  */
57
- const mapToRequestState = () => (source$) => source$.pipe(map((requestData) => ScSuccessRequestState(requestData)), startWith(ScLoadingRequestState), catchError(() => of(ScErrorRequestState)));
59
+ const mapToRequestState = () => (source$) => source$.pipe(map((requestData) => ScSuccessRequestState(requestData)), startWith(ScLoadingRequestState), catchError((error) => of(ScErrorRequestState(error))));
58
60
  /**
59
61
  * Извлекает данные из потока состояний запроса или возвращает null.
60
62
  */
@@ -3626,6 +3628,52 @@ class ScCatalogRedirectService {
3626
3628
  redirectToProductParent$(productIdOrSlug) {
3627
3629
  return this.redirectToParentCategory$(this.catalogService.getProductParentCategory$(productIdOrSlug));
3628
3630
  }
3631
+ /**
3632
+ * Оператор устойчивой обработки недоступной (HTTP-ошибка) категории в потоке данных: при 404 выполняет редирект
3633
+ * на родительскую категорию, при недоступном родителе или иной ошибке сообщает код через {@link onErrorCode}.
3634
+ * Ошибку не пробрасывает (завершает поток {@link EMPTY}), поэтому поток данных не завершается ошибкой.
3635
+ *
3636
+ * @param categoryIdOrSlug Идентификатор или slug недоступной категории.
3637
+ * @param onErrorCode Колбэк установки кода ошибки (например, для отображения страницы ошибки).
3638
+ */
3639
+ handleUnavailableCategory$(categoryIdOrSlug, onErrorCode) {
3640
+ return this.handleUnavailable$(() => this.redirectToCategoryParent$(categoryIdOrSlug), onErrorCode);
3641
+ }
3642
+ /**
3643
+ * Оператор устойчивой обработки недоступного (HTTP-ошибка) товара в потоке данных: при 404 выполняет редирект
3644
+ * на родительскую категорию товара, при недоступном родителе или иной ошибке сообщает код через {@link onErrorCode}.
3645
+ * Ошибку не пробрасывает (завершает поток {@link EMPTY}), поэтому поток данных не завершается ошибкой.
3646
+ *
3647
+ * @param productIdOrSlug Идентификатор или slug недоступного товара.
3648
+ * @param onErrorCode Колбэк установки кода ошибки (например, для отображения страницы ошибки).
3649
+ */
3650
+ handleUnavailableProduct$(productIdOrSlug, onErrorCode) {
3651
+ return this.handleUnavailable$(() => this.redirectToProductParent$(productIdOrSlug), onErrorCode);
3652
+ }
3653
+ /**
3654
+ * Базовый оператор устойчивой обработки недоступной сущности: при 404 — редирект на родителя (через {@link redirectToParent$}),
3655
+ * при недоступном родителе или ином {@link HttpErrorResponse} — {@link onErrorCode} с кодом ответа; всегда {@link EMPTY}.
3656
+ *
3657
+ * @param redirectToParent$ Фабрика потока редиректа на родительскую категорию.
3658
+ * @param onErrorCode Колбэк установки кода ошибки.
3659
+ */
3660
+ // eslint-disable-next-line class-methods-use-this
3661
+ handleUnavailable$(redirectToParent$, onErrorCode) {
3662
+ return (source$) => source$.pipe(catchError$1((error) => {
3663
+ if (error instanceof HttpErrorResponse && error.status === 404) {
3664
+ return redirectToParent$().pipe(catchError$1((redirectError) => {
3665
+ if (redirectError instanceof HttpErrorResponse) {
3666
+ onErrorCode(redirectError.status);
3667
+ }
3668
+ return EMPTY;
3669
+ }));
3670
+ }
3671
+ if (error instanceof HttpErrorResponse) {
3672
+ onErrorCode(error.status);
3673
+ }
3674
+ return EMPTY;
3675
+ }));
3676
+ }
3629
3677
  /**
3630
3678
  * Выполняет редирект на полученную родительскую категорию.
3631
3679
  *
@@ -4517,34 +4565,54 @@ const scGetCurrentRoute = (route) => {
4517
4565
  return currentRoute;
4518
4566
  };
4519
4567
 
4568
+ /**
4569
+ * Токен состояния запроса данных о категории (загрузка / успех / ошибка).
4570
+ */
4571
+ const SC_CATEGORY_STATE = new InjectionToken('A stream with current category request state');
4520
4572
  /**
4521
4573
  * Токен потока данных о категории.
4522
4574
  */
4523
4575
  const SC_CATEGORY_INFO = new InjectionToken('A stream with current category information');
4524
4576
  /**
4525
- * Фабрика создания потока данных о категории.
4577
+ * Фабрика создания потока состояния запроса данных о категории.
4578
+ *
4579
+ * При недоступной (HTTP 404) категории выполняет редирект на родительскую категорию; если родитель не найден
4580
+ * или ошибка иная — эмитит состояние ошибки (по нему компонент показывает страницу ошибки).
4526
4581
  *
4527
4582
  * @param router Сервис для работы с маршрутом.
4528
4583
  * @param route Маршрут.
4529
4584
  * @param catalogService Сервис для работы с каталогом.
4585
+ * @param catalogRedirectService Сервис редиректа на родительскую категорию.
4530
4586
  */
4531
- function categoryFactory(router, route, catalogService) {
4587
+ function categoryStateFactory(router, route, catalogService, catalogRedirectService) {
4532
4588
  return router.events.pipe(filter((event) => event instanceof NavigationEnd)).pipe(startWith$1(null), switchMap(() => {
4533
4589
  const categoryIdOrSlug = scGetCurrentRoute(route).snapshot.paramMap.get('categoryIdOrSlug');
4534
- if (categoryIdOrSlug) {
4535
- return catalogService.getCategoryCached$(categoryIdOrSlug);
4590
+ if (!categoryIdOrSlug) {
4591
+ return of(ScSuccessRequestState(null));
4536
4592
  }
4537
- return of(null);
4593
+ return catalogService.getCategoryCached$(categoryIdOrSlug).pipe(map$1((category) => ScSuccessRequestState(category)), startWith$1(ScLoadingRequestState), catchError$1((error) => {
4594
+ if (error instanceof HttpErrorResponse && error.status === 404) {
4595
+ return catalogRedirectService
4596
+ .redirectToCategoryParent$(categoryIdOrSlug)
4597
+ .pipe(catchError$1((redirectError) => of(ScErrorRequestState(redirectError))));
4598
+ }
4599
+ return of(ScErrorRequestState(error));
4600
+ }));
4538
4601
  }), shareReplay({ bufferSize: 1, refCount: true }), takeUntilDestroyed());
4539
4602
  }
4540
4603
  /**
4541
- * Провайдеры потока данных о категории.
4604
+ * Провайдеры состояния и потока данных о категории.
4542
4605
  */
4543
4606
  const CATEGORY_PROVIDERS = [
4607
+ {
4608
+ provide: SC_CATEGORY_STATE,
4609
+ deps: [Router, ActivatedRoute, ScCatalogService, ScCatalogRedirectService],
4610
+ useFactory: categoryStateFactory,
4611
+ },
4544
4612
  {
4545
4613
  provide: SC_CATEGORY_INFO,
4546
- deps: [Router, ActivatedRoute, ScCatalogService],
4547
- useFactory: categoryFactory,
4614
+ deps: [SC_CATEGORY_STATE],
4615
+ useFactory: (state$) => state$.pipe(mapRequestStateToDataOrNull()),
4548
4616
  },
4549
4617
  ];
4550
4618
 
@@ -4958,7 +5026,7 @@ class ScRequisitesService {
4958
5026
  /**
4959
5027
  * Создаёт запрос получения списка всех контактов.
4960
5028
  */
4961
- this.contacts$ = inject(SC_USER_INFO).pipe(switchMap(() => this.http.get(`${this.urls.apiUrl}/contacts`)), shareReplay(1));
5029
+ this.contacts$ = inject(SC_USER_INFO).pipe(switchMap(() => this.http.get(`${this.urls.apiUrl}/contacts`).pipe(catchError$1(() => of({ personal: [], socialMedia: [], directions: {} })))), shareReplay({ refCount: true, bufferSize: 1 }));
4962
5030
  /**
4963
5031
  * Создаёт запрос получения списка контактов для решения возникающих вопросов.
4964
5032
  */
@@ -4966,11 +5034,11 @@ class ScRequisitesService {
4966
5034
  /**
4967
5035
  * Создаёт запрос получения списка контактов розничных магазинов.
4968
5036
  */
4969
- this.retail$ = this.contacts$.pipe(map$1((contacts) => contacts.retail), first(), shareReplay(1));
5037
+ this.retail$ = this.contacts$.pipe(map$1((contacts) => contacts.retail), first(), shareReplay({ refCount: true, bufferSize: 1 }));
4970
5038
  /**
4971
5039
  * Создаёт запрос получения списка ссылок на социальные сети и другие каналы коммуникации.
4972
5040
  */
4973
- this.socialMedia$ = this.contacts$.pipe(map$1((contacts) => contacts.socialMedia), first(), shareReplay(1));
5041
+ this.socialMedia$ = this.contacts$.pipe(map$1((contacts) => contacts.socialMedia), first(), shareReplay({ refCount: true, bufferSize: 1 }));
4974
5042
  /**
4975
5043
  * Создаёт запрос получения списка контактов менеджеров по направлениям продаж.
4976
5044
  */
@@ -4981,11 +5049,11 @@ class ScRequisitesService {
4981
5049
  this.mapManagers(Number.parseFloat(key), manager);
4982
5050
  });
4983
5051
  }
4984
- }), first(), shareReplay(1));
5052
+ }), first(), shareReplay({ refCount: true, bufferSize: 1 }));
4985
5053
  /**
4986
5054
  * Создаёт запрос получения списка реквизитов компании.
4987
5055
  */
4988
- this.requisites$ = this.http.get(`${this.urls.apiUrl}/requisites`).pipe(shareReplay(1));
5056
+ this.requisites$ = this.http.get(`${this.urls.apiUrl}/requisites`).pipe(catchError$1(() => EMPTY), shareReplay({ refCount: true, bufferSize: 1 }));
4989
5057
  }
4990
5058
  /**
4991
5059
  * Устанавливает поисковый запрос в {@link BehaviorSubject}.
@@ -7635,36 +7703,36 @@ class SchemaOrgFactory {
7635
7703
  *
7636
7704
  * @param product Данные о продукте.
7637
7705
  * @param productPath Url путь к товару. В пути должен присутствовать шаблон ':id', обозначающий идентификатор товара для подстановки.
7706
+ * @param id Необязательный идентификатор сущности для поля `@id` в JSON-LD.
7638
7707
  */
7639
- generateForProductItem(product, productPath) {
7708
+ generateForProductItem(product, productPath, id) {
7640
7709
  const productUrl = `${this.urls.siteUrl}${productPath.replace(':id', this.idOrSlugPipe.transform(product).toString())}`;
7641
7710
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
7642
- const image = this.mediaImageTransformerPipe.transform(product?.getImagePreview() ?? product.image, true);
7711
+ const image = this.mediaImageTransformerPipe.transform(product?.getImagePreview?.() ?? product.image, true);
7643
7712
  const description = product.properties?.description;
7644
7713
  const priceValidUntil = this.normalizePriceValidUntil(product.discount?.expiredAt);
7645
7714
  return {
7646
7715
  '@context': 'https://schema.org',
7647
7716
  '@type': 'Product',
7717
+ ...(id && { '@id': id }),
7648
7718
  name: product.name,
7649
7719
  ...(image && { image }),
7650
7720
  ...(description && { description }),
7651
7721
  mpn: product.code,
7652
7722
  url: productUrl,
7653
- offers: [
7654
- {
7655
- '@type': 'Offer',
7656
- priceCurrency: 'RUB',
7657
- ...(priceValidUntil && { priceValidUntil }),
7658
- price: product.costRub,
7659
- availability: this.getAvailability(product),
7660
- itemCondition: 'https://schema.org/NewCondition',
7661
- seller: {
7662
- '@type': 'Organization',
7663
- name: this.companyInfo.name,
7664
- },
7665
- url: productUrl,
7723
+ offers: {
7724
+ '@type': 'Offer',
7725
+ priceCurrency: 'RUB',
7726
+ ...(priceValidUntil && { priceValidUntil }),
7727
+ price: product.costRub,
7728
+ availability: this.getAvailability(product),
7729
+ itemCondition: 'https://schema.org/NewCondition',
7730
+ seller: {
7731
+ '@type': 'Organization',
7732
+ name: this.companyInfo.name,
7666
7733
  },
7667
- ],
7734
+ url: productUrl,
7735
+ },
7668
7736
  };
7669
7737
  }
7670
7738
  /**
@@ -7675,13 +7743,15 @@ class SchemaOrgFactory {
7675
7743
  * @param categoryPath Url путь к категориям товаров. В пути должен присутствовать шаблон ':id', обозначающий идентификатор категории для подстановки.
7676
7744
  * @param productPath Url путь к товару. В пути должен присутствовать шаблон ':id', обозначающий идентификатор товара для подстановки.
7677
7745
  * @param breadcrumbs Данные о хлебных крошках для категории.
7746
+ * @param id Необязательный идентификатор сущности для поля `@id` в JSON-LD.
7678
7747
  */
7679
- generateForCategoryItem(category, products, categoryPath, productPath, breadcrumbs) {
7748
+ generateForCategoryItem(category, products, categoryPath, productPath, breadcrumbs, id) {
7680
7749
  const description = category.seo?.description ?? category.properties?.description;
7681
7750
  const image = this.mediaImageTransformerPipe.transform(category.image, true);
7682
7751
  return {
7683
7752
  '@context': 'https://schema.org',
7684
7753
  '@type': 'CollectionPage',
7754
+ ...(id && { '@id': id }),
7685
7755
  name: category.seo?.title ?? category.name,
7686
7756
  ...(description && { description }),
7687
7757
  url: this.getCategoryCanonicalUrl(category, categoryPath),
@@ -7699,12 +7769,6 @@ class SchemaOrgFactory {
7699
7769
  name: category.name,
7700
7770
  ...(description && { description }),
7701
7771
  },
7702
- // Информация о сайте.
7703
- isPartOf: {
7704
- '@type': 'WebSite',
7705
- name: this.companyInfo.name,
7706
- url: this.urls.siteUrl,
7707
- },
7708
7772
  // Связанные страницы.
7709
7773
  relatedLink: [
7710
7774
  `${this.urls.siteUrl}${categoryPath.replace(':id', '')}`,
@@ -7745,6 +7809,223 @@ class SchemaOrgFactory {
7745
7809
  ],
7746
7810
  };
7747
7811
  }
7812
+ /**
7813
+ * Генерирует и возвращает схему данных OnlineStore на основе данных компании из окружения.
7814
+ */
7815
+ // eslint-disable-next-line sonarjs/function-return-type
7816
+ generateForOrganization() {
7817
+ const postalAddress = this.createPostalAddress(this.companyInfo.address);
7818
+ return {
7819
+ '@context': 'https://schema.org',
7820
+ '@type': 'OnlineStore',
7821
+ '@id': `${this.urls.siteUrl}#organization`,
7822
+ name: this.companyInfo.name,
7823
+ legalName: this.companyInfo.name,
7824
+ url: this.urls.siteUrl,
7825
+ logo: this.urls.logoUrl,
7826
+ image: this.urls.logoUrl,
7827
+ email: this.companyInfo.mail,
7828
+ telephone: this.companyInfo.phone,
7829
+ ...(postalAddress && { address: postalAddress }),
7830
+ legalAddress: this.createPostalAddress({ streetAddress: this.companyInfo.legalAddress }),
7831
+ // TODO: Перечислить все розничные магазины.
7832
+ // hasPOS: [],
7833
+ taxID: this.companyInfo.inn,
7834
+ identifier: {
7835
+ '@type': 'PropertyValue',
7836
+ propertyID: 'OGRN',
7837
+ value: this.companyInfo.ogrn,
7838
+ },
7839
+ contactPoint: this.createOrganizationContactPoints(),
7840
+ ...(this.companyInfo.warehouseAddress.length > 0 && {
7841
+ location: this.companyInfo.warehouseAddress.map((warehouseAddress) => ({
7842
+ '@type': 'Place',
7843
+ name: `${this.companyInfo.name} — склад самовывоза`,
7844
+ address: warehouseAddress,
7845
+ })),
7846
+ }),
7847
+ };
7848
+ }
7849
+ /**
7850
+ * Генерирует и возвращает схему данных для указанной новости.
7851
+ *
7852
+ * @param news Данные о новости.
7853
+ * @param newsPath Url путь к новости. В пути должен присутствовать шаблон ':id', обозначающий идентификатор новости для подстановки.
7854
+ * @param id Необязательный идентификатор сущности для поля `@id` в JSON-LD.
7855
+ */
7856
+ generateForNewsArticle(news, newsPath, id) {
7857
+ const newsUrl = `${this.urls.siteUrl}${newsPath.replace(':id', news.id.toString())}`;
7858
+ const image = this.mediaImageTransformerPipe.transform(news.image, true);
7859
+ const headline = news.seo?.title ?? news.subject;
7860
+ const description = news.seo?.description;
7861
+ const datePublished = this.normalizeSchemaDate(news.createdAt);
7862
+ const articleBody = news.text ? this.stripHtml(news.text) : undefined;
7863
+ return {
7864
+ '@context': 'https://schema.org',
7865
+ '@type': 'NewsArticle',
7866
+ ...(id && { '@id': id }),
7867
+ headline,
7868
+ ...(description && { description }),
7869
+ ...(image && { image }),
7870
+ ...(datePublished && { datePublished }),
7871
+ ...(articleBody && { articleBody }),
7872
+ url: newsUrl,
7873
+ mainEntityOfPage: newsUrl,
7874
+ author: {
7875
+ '@type': 'Organization',
7876
+ name: this.companyInfo.name,
7877
+ url: this.urls.siteUrl,
7878
+ },
7879
+ publisher: {
7880
+ '@type': 'Organization',
7881
+ name: this.companyInfo.name,
7882
+ logo: {
7883
+ '@type': 'ImageObject',
7884
+ url: this.urls.logoUrl,
7885
+ },
7886
+ },
7887
+ };
7888
+ }
7889
+ /**
7890
+ * Генерирует и возвращает схему данных FAQPage для страницы с вопросами и ответами.
7891
+ *
7892
+ * @param items Список вопросов и ответов.
7893
+ * @param pagePath Url путь к странице FAQ.
7894
+ * @param pageName Необязательное название страницы.
7895
+ * @param id Необязательный идентификатор сущности для поля `@id` в JSON-LD.
7896
+ */
7897
+ generateForFaqPage(items, pagePath, pageName, id) {
7898
+ const pageUrl = `${this.urls.siteUrl}${pagePath}`;
7899
+ return {
7900
+ '@context': 'https://schema.org',
7901
+ '@type': 'FAQPage',
7902
+ ...(id && { '@id': id }),
7903
+ ...(pageName && { name: pageName }),
7904
+ url: pageUrl,
7905
+ mainEntity: items.map((item) => ({
7906
+ '@type': 'Question',
7907
+ name: item.question,
7908
+ acceptedAnswer: {
7909
+ '@type': 'Answer',
7910
+ text: this.stripHtml(item.answer),
7911
+ },
7912
+ })),
7913
+ };
7914
+ }
7915
+ /**
7916
+ * Объединяет несколько JSON-LD сущностей в один документ с `@graph`.
7917
+ *
7918
+ * @param data Массив JSON-LD сущностей для объединения.
7919
+ */
7920
+ // eslint-disable-next-line class-methods-use-this,sonarjs/function-return-type
7921
+ merge(data) {
7922
+ return {
7923
+ '@context': 'https://schema.org',
7924
+ '@graph': data.map((item) => omit(item, '@context')),
7925
+ };
7926
+ }
7927
+ /**
7928
+ * Формирует PostalAddress из структурированного адреса компании.
7929
+ *
7930
+ * @param address Структурированный адрес компании.
7931
+ */
7932
+ // eslint-disable-next-line class-methods-use-this
7933
+ createPostalAddress(address) {
7934
+ const streetAddress = [address.streetAddress, address.extendedAddress].filter(Boolean).join(', ');
7935
+ if (!streetAddress && !address.locality && !address.region && !address.postalCode && !address.country) {
7936
+ return undefined;
7937
+ }
7938
+ return {
7939
+ '@type': 'PostalAddress',
7940
+ ...(streetAddress && { streetAddress }),
7941
+ ...(address.locality && { addressLocality: address.locality }),
7942
+ ...(address.region && { addressRegion: address.region }),
7943
+ ...(address.postalCode && { postalCode: address.postalCode }),
7944
+ ...(address.country && { addressCountry: address.country }),
7945
+ };
7946
+ }
7947
+ /**
7948
+ * Формирует список ContactPoint для Organization.
7949
+ */
7950
+ createOrganizationContactPoints() {
7951
+ const contactPoints = [
7952
+ {
7953
+ '@type': 'ContactPoint',
7954
+ contactType: 'customer service',
7955
+ telephone: this.companyInfo.phone,
7956
+ email: this.companyInfo.mail,
7957
+ areaServed: 'RU',
7958
+ availableLanguage: 'Russian',
7959
+ },
7960
+ ];
7961
+ if (this.companyInfo.secondaryPhone) {
7962
+ contactPoints.push({
7963
+ '@type': 'ContactPoint',
7964
+ contactType: 'customer service',
7965
+ telephone: this.companyInfo.secondaryPhone,
7966
+ areaServed: 'RU',
7967
+ availableLanguage: 'Russian',
7968
+ });
7969
+ }
7970
+ if (this.companyInfo.hr) {
7971
+ contactPoints.push({
7972
+ '@type': 'ContactPoint',
7973
+ contactType: 'HR',
7974
+ telephone: this.companyInfo.hr.phone,
7975
+ email: this.companyInfo.hr.mail,
7976
+ areaServed: 'RU',
7977
+ availableLanguage: 'Russian',
7978
+ });
7979
+ }
7980
+ return contactPoints;
7981
+ }
7982
+ /**
7983
+ * Удаляет HTML-разметку из текста.
7984
+ *
7985
+ * @param html Текст с HTML-разметкой.
7986
+ */
7987
+ // eslint-disable-next-line class-methods-use-this
7988
+ stripHtml(html) {
7989
+ const withoutTags = html
7990
+ .split('<')
7991
+ .map((part, index) => {
7992
+ if (index === 0) {
7993
+ return part;
7994
+ }
7995
+ const closeTagIndex = part.indexOf('>');
7996
+ return closeTagIndex === -1 ? part : ` ${part.slice(closeTagIndex + 1)}`;
7997
+ })
7998
+ .join('');
7999
+ return withoutTags
8000
+ .replaceAll(/\s+/g, ' ')
8001
+ .replaceAll(/ ([.,!?;:])/g, '$1')
8002
+ .trim();
8003
+ }
8004
+ /**
8005
+ * Нормализует дату в формат Schema.org (ISO 8601).
8006
+ *
8007
+ * @param dateValue Дата для нормализации.
8008
+ */
8009
+ // eslint-disable-next-line class-methods-use-this
8010
+ normalizeSchemaDate(dateValue) {
8011
+ if (!dateValue) {
8012
+ return undefined;
8013
+ }
8014
+ if (/^\d{4}-\d{2}-\d{2}/.test(dateValue)) {
8015
+ return dateValue;
8016
+ }
8017
+ const withTimeMatch = /^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/.exec(dateValue);
8018
+ if (withTimeMatch) {
8019
+ const [, day, month, year, hours, minutes] = withTimeMatch;
8020
+ return `${year}-${month}-${day}T${hours}:${minutes}:00`;
8021
+ }
8022
+ const dateOnlyMatch = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(dateValue);
8023
+ if (!dateOnlyMatch) {
8024
+ return undefined;
8025
+ }
8026
+ const [, day, month, year] = dateOnlyMatch;
8027
+ return `${year}-${month}-${day}`;
8028
+ }
7748
8029
  /**
7749
8030
  * Нормализует дату окончания цены в формат Schema.org (YYYY-MM-DD).
7750
8031
  *
@@ -7766,9 +8047,9 @@ class SchemaOrgFactory {
7766
8047
  // eslint-disable-next-line class-methods-use-this
7767
8048
  getAvailability(product) {
7768
8049
  if (product.onOrder) {
7769
- return 'PreOrder';
8050
+ return 'https://schema.org/PreOrder';
7770
8051
  }
7771
- return product.stockCount?.length ? 'InStock' : 'OutOfStock';
8052
+ return product.stockCount?.length ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
7772
8053
  }
7773
8054
  /**
7774
8055
  * Проверяет, является ли категория виртуальной.
@@ -7812,21 +8093,19 @@ class SchemaOrgFactory {
7812
8093
  url: productUrl,
7813
8094
  ...(image && { image }),
7814
8095
  ...(description && { description }),
7815
- offers: [
7816
- {
7817
- '@type': 'Offer',
7818
- priceCurrency: 'RUB',
7819
- ...(priceValidUntil && { priceValidUntil }),
7820
- price: product.costRub,
7821
- availability: this.getAvailability(product),
7822
- itemCondition: 'https://schema.org/NewCondition',
7823
- seller: {
7824
- '@type': 'Organization',
7825
- name: this.companyInfo.name,
7826
- },
7827
- url: productUrl,
8096
+ offers: {
8097
+ '@type': 'Offer',
8098
+ priceCurrency: 'RUB',
8099
+ ...(priceValidUntil && { priceValidUntil }),
8100
+ price: product.costRub,
8101
+ availability: this.getAvailability(product),
8102
+ itemCondition: 'https://schema.org/NewCondition',
8103
+ seller: {
8104
+ '@type': 'Organization',
8105
+ name: this.companyInfo.name,
7828
8106
  },
7829
- ],
8107
+ url: productUrl,
8108
+ },
7830
8109
  },
7831
8110
  };
7832
8111
  }
@@ -7848,14 +8127,21 @@ class SchemaOrgFactory {
7848
8127
  itemListOrder: 'https://schema.org/ItemListUnordered',
7849
8128
  itemListElement: [
7850
8129
  // Добавляем подкатегории в начало списка.
7851
- ...childCategories.slice(0, 5).map((subCategory, index) => ({
7852
- '@type': 'ListItem',
7853
- position: index + 1,
7854
- name: subCategory.name,
7855
- url: this.getCategoryCanonicalUrl(subCategory, categoryPath),
7856
- image: this.mediaImageTransformerPipe.transform(subCategory.image, true),
7857
- description: subCategory.seo?.description ?? subCategory.properties?.description,
7858
- })),
8130
+ ...childCategories.slice(0, 5).map((subCategory, index) => {
8131
+ const image = this.mediaImageTransformerPipe.transform(subCategory.image, true);
8132
+ const description = subCategory.seo?.description ?? subCategory.properties?.description;
8133
+ return {
8134
+ '@type': 'ListItem',
8135
+ position: index + 1,
8136
+ item: {
8137
+ '@type': 'CollectionPage',
8138
+ name: subCategory.name,
8139
+ url: this.getCategoryCanonicalUrl(subCategory, categoryPath),
8140
+ ...(image && { image }),
8141
+ ...(description && { description }),
8142
+ },
8143
+ };
8144
+ }),
7859
8145
  // Добавляем товары после подкатегорий.
7860
8146
  ...products.slice(0, 10).map((product, index) => this.createProductsListItem(product, childCategories.length + index + 1, productPath)),
7861
8147
  ],
@@ -7868,37 +8154,46 @@ class SchemaOrgFactory {
7868
8154
  * @param categoryPath Url путь к категориям товаров. В пути должен присутствовать шаблон ':id', обозначающий идентификатор категории для подстановки.
7869
8155
  */
7870
8156
  createCatalogBreadcrumbsList(breadcrumbs, categoryPath) {
8157
+ const catalogUrl = `${this.urls.siteUrl}${categoryPath.replace(':id', '')}`;
7871
8158
  const breadcrumbItems = [
7872
8159
  // Всегда добавляем главную страницу.
7873
- {
7874
- '@type': 'ListItem',
7875
- position: 1,
7876
- name: 'Главная',
7877
- item: this.urls.siteUrl,
7878
- },
8160
+ this.createBreadcrumbListItem(1, 'Главная', this.urls.siteUrl),
7879
8161
  // Добавляем каталог.
7880
- {
7881
- '@type': 'ListItem',
7882
- position: 2,
7883
- name: 'Каталог',
7884
- item: `${this.urls.siteUrl}/${categoryPath}`,
7885
- },
8162
+ this.createBreadcrumbListItem(2, 'Каталог', catalogUrl),
7886
8163
  ];
7887
8164
  // Добавляем существующие breadcrumbs с корректировкой позиций.
7888
8165
  breadcrumbs.forEach((item, index) => {
7889
8166
  const link = isArray(item.routerLink) ? item.routerLink.join('/') : item.routerLink;
7890
- breadcrumbItems.push({
7891
- '@type': 'ListItem',
7892
- position: index + 3, // Начинаем с позиции 3 (после Главная и Каталог).
7893
- name: item.label,
7894
- item: link ? `${this.urls.siteUrl}/${link}` : undefined,
7895
- });
8167
+ breadcrumbItems.push(this.createBreadcrumbListItem(index + 3, item.label, link ? `${this.urls.siteUrl}/${link}` : undefined));
7896
8168
  });
7897
8169
  return {
7898
8170
  '@type': 'BreadcrumbList',
8171
+ itemListOrder: 'https://schema.org/ItemListOrderAscending',
7899
8172
  itemListElement: breadcrumbItems,
7900
8173
  };
7901
8174
  }
8175
+ /**
8176
+ * Создаёт элемент BreadcrumbList в формате, рекомендованном Schema.org.
8177
+ *
8178
+ * @param position Позиция элемента в цепочке.
8179
+ * @param name Название элемента.
8180
+ * @param url URL элемента.
8181
+ */
8182
+ // eslint-disable-next-line class-methods-use-this
8183
+ createBreadcrumbListItem(position, name, url) {
8184
+ return {
8185
+ '@type': 'ListItem',
8186
+ position,
8187
+ ...(url
8188
+ ? {
8189
+ item: {
8190
+ '@id': url,
8191
+ name,
8192
+ },
8193
+ }
8194
+ : { name }),
8195
+ };
8196
+ }
7902
8197
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: SchemaOrgFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
7903
8198
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: SchemaOrgFactory, providedIn: 'root' }); }
7904
8199
  }
@@ -7954,6 +8249,10 @@ class ScSeoService {
7954
8249
  * Объект {@link Document}, предоставляющий доступ к DOM страницы.
7955
8250
  */
7956
8251
  this.document = inject(DOCUMENT);
8252
+ /**
8253
+ * Фабрика для создания данных Schema.org.
8254
+ */
8255
+ this.schemaOrgFactory = inject(SchemaOrgFactory);
7957
8256
  /**
7958
8257
  * Фабрика для создания экземпляров {@link Renderer2}.
7959
8258
  */
@@ -7967,10 +8266,11 @@ class ScSeoService {
7967
8266
  * Устанавливает seo параметры на основе указанного ресурса. Если какого-то тега нет, будет установлено значение по умолчанию.
7968
8267
  *
7969
8268
  * @param resource Ресурс на основе которого необходимо установить мета-теги.
7970
- * @param img Изображение. Если не пришло, то будет установлено значение по умолчанию.
7971
- * @param robots Список robots-тегов для страницы.
8269
+ * @param img Изображение. Если не пришло, то будет установлено значение по умолчанию (logo компании).
8270
+ * @param robots Список robots-тегов для страницы. Значение по умолчанию (['index', 'follow']).
8271
+ * @param schemaOrgData Данные Schema.org для встраивания на странице.
7972
8272
  */
7973
- setFromResource(resource, img = this.urls.logoUrl, robots = ['index', 'follow']) {
8273
+ setFromResource(resource, img, robots, schemaOrgData) {
7974
8274
  const updatedResource = {
7975
8275
  title: this.replaceTemplates(resource?.title ?? this.defaultSeo.title, resource?.instanceData ?? {}),
7976
8276
  description: this.replaceTemplates(resource?.description ?? this.defaultSeo.description, resource?.instanceData ?? {}),
@@ -7978,21 +8278,24 @@ class ScSeoService {
7978
8278
  meta: resource?.meta ?? [],
7979
8279
  };
7980
8280
  const url = `${this.urls.siteUrl}${this.router.url}`;
7981
- this.setMetaTags(updatedResource.title, updatedResource.description, updatedResource.keywords, updatedResource.meta, robots);
7982
- this.setOpenGraphMetaTags(updatedResource.title, updatedResource.description, url, img);
8281
+ this.setMetaTags(updatedResource.title, updatedResource.description, updatedResource.keywords, updatedResource.meta, robots ?? ['index', 'follow']);
8282
+ this.setOpenGraphMetaTags(updatedResource.title, updatedResource.description, url, img ?? this.urls.logoUrl);
7983
8283
  this.setCanonicalLink(url);
8284
+ // Устанавливаем Schema.org параметры.
8285
+ this.setSchemaOrgData(this.schemaOrgFactory.merge([this.schemaOrgFactory.generateForOrganization(), ...(schemaOrgData ? [schemaOrgData] : [])]));
7984
8286
  }
7985
8287
  /**
7986
8288
  * Устанавливает seo параметры со значением по умолчанию.
7987
8289
  *
7988
8290
  * @param title Заголовок страницы.
7989
- * @param robots Список robots тегов для страницы.
8291
+ * @param robots Список robots тегов для страницы. Значение по умолчанию (['index', 'follow']).
8292
+ * @param schemaOrgData Данные Schema.org для встраивания на странице.
7990
8293
  */
7991
- setByDefault(title, robots = ['index', 'follow']) {
8294
+ setByDefault(title, robots, schemaOrgData) {
7992
8295
  this.setFromResource({
7993
8296
  title: `${title ?? this.defaultSeo.title} - ${this.companyInfo.name}`,
7994
8297
  description: title ? `${title}, ${this.defaultSeo.description}` : this.defaultSeo.description,
7995
- }, this.urls.logoUrl, robots);
8298
+ }, this.urls.logoUrl, robots ?? ['index', 'follow'], schemaOrgData);
7996
8299
  }
7997
8300
  /**
7998
8301
  * Устанавливает seo параметры со значением по умолчанию. Устанавливает значение "не индексировать" для поисковых роботов.
@@ -8023,9 +8326,7 @@ class ScSeoService {
8023
8326
  this.setOpenGraphMetaTags(title, description, url, this.mediaImageTransformerPipe.transform(item.image, true));
8024
8327
  this.setCanonicalLink(url);
8025
8328
  // Устанавливаем Schema.org параметры.
8026
- if (schemaOrgData) {
8027
- this.setSchemaOrgData(schemaOrgData);
8028
- }
8329
+ this.setSchemaOrgData(this.schemaOrgFactory.merge([this.schemaOrgFactory.generateForOrganization(), ...(schemaOrgData ? [schemaOrgData] : [])]));
8029
8330
  }
8030
8331
  /**
8031
8332
  * Заменяет шаблоны в тексте на значения из объекта.
@@ -8512,5 +8813,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
8512
8813
  * Generated bundle index. Do not edit.
8513
8814
  */
8514
8815
 
8515
- export { CATEGORY_PROVIDERS, EMPTY_CART, IS_BROWSER, IS_RUNNING_ON_TERMINAL, IS_SERVER, SC_ACCESS_AUTH_TOKEN_STORAGE_KEY, SC_API_KEYS, SC_AUTH_ADD_HEADER_REQUIRED, SC_AUTH_AS_CLIENT_DEFAULT_OPTIONS, SC_AUTH_AS_CLIENT_OPTIONS, SC_CACHE_LIFETIME, SC_CACHE_SETTINGS, SC_CATALOG_ITEM_PAGE_META, SC_CATALOG_ROOT_PAGE_META, SC_CATEGORY_INFO, SC_COMPANY_INFO, SC_COMPANY_NAME, SC_CONFIGURATOR_COMPONENTS, SC_CONFIGURATOR_SETTINGS, SC_COUNT_FIRST_DISCOUNTED_PRODUCTS, SC_COUNT_LAST_NEWS, SC_DATE_FORMAT, SC_DEFAULT_PAGE_META, SC_FEEDBACK_API, SC_GUEST_ENDPOINTS_TOKEN, SC_GUEST_PARAMETER_NAME_TOKEN, SC_GUEST_TOKEN_STORAGE_KEY, SC_ID_OR_SLUG_IN_ROUTE, SC_IS_HEADER_REQUIRED, SC_IS_HIDDEN_ERROR_ALERT, SC_IS_HIDDEN_ERROR_ALERT_HTTP_CONTEXT, SC_IS_LOGOUT_REQUEST, SC_IS_REFRESH_REQUIRED, SC_LINEAR_VALUES, SC_LINEAR_VALUES_TOKEN, SC_MAX_LENGTH_SEARCH_TERM, SC_MIN_LENGTH_SEARCH_TERM, SC_NEXT_PAGE_PAGINATION_CLICK, SC_ORDER_LOADER, SC_ORDER_LOADING, SC_PATH_IMAGE_NOT_FOUND, SC_PRODUCT_PAGINATION_DEFAULT_OPTIONS, SC_PRODUCT_PAGINATION_OPTIONS, SC_REFRESH_AUTH_TOKEN_STORAGE_KEY, SC_RELEASE, SC_SSR_HTTP_TIMEOUT, SC_UPDATE_INTERVAL, SC_URLS, SC_USER_INFO, SC_VACANCIES_DATA_SOURCE, SC_VIRTUAL_CATEGORY_INFO, SC_VIRTUAL_CATEGORY_PROVIDERS, SEARCH_TERM, SEARCH_TERM_PROVIDERS, ScAuthAsClientGuard, ScAuthInterceptor, ScAuthService, ScBanner, ScBannerService, ScCacheInterceptor, ScCacheSettings, ScCachedData, ScCachedItem, ScCart, ScCartService, ScCatalogFilterService, ScCatalogFormat, ScCatalogItem, ScCatalogRedirectService, ScCatalogService, ScCatalogableItem, ScCategory, ScClientType, ScCodedIdentity, ScConfiguratorService, ScContactsService, ScContragentService, ScConvertInterceptor, ScConvertersService, ScDateFormatInterceptor, ScDeletableNamedIdentity, ScDeliveryAddressService, ScDeliveryCost, ScDeliveryType, ScDocumentInfoNode, ScDocumentInfoTypesEnum, ScErrorRequestState, ScErrorsInterceptor, ScFavoriteService, ScFeedbackForms, ScFeedbackService, ScFilesService, ScFrequentlyAskedQuestionsService, ScGuestInterceptor, ScHiddenCatalogableItem, ScIBreadcrumbItem, ScISuggestionType, ScIconTypesEnum, ScIdOrSlugGuard, ScIdOrSlugPipe, ScIdentity, ScImage, ScImageHelper, ScLoadingRequestState, ScLocationsService, ScMediaImageTransformerPipe, ScMimeTypes, ScNamedIdentity, ScNews, ScNewsService, ScNotificationActionTypes, ScNotificationLevelNames, ScNotificationsService, ScOpfList, ScOptionsInterceptor, ScOrder, ScOrderBase, ScOrderDraft, ScOrderDraftShort, ScOrderDraftType, ScOrderDraftsService, ScOrderItem, ScOrderShort, ScOrderStateStatus, ScOrdersService, ScPaginationService, ScPaymentStatus, ScPaymentType, ScPhoneService, ScPrimaryCatalogableItem, ScProduct, ScProductTileType, ScQuestionnaireService, ScQuestionnaireStatusEnum, ScReclamationService, ScReclamationStatus, ScRecommendationService, ScReference, ScReferenceName, ScReferencesService, ScRequestStateStatusEnum, ScRequisitesService, ScRouteKeys, ScSearchService, ScSeoService, ScSocialType, ScSsrTimeoutInterceptor, ScSuccessRequestState, ScSuggestionService, ScSum, ScTokenService, ScUIService, ScUTMService, ScUnitsHelper, ScUpdatableNamedIdentity, ScUploadedFile, ScUser, ScUserMetadata, ScUserMetrikaGoalsEnum, ScUserMetrikaService, ScUserService, ScUserType, ScVCardService, ScVacanciesList, ScVacanciesService, ScVacancy, ScVerificationService, ScVirtualCategory, ScWarehouseService, SchemaOrgFactory, TERMINAL_PROVIDERS, USER_AGENT_TERMINAL, filterChangedByKey, loadingPlaceholder, mapRequestStateToDataOrNull, mapToRequestState, runningOnTerminalFactory, scGetCurrentRoute, scOrderIsLoaded, searchTermFactory };
8816
+ export { CATEGORY_PROVIDERS, EMPTY_CART, IS_BROWSER, IS_RUNNING_ON_TERMINAL, IS_SERVER, SC_ACCESS_AUTH_TOKEN_STORAGE_KEY, SC_API_KEYS, SC_AUTH_ADD_HEADER_REQUIRED, SC_AUTH_AS_CLIENT_DEFAULT_OPTIONS, SC_AUTH_AS_CLIENT_OPTIONS, SC_CACHE_LIFETIME, SC_CACHE_SETTINGS, SC_CATALOG_ITEM_PAGE_META, SC_CATALOG_ROOT_PAGE_META, SC_CATEGORY_INFO, SC_CATEGORY_STATE, SC_COMPANY_INFO, SC_COMPANY_NAME, SC_CONFIGURATOR_COMPONENTS, SC_CONFIGURATOR_SETTINGS, SC_COUNT_FIRST_DISCOUNTED_PRODUCTS, SC_COUNT_LAST_NEWS, SC_DATE_FORMAT, SC_DEFAULT_PAGE_META, SC_FEEDBACK_API, SC_GUEST_ENDPOINTS_TOKEN, SC_GUEST_PARAMETER_NAME_TOKEN, SC_GUEST_TOKEN_STORAGE_KEY, SC_ID_OR_SLUG_IN_ROUTE, SC_IS_HEADER_REQUIRED, SC_IS_HIDDEN_ERROR_ALERT, SC_IS_HIDDEN_ERROR_ALERT_HTTP_CONTEXT, SC_IS_LOGOUT_REQUEST, SC_IS_REFRESH_REQUIRED, SC_LINEAR_VALUES, SC_LINEAR_VALUES_TOKEN, SC_MAX_LENGTH_SEARCH_TERM, SC_MIN_LENGTH_SEARCH_TERM, SC_NEXT_PAGE_PAGINATION_CLICK, SC_ORDER_LOADER, SC_ORDER_LOADING, SC_PATH_IMAGE_NOT_FOUND, SC_PRODUCT_PAGINATION_DEFAULT_OPTIONS, SC_PRODUCT_PAGINATION_OPTIONS, SC_REFRESH_AUTH_TOKEN_STORAGE_KEY, SC_RELEASE, SC_SSR_HTTP_TIMEOUT, SC_UPDATE_INTERVAL, SC_URLS, SC_USER_INFO, SC_VACANCIES_DATA_SOURCE, SC_VIRTUAL_CATEGORY_INFO, SC_VIRTUAL_CATEGORY_PROVIDERS, SEARCH_TERM, SEARCH_TERM_PROVIDERS, ScAuthAsClientGuard, ScAuthInterceptor, ScAuthService, ScBanner, ScBannerService, ScCacheInterceptor, ScCacheSettings, ScCachedData, ScCachedItem, ScCart, ScCartService, ScCatalogFilterService, ScCatalogFormat, ScCatalogItem, ScCatalogRedirectService, ScCatalogService, ScCatalogableItem, ScCategory, ScClientType, ScCodedIdentity, ScConfiguratorService, ScContactsService, ScContragentService, ScConvertInterceptor, ScConvertersService, ScDateFormatInterceptor, ScDeletableNamedIdentity, ScDeliveryAddressService, ScDeliveryCost, ScDeliveryType, ScDocumentInfoNode, ScDocumentInfoTypesEnum, ScErrorRequestState, ScErrorsInterceptor, ScFavoriteService, ScFeedbackForms, ScFeedbackService, ScFilesService, ScFrequentlyAskedQuestionsService, ScGuestInterceptor, ScHiddenCatalogableItem, ScIBreadcrumbItem, ScISuggestionType, ScIconTypesEnum, ScIdOrSlugGuard, ScIdOrSlugPipe, ScIdentity, ScImage, ScImageHelper, ScLoadingRequestState, ScLocationsService, ScMediaImageTransformerPipe, ScMimeTypes, ScNamedIdentity, ScNews, ScNewsService, ScNotificationActionTypes, ScNotificationLevelNames, ScNotificationsService, ScOpfList, ScOptionsInterceptor, ScOrder, ScOrderBase, ScOrderDraft, ScOrderDraftShort, ScOrderDraftType, ScOrderDraftsService, ScOrderItem, ScOrderShort, ScOrderStateStatus, ScOrdersService, ScPaginationService, ScPaymentStatus, ScPaymentType, ScPhoneService, ScPrimaryCatalogableItem, ScProduct, ScProductTileType, ScQuestionnaireService, ScQuestionnaireStatusEnum, ScReclamationService, ScReclamationStatus, ScRecommendationService, ScReference, ScReferenceName, ScReferencesService, ScRequestStateStatusEnum, ScRequisitesService, ScRouteKeys, ScSearchService, ScSeoService, ScSocialType, ScSsrTimeoutInterceptor, ScSuccessRequestState, ScSuggestionService, ScSum, ScTokenService, ScUIService, ScUTMService, ScUnitsHelper, ScUpdatableNamedIdentity, ScUploadedFile, ScUser, ScUserMetadata, ScUserMetrikaGoalsEnum, ScUserMetrikaService, ScUserService, ScUserType, ScVCardService, ScVacanciesList, ScVacanciesService, ScVacancy, ScVerificationService, ScVirtualCategory, ScWarehouseService, SchemaOrgFactory, TERMINAL_PROVIDERS, USER_AGENT_TERMINAL, filterChangedByKey, loadingPlaceholder, mapRequestStateToDataOrNull, mapToRequestState, runningOnTerminalFactory, scGetCurrentRoute, scOrderIsLoaded, searchTermFactory };
8516
8817
  //# sourceMappingURL=snabcentr-client-core.mjs.map