@studyportals/fawkes 8.1.1 → 8.1.2-1

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.
@@ -10,7 +10,7 @@ import { AttendancePresenter } from '../../presenters/AttendancePresenter';
10
10
  import { DegreePresenter } from '../../presenters/DegreePresenter';
11
11
  export class CountryAttendanceDegree extends ProgrammesBaseIndexabilityPolicy {
12
12
  name = 'Country Attendance Degree Policy';
13
- description = `Determines indexing rules for pages filtered by country,
13
+ description = `Determines indexing rules for pages filtered by country,
14
14
  study format (online, on-campus, etc.), and degree type.`;
15
15
  filterKeys = [FilterKey.COUNTRY, FilterKey.DELIVERY_METHOD, FilterKey.DEGREE_TYPE];
16
16
  rules = [
@@ -10,7 +10,7 @@ import { DegreePresenter } from '../../presenters/DegreePresenter';
10
10
  import { DurationPresenter } from '../../presenters/DurationPresenter';
11
11
  export class CountryDurationDegree extends ProgrammesBaseIndexabilityPolicy {
12
12
  name = 'Country Duration Degree Policy';
13
- description = `Determines indexing rules for pages filtered by country,
13
+ description = `Determines indexing rules for pages filtered by country,
14
14
  duration (1 year, 2 years, etc.), and degree type.`;
15
15
  filterKeys = [FilterKey.COUNTRY, FilterKey.DURATION, FilterKey.DEGREE_TYPE];
16
16
  rules = [
@@ -8,7 +8,7 @@ import { CountryPresenter } from '../../presenters/CountryPresenter';
8
8
  import { EducationalFormPresenter } from '../../presenters/EducationalFormPresenter';
9
9
  export class CountryEducationalForm extends ProgrammesBaseIndexabilityPolicy {
10
10
  name = 'Country Educational Form Policy';
11
- description = `Determines indexing rules for pages filtered by both country,
11
+ description = `Determines indexing rules for pages filtered by both country,
12
12
  and educational form (academic, semester, summer school, etc.).`;
13
13
  filterKeys = [FilterKey.COUNTRY, FilterKey.EDUCATIONAL_FORM];
14
14
  rules = [
@@ -9,7 +9,7 @@ import { CountryPresenter } from '../../presenters/CountryPresenter';
9
9
  import { EducationalFormPresenter } from '../../presenters/EducationalFormPresenter';
10
10
  export class DisciplineCountryEducationalForm extends ProgrammesBaseIndexabilityPolicy {
11
11
  name = 'Discipline Country Educational Form Policy';
12
- description = `Determines indexing rules for pages filtered by discipline,
12
+ description = `Determines indexing rules for pages filtered by discipline,
13
13
  country, and educational form (academic, semester, summer school, etc.).`;
14
14
  filterKeys = [FilterKey.DISCIPLINES, FilterKey.COUNTRY, FilterKey.EDUCATIONAL_FORM];
15
15
  rules = [
@@ -8,7 +8,7 @@ import { DisciplinePresenter } from '../../presenters/DisciplinePresenter';
8
8
  import { EducationalFormPresenter } from '../../presenters/EducationalFormPresenter';
9
9
  export class DisciplineEducationalForm extends ProgrammesBaseIndexabilityPolicy {
10
10
  name = 'Discipline Educational Form Policy';
11
- description = `Determines indexing rules for pages filtered by both discipline,
11
+ description = `Determines indexing rules for pages filtered by both discipline,
12
12
  and educational form (academic, semester, summer school, etc.).`;
13
13
  filterKeys = [FilterKey.DISCIPLINES, FilterKey.EDUCATIONAL_FORM];
14
14
  rules = [
@@ -0,0 +1,14 @@
1
+ import { MonetaryGrant } from "schema-dts";
2
+ import { StructuredDataCurrencyConversionService } from "./interface/StructuredDataCurrencyConversionService";
3
+ import { SearchStructuredDataFactory } from "./SearchStructuredDataFactory";
4
+ import { IScholarshipCard } from "@studyportals/domain-client";
5
+ import { EntityDTO, OfferDTO, PaywallDTO } from "./dto";
6
+ export declare class ScholarshipStructuredDataFactory extends SearchStructuredDataFactory<IScholarshipCard> {
7
+ private currencyConversionService;
8
+ private cardIndex;
9
+ constructor(currencyConversionService: StructuredDataCurrencyConversionService);
10
+ protected buildStructuredDataForCard(entity: EntityDTO<IScholarshipCard>): Promise<MonetaryGrant | undefined>;
11
+ protected getOfferData(card: IScholarshipCard): Promise<OfferDTO | undefined>;
12
+ protected getPaywallData(card: IScholarshipCard): PaywallDTO | undefined;
13
+ private buildAmount;
14
+ }
@@ -0,0 +1,69 @@
1
+ import { SearchStructuredDataFactory } from "./SearchStructuredDataFactory";
2
+ export class ScholarshipStructuredDataFactory extends SearchStructuredDataFactory {
3
+ currencyConversionService;
4
+ cardIndex = 0;
5
+ constructor(currencyConversionService) {
6
+ super();
7
+ this.currencyConversionService = currencyConversionService;
8
+ }
9
+ async buildStructuredDataForCard(entity) {
10
+ const url = `https://${window.location.host}/scholarships/${entity.card.getProviderId()}/${entity.card.getScholarshipVirtualName()}`;
11
+ const amount = await this.buildAmount(entity.card);
12
+ const structuredData = {
13
+ "@type": "MonetaryGrant",
14
+ name: entity.card.getScholarshipTitle() || undefined,
15
+ description: entity.card.getDescriptionOfApplicationBasis() || undefined,
16
+ funder: entity.card.getProviderName() || undefined,
17
+ url,
18
+ amount
19
+ };
20
+ this.addPaywallToStructuredData(structuredData, entity.paywall);
21
+ return structuredData;
22
+ }
23
+ async getOfferData(card) {
24
+ const benefits = card.getDescriptionOfBenefits();
25
+ const hasMonetaryValue = benefits && !isNaN(Number(benefits.split(' ')[0]));
26
+ if (!hasMonetaryValue)
27
+ return;
28
+ const [amount, currency] = [Number(benefits.split(' ')[0]), benefits.split(' ')[1]];
29
+ return {
30
+ price: Math.round(await this.currencyConversionService.convert(amount, currency, 'USD')),
31
+ category: 'tuition',
32
+ validFrom: undefined
33
+ };
34
+ }
35
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
36
+ getPaywallData(card) {
37
+ const currentIndex = this.cardIndex++;
38
+ if (currentIndex < 2) {
39
+ return undefined;
40
+ }
41
+ return {
42
+ isAccessibleForFree: false,
43
+ cssSelector: '.scholarship-paywall',
44
+ accessMode: 'subscription'
45
+ };
46
+ }
47
+ async buildAmount(card) {
48
+ const benefits = card.getDescriptionOfBenefits();
49
+ const hasMonetaryValue = benefits && !isNaN(Number(benefits.split(' ')[0]));
50
+ let amount;
51
+ let currency;
52
+ if (hasMonetaryValue) {
53
+ [amount, currency] = [Number(benefits.split(' ')[0]), benefits.split(' ')[1]];
54
+ }
55
+ let value;
56
+ if (hasMonetaryValue) {
57
+ value = Math.round(await this.currencyConversionService.convert(amount, currency, 'USD'));
58
+ currency = 'USD';
59
+ }
60
+ else {
61
+ value = benefits || undefined;
62
+ }
63
+ return {
64
+ "@type": "MonetaryAmount",
65
+ value,
66
+ currency
67
+ };
68
+ }
69
+ }
@@ -4,9 +4,10 @@ import { OfferDTO } from './dto/OfferDTO';
4
4
  import { EntityDTO } from './dto/EntityDTO';
5
5
  import { FAQItemDto } from './dto/FAQItemDto';
6
6
  import { BreadcrumbDTO } from './dto/BreadcrumbDTO';
7
+ import { PaywallDTO } from './dto/PaywallDTO';
7
8
  export declare abstract class SearchStructuredDataFactory<TCard> {
8
- buildStructuredData(title: string, description: string, cards: TCard[], faqItems?: FAQItemDto[], breadcrumbs?: BreadcrumbDTO[]): Promise<WithContext<SearchResultsPage>>;
9
- protected abstract buildStructuredDataForCard(entity: EntityDTO<TCard>): Thing | undefined;
9
+ buildStructuredData(title: string, description: string, cards: TCard[], faqItems?: FAQItemDto[], breadcrumbs?: BreadcrumbDTO[], paywallData?: PaywallDTO): Promise<WithContext<SearchResultsPage>>;
10
+ protected abstract buildStructuredDataForCard(entity: EntityDTO<TCard>): Promise<Thing | undefined> | Thing | undefined;
10
11
  /**
11
12
  * Get the rating for a card.
12
13
  * This method should be overridden by subclasses to provide the specific rating logic.
@@ -25,10 +26,29 @@ export declare abstract class SearchStructuredDataFactory<TCard> {
25
26
  * @protected
26
27
  */
27
28
  protected getOfferData(card: TCard): Promise<OfferDTO | undefined>;
29
+ /**
30
+ * Get the paywall data for a card.
31
+ * This method should be overridden by subclasses to provide the specific paywall logic.
32
+ * If no paywall data is available, return undefined and no paywall information will be added to the card.
33
+ *
34
+ * @param card
35
+ * @protected
36
+ */
37
+ protected getPaywallData(card: TCard): PaywallDTO | undefined;
28
38
  private constructOptionalPageOffers;
29
39
  protected constructOptionalFaqPage(faqItems: FAQItemDto[]): FAQPage | undefined;
40
+ private constructOptionalPageParts;
30
41
  private constructOptionalBreadcrumbs;
42
+ private constructPaywallElement;
31
43
  protected constructAggregateRating(reviewRating?: ReviewRatingDTO): AggregateRating | undefined;
32
44
  protected constructOffer(offerData?: OfferDTO): Offer | undefined;
45
+ /**
46
+ * Add paywall information to any structured data object
47
+ *
48
+ * @param structuredData The structured data object to enhance
49
+ * @param paywallData The paywall data to add
50
+ * @protected
51
+ */
52
+ protected addPaywallToStructuredData(structuredData: Record<string, any>, paywallData?: PaywallDTO): void;
33
53
  private stripHtmlWithListRetain;
34
54
  }
@@ -1,20 +1,25 @@
1
1
  export class SearchStructuredDataFactory {
2
- async buildStructuredData(title, description, cards, faqItems = [], breadcrumbs = []) {
2
+ async buildStructuredData(title, description, cards, faqItems = [], breadcrumbs = [], paywallData) {
3
3
  const entities = [];
4
4
  const ratings = [];
5
5
  const offersData = [];
6
6
  for (const card of cards) {
7
7
  const reviewRating = this.getRating(card);
8
8
  const offer = await this.getOfferData(card);
9
+ const paywall = this.getPaywallData(card);
9
10
  if (reviewRating)
10
11
  ratings.push(reviewRating);
11
12
  if (offer)
12
13
  offersData.push(offer);
13
- entities.push({ card, offer, reviewRating });
14
+ entities.push({ card, offer, reviewRating, paywall });
15
+ }
16
+ const mainEntities = [];
17
+ for (const entity of entities) {
18
+ const structuredData = await this.buildStructuredDataForCard(entity);
19
+ if (!structuredData)
20
+ continue;
21
+ mainEntities.push(structuredData);
14
22
  }
15
- const mainEntities = entities
16
- .map(entity => this.buildStructuredDataForCard(entity))
17
- .filter(entity => entity !== undefined);
18
23
  const data = {
19
24
  '@context': 'https://schema.org',
20
25
  '@type': 'SearchResultsPage',
@@ -23,8 +28,11 @@ export class SearchStructuredDataFactory {
23
28
  'mainEntity': mainEntities
24
29
  };
25
30
  data.offers = this.constructOptionalPageOffers(offersData);
26
- data.hasPart = this.constructOptionalFaqPage(faqItems);
31
+ data.hasPart = this.constructOptionalPageParts(faqItems, paywallData);
27
32
  data.breadcrumb = this.constructOptionalBreadcrumbs(breadcrumbs);
33
+ if (paywallData) {
34
+ data.isAccessibleForFree = paywallData.isAccessibleForFree;
35
+ }
28
36
  return data;
29
37
  }
30
38
  /**
@@ -48,7 +56,19 @@ export class SearchStructuredDataFactory {
48
56
  * @protected
49
57
  */
50
58
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
51
- async getOfferData(card) {
59
+ getOfferData(card) {
60
+ return Promise.resolve(undefined);
61
+ }
62
+ /**
63
+ * Get the paywall data for a card.
64
+ * This method should be overridden by subclasses to provide the specific paywall logic.
65
+ * If no paywall data is available, return undefined and no paywall information will be added to the card.
66
+ *
67
+ * @param card
68
+ * @protected
69
+ */
70
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
71
+ getPaywallData(card) {
52
72
  return undefined;
53
73
  }
54
74
  constructOptionalPageOffers(offersData) {
@@ -94,6 +114,21 @@ export class SearchStructuredDataFactory {
94
114
  }))
95
115
  };
96
116
  }
117
+ constructOptionalPageParts(faqItems, paywallData) {
118
+ const parts = [];
119
+ const faqPage = this.constructOptionalFaqPage(faqItems);
120
+ if (faqPage) {
121
+ parts.push(faqPage);
122
+ }
123
+ if (paywallData && paywallData.cssSelector) {
124
+ parts.push(this.constructPaywallElement(paywallData));
125
+ }
126
+ if (parts.length === 0)
127
+ return undefined;
128
+ if (parts.length === 1 && parts[0]['@type'] === 'FAQPage')
129
+ return parts[0];
130
+ return parts;
131
+ }
97
132
  constructOptionalBreadcrumbs(breadcrumbs) {
98
133
  if (breadcrumbs.length <= 0)
99
134
  return;
@@ -108,6 +143,16 @@ export class SearchStructuredDataFactory {
108
143
  }))
109
144
  };
110
145
  }
146
+ constructPaywallElement(paywallData) {
147
+ const element = {
148
+ '@type': 'WebPageElement',
149
+ 'isAccessibleForFree': paywallData.isAccessibleForFree
150
+ };
151
+ if (paywallData.cssSelector) {
152
+ element.cssSelector = paywallData.cssSelector;
153
+ }
154
+ return element;
155
+ }
111
156
  constructAggregateRating(reviewRating) {
112
157
  if (!reviewRating)
113
158
  return;
@@ -131,6 +176,18 @@ export class SearchStructuredDataFactory {
131
176
  'category': 'tuition'
132
177
  };
133
178
  }
179
+ /**
180
+ * Add paywall information to any structured data object
181
+ *
182
+ * @param structuredData The structured data object to enhance
183
+ * @param paywallData The paywall data to add
184
+ * @protected
185
+ */
186
+ addPaywallToStructuredData(structuredData, paywallData) {
187
+ if (!paywallData)
188
+ return;
189
+ structuredData.isAccessibleForFree = paywallData.isAccessibleForFree;
190
+ }
134
191
  stripHtmlWithListRetain(html) {
135
192
  const tempDiv = document.createElement('div');
136
193
  tempDiv.innerHTML = html;
@@ -1,7 +1,9 @@
1
1
  import { OfferDTO } from "./OfferDTO";
2
2
  import { ReviewRatingDTO } from "./ReviewRatingDTO";
3
+ import { PaywallDTO } from "./PaywallDTO";
3
4
  export interface EntityDTO<TCard> {
4
5
  card: TCard;
5
6
  offer?: OfferDTO;
6
7
  reviewRating?: ReviewRatingDTO;
8
+ paywall?: PaywallDTO;
7
9
  }
@@ -0,0 +1,14 @@
1
+ export interface PaywallDTO {
2
+ /**
3
+ * Whether the content is behind a paywall
4
+ */
5
+ isAccessibleForFree: boolean;
6
+ /**
7
+ * CSS selector for the paywall element (optional)
8
+ */
9
+ cssSelector?: string;
10
+ /**
11
+ * Type of access restriction
12
+ */
13
+ accessMode?: 'subscription' | 'payment' | 'registration' | 'free';
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,5 +2,6 @@ export * from './BreadcrumbDTO';
2
2
  export * from './EntityDTO';
3
3
  export * from './FAQItemDto';
4
4
  export * from './OfferDTO';
5
+ export * from './PaywallDTO';
5
6
  export * from './ReviewRatingDTO';
6
7
  export * from './OrganisationDTO';
@@ -2,5 +2,6 @@ export * from './BreadcrumbDTO';
2
2
  export * from './EntityDTO';
3
3
  export * from './FAQItemDto';
4
4
  export * from './OfferDTO';
5
+ export * from './PaywallDTO';
5
6
  export * from './ReviewRatingDTO';
6
7
  export * from './OrganisationDTO';
package/package.json CHANGED
@@ -1,105 +1,105 @@
1
- {
2
- "name": "@studyportals/fawkes",
3
- "version": "8.1.1",
4
- "description": "A package to centralize SEO related logic for SBLP and Sitemap Generator.",
5
- "files": [
6
- "./dist"
7
- ],
8
- "scripts": {
9
- "prepush": "npm run test",
10
- "precommit": "npm run lint",
11
- "compile": "npx tsc && tsc-alias && rm -r ./dist/tests",
12
- "build": "npm run clean && npm run compile",
13
- "clean": "rimraf \"!(node_modules)/**/dist\"",
14
- "prepare-deployment": "npm run test && npm run build",
15
- "publish-major": "npm run prepare-deployment && npm version major && npm publish",
16
- "publish-beta": "npm run prepare-deployment && npm version prerelease && npm publish --tag beta --access=public",
17
- "publish-patch": "npm run prepare-deployment && npm version patch && npm publish",
18
- "publish-minor": "npm run prepare-deployment && npm version minor && npm publish",
19
- "prepare": "husky install",
20
- "test": "vitest run --coverage",
21
- "test:dev": "vitest --coverage tests/programmes",
22
- "lint": "eslint . --ext .ts",
23
- "lint:fix": "eslint . --ext .ts --fix",
24
- "prettier:fix": "npx prettier --use-tabs --ignore-path .gitignore --write ."
25
- },
26
- "exports": {
27
- "./organisations-search-seo": {
28
- "import": "./dist/organisations-seo/index.js",
29
- "require": "./dist/organisations-seo/index.js",
30
- "types": "./dist/organisations-seo/index.d.ts"
31
- },
32
- "./programmes-search-seo": {
33
- "import": "./dist/programmes-seo/index.js",
34
- "require": "./dist/programmes-seo/index.js",
35
- "types": "./dist/programmes-seo/index.d.ts"
36
- },
37
- "./scholarships-search-seo": {
38
- "import": "./dist/scholarships-seo/index.js",
39
- "require": "./dist/scholarships-seo/index.js",
40
- "types": "./dist/scholarships-seo/index.d.ts"
41
- },
42
- "./sitemap-generator-seo": {
43
- "import": "./dist/sitemap-generator-seo/index.js",
44
- "require": "./dist/sitemap-generator-seo/index.js",
45
- "types": "./dist/sitemap-generator-seo/index.d.ts"
46
- },
47
- "./structured-data-seo": {
48
- "import": "./dist/structured-data-seo/index.js",
49
- "require": "./dist/structured-data-seo/index.js",
50
- "types": "./dist/structured-data-seo/index.d.ts"
51
- }
52
- },
53
- "typesVersions": {
54
- "*": {
55
- "organisations-search-seo": [
56
- "dist/organisations-seo/index.d.ts"
57
- ],
58
- "programmes-search-seo": [
59
- "dist/programmes-seo/index.d.ts"
60
- ],
61
- "scholarships-search-seo": [
62
- "dist/scholarships-seo/index.d.ts"
63
- ],
64
- "sitemap-generator-seo": [
65
- "dist/sitemap-generator-seo/index.d.ts"
66
- ],
67
- "structured-data-seo": [
68
- "dist/structured-data-seo/index.d.ts"
69
- ],
70
- "*": [
71
- "dist/index.d.ts"
72
- ]
73
- }
74
- },
75
- "author": "The Jedi Council",
76
- "license": "ISC",
77
- "devDependencies": {
78
- "@adobe/structured-data-validator": "^1.4.1",
79
- "@studyportals/code-style": "^2.2.1",
80
- "@studyportals/webpack-helper": "^6.0.6",
81
- "@vitest/coverage-istanbul": "^2.1.8",
82
- "husky": "^8.0.3",
83
- "jsdom": "^26.0.0",
84
- "prettier": "^3.5.3",
85
- "tsc-alias": "^1.8.11",
86
- "typemoq": "^2.1.0",
87
- "typescript": "^5.7.3",
88
- "vitest": "^2.1.8",
89
- "schema-dts": "^1.1.5",
90
- "ts-loader": "^9.5.2"
91
- },
92
- "dependencies": {
93
- "@studyportals/domain-client": "^6.3.0",
94
- "@studyportals/ranking-api-interface": "^1.3.12",
95
- "@studyportals/search-filters": "^6.1.0",
96
- "@studyportals/static-domain-data": "^6.1.0"
97
- },
98
- "optionalDependencies": {
99
- "@rollup/rollup-linux-x64-gnu": "4.24.0"
100
- },
101
- "engines": {
102
- "node": ">=18 <=22",
103
- "npm": ">=8 <=10"
104
- }
105
- }
1
+ {
2
+ "name": "@studyportals/fawkes",
3
+ "version": "8.1.2-1",
4
+ "description": "A package to centralize SEO related logic for SBLP and Sitemap Generator.",
5
+ "files": [
6
+ "./dist"
7
+ ],
8
+ "scripts": {
9
+ "prepush": "npm run test",
10
+ "precommit": "npm run lint",
11
+ "compile": "npx tsc && tsc-alias && rm -r ./dist/tests",
12
+ "build": "npm run clean && npm run compile",
13
+ "clean": "rimraf \"!(node_modules)/**/dist\"",
14
+ "prepare-deployment": "npm run test && npm run build",
15
+ "publish-major": "npm run prepare-deployment && npm version major && npm publish",
16
+ "publish-beta": "npm run prepare-deployment && npm version prerelease && npm publish --tag beta --access=public",
17
+ "publish-patch": "npm run prepare-deployment && npm version patch && npm publish",
18
+ "publish-minor": "npm run prepare-deployment && npm version minor && npm publish",
19
+ "prepare": "husky install",
20
+ "test": "vitest run --coverage",
21
+ "test:dev": "vitest --coverage tests/programmes",
22
+ "lint": "eslint . --ext .ts",
23
+ "lint:fix": "eslint . --ext .ts --fix",
24
+ "prettier:fix": "npx prettier --use-tabs --ignore-path .gitignore --write ."
25
+ },
26
+ "exports": {
27
+ "./organisations-search-seo": {
28
+ "import": "./dist/organisations-seo/index.js",
29
+ "require": "./dist/organisations-seo/index.js",
30
+ "types": "./dist/organisations-seo/index.d.ts"
31
+ },
32
+ "./programmes-search-seo": {
33
+ "import": "./dist/programmes-seo/index.js",
34
+ "require": "./dist/programmes-seo/index.js",
35
+ "types": "./dist/programmes-seo/index.d.ts"
36
+ },
37
+ "./scholarships-search-seo": {
38
+ "import": "./dist/scholarships-seo/index.js",
39
+ "require": "./dist/scholarships-seo/index.js",
40
+ "types": "./dist/scholarships-seo/index.d.ts"
41
+ },
42
+ "./sitemap-generator-seo": {
43
+ "import": "./dist/sitemap-generator-seo/index.js",
44
+ "require": "./dist/sitemap-generator-seo/index.js",
45
+ "types": "./dist/sitemap-generator-seo/index.d.ts"
46
+ },
47
+ "./structured-data-seo": {
48
+ "import": "./dist/structured-data-seo/index.js",
49
+ "require": "./dist/structured-data-seo/index.js",
50
+ "types": "./dist/structured-data-seo/index.d.ts"
51
+ }
52
+ },
53
+ "typesVersions": {
54
+ "*": {
55
+ "organisations-search-seo": [
56
+ "dist/organisations-seo/index.d.ts"
57
+ ],
58
+ "programmes-search-seo": [
59
+ "dist/programmes-seo/index.d.ts"
60
+ ],
61
+ "scholarships-search-seo": [
62
+ "dist/scholarships-seo/index.d.ts"
63
+ ],
64
+ "sitemap-generator-seo": [
65
+ "dist/sitemap-generator-seo/index.d.ts"
66
+ ],
67
+ "structured-data-seo": [
68
+ "dist/structured-data-seo/index.d.ts"
69
+ ],
70
+ "*": [
71
+ "dist/index.d.ts"
72
+ ]
73
+ }
74
+ },
75
+ "author": "The Jedi Council",
76
+ "license": "ISC",
77
+ "devDependencies": {
78
+ "@adobe/structured-data-validator": "^1.4.1",
79
+ "@studyportals/code-style": "^2.2.1",
80
+ "@studyportals/webpack-helper": "^6.0.6",
81
+ "@vitest/coverage-istanbul": "^2.1.8",
82
+ "husky": "^8.0.3",
83
+ "jsdom": "^26.0.0",
84
+ "prettier": "^3.5.3",
85
+ "tsc-alias": "^1.8.11",
86
+ "typemoq": "^2.1.0",
87
+ "typescript": "^5.7.3",
88
+ "vitest": "^2.1.8",
89
+ "schema-dts": "^1.1.5",
90
+ "ts-loader": "^9.5.2"
91
+ },
92
+ "dependencies": {
93
+ "@studyportals/domain-client": "^6.3.0",
94
+ "@studyportals/ranking-api-interface": "^1.3.12",
95
+ "@studyportals/search-filters": "^6.1.0",
96
+ "@studyportals/static-domain-data": "^6.1.0"
97
+ },
98
+ "optionalDependencies": {
99
+ "@rollup/rollup-linux-x64-gnu": "4.24.0"
100
+ },
101
+ "engines": {
102
+ "node": ">=18 <=22",
103
+ "npm": ">=8 <=10"
104
+ }
105
+ }
@@ -1,15 +0,0 @@
1
- import { ISearchDependencies } from '../../common/ISearchDependencies';
2
- import { FilterKeyValuesMap } from '../../common/FilterKeyValuesMap';
3
- import { ISearchApiClient } from '../../sitemap-generator/ISearchApiClient';
4
- import { IRule } from '../../common/IRule';
5
- export declare class AtLeastSevenResultsRule implements IRule {
6
- private readonly minimumResultsCount;
7
- private readonly maximumPageSize;
8
- private readonly searchApiClient?;
9
- constructor(searchApiClient?: ISearchApiClient);
10
- forSearch(dependencies: ISearchDependencies): Promise<boolean>;
11
- forSitemapGeneratorWithPageNumber(filterKeyValues: FilterKeyValuesMap, pageNumber: number): Promise<boolean>;
12
- forSitemapGenerator(): Promise<boolean>;
13
- getName(): string;
14
- getDescription(): string;
15
- }
@@ -1,30 +0,0 @@
1
- import { DependencyMissingError } from '../../errors/DependencyMissingError';
2
- export class AtLeastSevenResultsRule {
3
- minimumResultsCount = 7;
4
- maximumPageSize = 20;
5
- searchApiClient;
6
- constructor(searchApiClient) {
7
- this.searchApiClient = searchApiClient;
8
- }
9
- forSearch(dependencies) {
10
- const { applicationState } = dependencies;
11
- const resultCount = applicationState.getNumberOfResults();
12
- return Promise.resolve(resultCount >= this.minimumResultsCount);
13
- }
14
- async forSitemapGeneratorWithPageNumber(filterKeyValues, pageNumber) {
15
- if (!this.searchApiClient) {
16
- throw new DependencyMissingError('SearchApiClient');
17
- }
18
- const count = await this.searchApiClient.getCount(filterKeyValues);
19
- return count >= (pageNumber - 1) * this.maximumPageSize + this.minimumResultsCount;
20
- }
21
- forSitemapGenerator() {
22
- throw new Error('Method not implemented.');
23
- }
24
- getName() {
25
- return 'AtLeastSevenResultsRule';
26
- }
27
- getDescription() {
28
- return 'Is indexable if there are at least 2 results.';
29
- }
30
- }
@@ -1,13 +0,0 @@
1
- import { IRule } from '../../common/IRule';
2
- import { IOrganisationSearchDependencies } from '../types/IOrganisationSearchDependencies';
3
- import { FilterKeyValuesMap } from '../../common/FilterKeyValuesMap';
4
- import { IRankingApiClient } from '../../sitemap-generator/IRankingApiClient';
5
- export declare class AtLeastTwoRankedResultsRule implements IRule {
6
- private readonly minimumRankedResultsCount;
7
- private readonly rankingApiClient?;
8
- constructor(rankingApiClient?: IRankingApiClient);
9
- forSearch(dependencies: IOrganisationSearchDependencies): Promise<boolean>;
10
- forSitemapGenerator(filterKeyValues: FilterKeyValuesMap): Promise<boolean>;
11
- getName(): string;
12
- getDescription(): string;
13
- }
@@ -1,26 +0,0 @@
1
- import { DependencyMissingError } from '../../errors/DependencyMissingError';
2
- export class AtLeastTwoRankedResultsRule {
3
- minimumRankedResultsCount = 2;
4
- rankingApiClient;
5
- constructor(rankingApiClient) {
6
- this.rankingApiClient = rankingApiClient;
7
- }
8
- forSearch(dependencies) {
9
- const { applicationState } = dependencies;
10
- const rankedResultsCount = applicationState.getRankedResultsCount();
11
- return Promise.resolve(rankedResultsCount >= this.minimumRankedResultsCount);
12
- }
13
- async forSitemapGenerator(filterKeyValues) {
14
- if (!this.rankingApiClient) {
15
- throw new DependencyMissingError('RankingApiClient');
16
- }
17
- const rankedResultsCount = await this.rankingApiClient.getRankedOrganisationCount(filterKeyValues);
18
- return rankedResultsCount >= this.minimumRankedResultsCount;
19
- }
20
- getName() {
21
- return 'AtLeastTwoRankedResultsRule';
22
- }
23
- getDescription() {
24
- return `At least ${this.minimumRankedResultsCount} ranked results are available`;
25
- }
26
- }
@@ -1,15 +0,0 @@
1
- import { ISearchDependencies } from '../../common/ISearchDependencies';
2
- import { FilterKeyValuesMap } from '../../common/FilterKeyValuesMap';
3
- import { ISearchApiClient } from '../../sitemap-generator/ISearchApiClient';
4
- import { IProgrammeRule } from '../IProgrammeRule';
5
- export declare class AtLeastSevenResultsRule implements IProgrammeRule {
6
- private readonly minimumResultsCount;
7
- private readonly maximumPageSize;
8
- private readonly searchApiClient?;
9
- constructor(searchApiClient?: ISearchApiClient);
10
- forSearch(dependencies: ISearchDependencies): Promise<boolean>;
11
- forSitemapGeneratorWithPageNumber(filterKeyValues: FilterKeyValuesMap, pageNumber: number): Promise<boolean>;
12
- forSitemapGenerator(): Promise<boolean>;
13
- getName(): string;
14
- getDescription(): string;
15
- }
@@ -1,30 +0,0 @@
1
- import { DependencyMissingError } from '../../errors/DependencyMissingError';
2
- export class AtLeastSevenResultsRule {
3
- minimumResultsCount = 7;
4
- maximumPageSize = 20;
5
- searchApiClient;
6
- constructor(searchApiClient) {
7
- this.searchApiClient = searchApiClient;
8
- }
9
- forSearch(dependencies) {
10
- const { applicationState } = dependencies;
11
- const resultCount = applicationState.getNumberOfResults();
12
- return Promise.resolve(resultCount >= this.minimumResultsCount);
13
- }
14
- async forSitemapGeneratorWithPageNumber(filterKeyValues, pageNumber) {
15
- if (!this.searchApiClient) {
16
- throw new DependencyMissingError('SearchApiClient');
17
- }
18
- const count = await this.searchApiClient.getCount(filterKeyValues);
19
- return count >= (pageNumber - 1) * this.maximumPageSize + this.minimumResultsCount;
20
- }
21
- forSitemapGenerator() {
22
- throw new Error('Method not implemented.');
23
- }
24
- getName() {
25
- return 'AtLeastSevenResultsRule';
26
- }
27
- getDescription() {
28
- return 'Is indexable if there are at least 7 results.';
29
- }
30
- }