@studyportals/fawkes 8.2.2 → 8.2.4-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.
- package/README.md +176 -176
- package/dist/src/common/IPresenter.d.ts +1 -1
- package/dist/src/enums/AttendanceFilterOptionValue.d.ts +5 -0
- package/dist/src/enums/AttendanceFilterOptionValue.js +6 -0
- package/dist/src/enums/DegreeTypeFilterOptionValue.d.ts +29 -0
- package/dist/src/enums/DegreeTypeFilterOptionValue.js +30 -0
- package/dist/src/enums/DurationFilterOptionValue.d.ts +16 -0
- package/dist/src/enums/DurationFilterOptionValue.js +17 -0
- package/dist/src/enums/EducationalFormFilterOptionValue.d.ts +8 -0
- package/dist/src/enums/EducationalFormFilterOptionValue.js +9 -0
- package/dist/src/enums/FilterCombinations.d.ts +1 -0
- package/dist/src/enums/FilterCombinations.js +1 -0
- package/dist/src/enums/FilterKey.d.ts +21 -0
- package/dist/src/enums/FilterKey.js +22 -0
- package/dist/src/enums/FormatFilterOptionValue.d.ts +4 -0
- package/dist/src/enums/FormatFilterOptionValue.js +5 -0
- package/dist/src/enums/SpecialProgrammesFilterOptionValue.d.ts +5 -0
- package/dist/src/enums/SpecialProgrammesFilterOptionValue.js +6 -0
- package/dist/src/enums/TuitionFeeFilterOptionValue.d.ts +3 -0
- package/dist/src/enums/TuitionFeeFilterOptionValue.js +4 -0
- package/dist/src/organisations/SearchIndexabilityManager.js +2 -1
- package/dist/src/organisations/policies/index.d.ts +1 -0
- package/dist/src/organisations/policies/index.js +1 -0
- package/dist/src/organisations/policies/our-picks/City.d.ts +12 -0
- package/dist/src/organisations/policies/our-picks/City.js +35 -0
- package/dist/src/organisations/rules/OnlyFullLocationFiltersSelectedRule.d.ts +10 -0
- package/dist/src/organisations/rules/OnlyFullLocationFiltersSelectedRule.js +32 -0
- package/dist/src/presenters/AreaPresenter.d.ts +1 -0
- package/dist/src/presenters/AreaPresenter.js +3 -0
- package/dist/src/presenters/CityPresenter.d.ts +14 -0
- package/dist/src/presenters/CityPresenter.js +60 -0
- package/dist/src/presenters/fragments/ICityFragment.d.ts +5 -0
- package/dist/src/programmes/policies/CountryAttendanceDegree.js +1 -1
- package/dist/src/programmes/policies/CountryDurationDegree.js +1 -1
- package/dist/src/programmes/policies/CountryEducationalForm.js +1 -1
- package/dist/src/programmes/policies/DisciplineArea.d.ts +1 -1
- package/dist/src/programmes/policies/DisciplineArea.js +1 -1
- package/dist/src/programmes/policies/DisciplineCountryEducationalForm.js +1 -1
- package/dist/src/programmes/policies/DisciplineEducationalForm.js +1 -1
- package/dist/src/programmes/rules/ErasmusOrJointSpecialProgrammesRule.js +2 -2
- package/dist/src/sitemap-generator/ISearchApiClient.d.ts +9 -0
- package/dist/src/sitemap-generator/OrganisationsSitemapUrlGeneratorManager.js +2 -1
- package/dist/src/structured-data/ScholarshipStructuredDataFactory.d.ts +17 -0
- package/dist/src/structured-data/ScholarshipStructuredDataFactory.js +63 -0
- package/dist/src/structured-data/SearchStructuredDataFactory.d.ts +16 -3
- package/dist/src/structured-data/SearchStructuredDataFactory.js +71 -7
- package/dist/src/structured-data/dto/EntityDTO.d.ts +2 -0
- package/dist/src/structured-data/dto/PaywallDTO.d.ts +14 -0
- package/dist/src/structured-data/dto/index.d.ts +1 -0
- package/dist/src/structured-data/dto/index.js +1 -0
- package/dist/src/structured-data/index.d.ts +1 -0
- package/dist/src/structured-data/index.js +1 -0
- package/package.json +105 -105
- package/dist/src/common/IPolicyMetaData.d.ts +0 -6
- package/dist/src/organisations/OrganisationsRuleBasedIndexabilityPolicy.d.ts +0 -11
- package/dist/src/organisations/OrganisationsRuleBasedIndexabilityPolicy.js +0 -19
- package/dist/src/organisations/rules/OnlineAttendanceRule.d.ts +0 -6
- package/dist/src/organisations/rules/OnlineAttendanceRule.js +0 -14
- package/dist/src/programmes/IProgrammeSearchApplicationState.d.ts +0 -4
- package/dist/src/programmes/IProgrammeSearchDependencies.d.ts +0 -7
- package/dist/src/programmes/IProgrammeSearchDependencies.js +0 -1
- package/dist/src/programmes/IProgrammesSeoDependencies.d.ts +0 -2
- package/dist/src/programmes/IProgrammesSeoDependencies.js +0 -1
- package/dist/src/programmes/policies/DisciplineCountryDegree.d.ts +0 -16
- package/dist/src/programmes/policies/DisciplineCountryDegree.js +0 -53
- package/dist/src/programmes/rules/DegreeAttendanceSpecificRule.d.ts +0 -10
- package/dist/src/programmes/rules/DegreeAttendanceSpecificRule.js +0 -42
- package/dist/src/programmes/rules/DegreeCountrySpecificRule.d.ts +0 -10
- package/dist/src/programmes/rules/DegreeCountrySpecificRule.js +0 -43
- package/dist/src/programmes/rules/DegreeCountryTuitionFeeRule.d.ts +0 -11
- package/dist/src/programmes/rules/DegreeCountryTuitionFeeRule.js +0 -40
- package/dist/src/programmes/rules/MBACountryAttendanceRule.d.ts +0 -11
- package/dist/src/programmes/rules/MBACountryAttendanceRule.js +0 -40
- package/dist/src/programmes/rules/MasterOfArtsDisciplineRule.d.ts +0 -10
- package/dist/src/programmes/rules/MasterOfArtsDisciplineRule.js +0 -41
- package/dist/src/programmes/rules/MasterOfEducationCountryDisciplineRule.d.ts +0 -11
- package/dist/src/programmes/rules/MasterOfEducationCountryDisciplineRule.js +0 -69
- package/dist/src/programmes/rules/MasterOfLawsCountryAttendanceRule.d.ts +0 -12
- package/dist/src/programmes/rules/MasterOfLawsCountryAttendanceRule.js +0 -43
- package/dist/src/programmes/rules/MasterOfLawsCountryTuitionFeeRule.d.ts +0 -12
- package/dist/src/programmes/rules/MasterOfLawsCountryTuitionFeeRule.js +0 -46
- package/dist/src/programmes/rules/MasterOfPhilosophyCountryAttendanceRule.d.ts +0 -12
- package/dist/src/programmes/rules/MasterOfPhilosophyCountryAttendanceRule.js +0 -43
- package/dist/src/programmes/rules/MasterOfScienceDisciplineRule.d.ts +0 -10
- package/dist/src/programmes/rules/MasterOfScienceDisciplineRule.js +0 -41
- package/dist/src/sitemap-generator/IPageNumberProvider.d.ts +0 -3
- package/dist/src/sitemap-generator/IPageNumberProvider.js +0 -1
- /package/dist/src/{common/IPolicyMetaData.js → presenters/fragments/ICityFragment.js} +0 -0
- /package/dist/src/{programmes/IProgrammeSearchApplicationState.js → structured-data/dto/PaywallDTO.js} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FilterCombinations } from '../../../enums/FilterCombinations';
|
|
2
|
+
import { FilterKey } from '@studyportals/search-filters/server-side';
|
|
3
|
+
import { SortingOptions } from '../../../enums/SortingOptions';
|
|
4
|
+
import { OrganisationsSeoIndexabilityPolicy } from '../../../organisations/policies/OrganisationsSeoIndexabilityPolicy';
|
|
5
|
+
import { CityPresenter } from '../../../presenters/CityPresenter';
|
|
6
|
+
import { OnlyFullLocationFiltersSelectedRule } from '../../../organisations/rules/OnlyFullLocationFiltersSelectedRule';
|
|
7
|
+
export class City extends OrganisationsSeoIndexabilityPolicy {
|
|
8
|
+
name = 'City Policy';
|
|
9
|
+
description = 'Controls indexing of pages filtered by geographic cities, ensuring only SEO-valuable regional content with sufficient search volume is indexed.';
|
|
10
|
+
sortingOption = SortingOptions.OUR_PICKS;
|
|
11
|
+
baseRules = [
|
|
12
|
+
new OnlyFullLocationFiltersSelectedRule()
|
|
13
|
+
];
|
|
14
|
+
async generateUrls() {
|
|
15
|
+
const cityFragments = await CityPresenter
|
|
16
|
+
.getInstance(this.dependencies.searchApiClient)
|
|
17
|
+
.getFragments();
|
|
18
|
+
const paths = [];
|
|
19
|
+
for (const city of cityFragments) {
|
|
20
|
+
const filterKeyValues = new Map([
|
|
21
|
+
[FilterKey.CITY, [city.id]],
|
|
22
|
+
[FilterKey.AREA, [city.areaId]],
|
|
23
|
+
[FilterKey.COUNTRY, [city.countryId]]
|
|
24
|
+
]);
|
|
25
|
+
const result = await this.checkRulesForSitemap(filterKeyValues);
|
|
26
|
+
if (result) {
|
|
27
|
+
paths.push(this.getPathWithSortingOption(city.path));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return paths;
|
|
31
|
+
}
|
|
32
|
+
get filterCombination() {
|
|
33
|
+
return FilterCombinations.CITY;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IRule } from '../../common/IRule';
|
|
2
|
+
import { ISearchDependencies } from '../../common/ISearchDependencies';
|
|
3
|
+
export declare class OnlyFullLocationFiltersSelectedRule implements IRule {
|
|
4
|
+
private readonly COUNTRIES_WITH_AREAS;
|
|
5
|
+
getName(): string;
|
|
6
|
+
forSearch(dependencies: ISearchDependencies): Promise<boolean>;
|
|
7
|
+
forSitemapGenerator(): Promise<boolean>;
|
|
8
|
+
getDescription(): string;
|
|
9
|
+
private isEligible;
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FilterKey } from '@studyportals/search-filters/server-side';
|
|
2
|
+
export class OnlyFullLocationFiltersSelectedRule {
|
|
3
|
+
COUNTRIES_WITH_AREAS = ["30", "82", "202"]; //UK, US, AU
|
|
4
|
+
getName() {
|
|
5
|
+
return `OnlyFullLocationFiltersSelectedRule`;
|
|
6
|
+
}
|
|
7
|
+
async forSearch(dependencies) {
|
|
8
|
+
return await this.isEligible(dependencies);
|
|
9
|
+
}
|
|
10
|
+
forSitemapGenerator() {
|
|
11
|
+
//TODO
|
|
12
|
+
return Promise.resolve(false);
|
|
13
|
+
}
|
|
14
|
+
getDescription() {
|
|
15
|
+
return `Only city, country and area (if US, UK or AU) filters are selected`;
|
|
16
|
+
}
|
|
17
|
+
async isEligible(dependencies) {
|
|
18
|
+
const seoInfoBase = dependencies.seoInfoBase;
|
|
19
|
+
const filterState = dependencies.filterState;
|
|
20
|
+
const singleCitySelected = await seoInfoBase.singleSelectionFor(FilterKey.CITY, filterState);
|
|
21
|
+
const singleAreaSelected = await seoInfoBase.singleSelectionFor(FilterKey.AREA, filterState);
|
|
22
|
+
const singleCountrySelected = await seoInfoBase.singleSelectionFor(FilterKey.COUNTRY, filterState);
|
|
23
|
+
const needArea = filterState.getSelectedValuesFor(FilterKey.COUNTRY)
|
|
24
|
+
.some((countryId) => this.COUNTRIES_WITH_AREAS.includes(countryId));
|
|
25
|
+
if (needArea) {
|
|
26
|
+
const cityAreaAndCountryOnly = await seoInfoBase.selectionOnlyFor([FilterKey.CITY, FilterKey.COUNTRY, FilterKey.AREA], filterState);
|
|
27
|
+
return singleCitySelected && singleCountrySelected && singleAreaSelected && cityAreaAndCountryOnly;
|
|
28
|
+
}
|
|
29
|
+
const cityAndCountryOnly = await seoInfoBase.selectionOnlyFor([FilterKey.CITY, FilterKey.COUNTRY], filterState);
|
|
30
|
+
return singleCitySelected && singleCountrySelected && cityAndCountryOnly;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FilterKey } from '@studyportals/search-filters/server-side';
|
|
2
|
+
import { ICityFragment } from './fragments/ICityFragment';
|
|
3
|
+
import { IPresenter, ISearchApiClient } from '../../sitemap-generator-seo';
|
|
4
|
+
export declare class CityPresenter implements IPresenter {
|
|
5
|
+
readonly filterKey: FilterKey;
|
|
6
|
+
private searchApiClient;
|
|
7
|
+
private static instance;
|
|
8
|
+
private resultSize;
|
|
9
|
+
private constructor();
|
|
10
|
+
private fragmentsPromise;
|
|
11
|
+
static getInstance(searchApiClient: ISearchApiClient): CityPresenter;
|
|
12
|
+
getFragments(): Promise<ICityFragment[]>;
|
|
13
|
+
private fetchCities;
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { FilterKey } from '@studyportals/search-filters/server-side';
|
|
2
|
+
import { countriesExtendedAll } from '@studyportals/static-domain-data/countries/countries-extended';
|
|
3
|
+
import { countryGetIsoCodePath } from '@studyportals/static-domain-data/countries/country-get-iso-path';
|
|
4
|
+
import { areaGetPath } from '@studyportals/static-domain-data/areas/area-get-path';
|
|
5
|
+
export class CityPresenter {
|
|
6
|
+
filterKey = FilterKey.CITY;
|
|
7
|
+
searchApiClient;
|
|
8
|
+
static instance;
|
|
9
|
+
resultSize = 600;
|
|
10
|
+
constructor(searchApiClient) {
|
|
11
|
+
this.searchApiClient = searchApiClient;
|
|
12
|
+
}
|
|
13
|
+
fragmentsPromise = null;
|
|
14
|
+
static getInstance(searchApiClient) {
|
|
15
|
+
if (!CityPresenter.instance) {
|
|
16
|
+
CityPresenter.instance = new CityPresenter(searchApiClient);
|
|
17
|
+
}
|
|
18
|
+
return CityPresenter.instance;
|
|
19
|
+
}
|
|
20
|
+
async getFragments() {
|
|
21
|
+
if (!this.fragmentsPromise) {
|
|
22
|
+
this.fragmentsPromise = this.fetchCities();
|
|
23
|
+
}
|
|
24
|
+
return this.fragmentsPromise;
|
|
25
|
+
}
|
|
26
|
+
async fetchCities() {
|
|
27
|
+
try {
|
|
28
|
+
const data = await this.searchApiClient.getCities();
|
|
29
|
+
const cityFragments = data.map(city => {
|
|
30
|
+
const country = countriesExtendedAll.find(item => item.iso?.toLowerCase() === city.countryIsoCode?.toLowerCase());
|
|
31
|
+
let cityPath = ``;
|
|
32
|
+
const countryPath = countryGetIsoCodePath(country?.iso || '');
|
|
33
|
+
if (city.areaId === null || city.areaId === undefined) {
|
|
34
|
+
cityPath += countryPath;
|
|
35
|
+
}
|
|
36
|
+
if (!city.areaId || city.areaId !== null) {
|
|
37
|
+
const area = areaGetPath(city.areaId?.toString() || '');
|
|
38
|
+
if (area && area !== '') {
|
|
39
|
+
cityPath += `/${area}`;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
cityPath += countryPath;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
cityPath += `/${city.virtualPath}`;
|
|
46
|
+
return {
|
|
47
|
+
id: city.id.toString(),
|
|
48
|
+
path: cityPath,
|
|
49
|
+
areaId: city.areaId?.toString(),
|
|
50
|
+
countryId: country ? country.id.toString() : '',
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
return cityFragments;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('Error fetching city data:', error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -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 = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FilterKey } from
|
|
1
|
+
import { FilterKey } from "../../enums/FilterKey";
|
|
2
2
|
import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedRule';
|
|
3
3
|
import { SingleValueSelectedForFilterRule } from '../../common/rules/SingleValueSelectedForFilterRule';
|
|
4
4
|
import { FilterCombinations } from '../../enums/FilterCombinations';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FilterKey } from
|
|
1
|
+
import { FilterKey } from "../../enums/FilterKey";
|
|
2
2
|
import { OnlyFiltersSelectedRule } from '../../common/rules/OnlyFiltersSelectedRule';
|
|
3
3
|
import { SingleValueSelectedForFilterRule } from '../../common/rules/SingleValueSelectedForFilterRule';
|
|
4
4
|
import { FilterCombinations } from '../../enums/FilterCombinations';
|
|
@@ -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 = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FilterKey } from
|
|
2
|
-
import { SpecialProgrammesFilterOptionValue } from '@studyportals/search-filters';
|
|
1
|
+
import { FilterKey } from "../../enums/FilterKey";
|
|
3
2
|
import { BaseProgrammeRule } from '../BaseProgrammeRule';
|
|
3
|
+
import { SpecialProgrammesFilterOptionValue } from "../../enums/SpecialProgrammesFilterOptionValue";
|
|
4
4
|
export class ErasmusOrJointSpecialProgrammesRule extends BaseProgrammeRule {
|
|
5
5
|
forSearch(dependencies) {
|
|
6
6
|
const { seoInfoBase, filterState } = dependencies;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { FilterKeyValuesMap } from '../common/FilterKeyValuesMap';
|
|
2
2
|
export interface ISearchApiClient {
|
|
3
3
|
getOrganisationIds(): Promise<number[]>;
|
|
4
|
+
getCityIds?(): Promise<number[]>;
|
|
5
|
+
getCities(): Promise<{
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
virtualPath: string;
|
|
9
|
+
areaId: number | null;
|
|
10
|
+
countryIsoCode: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
getProgrammeCount(filterKeyValues: FilterKeyValuesMap): Promise<number>;
|
|
4
13
|
getCount(filterKeyValues: FilterKeyValuesMap): Promise<number>;
|
|
5
14
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DependencyTypes } from '../enums/DependencyTypes';
|
|
2
2
|
import { BaseSitemapUrlGeneratorManager } from './BaseSitemapUrlGeneratorManager';
|
|
3
|
-
import { Area, AreaAttendance, Attendance, Continent, Country, CountryAttendance, RankedArea, RankedAreaDiscipline, RankedAttendance, RankedAttendanceDiscipline, RankedContinent, RankedContinentAttendance, RankedCountry, RankedCountryAttendance, RankedCountryDiscipline, RankedDiscipline, RankedUnfiltered, Unfiltered } from '../organisations/policies';
|
|
3
|
+
import { City, Area, AreaAttendance, Attendance, Continent, Country, CountryAttendance, RankedArea, RankedAreaDiscipline, RankedAttendance, RankedAttendanceDiscipline, RankedContinent, RankedContinentAttendance, RankedCountry, RankedCountryAttendance, RankedCountryDiscipline, RankedDiscipline, RankedUnfiltered, Unfiltered } from '../organisations/policies';
|
|
4
4
|
import { AreaPresenter } from '../presenters/AreaPresenter';
|
|
5
5
|
import { AttendancePresenter } from '../presenters/AttendancePresenter';
|
|
6
6
|
import { ContinentPresenter } from '../presenters/ContinentPresenter';
|
|
@@ -22,6 +22,7 @@ export class OrganisationsSitemapUrlGeneratorManager extends BaseSitemapUrlGener
|
|
|
22
22
|
rankingApiClient
|
|
23
23
|
};
|
|
24
24
|
this.policies = [
|
|
25
|
+
new City(dependencies),
|
|
25
26
|
new Area(dependencies),
|
|
26
27
|
new AreaAttendance(dependencies),
|
|
27
28
|
new Attendance(dependencies),
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
type MonetaryGrantWithPaywall = MonetaryGrant & {
|
|
7
|
+
isAccessibleForFree?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare class ScholarshipStructuredDataFactory extends SearchStructuredDataFactory<IScholarshipCard> {
|
|
10
|
+
private currencyConversionService;
|
|
11
|
+
constructor(currencyConversionService: StructuredDataCurrencyConversionService);
|
|
12
|
+
protected buildStructuredDataForCard(entity: EntityDTO<IScholarshipCard>): Promise<MonetaryGrantWithPaywall | undefined>;
|
|
13
|
+
protected getOfferData(card: IScholarshipCard): Promise<OfferDTO | undefined>;
|
|
14
|
+
protected getPaywallData(): PaywallDTO | undefined;
|
|
15
|
+
private buildAmount;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SearchStructuredDataFactory } from "./SearchStructuredDataFactory";
|
|
2
|
+
export class ScholarshipStructuredDataFactory extends SearchStructuredDataFactory {
|
|
3
|
+
currencyConversionService;
|
|
4
|
+
constructor(currencyConversionService) {
|
|
5
|
+
super();
|
|
6
|
+
this.currencyConversionService = currencyConversionService;
|
|
7
|
+
}
|
|
8
|
+
async buildStructuredDataForCard(entity) {
|
|
9
|
+
const url = `https://${window.location.host}${entity.card.getScholarshipVirtualName()}`;
|
|
10
|
+
const amount = await this.buildAmount(entity.card);
|
|
11
|
+
const structuredData = {
|
|
12
|
+
"@type": "MonetaryGrant",
|
|
13
|
+
name: entity.card.getScholarshipTitle() || undefined,
|
|
14
|
+
description: entity.card.getDescriptionOfApplicationBasis() || undefined,
|
|
15
|
+
funder: entity.card.getProviderName() || undefined,
|
|
16
|
+
url,
|
|
17
|
+
amount
|
|
18
|
+
};
|
|
19
|
+
this.addPaywallToStructuredData(structuredData, entity.paywall);
|
|
20
|
+
return structuredData;
|
|
21
|
+
}
|
|
22
|
+
async getOfferData(card) {
|
|
23
|
+
const benefits = card.getDescriptionOfBenefits();
|
|
24
|
+
const hasMonetaryValue = benefits && !isNaN(Number(benefits.split(' ')[0]));
|
|
25
|
+
if (!hasMonetaryValue)
|
|
26
|
+
return;
|
|
27
|
+
const [amount, currency] = [Number(benefits.split(' ')[0]), benefits.split(' ')[1]];
|
|
28
|
+
return {
|
|
29
|
+
price: Math.round(await this.currencyConversionService.convert(amount, currency, 'USD')),
|
|
30
|
+
category: 'tuition',
|
|
31
|
+
validFrom: undefined
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
getPaywallData() {
|
|
35
|
+
return {
|
|
36
|
+
isAccessibleForFree: false,
|
|
37
|
+
cssSelector: '.Paywalled',
|
|
38
|
+
accessMode: 'registration'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async buildAmount(card) {
|
|
42
|
+
const benefits = card.getDescriptionOfBenefits();
|
|
43
|
+
const hasMonetaryValue = benefits && !isNaN(Number(benefits.split(' ')[0]));
|
|
44
|
+
let amount;
|
|
45
|
+
let currency;
|
|
46
|
+
if (hasMonetaryValue) {
|
|
47
|
+
[amount, currency] = [Number(benefits.split(' ')[0]), benefits.split(' ')[1]];
|
|
48
|
+
}
|
|
49
|
+
let value;
|
|
50
|
+
if (hasMonetaryValue) {
|
|
51
|
+
value = Math.round(await this.currencyConversionService.convert(amount, currency, 'USD'));
|
|
52
|
+
currency = 'USD';
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
value = benefits || undefined;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
"@type": "MonetaryAmount",
|
|
59
|
+
value,
|
|
60
|
+
currency
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { SearchResultsPage, WithContext, Offer, AggregateRating, Thing, FAQPage } from 'schema-dts';
|
|
1
|
+
import { SearchResultsPage, WithContext, Offer, AggregateRating, Thing, FAQPage, WebPage } from 'schema-dts';
|
|
2
2
|
import { ReviewRatingDTO } from './dto/ReviewRatingDTO';
|
|
3
3
|
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
|
-
|
|
9
|
+
buildStructuredData(title: string, description: string, cards: TCard[], faqItems?: FAQItemDto[], breadcrumbs?: BreadcrumbDTO[], listItemStructure?: boolean): Promise<WithContext<SearchResultsPage | WebPage>>;
|
|
10
|
+
private generateMainEntities;
|
|
11
|
+
protected abstract buildStructuredDataForCard(entity: EntityDTO<TCard>): Promise<Thing | undefined> | Thing | undefined;
|
|
10
12
|
/**
|
|
11
13
|
* Get the rating for a card.
|
|
12
14
|
* This method should be overridden by subclasses to provide the specific rating logic.
|
|
@@ -25,10 +27,21 @@ export declare abstract class SearchStructuredDataFactory<TCard> {
|
|
|
25
27
|
* @protected
|
|
26
28
|
*/
|
|
27
29
|
protected getOfferData(card: TCard): Promise<OfferDTO | undefined>;
|
|
30
|
+
protected getPaywallData(): PaywallDTO | undefined;
|
|
28
31
|
private constructOptionalPageOffers;
|
|
29
32
|
protected constructOptionalFaqPage(faqItems: FAQItemDto[]): FAQPage | undefined;
|
|
33
|
+
private constructOptionalPageParts;
|
|
30
34
|
private constructOptionalBreadcrumbs;
|
|
35
|
+
private constructPaywallElement;
|
|
31
36
|
protected constructAggregateRating(reviewRating?: ReviewRatingDTO): AggregateRating | undefined;
|
|
32
37
|
protected constructOffer(offerData?: OfferDTO): Offer | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Add paywall information to any structured data object
|
|
40
|
+
*
|
|
41
|
+
* @param structuredData The structured data object to enhance
|
|
42
|
+
* @param paywallData The paywall data to add
|
|
43
|
+
* @protected
|
|
44
|
+
*/
|
|
45
|
+
protected addPaywallToStructuredData(structuredData: Record<string, any>, paywallData?: PaywallDTO): void;
|
|
33
46
|
private stripHtmlWithListRetain;
|
|
34
47
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export class SearchStructuredDataFactory {
|
|
2
|
-
async buildStructuredData(title, description, cards, faqItems = [], breadcrumbs = []) {
|
|
2
|
+
async buildStructuredData(title, description, cards, faqItems = [], breadcrumbs = [], listItemStructure) {
|
|
3
3
|
const entities = [];
|
|
4
4
|
const ratings = [];
|
|
5
5
|
const offersData = [];
|
|
@@ -12,21 +12,45 @@ export class SearchStructuredDataFactory {
|
|
|
12
12
|
offersData.push(offer);
|
|
13
13
|
entities.push({ card, offer, reviewRating });
|
|
14
14
|
}
|
|
15
|
-
const mainEntities = entities
|
|
16
|
-
.map(entity => this.buildStructuredDataForCard(entity))
|
|
17
|
-
.filter(entity => entity !== undefined);
|
|
15
|
+
const mainEntities = await this.generateMainEntities(entities, listItemStructure);
|
|
18
16
|
const data = {
|
|
19
17
|
'@context': 'https://schema.org',
|
|
20
|
-
'@type': 'SearchResultsPage',
|
|
18
|
+
'@type': !listItemStructure ? 'SearchResultsPage' : 'WebPage',
|
|
21
19
|
'name': title,
|
|
22
20
|
description,
|
|
23
21
|
'mainEntity': mainEntities
|
|
24
22
|
};
|
|
23
|
+
const paywallData = this.getPaywallData();
|
|
25
24
|
data.offers = this.constructOptionalPageOffers(offersData);
|
|
26
|
-
data.hasPart = this.
|
|
25
|
+
data.hasPart = this.constructOptionalPageParts(faqItems, paywallData);
|
|
27
26
|
data.breadcrumb = this.constructOptionalBreadcrumbs(breadcrumbs);
|
|
27
|
+
if (paywallData) {
|
|
28
|
+
data.isAccessibleForFree = paywallData.isAccessibleForFree;
|
|
29
|
+
}
|
|
28
30
|
return data;
|
|
29
31
|
}
|
|
32
|
+
async generateMainEntities(entities, listItemStructure) {
|
|
33
|
+
const mainEntities = [];
|
|
34
|
+
for (const entity of entities) {
|
|
35
|
+
const structuredData = await this.buildStructuredDataForCard(entity);
|
|
36
|
+
if (!structuredData)
|
|
37
|
+
continue;
|
|
38
|
+
mainEntities.push(structuredData);
|
|
39
|
+
}
|
|
40
|
+
if (listItemStructure) {
|
|
41
|
+
const listItems = mainEntities.map((entity, index) => ({
|
|
42
|
+
'@type': 'ListItem',
|
|
43
|
+
'position': index + 1,
|
|
44
|
+
'item': entity
|
|
45
|
+
}));
|
|
46
|
+
return [{
|
|
47
|
+
'@type': 'ItemList',
|
|
48
|
+
'numberOfItems': mainEntities.length,
|
|
49
|
+
'itemListElement': listItems
|
|
50
|
+
}];
|
|
51
|
+
}
|
|
52
|
+
return mainEntities;
|
|
53
|
+
}
|
|
30
54
|
/**
|
|
31
55
|
* Get the rating for a card.
|
|
32
56
|
* This method should be overridden by subclasses to provide the specific rating logic.
|
|
@@ -48,7 +72,10 @@ export class SearchStructuredDataFactory {
|
|
|
48
72
|
* @protected
|
|
49
73
|
*/
|
|
50
74
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
51
|
-
|
|
75
|
+
getOfferData(card) {
|
|
76
|
+
return Promise.resolve(undefined);
|
|
77
|
+
}
|
|
78
|
+
getPaywallData() {
|
|
52
79
|
return undefined;
|
|
53
80
|
}
|
|
54
81
|
constructOptionalPageOffers(offersData) {
|
|
@@ -94,6 +121,21 @@ export class SearchStructuredDataFactory {
|
|
|
94
121
|
}))
|
|
95
122
|
};
|
|
96
123
|
}
|
|
124
|
+
constructOptionalPageParts(faqItems, paywallData) {
|
|
125
|
+
const parts = [];
|
|
126
|
+
const faqPage = this.constructOptionalFaqPage(faqItems);
|
|
127
|
+
if (faqPage) {
|
|
128
|
+
parts.push(faqPage);
|
|
129
|
+
}
|
|
130
|
+
if (paywallData && paywallData.cssSelector) {
|
|
131
|
+
parts.push(this.constructPaywallElement(paywallData));
|
|
132
|
+
}
|
|
133
|
+
if (parts.length === 0)
|
|
134
|
+
return undefined;
|
|
135
|
+
if (parts.length === 1 && parts[0]['@type'] === 'FAQPage')
|
|
136
|
+
return parts[0];
|
|
137
|
+
return parts;
|
|
138
|
+
}
|
|
97
139
|
constructOptionalBreadcrumbs(breadcrumbs) {
|
|
98
140
|
if (breadcrumbs.length <= 0)
|
|
99
141
|
return;
|
|
@@ -108,6 +150,16 @@ export class SearchStructuredDataFactory {
|
|
|
108
150
|
}))
|
|
109
151
|
};
|
|
110
152
|
}
|
|
153
|
+
constructPaywallElement(paywallData) {
|
|
154
|
+
const element = {
|
|
155
|
+
'@type': 'WebPageElement',
|
|
156
|
+
'isAccessibleForFree': paywallData.isAccessibleForFree
|
|
157
|
+
};
|
|
158
|
+
if (paywallData.cssSelector) {
|
|
159
|
+
element.cssSelector = paywallData.cssSelector;
|
|
160
|
+
}
|
|
161
|
+
return element;
|
|
162
|
+
}
|
|
111
163
|
constructAggregateRating(reviewRating) {
|
|
112
164
|
if (!reviewRating)
|
|
113
165
|
return;
|
|
@@ -131,6 +183,18 @@ export class SearchStructuredDataFactory {
|
|
|
131
183
|
'category': 'tuition'
|
|
132
184
|
};
|
|
133
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Add paywall information to any structured data object
|
|
188
|
+
*
|
|
189
|
+
* @param structuredData The structured data object to enhance
|
|
190
|
+
* @param paywallData The paywall data to add
|
|
191
|
+
* @protected
|
|
192
|
+
*/
|
|
193
|
+
addPaywallToStructuredData(structuredData, paywallData) {
|
|
194
|
+
if (!paywallData)
|
|
195
|
+
return;
|
|
196
|
+
structuredData.isAccessibleForFree = paywallData.isAccessibleForFree;
|
|
197
|
+
}
|
|
134
198
|
stripHtmlWithListRetain(html) {
|
|
135
199
|
const tempDiv = document.createElement('div');
|
|
136
200
|
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
|
+
}
|