Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-141870: Update seotech-video-url API endpoint #3194

Merged
merged 4 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 69 additions & 5 deletions libs/features/seotech/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# SEOTECH
# seotech

Collection of SEO-related features that use the SEOTECH service.
Collection of SEO-related features that use the `seotech` service.
See [structured-data](https://milo.adobe.com/docs/authoring/structured-data) for authoring documentation including examples.
See [SEOTECH Wiki](https://wiki.corp.adobe.com/display/seoteam/SEOTECH) for documentation regarding the service.
See [seotech](https://git.corp.adobe.com/pages/wcms/seotech/) for documentation regarding the service.

## Video
## Usage

### 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.
Expand All @@ -21,7 +23,7 @@ Video Platforms:

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

## Structured Data
### Structured Data

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

Expand All @@ -30,3 +32,65 @@ Metadata Properties:
- `seotech-structured-data`: `on` to enable SEOTECH lookup

See [Structured Data for Milo](https://wiki.corp.adobe.com/x/YpPwwg) (Corp Only) for complete documentation.

<!-- MARK: dev -->
## Development

### Proxy

adobe.com proxies the `/seotech` (Milo) endpoints of the seotech API:

- Production: https://www.adobe.com/seotech/api/
- Stage: https://www.stage.adobe.com/seotech/api/

We always use production seotech regardless of Milo environment.

### Unit Tests

Test seotech only:

npm run test:file -- test/features/seotech/seotech.test.js

### JSON-LD

You can confirm expected JSON-LD is on you page using console:

Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map((el) => JSON.parse(el.innerText))

### seotech-video-url

Proxy Examples:

- Adobe: https://www.adobe.com/seotech/api/json-ld/types/video-object/providers/adobe/26535
- YouTube: https://www.adobe.com/seotech/api/json-ld/types/video-object/providers/youtube/dQw4w9WgXcQ
- Error (400 Bad Provider): https://www.adobe.com/seotech/api/json-ld/types/video-object/providers/foo/bar
- Error (404 Not Found): https://www.adobe.com/seotech/api/json-ld/types/video-object/providers/youtube/lolz

#### Test Pages

Create/edit test pages as necessary: [milo/drafts/seotech/features/seotech-video](https://adobe.sharepoint.com/:f:/r/sites/adobecom/Shared%20Documents/milo/drafts/seotech/milo/features/seotech-video?csf=1&web=1&e=OWVZmT)

Develop with `aem up` using local urls:

- http://localhost:3000/drafts/seotech/milo/features/seotech-video/adobe
- http://localhost:3000/drafts/seotech/milo/features/seotech-video/youtube
- http://localhost:3000/drafts/seotech/milo/features/seotech-video/error

Test your development branch once you push.
Remember to change _stage_ and _adobecom_ to your own branch and user as needed:

- https://stage--milo--adobecom.aem.page/drafts/seotech/milo/features/seotech-video/adobe
- https://stage--milo--adobecom.aem.page/drafts/seotech/milo/features/seotech-video/youtube
- https://stage--milo--adobecom.aem.page/drafts/seotech/milo/features/seotech-video/error

Test stage urls once your PR is merged into stage:

- https://milo.stage.adobe.com/drafts/seotech/milo/features/seotech-video/adobe
- https://milo.stage.adobe.com/drafts/seotech/milo/features/seotech-video/youtube
- https://milo.stage.adobe.com/drafts/seotech/milo/features/seotech-video/error

Test production urls once stage is merged into main:

- https://milo.adobe.com/drafts/seotech/milo/features/seotech-video/adobe
- https://milo.adobe.com/drafts/seotech/milo/features/seotech-video/youtube
- https://milo.adobe.com/drafts/seotech/milo/features/seotech-video/error
43 changes: 30 additions & 13 deletions libs/features/seotech/seotech.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export const SEOTECH_API_URL_PROD = 'https://14257-seotech.adobeioruntime.net';
export const SEOTECH_API_URL_STAGE = 'https://14257-seotech-stage.adobeioruntime.net';
export const PROD_BASE_URL = 'https://www.adobe.com/seotech/api';

export const REGEX_ADOBETV = /(?:https?:\/\/)?(?:stage-)?video.tv.adobe.com\/v\/([\d]+)/;
export const REGEX_YOUTUBE = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?([a-zA-Z0-9_-]+)/;
export const VIDEO_OBJECT_PROVIDERS = [
{ provider: 'adobe', regex: REGEX_ADOBETV },
{ provider: 'youtube', regex: REGEX_YOUTUBE },
];

export function logError(msg) {
window.lana?.log(`SEOTECH: ${msg}`, {
Expand All @@ -10,11 +16,23 @@ export function logError(msg) {
});
}

export async function getVideoObject(url, options) {
const { env } = options;
const videoUrl = new URL(url)?.href;
const baseUrl = env === 'prod' ? SEOTECH_API_URL_PROD : SEOTECH_API_URL_STAGE;
const videoObjectUrl = `${baseUrl}/api/v1/web/seotech/getVideoObject?url=${videoUrl}`;
export function parseVideoUrl(url, providers = VIDEO_OBJECT_PROVIDERS) {
for (const { regex, provider } of providers) {
hparra marked this conversation as resolved.
Show resolved Hide resolved
const match = url.match(regex);
if (match) {
return { provider, id: match[1] };
}
}
return null;
}

export async function getVideoObject(url, { baseUrl = PROD_BASE_URL } = {}) {
const parsedUrl = parseVideoUrl(url);
if (!parsedUrl) {
throw new Error(`Invalid video url: ${url}`);
}
const { provider, id } = parsedUrl;
const videoObjectUrl = `${baseUrl}/json-ld/types/video-object/providers/${provider}/${id}`;
const resp = await fetch(videoObjectUrl, { headers: { 'Content-Type': 'application/json' } });
const body = await resp?.json();
if (!resp.ok) {
Expand Down Expand Up @@ -42,9 +60,8 @@ export async function sha256(message) {
return hashHex;
}

export async function getStructuredData(bucket, id, options) {
export async function getStructuredData(bucket, id, { baseUrl = PROD_BASE_URL } = {}) {
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;
Expand All @@ -55,6 +72,7 @@ export async function getStructuredData(bucket, id, options) {
export async function appendScriptTag({ locationUrl, getMetadata, createTag, getConfig }) {
const url = new URL(locationUrl);
const params = new URLSearchParams(url.search);
const baseUrl = params.get('seotech-api-base-url') || undefined;
const append = (obj, className) => {
if (!obj) return;
const attributes = { type: 'application/ld+json' };
Expand All @@ -67,14 +85,13 @@ export async function appendScriptTag({ locationUrl, getMetadata, createTag, get
if (getMetadata('seotech-structured-data') === 'on') {
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')) {
const env = getConfig()?.env?.name;
promises.push(getVideoObject(getMetadata('seotech-video-url'), { env })
const videoUrl = getMetadata('seotech-video-url');
if (videoUrl) {
promises.push(getVideoObject(videoUrl, { baseUrl })
.then((videoObject) => append(videoObject, 'seotech-video-url'))
.catch((e) => logError(e.message)));
}
Expand Down
74 changes: 69 additions & 5 deletions test/features/seotech/seotech.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,72 @@ import { getConfig, createTag } from '../../../libs/utils/utils.js';
import {
appendScriptTag,
sha256,
REGEX_ADOBETV,
REGEX_YOUTUBE,
} from '../../../libs/features/seotech/seotech.js';

describe('REGEX_ADOBETV', () => {
const testCases = [
{
url: 'https://video.tv.adobe.com/v/26535',
expected: '26535',
},
{
url: 'https://video.tv.adobe.com/v/26535/',
expected: '26535',
},
{
url: 'https://stage-video.tv.adobe.com/v/26535',
expected: '26535',
},
{
url: 'https://blah.com/26535',
expected: null,
},
];
testCases.forEach(({ url, expected }) => {
it(`should ${expected ? 'parse' : 'not parse'} adobetv url: ${url}`, () => {
const match = url.match(REGEX_ADOBETV);
if (expected) {
expect(match[1]).to.equal(expected);
} else {
expect(match).to.be.null;
}
});
});
});

describe('REGEX_YOUTUBE', () => {
const testCases = [
{
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
expected: 'dQw4w9WgXcQ',
},
{
url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
expected: 'dQw4w9WgXcQ',
},
{
url: 'https://youtu.be/dQw4w9WgXcQ',
expected: 'dQw4w9WgXcQ',
},
{
url: 'https://www.example.com/watch?v=dQw4w9WgXcQ',
expected: null,
},
];
testCases.forEach(({ url, expected }) => {
it(`should ${expected ? 'parse' : 'not parse'} youtube url: ${url}`, () => {
const match = url.match(REGEX_YOUTUBE);
if (expected) {
expect(match[1]).to.equal(expected);
} else {
expect(match).to.be.null;
}
});
});
});

describe('sha256', () => {
it('should return a hash', async () => {
const message = 'hello';
Expand Down Expand Up @@ -88,13 +152,13 @@ describe('seotech', () => {
await appendScriptTag(
{ locationUrl: window.location.href, getMetadata, getConfig, createTag },
);
expect(lanaStub.calledOnceWith('SEOTECH: Failed to construct \'URL\': Invalid URL')).to.be.true;
expect(lanaStub.calledOnceWith('SEOTECH: Invalid video url: fake')).to.be.true;
});

it('should not append JSON-LD if url not found', async () => {
const lanaStub = stub(window.lana, 'log');
const getMetadata = stub().returns(null);
getMetadata.withArgs('seotech-video-url').returns('http://fake');
getMetadata.withArgs('seotech-video-url').returns('https://youtu.be/fake');
const fetchStub = stub(window, 'fetch');
fetchStub.returns(Promise.resolve(Response.json(
{ error: 'ERROR!' },
Expand All @@ -104,7 +168,7 @@ describe('seotech', () => {
{ locationUrl: window.location.href, getMetadata, getConfig, createTag },
);
expect(fetchStub.calledOnceWith(
'https://14257-seotech-stage.adobeioruntime.net/api/v1/web/seotech/getVideoObject?url=http://fake/',
'https://www.adobe.com/seotech/api/json-ld/types/video-object/providers/youtube/fake',
)).to.be.true;
expect(lanaStub.calledOnceWith('SEOTECH: Failed to fetch video: ERROR!')).to.be.true;
});
Expand All @@ -113,7 +177,7 @@ describe('seotech', () => {
const lanaStub = stub(window.lana, 'log');
const fetchStub = stub(window, 'fetch');
const getMetadata = stub().returns(null);
getMetadata.withArgs('seotech-video-url').returns('http://fake');
getMetadata.withArgs('seotech-video-url').returns('https://youtu.be/dQw4w9WgXcQ');
const expectedVideoObject = {
'@context': 'http://schema.org',
'@type': 'VideoObject',
Expand All @@ -127,7 +191,7 @@ describe('seotech', () => {
{ locationUrl: window.location.href, getMetadata, getConfig, createTag },
);
expect(fetchStub.calledOnceWith(
'https://14257-seotech-stage.adobeioruntime.net/api/v1/web/seotech/getVideoObject?url=http://fake/',
'https://www.adobe.com/seotech/api/json-ld/types/video-object/providers/youtube/dQw4w9WgXcQ',
)).to.be.true;
const el = await waitForElement('script[type="application/ld+json"]');
const obj = JSON.parse(el.text);
Expand Down
6 changes: 3 additions & 3 deletions test/utils/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ describe('Utils', () => {
it('should append to title using string from metadata', async () => {
const expected = 'Document Title NOODLE';
await utils.loadArea();
await waitFor(() => document.title === expected);
await waitFor(() => document.title === expected, 2000);
expect(document.title).to.equal(expected);
});
});
Expand All @@ -687,10 +687,10 @@ describe('Utils', () => {
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';
const expectedError = 'SEOTECH: Invalid video url: FAKE';
await utils.loadArea();
const lanaStub = sinon.stub(window.lana, 'log');
await waitFor(() => lanaStub.calledOnceWith(expectedError));
await waitFor(() => lanaStub.calledOnceWith(expectedError), 2000);
expect(lanaStub.calledOnceWith(expectedError)).to.be.true;
});
});
Expand Down
Loading