@studyportals/fawkes 8.7.2 → 9.0.1-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.
Files changed (62) hide show
  1. package/README.md +176 -176
  2. package/dist/sitemap-generator-seo/index.d.ts +1 -1
  3. package/dist/src/common/FilterKeyToFilterTypeMap.d.ts +2 -0
  4. package/dist/src/common/FilterKeyToFilterTypeMap.js +14 -0
  5. package/dist/src/common/FilterTypeMap.d.ts +2 -0
  6. package/dist/src/common/FilterTypeMap.js +1 -0
  7. package/dist/src/organisations/SearchIndexabilityManager.js +1 -2
  8. package/dist/src/organisations/policies/RankedOrganisationsSeoIndexabilityPolicy.js +1 -1
  9. package/dist/src/organisations/policies/index.d.ts +0 -1
  10. package/dist/src/organisations/policies/index.js +0 -1
  11. package/dist/src/organisations/policies/our-picks/AreaAttendance.d.ts +1 -1
  12. package/dist/src/organisations/policies/our-picks/AreaAttendance.js +16 -14
  13. package/dist/src/organisations/policies/our-picks/CountryAttendance.js +15 -13
  14. package/dist/src/organisations/policies/ranked/RankedArea.js +1 -1
  15. package/dist/src/organisations/policies/ranked/RankedAreaDiscipline.d.ts +1 -0
  16. package/dist/src/organisations/policies/ranked/RankedAreaDiscipline.js +1 -0
  17. package/dist/src/organisations/policies/ranked/RankedAttendance.js +3 -1
  18. package/dist/src/organisations/policies/ranked/RankedAttendanceDiscipline.js +2 -1
  19. package/dist/src/organisations/policies/ranked/RankedContinent.js +3 -1
  20. package/dist/src/organisations/policies/ranked/RankedContinentAttendance.js +15 -13
  21. package/dist/src/organisations/policies/ranked/RankedCountry.js +3 -1
  22. package/dist/src/organisations/policies/ranked/RankedCountryAttendance.js +1 -0
  23. package/dist/src/organisations/policies/ranked/RankedCountryDiscipline.js +30 -14
  24. package/dist/src/organisations/policies/ranked/RankedDiscipline.js +14 -8
  25. package/dist/src/organisations/policies/ranked/RankedUnfiltered.js +10 -4
  26. package/dist/src/organisations/rules/MinimumAmountOfRankedResultsRule.d.ts +3 -4
  27. package/dist/src/organisations/rules/MinimumAmountOfRankedResultsRule.js +12 -8
  28. package/dist/src/organisations/types/IOrganisationsSitemapDependencies.d.ts +0 -2
  29. package/dist/src/programmes/policies/CountryAttendanceDegree.js +1 -1
  30. package/dist/src/programmes/policies/CountryDurationDegree.js +1 -1
  31. package/dist/src/programmes/policies/CountryEducationalForm.js +1 -1
  32. package/dist/src/programmes/policies/DegreeAttendanceTuitionFee.d.ts +1 -1
  33. package/dist/src/programmes/policies/DegreeAttendanceTuitionFee.js +6 -6
  34. package/dist/src/programmes/policies/DegreeContinentTuitionFee.d.ts +1 -1
  35. package/dist/src/programmes/policies/DegreeContinentTuitionFee.js +6 -6
  36. package/dist/src/programmes/policies/DegreeFormatTuitionFee.js +5 -5
  37. package/dist/src/programmes/policies/DisciplineCountryEducationalForm.js +1 -1
  38. package/dist/src/programmes/policies/DisciplineEducationalForm.js +1 -1
  39. package/dist/src/scholarships/SearchIndexabilityManager.d.ts +2 -1
  40. package/dist/src/scholarships/SearchIndexabilityManager.js +3 -2
  41. package/dist/src/scholarships/policies/Area.d.ts +4 -5
  42. package/dist/src/scholarships/policies/Area.js +14 -7
  43. package/dist/src/scholarships/policies/DisciplineCountry.d.ts +4 -4
  44. package/dist/src/scholarships/policies/DisciplineCountry.js +15 -6
  45. package/dist/src/scholarships/policies/ScholarshipSeoIndexabilityPolicy.d.ts +2 -0
  46. package/dist/src/scholarships/policies/ScholarshipSeoIndexabilityPolicy.js +4 -0
  47. package/dist/src/scholarships/policies/UniversityCountry.d.ts +5 -3
  48. package/dist/src/scholarships/policies/UniversityCountry.js +17 -5
  49. package/dist/src/scholarships/rules/NotOnPortalsRule.d.ts +12 -0
  50. package/dist/src/scholarships/rules/NotOnPortalsRule.js +24 -0
  51. package/dist/src/scholarships/rules/OnlyCountriesDisciplineCountryRule.d.ts +13 -0
  52. package/dist/src/scholarships/rules/OnlyCountriesDisciplineCountryRule.js +40 -0
  53. package/dist/src/scholarships/types/IScholarshipSearchDependencies.d.ts +5 -0
  54. package/dist/src/scholarships/types/IScholarshipSearchDependencies.js +1 -0
  55. package/dist/src/scholarships/types/IScholarshipSeoDependencies.d.ts +2 -2
  56. package/dist/src/scholarships/types/IScholarshipSitemapDependencies.d.ts +2 -0
  57. package/dist/src/sitemap-generator/ISearchApiClient.d.ts +1 -0
  58. package/dist/src/sitemap-generator/OrganisationsSitemapUrlGeneratorManager.d.ts +1 -2
  59. package/dist/src/sitemap-generator/OrganisationsSitemapUrlGeneratorManager.js +2 -4
  60. package/dist/src/sitemap-generator/ScholarshipsSitemapUrlGeneratorManager.d.ts +2 -1
  61. package/dist/src/sitemap-generator/ScholarshipsSitemapUrlGeneratorManager.js +3 -2
  62. package/package.json +106 -105
@@ -22,6 +22,7 @@ export class RankedCountryAttendance extends RankedOrganisationsSeoIndexabilityP
22
22
  const countryFragments = CountryPresenter.getInstance().getFragments();
23
23
  const attendanceFragments = AttendancePresenter.getInstance().getFragments();
24
24
  const paths = [];
25
+ // Process sequentially to respect API rate limits
25
26
  for (const country of countryFragments) {
26
27
  for (const attendance of attendanceFragments) {
27
28
  const filterKeyValues = new Map([
@@ -5,6 +5,7 @@ import { OnlyFiltersSelectedRule } from '../../../common/rules/OnlyFiltersSelect
5
5
  import { CountryPresenter } from '../../../presenters/CountryPresenter';
6
6
  import { DisciplinePresenter } from '../../../presenters/DisciplinePresenter';
7
7
  import { RankedOrganisationsSeoIndexabilityPolicy } from '../RankedOrganisationsSeoIndexabilityPolicy';
8
+ const DEFAULT_CONCURRENT_REQUESTS = 20; // Maximum number of concurrent requests to avoid rate limiting
8
9
  export class RankedCountryDiscipline extends RankedOrganisationsSeoIndexabilityPolicy {
9
10
  name = 'Ranked Country Discipline Policy';
10
11
  description = 'Determines which ranking-sorted pages at the intersection of country and discipline filters qualify for indexing based on search value.';
@@ -17,22 +18,37 @@ export class RankedCountryDiscipline extends RankedOrganisationsSeoIndexabilityP
17
18
  super(dependencies);
18
19
  }
19
20
  async generateUrls() {
20
- const countryFragments = CountryPresenter.getInstance().getFragments();
21
- const disciplineFragments = DisciplinePresenter.getInstance().getFragments();
22
- const paths = [];
23
- for (const country of countryFragments) {
24
- for (const discipline of disciplineFragments) {
25
- const filterKeyValues = new Map([
26
- [FilterKey.COUNTRY, [country.id]],
27
- [FilterKey.DISCIPLINES, [discipline.id]]
28
- ]);
29
- const result = await this.checkRulesForSitemap(filterKeyValues);
30
- if (result) {
31
- paths.push(this.getPathWithSortingOption(`${country.path}/${discipline.path}`));
32
- }
21
+ try {
22
+ const countryFragments = CountryPresenter.getInstance().getFragments();
23
+ const disciplineFragments = DisciplinePresenter.getInstance().getFragments();
24
+ const paths = [];
25
+ const configuredConcurrency = Number.parseInt(process.env.SITEMAP_COMBINATION_CONCURRENCY
26
+ || DEFAULT_CONCURRENT_REQUESTS.toString(), 10);
27
+ const concurrencyLimit = Math.max(1, configuredConcurrency); // Ensure at least 1
28
+ // Create all combinations
29
+ const combinations = countryFragments.flatMap(country => disciplineFragments.map(discipline => ({ country, discipline })));
30
+ // Process in batches to respect API rate limits
31
+ for (let i = 0; i < combinations.length; i += concurrencyLimit) {
32
+ const batch = combinations.slice(i, i + concurrencyLimit);
33
+ const results = await Promise.all(batch.map(async ({ country, discipline }) => {
34
+ const filterKeyValues = new Map([
35
+ [FilterKey.COUNTRY, [country.id]],
36
+ [FilterKey.DISCIPLINES, [discipline.id]]
37
+ ]);
38
+ const result = await this.checkRulesForSitemap(filterKeyValues);
39
+ if (result) {
40
+ return this.getPathWithSortingOption(`${country.path}/${discipline.path}`);
41
+ }
42
+ return null;
43
+ }));
44
+ paths.push(...results.filter((path) => path !== null));
33
45
  }
46
+ return paths;
47
+ }
48
+ catch (error) {
49
+ console.error('Error generating URLs for RankedCountryDiscipline:', error);
50
+ return [];
34
51
  }
35
- return paths;
36
52
  }
37
53
  get filterCombination() {
38
54
  return FilterCombinations.RANKED_DISCIPLINE_COUNTRY;
@@ -15,16 +15,22 @@ export class RankedDiscipline extends RankedOrganisationsSeoIndexabilityPolicy {
15
15
  super(dependencies);
16
16
  }
17
17
  async generateUrls() {
18
- const disciplineFragments = DisciplinePresenter.getInstance().getFragments();
19
- const paths = [];
20
- for (const discipline of disciplineFragments) {
21
- const filterKeyValues = new Map([[FilterKey.DISCIPLINES, [discipline.id]]]);
22
- const result = await this.checkRulesForSitemap(filterKeyValues);
23
- if (result) {
24
- paths.push(this.getPathWithSortingOption(discipline.path));
18
+ try {
19
+ const disciplineFragments = DisciplinePresenter.getInstance().getFragments();
20
+ const paths = [];
21
+ for (const discipline of disciplineFragments) {
22
+ const filterKeyValues = new Map([[FilterKey.DISCIPLINES, [discipline.id]]]);
23
+ const result = await this.checkRulesForSitemap(filterKeyValues);
24
+ if (result) {
25
+ paths.push(this.getPathWithSortingOption(discipline.path));
26
+ }
25
27
  }
28
+ return paths;
29
+ }
30
+ catch (error) {
31
+ console.error('Error generating URLs for RankedDiscipline:', error);
32
+ return [];
26
33
  }
27
- return paths;
28
34
  }
29
35
  get filterCombination() {
30
36
  return FilterCombinations.RANKED_DISCIPLINE;
@@ -9,11 +9,17 @@ export class RankedUnfiltered extends RankedOrganisationsSeoIndexabilityPolicy {
9
9
  super(dependencies);
10
10
  }
11
11
  async generateUrls() {
12
- const filterKeyValues = new Map([]);
13
- if (await this.checkRulesForSitemap(filterKeyValues)) {
14
- return [this.getPathWithSortingOption('')];
12
+ try {
13
+ const filterKeyValues = new Map([]);
14
+ if (await this.checkRulesForSitemap(filterKeyValues)) {
15
+ return [this.getPathWithSortingOption('')];
16
+ }
17
+ return [];
18
+ }
19
+ catch (error) {
20
+ console.error('Error generating URLs for RankedUnfiltered:', error);
21
+ return [];
15
22
  }
16
- return [];
17
23
  }
18
24
  get filterCombination() {
19
25
  return FilterCombinations.RANKED_UNFILTERED;
@@ -1,11 +1,10 @@
1
1
  import { IRule } from '../../common/IRule';
2
2
  import { IOrganisationSearchDependencies } from '../types/IOrganisationSearchDependencies';
3
- import { FilterKeyValuesMap } from '../../common/FilterKeyValuesMap';
4
- import { IRankingApiClient } from '../../sitemap-generator/IRankingApiClient';
3
+ import { FilterKeyValuesMap, ISearchApiClient } from '../../../sitemap-generator-seo';
5
4
  export declare class MinimumAmountOfRankedResultsRule implements IRule {
6
5
  private readonly minimumRankedResultsCount;
7
- private readonly rankingApiClient?;
8
- constructor(rankingApiClient?: IRankingApiClient);
6
+ private readonly searchApiClient?;
7
+ constructor(searchApiClient?: ISearchApiClient);
9
8
  forSearch(dependencies: IOrganisationSearchDependencies): Promise<boolean>;
10
9
  forSitemapGenerator(filterKeyValues: FilterKeyValuesMap): Promise<boolean>;
11
10
  getName(): string;
@@ -1,9 +1,9 @@
1
1
  import { DependencyMissingError } from '../../errors/DependencyMissingError';
2
2
  export class MinimumAmountOfRankedResultsRule {
3
- minimumRankedResultsCount = 7;
4
- rankingApiClient;
5
- constructor(rankingApiClient) {
6
- this.rankingApiClient = rankingApiClient;
3
+ minimumRankedResultsCount = 4;
4
+ searchApiClient;
5
+ constructor(searchApiClient) {
6
+ this.searchApiClient = searchApiClient;
7
7
  }
8
8
  forSearch(dependencies) {
9
9
  const { applicationState } = dependencies;
@@ -11,14 +11,18 @@ export class MinimumAmountOfRankedResultsRule {
11
11
  return Promise.resolve(rankedResultsCount >= this.minimumRankedResultsCount);
12
12
  }
13
13
  async forSitemapGenerator(filterKeyValues) {
14
- if (!this.rankingApiClient) {
15
- throw new DependencyMissingError('RankingApiClient');
14
+ if (!this.searchApiClient) {
15
+ throw new DependencyMissingError('SearchApiClient');
16
16
  }
17
- const rankedResultsCount = await this.rankingApiClient.getRankedOrganisationCount(filterKeyValues);
17
+ if (!this.searchApiClient.getCountWithRankings) {
18
+ const filterKeys = [...filterKeyValues.keys()];
19
+ throw new DependencyMissingError(`CountwithRankings ${filterKeys.join(',')}`);
20
+ }
21
+ const rankedResultsCount = await this.searchApiClient.getCountWithRankings(filterKeyValues);
18
22
  return rankedResultsCount >= this.minimumRankedResultsCount;
19
23
  }
20
24
  getName() {
21
- return 'AtLeastTwoRankedResultsRule';
25
+ return 'AtLeastFourRankedResultsRule';
22
26
  }
23
27
  getDescription() {
24
28
  return `At least ${this.minimumRankedResultsCount} ranked results are available`;
@@ -1,9 +1,7 @@
1
1
  import { ISitemapDependencies } from '../../common/ISitemapDependencies';
2
- import { IRankingApiClient } from '../../sitemap-generator/IRankingApiClient';
3
2
  import { PortalType } from '@studyportals/domain-client';
4
3
  import { IOrganisationsClient } from '../../sitemap-generator/IOrganisationsClient';
5
4
  export interface IOrganisationsSitemapDependencies extends ISitemapDependencies {
6
5
  organisationsClient: IOrganisationsClient;
7
6
  portalType: PortalType;
8
- rankingApiClient: IRankingApiClient;
9
7
  }
@@ -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;
@@ -3,7 +3,7 @@ import { FilterCombinations } from '../../enums/FilterCombinations';
3
3
  import { ProgrammesBaseIndexabilityPolicy } from '../ProgrammesBaseIndexabilityPolicy';
4
4
  import { IProgrammeSeoDependencies } from '../types/IProgrammeSeoDependencies';
5
5
  import { IRule } from '../../common/IRule';
6
- export declare class DegreeAttendanceTuitionFee extends ProgrammesBaseIndexabilityPolicy {
6
+ export declare class DegreeTuitionFeeAttendance extends ProgrammesBaseIndexabilityPolicy {
7
7
  readonly name: string;
8
8
  readonly description: string;
9
9
  readonly filterKeys: FilterKey[];
@@ -10,19 +10,19 @@ import { AttendancePresenter } from '../../presenters/AttendancePresenter';
10
10
  import { OnlineAttendanceRule } from '../../common/rules/OnlineAttendanceRule';
11
11
  import { IndexableDegreeRule } from '../rules/IndexableDegreeRule';
12
12
  import { AffordableTuitionFeeRule } from '../rules/AffordableTuitionFeeRule';
13
- export class DegreeAttendanceTuitionFee extends ProgrammesBaseIndexabilityPolicy {
13
+ export class DegreeTuitionFeeAttendance extends ProgrammesBaseIndexabilityPolicy {
14
14
  name = 'Tuition Fee Attendance Degree Policy';
15
15
  description = 'Determines indexing rules for pages filtered by tuition fee, attendance, and degree type.';
16
- filterKeys = [FilterKey.TUITION_FEE, FilterKey.DELIVERY_METHOD, FilterKey.DEGREE_TYPE];
16
+ filterKeys = [FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE, FilterKey.DELIVERY_METHOD];
17
17
  rules;
18
18
  constructor(dependencies) {
19
19
  super(dependencies);
20
20
  const portalType = dependencies.portalType;
21
21
  this.rules = [
22
22
  new SingleValueSelectedForFilterRule(FilterKey.DEGREE_TYPE),
23
- new SingleValueSelectedForFilterRule(FilterKey.DELIVERY_METHOD),
24
23
  new SingleValueSelectedForFilterRule(FilterKey.TUITION_FEE),
25
- new OnlyFiltersSelectedRule([FilterKey.DEGREE_TYPE, FilterKey.DELIVERY_METHOD, FilterKey.TUITION_FEE]),
24
+ new SingleValueSelectedForFilterRule(FilterKey.DELIVERY_METHOD),
25
+ new OnlyFiltersSelectedRule([FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE, FilterKey.DELIVERY_METHOD]),
26
26
  new IndexableDegreeRule(portalType),
27
27
  new OnlineAttendanceRule(),
28
28
  new AffordableTuitionFeeRule()
@@ -42,9 +42,9 @@ export class DegreeAttendanceTuitionFee extends ProgrammesBaseIndexabilityPolicy
42
42
  for (const degree of degreeFragments) {
43
43
  for (const attendance of attendanceFragments) {
44
44
  const filterKeyValues = new Map([
45
- [FilterKey.DELIVERY_METHOD, [attendance.id]],
46
45
  [FilterKey.DEGREE_TYPE, [degree.id]],
47
- [FilterKey.TUITION_FEE, [affordableTuition.id]]
46
+ [FilterKey.TUITION_FEE, [affordableTuition.id]],
47
+ [FilterKey.DELIVERY_METHOD, [attendance.id]]
48
48
  ]);
49
49
  const result = await this.checkRulesForSitemapWithPageNumber(filterKeyValues, pageNumber);
50
50
  if (result) {
@@ -3,7 +3,7 @@ import { FilterCombinations } from '../../enums/FilterCombinations';
3
3
  import { ProgrammesBaseIndexabilityPolicy } from '../ProgrammesBaseIndexabilityPolicy';
4
4
  import { IProgrammeSeoDependencies } from '../types/IProgrammeSeoDependencies';
5
5
  import { IRule } from '../../common/IRule';
6
- export declare class DegreeContinentTuitionFee extends ProgrammesBaseIndexabilityPolicy {
6
+ export declare class DegreeTuitionFeeContinent extends ProgrammesBaseIndexabilityPolicy {
7
7
  readonly name: string;
8
8
  readonly description: string;
9
9
  readonly filterKeys: FilterKey[];
@@ -7,10 +7,10 @@ import { ProgrammesBaseIndexabilityPolicy } from '../ProgrammesBaseIndexabilityP
7
7
  import { DegreePresenter } from '../../presenters/DegreePresenter';
8
8
  import { ContinentPresenter } from '../../presenters/ContinentPresenter';
9
9
  import { TuitionFeePresenter } from '../../presenters/TuitionFeePresenter';
10
- export class DegreeContinentTuitionFee extends ProgrammesBaseIndexabilityPolicy {
11
- name = 'Degree Continent Tuition Fee Policy';
12
- description = 'Determines indexing rules for pages filtered by degree type, continent, and tuition fee.';
13
- filterKeys = [FilterKey.DEGREE_TYPE, FilterKey.CONTINENT, FilterKey.TUITION_FEE];
10
+ export class DegreeTuitionFeeContinent extends ProgrammesBaseIndexabilityPolicy {
11
+ name = 'Degree Tuition Fee Continent Policy';
12
+ description = 'Determines indexing rules for pages filtered by degree type, tuition fee, and continent.';
13
+ filterKeys = [FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE, FilterKey.CONTINENT];
14
14
  rules;
15
15
  constructor(dependencies) {
16
16
  super(dependencies);
@@ -19,7 +19,7 @@ export class DegreeContinentTuitionFee extends ProgrammesBaseIndexabilityPolicy
19
19
  new SingleValueSelectedForFilterRule(FilterKey.CONTINENT),
20
20
  new SingleValueSelectedForFilterRule(FilterKey.DEGREE_TYPE),
21
21
  new SingleValueSelectedForFilterRule(FilterKey.TUITION_FEE),
22
- new OnlyFiltersSelectedRule([FilterKey.CONTINENT, FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE]),
22
+ new OnlyFiltersSelectedRule([FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE, FilterKey.CONTINENT]),
23
23
  new IndexableDegreeRule(portalType)
24
24
  ];
25
25
  this.initateBaseRules();
@@ -34,9 +34,9 @@ export class DegreeContinentTuitionFee extends ProgrammesBaseIndexabilityPolicy
34
34
  for (const degree of degreeFragments) {
35
35
  for (const tuitionFee of tuitionFeeFragments) {
36
36
  const filterKeyValues = new Map([
37
- [FilterKey.CONTINENT, [continent.id]],
38
37
  [FilterKey.DEGREE_TYPE, [degree.id]],
39
38
  [FilterKey.TUITION_FEE, [tuitionFee.id]],
39
+ [FilterKey.CONTINENT, [continent.id]],
40
40
  ]);
41
41
  const result = await this.checkRulesForSitemapWithPageNumber(filterKeyValues, pageNumber);
42
42
  if (result) {
@@ -12,16 +12,16 @@ import { PartTimeFormatRule } from '../rules/PartTimeFormatRule';
12
12
  export class DegreeFormatTuitionFee extends ProgrammesBaseIndexabilityPolicy {
13
13
  name = 'Degree Format Tuition Fee Policy';
14
14
  description = 'Determines indexing rules for pages filtered by degree type, format and tuition fee.';
15
- filterKeys = [FilterKey.DEGREE_TYPE, FilterKey.ATTENDANCE, FilterKey.TUITION_FEE];
15
+ filterKeys = [FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE, FilterKey.ATTENDANCE];
16
16
  rules;
17
17
  constructor(dependencies) {
18
18
  super(dependencies);
19
19
  const portalType = dependencies.portalType;
20
20
  this.rules = [
21
- new SingleValueSelectedForFilterRule(FilterKey.ATTENDANCE),
22
21
  new SingleValueSelectedForFilterRule(FilterKey.DEGREE_TYPE),
23
22
  new SingleValueSelectedForFilterRule(FilterKey.TUITION_FEE),
24
- new OnlyFiltersSelectedRule([FilterKey.ATTENDANCE, FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE]),
23
+ new SingleValueSelectedForFilterRule(FilterKey.ATTENDANCE),
24
+ new OnlyFiltersSelectedRule([FilterKey.DEGREE_TYPE, FilterKey.TUITION_FEE, FilterKey.ATTENDANCE]),
25
25
  new IndexableDegreeRule(portalType),
26
26
  new AffordableTuitionFeeRule(),
27
27
  new PartTimeFormatRule()
@@ -38,9 +38,9 @@ export class DegreeFormatTuitionFee extends ProgrammesBaseIndexabilityPolicy {
38
38
  for (const degree of degreeFragments) {
39
39
  for (const tuitionFee of tuitionFeeFragments) {
40
40
  const filterKeyValues = new Map([
41
- [FilterKey.ATTENDANCE, [format.id]],
42
41
  [FilterKey.DEGREE_TYPE, [degree.id]],
43
- [FilterKey.TUITION_FEE, [tuitionFee.id]]
42
+ [FilterKey.TUITION_FEE, [tuitionFee.id]],
43
+ [FilterKey.ATTENDANCE, [format.id]],
44
44
  ]);
45
45
  const result = await this.checkRulesForSitemapWithPageNumber(filterKeyValues, pageNumber);
46
46
  if (result) {
@@ -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;
@@ -3,9 +3,10 @@ import { ISeoInfoBase } from '../common/ISeoInfoBase';
3
3
  import { ISeoFilterState } from '../common/ISeoFilterState';
4
4
  import { ISearchApplicationState } from '../common/ISearchApplicationState';
5
5
  import { ISearchIndexabilityManager } from '../common/ISearchIndexabilityManager';
6
+ import { PortalType } from '@studyportals/domain-client';
6
7
  export declare class SearchIndexabilityManager implements ISearchIndexabilityManager {
7
8
  private policies;
8
- constructor(seoInfoBase: ISeoInfoBase, filterState: ISeoFilterState, applicationState: ISearchApplicationState);
9
+ constructor(seoInfoBase: ISeoInfoBase, filterState: ISeoFilterState, applicationState: ISearchApplicationState, portalType?: PortalType);
9
10
  shouldIndex(): Promise<boolean>;
10
11
  getPolicies(): ISeoIndexabilityPolicy[];
11
12
  }
@@ -7,12 +7,13 @@ import { Unfiltered } from './policies/Unfiltered';
7
7
  import { DependencyTypes } from '../enums/DependencyTypes';
8
8
  export class SearchIndexabilityManager {
9
9
  policies;
10
- constructor(seoInfoBase, filterState, applicationState) {
10
+ constructor(seoInfoBase, filterState, applicationState, portalType) {
11
11
  const dependencies = {
12
12
  dependencyType: DependencyTypes.SEARCH,
13
13
  seoInfoBase,
14
14
  filterState,
15
- applicationState
15
+ applicationState,
16
+ portalType
16
17
  };
17
18
  this.policies = [
18
19
  new Area(dependencies),
@@ -1,13 +1,12 @@
1
+ import { IRule } from '../../common/IRule';
2
+ import { IScholarshipSeoDependencies } from '../types/IScholarshipSeoDependencies';
1
3
  import { FilterCombinations } from '../../enums/FilterCombinations';
2
- import { ExceptAustraliaAreasRule } from '../rules/ExceptAustaliaAreasRule';
3
- import { SearchVolumeAreasRule } from '../rules/SearchVolumeAreasRule';
4
4
  import { ScholarshipSeoIndexabilityPolicy } from './ScholarshipSeoIndexabilityPolicy';
5
- import { SingleValueSelectedForFilterRule } from '../../common/rules/SingleValueSelectedForFilterRule';
6
- import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedRule';
7
5
  export declare class Area extends ScholarshipSeoIndexabilityPolicy {
8
6
  readonly name: string;
9
7
  readonly description: string;
10
- protected readonly baseRules: (ExceptAustraliaAreasRule | SearchVolumeAreasRule | SingleValueSelectedForFilterRule | OnlyFiltersSelectedRule)[];
8
+ protected readonly baseRules: IRule[];
9
+ constructor(dependencies: IScholarshipSeoDependencies);
11
10
  get filterCombination(): FilterCombinations;
12
11
  protected generateUrls(): Promise<string[]>;
13
12
  }
@@ -1,3 +1,5 @@
1
+ import { NotOnPortalsRule } from '../rules/NotOnPortalsRule';
2
+ import { PortalType } from '@studyportals/domain-client';
1
3
  import { FilterKey } from '@studyportals/search-filters/server-side';
2
4
  import { FilterCombinations } from '../../enums/FilterCombinations';
3
5
  import { ExceptAustraliaAreasRule } from '../rules/ExceptAustaliaAreasRule';
@@ -9,13 +11,18 @@ import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedR
9
11
  export class Area extends ScholarshipSeoIndexabilityPolicy {
10
12
  name = 'Area Policy';
11
13
  description = 'Controls indexing of pages filtered by geographic areas, ensuring only SEO-valuable regional content with sufficient search volume is indexed while respecting country associations.';
12
- baseRules = [
13
- new SingleValueSelectedForFilterRule(FilterKey.AREA),
14
- new SingleValueSelectedForFilterRule(FilterKey.COUNTRY),
15
- new OnlyFiltersSelectedRule([FilterKey.AREA, FilterKey.COUNTRY]),
16
- new ExceptAustraliaAreasRule(),
17
- new SearchVolumeAreasRule()
18
- ];
14
+ baseRules;
15
+ constructor(dependencies) {
16
+ super(dependencies);
17
+ this.baseRules = [
18
+ new NotOnPortalsRule([PortalType.BACHELOR, PortalType.PHD], this.portalType),
19
+ new SingleValueSelectedForFilterRule(FilterKey.AREA),
20
+ new SingleValueSelectedForFilterRule(FilterKey.COUNTRY),
21
+ new OnlyFiltersSelectedRule([FilterKey.AREA, FilterKey.COUNTRY]),
22
+ new ExceptAustraliaAreasRule(),
23
+ new SearchVolumeAreasRule()
24
+ ];
25
+ }
19
26
  get filterCombination() {
20
27
  return FilterCombinations.AREA;
21
28
  }
@@ -1,12 +1,12 @@
1
+ import { IRule } from '../../common/IRule';
2
+ import { IScholarshipSeoDependencies } from '../types/IScholarshipSeoDependencies';
1
3
  import { FilterCombinations } from '../../enums/FilterCombinations';
2
- import { SearchVolumeCountriesDisciplinesRule } from '../rules/SearchVolumeCountriesDisciplinesRule';
3
4
  import { ScholarshipSeoIndexabilityPolicy } from './ScholarshipSeoIndexabilityPolicy';
4
- import { SingleValueSelectedForFilterRule } from '../../common/rules/SingleValueSelectedForFilterRule';
5
- import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedRule';
6
5
  export declare class DisciplineCountry extends ScholarshipSeoIndexabilityPolicy {
7
6
  readonly name: string;
8
7
  readonly description: string;
9
- protected readonly baseRules: (SingleValueSelectedForFilterRule | OnlyFiltersSelectedRule | SearchVolumeCountriesDisciplinesRule)[];
8
+ protected readonly baseRules: IRule[];
9
+ constructor(dependencies: IScholarshipSeoDependencies);
10
10
  get filterCombination(): FilterCombinations;
11
11
  protected generateUrls(): Promise<string[]>;
12
12
  }
@@ -1,3 +1,5 @@
1
+ import { NotOnPortalsRule } from '../rules/NotOnPortalsRule';
2
+ import { PortalType } from '@studyportals/domain-client';
1
3
  import { FilterKey } from '@studyportals/search-filters/server-side';
2
4
  import { FilterCombinations } from '../../enums/FilterCombinations';
3
5
  import { SearchVolumeCountriesDisciplinesRule } from '../rules/SearchVolumeCountriesDisciplinesRule';
@@ -6,15 +8,22 @@ import { DisciplinePresenter } from '../../presenters/DisciplinePresenter';
6
8
  import { ScholarshipSeoIndexabilityPolicy } from './ScholarshipSeoIndexabilityPolicy';
7
9
  import { SingleValueSelectedForFilterRule } from '../../common/rules/SingleValueSelectedForFilterRule';
8
10
  import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedRule';
11
+ import { OnlyCountriesDisciplineCountryRule } from '../rules/OnlyCountriesDisciplineCountryRule';
9
12
  export class DisciplineCountry extends ScholarshipSeoIndexabilityPolicy {
10
13
  name = 'Discipline Policy';
11
14
  description = 'Determines which discipline-filtered pages qualify for indexing based on search volume thresholds and content availability within specific academic fields.';
12
- baseRules = [
13
- new SingleValueSelectedForFilterRule(FilterKey.DISCIPLINES),
14
- new SingleValueSelectedForFilterRule(FilterKey.COUNTRY),
15
- new OnlyFiltersSelectedRule([FilterKey.DISCIPLINES, FilterKey.COUNTRY]),
16
- new SearchVolumeCountriesDisciplinesRule()
17
- ];
15
+ baseRules;
16
+ constructor(dependencies) {
17
+ super(dependencies);
18
+ this.baseRules = [
19
+ new NotOnPortalsRule([PortalType.BACHELOR, PortalType.PHD], this.portalType),
20
+ new SingleValueSelectedForFilterRule(FilterKey.DISCIPLINES),
21
+ new SingleValueSelectedForFilterRule(FilterKey.COUNTRY),
22
+ new OnlyFiltersSelectedRule([FilterKey.DISCIPLINES, FilterKey.COUNTRY]),
23
+ new SearchVolumeCountriesDisciplinesRule(),
24
+ new OnlyCountriesDisciplineCountryRule()
25
+ ];
26
+ }
18
27
  get filterCombination() {
19
28
  return FilterCombinations.DISCIPLINE_COUNTRY;
20
29
  }
@@ -1,7 +1,9 @@
1
1
  import { BaseSeoIndexabilityPolicy } from '../../common/policies/BaseSeoIndexabilityPolicy';
2
2
  import { IScholarshipSeoDependencies } from '../types/IScholarshipSeoDependencies';
3
3
  import { IRule } from '../../common/IRule';
4
+ import { PortalType } from '@studyportals/domain-client';
4
5
  export declare abstract class ScholarshipSeoIndexabilityPolicy extends BaseSeoIndexabilityPolicy<IScholarshipSeoDependencies> {
5
6
  protected abstract readonly baseRules: IRule[];
7
+ protected get portalType(): PortalType;
6
8
  protected get rules(): IRule[];
7
9
  }
@@ -1,6 +1,10 @@
1
1
  import { BaseSeoIndexabilityPolicy } from '../../common/policies/BaseSeoIndexabilityPolicy';
2
2
  import { IndexablePageNumberRule } from '../../common/rules/IndexablePageNumberRule';
3
+ import { PortalType } from '@studyportals/domain-client';
3
4
  export class ScholarshipSeoIndexabilityPolicy extends BaseSeoIndexabilityPolicy {
5
+ get portalType() {
6
+ return this.dependencies.portalType ?? PortalType.MASTER;
7
+ }
4
8
  get rules() {
5
9
  return [...this.baseRules, new IndexablePageNumberRule()];
6
10
  }
@@ -1,11 +1,13 @@
1
+ import { IRule } from '../../common/IRule';
2
+ import { IScholarshipSeoDependencies } from '../types/IScholarshipSeoDependencies';
1
3
  import { FilterCombinations } from '../../enums/FilterCombinations';
2
4
  import { ScholarshipSeoIndexabilityPolicy } from './ScholarshipSeoIndexabilityPolicy';
3
- import { SingleValueSelectedForFilterRule } from '../../common/rules/SingleValueSelectedForFilterRule';
4
- import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedRule';
5
5
  export declare class UniversityCountry extends ScholarshipSeoIndexabilityPolicy {
6
6
  readonly name: string;
7
7
  readonly description: string;
8
- protected readonly baseRules: (SingleValueSelectedForFilterRule | OnlyFiltersSelectedRule)[];
8
+ protected readonly baseRules: IRule[];
9
+ private readonly portalRule;
10
+ constructor(dependencies: IScholarshipSeoDependencies);
9
11
  get filterCombination(): FilterCombinations;
10
12
  protected generateUrls(): Promise<string[]>;
11
13
  }
@@ -1,3 +1,5 @@
1
+ import { NotOnPortalsRule } from '../rules/NotOnPortalsRule';
2
+ import { PortalType } from '@studyportals/domain-client';
1
3
  import { FilterKey } from '@studyportals/search-filters/server-side';
2
4
  import { FilterCombinations } from '../../enums/FilterCombinations';
3
5
  import { CountryPresenter } from '../../presenters/CountryPresenter';
@@ -7,15 +9,25 @@ import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedR
7
9
  export class UniversityCountry extends ScholarshipSeoIndexabilityPolicy {
8
10
  name = 'University Country Policy';
9
11
  description = 'Regulates indexing for pages filtered by both university and country, prioritizing institution-specific content within national boundaries that meets SEO criteria.';
10
- baseRules = [
11
- new SingleValueSelectedForFilterRule(FilterKey.ORGANISATIONS),
12
- new SingleValueSelectedForFilterRule(FilterKey.COUNTRY),
13
- new OnlyFiltersSelectedRule([FilterKey.ORGANISATIONS, FilterKey.COUNTRY])
14
- ];
12
+ baseRules;
13
+ portalRule;
14
+ constructor(dependencies) {
15
+ super(dependencies);
16
+ this.portalRule = new NotOnPortalsRule([PortalType.BACHELOR], this.portalType);
17
+ this.baseRules = [
18
+ this.portalRule,
19
+ new SingleValueSelectedForFilterRule(FilterKey.ORGANISATIONS),
20
+ new SingleValueSelectedForFilterRule(FilterKey.COUNTRY),
21
+ new OnlyFiltersSelectedRule([FilterKey.ORGANISATIONS, FilterKey.COUNTRY])
22
+ ];
23
+ }
15
24
  get filterCombination() {
16
25
  return FilterCombinations.UNIVERSITY_COUNTRY;
17
26
  }
18
27
  async generateUrls() {
28
+ if (!(await this.portalRule.forSitemapGenerator())) {
29
+ return [];
30
+ }
19
31
  const { searchApiClient, organisationsClient } = this.dependencies;
20
32
  try {
21
33
  const organisationIds = await searchApiClient.getOrganisationIds();
@@ -0,0 +1,12 @@
1
+ import { IRule } from '../../common/IRule';
2
+ import { PortalType } from '@studyportals/domain-client';
3
+ export declare class NotOnPortalsRule implements IRule {
4
+ private readonly blockedPortals;
5
+ private readonly portalType;
6
+ constructor(blockedPortals: PortalType[], portalType?: PortalType);
7
+ getName(): string;
8
+ forSearch(): Promise<boolean>;
9
+ forSitemapGenerator(): Promise<boolean>;
10
+ private isAllowedOnPortal;
11
+ getDescription(): string;
12
+ }
@@ -0,0 +1,24 @@
1
+ import { PortalType } from '@studyportals/domain-client';
2
+ export class NotOnPortalsRule {
3
+ blockedPortals;
4
+ portalType;
5
+ constructor(blockedPortals, portalType = PortalType.MASTER) {
6
+ this.blockedPortals = blockedPortals;
7
+ this.portalType = portalType;
8
+ }
9
+ getName() {
10
+ return 'NotOnPortalsRule';
11
+ }
12
+ forSearch() {
13
+ return Promise.resolve(this.isAllowedOnPortal());
14
+ }
15
+ forSitemapGenerator() {
16
+ return Promise.resolve(this.isAllowedOnPortal());
17
+ }
18
+ isAllowedOnPortal() {
19
+ return !this.blockedPortals.includes(this.portalType);
20
+ }
21
+ getDescription() {
22
+ return `Is indexable if the portal type is not one of: ${this.blockedPortals.join(', ')}.`;
23
+ }
24
+ }