Skip to content

Commit

Permalink
MWPW-131747 - Support structured data API for Video SEO (#1065)
Browse files Browse the repository at this point in the history
* Add seotech-video feature
  • Loading branch information
hparra authored Aug 4, 2023
1 parent aaa3578 commit 41c59f2
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 2 deletions.
4 changes: 2 additions & 2 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# Milo Core (alphabetical order)
/fstab.yaml @adobecom/admins
/libs/features/ @adobecom/admins
/libs/features/seotech/ @hparra
/libs/features/title-append/ @hparra
/libs/martech/ @adobecom/admins
/libs/scripts/ @adobecom/admins
/libs/utils/ @adobecom/admins
Expand Down Expand Up @@ -41,5 +43,3 @@
# Milo Tools (alphabetical order)
/tools/caas-import/ @chrischrischris
/tools/send-to-caas/ @chrischrischris


21 changes: 21 additions & 0 deletions libs/features/seotech/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SEOTECH

Collection of SEO-related features that use the SEOTECH service.
For more details see [SEOTECH API](https://wiki.corp.adobe.com/display/seoteam/SEOTECH+API) (Corp Only).

## Video

This feature selects a video url from the page then queries the SEOTECH service for structured data.
If a valid VideoObject is returned then it is appended to the head of the document as JSON-LD.

Metadata Properties:

- `seotech-video-url`: url for query sent to /getVideoObject

Video Platforms:

- YouTube: Supported
- Adobe TV: WIP
- BYO HTML5: TBD

See [video-metadata](../../blocks/video-metadata/) if you need to define a specific VideoObject on your page.
34 changes: 34 additions & 0 deletions libs/features/seotech/seotech.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const SEOTECH_API_URL_PROD = 'https://14257-seotech.adobeioruntime.net';
export const SEOTECH_API_URL_STAGE = 'https://14257-seotech-stage.adobeioruntime.net';

export function logError(msg) {
window.lana?.log(`SEOTECH: ${msg}`, {
debug: false,
implicitSampleRate: 100,
sampleRate: 100,
tags: 'errorType=seotech',
});
}

export async function getVideoObject(url, seotechAPIUrl) {
const videoUrl = new URL(url)?.href;
const videoObjectUrl = `${seotechAPIUrl}/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) {
throw new Error(`Failed to fetch video: ${body?.error}`);
}
return body.videoObject;
}

export default async function appendVideoObjectScriptTag(url, { createTag, getConfig }) {
const seotechAPIUrl = getConfig()?.env?.name === 'prod'
? SEOTECH_API_URL_PROD : SEOTECH_API_URL_STAGE;
try {
const obj = await getVideoObject(url, seotechAPIUrl);
const script = createTag('script', { type: 'application/ld+json' }, JSON.stringify(obj));
document.head.append(script);
} catch (e) {
logError(e.message);
}
}
5 changes: 5 additions & 0 deletions libs/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -849,13 +849,18 @@ export async function loadArea(area = document) {
if (isDoc) {
const georouting = getMetadata('georouting') || config.geoRouting;
if (georouting === 'on') {
// eslint-disable-next-line import/no-cycle
const { default: loadGeoRouting } = await import('../features/georoutingv2/georoutingv2.js');
loadGeoRouting(config, createTag, getMetadata, loadBlock, loadStyle);
}
const appendage = getMetadata('title-append');
if (appendage) {
import('../features/title-append/title-append.js').then((module) => module.default(appendage));
}
const seotechVideoUrl = getMetadata('seotech-video-url');
if (seotechVideoUrl) {
import('../features/seotech/seotech.js').then((module) => module.default(seotechVideoUrl, { createTag, getConfig }));
}
const richResults = getMetadata('richresults');
if (richResults) {
const { default: addRichResults } = await import('../features/richresults.js');
Expand Down
58 changes: 58 additions & 0 deletions test/features/seotech/seotech.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { expect } from '@esm-bundle/chai';
import { stub } from 'sinon';
import { waitForElement } from '../../helpers/waitfor.js';

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

describe('seotech', () => {
describe('appendVideoObjectScriptTag', () => {
beforeEach(async () => {
window.lana = { log: () => console.log('LANA NOT STUBBED!') };
});
afterEach(() => {
window.fetch?.restore?.();
window.lana?.restore?.();
});

it('should not append JSON-LD if url is invalid', async () => {
const lanaStub = stub(window.lana, 'log');
await appendVideoObjectScriptTag('', { getConfig, createTag });
expect(lanaStub.calledOnceWith('SEOTECH: Failed to construct \'URL\': Invalid URL')).to.be.true;
});

it('should not append JSON-LD if url not found', async () => {
const lanaStub = stub(window.lana, 'log');
const fetchStub = stub(window, 'fetch');
fetchStub.returns(Promise.resolve(Response.json(
{ error: 'ERROR!' },
{ status: 400 },
)));
await appendVideoObjectScriptTag('http://fake', { getConfig, createTag });
expect(fetchStub.calledOnceWith(
'https://14257-seotech-stage.adobeioruntime.net/api/v1/web/seotech/getVideoObject?url=http://fake/',
)).to.be.true;
expect(lanaStub.calledOnceWith('SEOTECH: Failed to fetch video: ERROR!')).to.be.true;
});

it('should append JSON-LD', async () => {
const fetchStub = stub(window, 'fetch');
const expectedVideoObject = {
'@context': 'http://schema.org',
'@type': 'VideoObject',
name: 'fake',
};
fetchStub.returns(Promise.resolve(Response.json(
{ videoObject: expectedVideoObject },
{ status: 200 },
)));
await appendVideoObjectScriptTag('http://fake', { getConfig, createTag });
expect(fetchStub.calledOnceWith(
'https://14257-seotech-stage.adobeioruntime.net/api/v1/web/seotech/getVideoObject?url=http://fake/',
)).to.be.true;
const el = await waitForElement('script[type="application/ld+json"]');
const obj = JSON.parse(el.text);
expect(obj).to.deep.equal(expectedVideoObject);
});
});
});
3 changes: 3 additions & 0 deletions test/utils/mocks/head-seotech-video.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<title>Document Title</title>
<meta name="seotech-video-url" content="FAKE">
<link rel="icon" href="data:,">
17 changes: 17 additions & 0 deletions test/utils/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,21 @@ describe('Utils', () => {
expect(document.title).to.equal(expected);
});
});

describe('seotech', async () => {
beforeEach(async () => {
window.lana = { log: (msg) => console.error(msg) };
document.head.innerHTML = await readFile({ path: './mocks/head-seotech-video.html' });
});
afterEach(() => {
window.lana.release?.();
});
it('should import feature when metadata is defined and error if invalid', async () => {
const expectedError = 'SEOTECH: Failed to construct \'URL\': Invalid URL';
await utils.loadArea();
const lanaStub = sinon.stub(window.lana, 'log');
await waitFor(() => lanaStub.calledOnceWith(expectedError));
expect(lanaStub.calledOnceWith(expectedError)).to.be.true;
});
});
});

0 comments on commit 41c59f2

Please sign in to comment.