From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- src/utils/seo/developer-page.ts | 174 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/utils/seo/developer-page.ts (limited to 'src/utils/seo/developer-page.ts') diff --git a/src/utils/seo/developer-page.ts b/src/utils/seo/developer-page.ts new file mode 100644 index 0000000..914dd08 --- /dev/null +++ b/src/utils/seo/developer-page.ts @@ -0,0 +1,174 @@ +import { + type Opt, + unwrapOptional as unwrap, +} from '@jet/environment/types/optional'; +import type { Organization, WithContext } from 'schema-dts'; + +import type { AppStoreObjectGraph } from '@jet-app/app-store/foundation/runtime/app-store-object-graph'; +import { + type Data, + type DataContainer, + dataFromDataContainer, +} from '@jet-app/app-store/foundation/media/data-structure'; +import { attributeAsString } from '@jet-app/app-store/foundation/media/attributes'; +import { relationshipCollection } from '@jet-app/app-store/foundation/media/relationships'; + +import type I18N from '@amp/web-apps-localization'; +import type { SeoData } from '@amp/web-app-components/src/components/MetaTags/types'; + +import { uniqueById } from '~/utils/array'; +import { basicSoftwareApplicationSchema } from '~/utils/seo/product-page'; + +/** + * Generate a basic {@linkcode Person} schema for a "developer" page + * + * Note: this is appropriate to be embedded into another schema that + * needs to reference the developer + */ +export function basicDeveloperSchema(data: Data) { + return { + '@type': 'Organization', + name: attributeAsString(data, 'name') ?? undefined, + url: attributeAsString(data, 'url') ?? undefined, + } satisfies Organization; +} + +export function buildDeveloperDescription( + props: { + name: string; + }, + appData: Data[], + i18n: I18N, +) { + const { name: developerName } = props; + + switch (appData.length) { + case 0: + return i18n.t( + 'ASE.Web.AppStore.Meta.Developer.Description.ZeroApps', + { + developerName, + }, + ); + case 1: + return i18n.t( + 'ASE.Web.AppStore.Meta.Developer.Description.OneApp', + { + developerName, + listing1: attributeAsString(appData[0], 'name'), + }, + ); + case 2: + return i18n.t( + 'ASE.Web.AppStore.Meta.Developer.Description.TwoApps', + { + developerName, + listing1: attributeAsString(appData[0], 'name'), + listing2: attributeAsString(appData[1], 'name'), + }, + ); + case 3: + return i18n.t( + 'ASE.Web.AppStore.Meta.Developer.Description.ThreeApps', + { + developerName, + listing1: attributeAsString(appData[0], 'name'), + listing2: attributeAsString(appData[1], 'name'), + listing3: attributeAsString(appData[2], 'name'), + }, + ); + default: + return i18n.t( + 'ASE.Web.AppStore.Meta.Developer.Description.ManyApps', + { + developerName, + listing1: attributeAsString(appData[0], 'name'), + listing2: attributeAsString(appData[1], 'name'), + listing3: attributeAsString(appData[2], 'name'), + }, + ); + } +} + +/** + * Builds the Schema.org meta-data for a "Developer" page + * + * @param objectGraph The Object Graph + * @param developerPageData The `Data` for the Developer page + * @param appData The `Data` for all apps related to the Developer apge + * @param props Pre-formatted properties also used outside of the Schema + * @returns + */ +function developerOrganizationSchemaSeoData( + objectGraph: AppStoreObjectGraph, + developerPageData: Data, + appData: Data[], + props: { + description: string; + }, +): Opt> { + const { description } = props; + + const schemaContent: WithContext = { + '@context': 'https://schema.org', + + ...basicDeveloperSchema(developerPageData), + + description, + hasOfferCatalog: { + '@type': 'OfferCatalog', + itemListElement: appData.map((app) => + basicSoftwareApplicationSchema(objectGraph, app), + ), + }, + }; + + return { + schemaName: 'developer', + schemaContent, + }; +} + +/** + * Builds the full `SeoData` requirements for a "Developer" page + */ +export function seoDataForDeveloperPage( + objectGraph: AppStoreObjectGraph, + container: Opt, + i18n: I18N, +): Partial { + if (!container) { + return {}; + } + + const developerPageData = dataFromDataContainer(objectGraph, container); + if (!developerPageData) { + return {}; + } + + const allApps = uniqueById([ + ...unwrap(relationshipCollection(developerPageData, 'atv-apps')), + ...unwrap(relationshipCollection(developerPageData, 'app-bundles')), + ...unwrap(relationshipCollection(developerPageData, 'imessage-apps')), + ...unwrap(relationshipCollection(developerPageData, 'ios-apps')), + ...unwrap(relationshipCollection(developerPageData, 'mac-apps')), + ...unwrap(relationshipCollection(developerPageData, 'watch-apps')), + ]); + + const name = unwrap(attributeAsString(developerPageData, 'name')); + const description = buildDeveloperDescription({ name }, allApps, i18n); + + return { + description, + socialDescription: description, + appleDescription: description, + ...developerOrganizationSchemaSeoData( + objectGraph, + developerPageData, + allApps, + { + description, + }, + ), + }; +} -- cgit v1.2.3