Skip to content

Commit

Permalink
MWPW-146237: Repurpose seotech-structured-data feature for arbitrary …
Browse files Browse the repository at this point in the history
…JSON-LD (#2578)

* Add necessary helper functions

* Fix two checks

* Replace getStructuredData

* Move things around

* Fix regex?

* Add env

* Support localhost

* Update paths

* Fix test

* Use seotech api instead

* Update env/subdomain

* Fix test properly

* Update endoint

* Add homepage

* Update README

* Rework endpoint handling

* Add class to script tags

* More doc updates
  • Loading branch information
hparra authored Aug 7, 2024
1 parent d6834ea commit f36fea1
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 46 deletions.
12 changes: 4 additions & 8 deletions libs/features/seotech/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,17 @@ Metadata Properties:
Video Platforms:

- YouTube: Supported
- Adobe TV: WIP
- BYO HTML5: TBD
- Adobe TV: Supported
- BYO HTML5: See "Structured Data"

See [video-metadata](../../blocks/video-metadata/) if you need to define a specific VideoObject on your page.

## Structured Data

This feature queries the SEOTECH service for structured data that should be added to the page.
This feature queries the SEOTECH service for adhoc structured data that should be added to the page.

Metadata Properties:

- `seotech-structured-data`: `on` to enable SEOTECH lookup
- `seotech-sheet-url`: url of Franklin Spreadsheet JSON (Optional)

You can also specify `seotech-sheet-url` as a query parameter.
Otherwise SEOTECH will search for _/structured-data.json_ at the root of the current page.

See [seotech page](https://git.corp.adobe.com/pages/wcms/seotech/) (Corp Only) for list of supported structured data types.
See [Structured Data for Milo](https://wiki.corp.adobe.com/x/YpPwwg) (Corp Only) for complete documentation.
73 changes: 46 additions & 27 deletions libs/features/seotech/seotech.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ export function logError(msg) {
});
}

export async function getVideoObject(url, seotechAPIUrl) {
export async function getVideoObject(url, options) {
const { env } = options;
const videoUrl = new URL(url)?.href;
const videoObjectUrl = `${seotechAPIUrl}/api/v1/web/seotech/getVideoObject?url=${videoUrl}`;
const baseUrl = env === 'prod' ? SEOTECH_API_URL_PROD : SEOTECH_API_URL_STAGE;
const videoObjectUrl = `${baseUrl}/api/v1/web/seotech/getVideoObject?url=${videoUrl}`;
const resp = await fetch(videoObjectUrl, { headers: { 'Content-Type': 'application/json' } });
const body = await resp?.json();
if (!resp.ok) {
Expand All @@ -21,42 +23,59 @@ export async function getVideoObject(url, seotechAPIUrl) {
return body.videoObject;
}

export async function getStructuredData(url, sheetUrl, seotechAPIUrl) {
const apiUrl = new URL(seotechAPIUrl);
apiUrl.pathname = '/api/v1/web/seotech/getStructuredData';
apiUrl.searchParams.set('url', url);
if (sheetUrl) {
apiUrl.searchParams.set('sheetUrl', sheetUrl);
}
const resp = await fetch(apiUrl.href, { headers: { 'Content-Type': 'application/json' } });
const body = await resp?.json();
if (!resp.ok) {
throw new Error(`Failed to fetch structured data: ${body?.error}`);
}
return body.objects;
// https://github.com/orgs/adobecom/discussions/2633
export function getRepoByImsClientId(imsClientId) {
return {
'adobedotcom-cc': 'cc',
acrobatmilo: 'dc',
bacom: 'bacom',
homepage_milo: 'homepage',
milo: 'milo',
}[imsClientId];
}

export async function appendScriptTag({ locationUrl, getMetadata, createTag, getConfig }) {
const windowUrl = new URL(locationUrl);
const seotechAPIUrl = getConfig()?.env?.name === 'prod'
? SEOTECH_API_URL_PROD : SEOTECH_API_URL_STAGE;
export async function sha256(message) {
const msgUint8 = new TextEncoder().encode(message);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}

export async function getStructuredData(bucket, id, options) {
if (!bucket || !id) throw new Error('bucket and id are required');
const { baseUrl } = options;
const url = `${baseUrl}/structured-data/${bucket}/${id}`;
const resp = await fetch(url);
if (!resp || !resp.ok) return null;
const body = await resp.json();
return body;
}

const append = (obj) => {
const script = createTag('script', { type: 'application/ld+json' }, JSON.stringify(obj));
export async function appendScriptTag({ locationUrl, getMetadata, createTag, getConfig }) {
const url = new URL(locationUrl);
const params = new URLSearchParams(url.search);
const append = (obj, className) => {
if (!obj) return;
const attributes = { type: 'application/ld+json' };
if (className) attributes.class = className;
const script = createTag('script', attributes, JSON.stringify(obj));
document.head.append(script);
};

const promises = [];
if (getMetadata('seotech-structured-data') === 'on') {
const pageUrl = `${windowUrl.origin}${windowUrl.pathname}`;
const sheetUrl = (new URLSearchParams(windowUrl.search)).get('seotech-sheet-url') || getMetadata('seotech-sheet-url');
promises.push(getStructuredData(pageUrl, sheetUrl, seotechAPIUrl)
.then((objects) => objects.forEach((obj) => append(obj)))
const bucket = getRepoByImsClientId(getConfig()?.imsClientId);
const id = await sha256(url.pathname?.replace('.html', ''));
const baseUrl = params.get('seotech-api-base-url') || 'https://www.adobe.com/seotech/api';
promises.push(getStructuredData(bucket, id, { baseUrl })
.then((obj) => append(obj, 'seotech-structured-data'))
.catch((e) => logError(e.message)));
}
if (getMetadata('seotech-video-url')) {
promises.push(getVideoObject(getMetadata('seotech-video-url'), seotechAPIUrl)
.then((videoObject) => append(videoObject))
const env = getConfig()?.env?.name;
promises.push(getVideoObject(getMetadata('seotech-video-url'), { env })
.then((videoObject) => append(videoObject, 'seotech-video-url'))
.catch((e) => logError(e.message)));
}
return Promise.all(promises);
Expand Down
34 changes: 23 additions & 11 deletions test/features/seotech/seotech.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ import { stub } from 'sinon';
import { waitForElement } from '../../helpers/waitfor.js';

import { getConfig, createTag } from '../../../libs/utils/utils.js';
import { appendScriptTag } from '../../../libs/features/seotech/seotech.js';
import {
appendScriptTag,
sha256,
} from '../../../libs/features/seotech/seotech.js';

describe('sha256', () => {
it('should return a hash', async () => {
const message = 'hello';
const hash = await sha256(message);
expect(hash).to.equal('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
});
});

describe('seotech', () => {
describe('appendScriptTag + seotech-structured-data', () => {
Expand All @@ -16,27 +27,28 @@ describe('seotech', () => {
});

it('should not append JSON-LD', async () => {
const lanaStub = stub(window.lana, 'log');
const locationUrl = 'https://main--cc--adobecom.hlx.page/in/creativecloud/example2?foo=bar&seotech-env=stage';
stub(window.lana, 'log');
const getMetadata = stub().returns(null);
getMetadata.withArgs('seotech-structured-data').returns('on');
const getConfigStub = stub().returns({ imsClientId: 'adobedotcom-cc' });
const fetchStub = stub(window, 'fetch');
fetchStub.returns(Promise.resolve(Response.json(
{ error: 'ERROR!' },
{ status: 400 },
)));
await appendScriptTag(
{ locationUrl: window.location.href, getMetadata, getConfig, createTag },
{ locationUrl, getMetadata, getConfig: getConfigStub, createTag },
);
const expectedApiCall = 'https://14257-seotech-stage.adobeioruntime.net/api/v1/web/seotech/getStructuredData?url=http%3A%2F%2Flocalhost%3A2000%2F';
expect(fetchStub.getCall(0).firstArg).to.equal(expectedApiCall);
expect(lanaStub.getCall(0).firstArg).to.equal('SEOTECH: Failed to fetch structured data: ERROR!');
const expectedApiCall = 'https://www.adobe.com/seotech/api/structured-data/cc/3e2d1ce8ccf0e45d42d33e0f190fc306ab1ee0f2890c8ff5da27414f8014ceb2';
expect(fetchStub.getCall(0)?.firstArg).to.equal(expectedApiCall);
});

it('should append JSON-LD', async () => {
const locationUrl = 'http://localhost:2000/?seotech-sheet-url=http://foo';
const locationUrl = 'https://main--cc--adobecom.hlx.page/in/creativecloud/example?foo=bar';
const lanaStub = stub(window.lana, 'log');
const fetchStub = stub(window, 'fetch');
const getConfigStub = stub().returns({ env: { name: 'prod' } });
const getConfigStub = stub().returns({ imsClientId: 'adobedotcom-cc' });
const getMetadata = stub().returns(null);
getMetadata.withArgs('seotech-structured-data').returns('on');
const expectedObject = {
Expand All @@ -45,14 +57,14 @@ describe('seotech', () => {
name: 'fake',
};
fetchStub.returns(Promise.resolve(Response.json(
{ objects: [expectedObject] },
{ ...expectedObject },
{ status: 200 },
)));
await appendScriptTag(
{ locationUrl, getMetadata, getConfig: getConfigStub, createTag },
);
const expectedApiCall = 'https://14257-seotech.adobeioruntime.net/api/v1/web/seotech/getStructuredData?url=http%3A%2F%2Flocalhost%3A2000%2F&sheetUrl=http%3A%2F%2Ffoo';
expect(fetchStub.getCall(0).firstArg).to.equal(expectedApiCall);
const expectedApiCall = 'https://www.adobe.com/seotech/api/structured-data/cc/f0f5cec5d8b70cf798b602c3586da39e93b9638d9b8001b3a4298605dc5f6ebe';
expect(fetchStub.getCall(0)?.firstArg).to.equal(expectedApiCall);
const el = await waitForElement('script[type="application/ld+json"]');
const obj = JSON.parse(el.text);
expect(obj).to.deep.equal(expectedObject);
Expand Down

0 comments on commit f36fea1

Please sign in to comment.