developer-page.ts
1 import { 2 type Opt, 3 unwrapOptional as unwrap, 4 } from '@jet/environment/types/optional'; 5 import type { Organization, WithContext } from 'schema-dts'; 6 7 import type { AppStoreObjectGraph } from '@jet-app/app-store/foundation/runtime/app-store-object-graph'; 8 import { 9 type Data, 10 type DataContainer, 11 dataFromDataContainer, 12 } from '@jet-app/app-store/foundation/media/data-structure'; 13 import { attributeAsString } from '@jet-app/app-store/foundation/media/attributes'; 14 import { relationshipCollection } from '@jet-app/app-store/foundation/media/relationships'; 15 16 import type I18N from '@amp/web-apps-localization'; 17 import type { SeoData } from '@amp/web-app-components/src/components/MetaTags/types'; 18 19 import { uniqueById } from '~/utils/array'; 20 import { basicSoftwareApplicationSchema } from '~/utils/seo/product-page'; 21 22 /** 23 * Generate a basic {@linkcode Person} schema for a "developer" page 24 * 25 * Note: this is appropriate to be embedded into another schema that 26 * needs to reference the developer 27 */ 28 export function basicDeveloperSchema(data: Data) { 29 return { 30 '@type': 'Organization', 31 name: attributeAsString(data, 'name') ?? undefined, 32 url: attributeAsString(data, 'url') ?? undefined, 33 } satisfies Organization; 34 } 35 36 export function buildDeveloperDescription( 37 props: { 38 name: string; 39 }, 40 appData: Data[], 41 i18n: I18N, 42 ) { 43 const { name: developerName } = props; 44 45 switch (appData.length) { 46 case 0: 47 return i18n.t( 48 'ASE.Web.AppStore.Meta.Developer.Description.ZeroApps', 49 { 50 developerName, 51 }, 52 ); 53 case 1: 54 return i18n.t( 55 'ASE.Web.AppStore.Meta.Developer.Description.OneApp', 56 { 57 developerName, 58 listing1: attributeAsString(appData[0], 'name'), 59 }, 60 ); 61 case 2: 62 return i18n.t( 63 'ASE.Web.AppStore.Meta.Developer.Description.TwoApps', 64 { 65 developerName, 66 listing1: attributeAsString(appData[0], 'name'), 67 listing2: attributeAsString(appData[1], 'name'), 68 }, 69 ); 70 case 3: 71 return i18n.t( 72 'ASE.Web.AppStore.Meta.Developer.Description.ThreeApps', 73 { 74 developerName, 75 listing1: attributeAsString(appData[0], 'name'), 76 listing2: attributeAsString(appData[1], 'name'), 77 listing3: attributeAsString(appData[2], 'name'), 78 }, 79 ); 80 default: 81 return i18n.t( 82 'ASE.Web.AppStore.Meta.Developer.Description.ManyApps', 83 { 84 developerName, 85 listing1: attributeAsString(appData[0], 'name'), 86 listing2: attributeAsString(appData[1], 'name'), 87 listing3: attributeAsString(appData[2], 'name'), 88 }, 89 ); 90 } 91 } 92 93 /** 94 * Builds the Schema.org meta-data for a "Developer" page 95 * 96 * @param objectGraph The Object Graph 97 * @param developerPageData The `Data` for the Developer page 98 * @param appData The `Data` for all apps related to the Developer apge 99 * @param props Pre-formatted properties also used outside of the Schema 100 * @returns 101 */ 102 function developerOrganizationSchemaSeoData( 103 objectGraph: AppStoreObjectGraph, 104 developerPageData: Data, 105 appData: Data[], 106 props: { 107 description: string; 108 }, 109 ): Opt<Partial<SeoData>> { 110 const { description } = props; 111 112 const schemaContent: WithContext<Organization> = { 113 '@context': 'https://schema.org', 114 115 ...basicDeveloperSchema(developerPageData), 116 117 description, 118 hasOfferCatalog: { 119 '@type': 'OfferCatalog', 120 itemListElement: appData.map((app) => 121 basicSoftwareApplicationSchema(objectGraph, app), 122 ), 123 }, 124 }; 125 126 return { 127 schemaName: 'developer', 128 schemaContent, 129 }; 130 } 131 132 /** 133 * Builds the full `SeoData` requirements for a "Developer" page 134 */ 135 export function seoDataForDeveloperPage( 136 objectGraph: AppStoreObjectGraph, 137 container: Opt<DataContainer>, 138 i18n: I18N, 139 ): Partial<SeoData> { 140 if (!container) { 141 return {}; 142 } 143 144 const developerPageData = dataFromDataContainer(objectGraph, container); 145 if (!developerPageData) { 146 return {}; 147 } 148 149 const allApps = uniqueById([ 150 ...unwrap(relationshipCollection(developerPageData, 'atv-apps')), 151 ...unwrap(relationshipCollection(developerPageData, 'app-bundles')), 152 ...unwrap(relationshipCollection(developerPageData, 'imessage-apps')), 153 ...unwrap(relationshipCollection(developerPageData, 'ios-apps')), 154 ...unwrap(relationshipCollection(developerPageData, 'mac-apps')), 155 ...unwrap(relationshipCollection(developerPageData, 'watch-apps')), 156 ]); 157 158 const name = unwrap(attributeAsString(developerPageData, 'name')); 159 const description = buildDeveloperDescription({ name }, allApps, i18n); 160 161 return { 162 description, 163 socialDescription: description, 164 appleDescription: description, 165 ...developerOrganizationSchemaSeoData( 166 objectGraph, 167 developerPageData, 168 allApps, 169 { 170 description, 171 }, 172 ), 173 }; 174 }