Skip to content

Commit

Permalink
[Issue-25959]: seo improvements validate when favicon or preview imag…
Browse files Browse the repository at this point in the history
…e are broken (#26443)

* Fixing facebook meta

* Copy change

* #25959 Fixing method to validate if the image does not exits

* Progres on #25959

* #25959 Adding Progress on image broken

* #25959 image not-found

* #25959 Adding new component

* #25959 adding preview component

* #25959 Adding image broken

* #25959 Adding image broken

* #25959 Adding image broken

* #25959 Css fixes'

* #25959 Css fixes'

* #25959 Fixing google validation

* #25959 Added testing to the preview components

* #25959 fix image not found

* #25959 fix image not found

* PR feedback

* PR Feedback

* Adding meta tags service

* PR feedback

* PR feedback

* Fixing limit
  • Loading branch information
manuelrojas authored and dsolistorres committed Nov 6, 2023
1 parent ebc9007 commit 054d1c7
Show file tree
Hide file tree
Showing 16 changed files with 359 additions and 60 deletions.
Expand Up @@ -35,6 +35,7 @@ export enum SEO_LIMITS {
MAX_IMAGE_BYTES = 8000000,
MAX_TWITTER_IMAGE_BYTES = 5000000,
MAX_TWITTER_DESCRIPTION_LENGTH = 200,
MIN_TWITTER_DESCRIPTION_LENGTH = 30,
MIN_TWITTER_TITLE_LENGTH = 30,
MAX_TWITTER_TITLE_LENGTH = 70
}
Expand Down Expand Up @@ -185,3 +186,5 @@ export interface SocialMediaOption {
icon: string;
description: string;
}

export const IMG_NOT_FOUND_KEY = 'not-found';
Expand Up @@ -9,11 +9,13 @@ import { MockDotMessageService } from '@dotcms/utils-testing';
import { DotSeoMetaTagsService } from './dot-seo-meta-tags.service';

import { seoOGTagsResultOgMock } from '../../../seo/components/dot-results-seo-tool/mocks';
import { IMG_NOT_FOUND_KEY } from '../dot-edit-content-html/models/meta-tags-model';

describe('DotSetMetaTagsService', () => {
let service: DotSeoMetaTagsService;
let testDoc: Document;
let head: HTMLElement;
let getImageFileSizeSpy;

beforeEach(() => {
TestBed.configureTestingModule({
Expand Down Expand Up @@ -116,7 +118,7 @@ describe('DotSetMetaTagsService', () => {
]
});
service = TestBed.inject(DotSeoMetaTagsService);
spyOn(service, 'getImageFileSize').and.returnValue(
getImageFileSizeSpy = spyOn(service, 'getImageFileSize').and.returnValue(
of({
length: 8000000,
url: 'https://www.dotcms.com/dA/4e870b9fe0/1200w/jpeg/70/dotcms-defualt-og.jpg'
Expand Down Expand Up @@ -283,4 +285,57 @@ describe('DotSetMetaTagsService', () => {
done();
});
});

it('should og:image meta tag not found!', (done) => {
const imageDocument: Document = document.implementation.createDocument(
'http://www.w3.org/1999/xhtml',
'html',
null
);

const head = imageDocument.createElement('head');
imageDocument.documentElement.appendChild(head);

const ogImage = imageDocument.createElement('og:image');
imageDocument.documentElement.appendChild(ogImage);
head.appendChild(ogImage);

getImageFileSizeSpy.and.callFake(() => {
return of({
length: 0,
url: IMG_NOT_FOUND_KEY
});
});

service.getMetaTagsResults(imageDocument).subscribe((value) => {
expect(value[1].items[0].message).toEqual('<code>og:image</code> meta tag not found!');
done();
});
});

it('should og:image meta tag not found!', (done) => {
const descriptionDocument: Document = document.implementation.createDocument(
'http://www.w3.org/1999/xhtml',
'html',
null
);

const head = descriptionDocument.createElement('head');
descriptionDocument.documentElement.appendChild(head);

const ogDescription = descriptionDocument.createElement('meta');
ogDescription.setAttribute('property', 'og:description');
ogDescription.setAttribute(
'content',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla pharetra maximus enim ac tincidunt. Vivamus vestibulum sed enim sed consectetur. Nulla malesuada libero a tristique bibendum. Suspendisse blandit ligula velit, eu volutpat arcu ornare sed.'
);
head.appendChild(ogDescription);

service.getMetaTagsResults(descriptionDocument).subscribe((value) => {
expect(value[5].items[0].message).toEqual(
'<code>og:description</code> meta tag found, but has more than 150 characters.'
);
done();
});
});
});
Expand Up @@ -2,7 +2,7 @@ import { Observable, forkJoin, from, of } from 'rxjs';

import { Injectable } from '@angular/core';

import { map, switchMap } from 'rxjs/operators';
import { catchError, map, switchMap } from 'rxjs/operators';

import { DotMessageService, DotUploadService } from '@dotcms/data-access';
import { DotCMSTempFile } from '@dotcms/dotcms-models';
Expand All @@ -20,12 +20,14 @@ import {
ImageMetaData,
OpenGraphOptions,
SEO_TAGS,
SEO_MEDIA_TYPES
SEO_MEDIA_TYPES,
IMG_NOT_FOUND_KEY
} from '../dot-edit-content-html/models/meta-tags-model';

@Injectable()
export class DotSeoMetaTagsService {
readMoreValues: Record<SEO_MEDIA_TYPES, string[]>;
seoMedia: string;

constructor(
private dotMessageService: DotMessageService,
Expand Down Expand Up @@ -70,7 +72,7 @@ export class DotSeoMetaTagsService {

metaTagsObject['faviconElements'] = favicon;
metaTagsObject['titleElements'] = title;
metaTagsObject['favicon'] = (favicon[0] as HTMLLinkElement)?.href;
metaTagsObject['favicon'] = (favicon[0] as HTMLLinkElement)?.href || null;
metaTagsObject['title'] = title[0]?.innerText;
metaTagsObject['titleOgElements'] = titleOgElements;
metaTagsObject['imageOgElements'] = imagesOgElements;
Expand Down Expand Up @@ -189,7 +191,10 @@ export class DotSeoMetaTagsService {
const favicon = metaTagsObject['favicon'];
const faviconElements = metaTagsObject['faviconElements'];

if (faviconElements.length === 0) {
if (
faviconElements.length <= SEO_LIMITS.MAX_FAVICONS &&
this.areAllFalsyOrEmpty([favicon])
) {
items.push(
this.getErrorItem(this.dotMessageService.get('seo.rules.favicon.not.found'))
);
Expand Down Expand Up @@ -258,13 +263,13 @@ export class DotSeoMetaTagsService {
);
}

if (description && this.areAllFalsyOrEmpty([ogDescription, descriptionOgElements])) {
if (description && this.areAllFalsyOrEmpty([ogDescription])) {
result.push(
this.getErrorItem(this.dotMessageService.get('seo.rules.og-description.not.found'))
);
}

if (ogDescription?.length === 0) {
if (descriptionOgElements?.length >= 1 && this.areAllFalsyOrEmpty([ogDescription])) {
result.push(
this.getErrorItem(
this.dotMessageService.get('seo.rules.og-description.found.empty')
Expand Down Expand Up @@ -444,16 +449,22 @@ export class DotSeoMetaTagsService {
const imageOg = metaTagsObject['og:image'];

return this.getImageFileSize(imageOg).pipe(
switchMap((imageMetaData) => {
switchMap((imageMetaData: ImageMetaData) => {
const result: SeoRulesResult[] = [];

if (imageOg && imageMetaData.length <= SEO_LIMITS.MAX_IMAGE_BYTES) {
if (
imageMetaData?.url !== IMG_NOT_FOUND_KEY &&
imageMetaData.length <= SEO_LIMITS.MAX_IMAGE_BYTES
) {
result.push(
this.getDoneItem(this.dotMessageService.get('seo.rules.og-image.found'))
);
}

if (this.areAllFalsyOrEmpty([imageOgElements, imageOg])) {
if (
imageMetaData?.url === IMG_NOT_FOUND_KEY ||
this.areAllFalsyOrEmpty([imageOgElements, imageOg])
) {
result.push(
this.getErrorItem(
this.dotMessageService.get('seo.rules.og-image.not.found')
Expand Down Expand Up @@ -522,15 +533,25 @@ export class DotSeoMetaTagsService {
const result: SeoRulesResult[] = [];
const titleCardElements = metaTagsObject['twitterTitleElements'];
const titleCard = metaTagsObject['twitter:title'];
const title = metaTagsObject['title'];
const titleElements = metaTagsObject['titleElements'];

if (this.areAllFalsyOrEmpty([titleCard, titleCardElements])) {
if (title && this.areAllFalsyOrEmpty([titleCard, titleCardElements])) {
result.push(
this.getErrorItem(
this.dotMessageService.get('seo.rules.twitter-card-title.not.found')
)
);
}

if (this.areAllFalsyOrEmpty([title, titleCard, titleElements, titleCardElements])) {
result.push(
this.getErrorItem(
this.dotMessageService.get('seo.rules.twitter-card-title.title.not.found')
)
);
}

if (titleCardElements?.length > 1) {
result.push(
this.getErrorItem(
Expand Down Expand Up @@ -621,7 +642,19 @@ export class DotSeoMetaTagsService {

if (
twitterDescription &&
twitterDescription.length < SEO_LIMITS.MAX_TWITTER_DESCRIPTION_LENGTH
twitterDescription.length < SEO_LIMITS.MIN_TWITTER_DESCRIPTION_LENGTH
) {
result.push(
this.getWarningItem(
this.dotMessageService.get('seo.rules.twitter-card-description.less')
)
);
}

if (
twitterDescription &&
twitterDescription?.length > SEO_LIMITS.MIN_TWITTER_DESCRIPTION_LENGTH &&
twitterDescription?.length < SEO_LIMITS.MAX_TWITTER_DESCRIPTION_LENGTH
) {
result.push(
this.getDoneItem(
Expand All @@ -638,25 +671,40 @@ export class DotSeoMetaTagsService {
const twitterImage = metaTagsObject['twitter:image'];

return this.getImageFileSize(twitterImage).pipe(
switchMap((imageMetaData) => {
switchMap((imageMetaData: ImageMetaData) => {
const result: SeoRulesResult[] = [];

if (twitterImage && imageMetaData.length <= SEO_LIMITS.MAX_IMAGE_BYTES) {
if (
imageMetaData?.url !== IMG_NOT_FOUND_KEY &&
imageMetaData.length <= SEO_LIMITS.MAX_IMAGE_BYTES
) {
result.push(
this.getDoneItem(
this.dotMessageService.get('seo.rules.twitter-image.found')
)
);
}

if (this.areAllFalsyOrEmpty([twitterImage, twitterImageElements])) {
if (
imageMetaData?.url === IMG_NOT_FOUND_KEY ||
this.areAllFalsyOrEmpty([twitterImage, twitterImageElements])
) {
result.push(
this.getErrorItem(
this.dotMessageService.get('seo.rules.twitter-image.not.found')
)
);
}

if (twitterImageElements?.length >= 1 && this.areAllFalsyOrEmpty([twitterImage])) {
result.push(
this.getErrorItem(
this.dotMessageService.get(
'seo.rules.twitter-image.more.one.found.empty'
)
)
);
}

if (twitterImageElements?.length > 1) {
result.push(
this.getErrorItem(
Expand Down Expand Up @@ -753,28 +801,48 @@ export class DotSeoMetaTagsService {
}

/**
* This uploads the image temporaly to get the file size, only if it is external
* This uploads the image temporaly to get the file size, only if it is external.
* Checks if the imageUrl has been sent.
* @param imageUrl string
* @returns
*/
getImageFileSize(imageUrl: string): Observable<DotCMSTempFile | ImageMetaData> {
return from(
fetch(imageUrl)
.then((response) => response.blob())
.then((blob) => {
return {
length: blob.size,
url: imageUrl
};
})
.catch((error) => {
console.warn(
'Getting the file size from an external URL failed, so we upload it to the server:',
error
);
if (!imageUrl) {
return of({
length: 0,
url: IMG_NOT_FOUND_KEY
});
}

return from(fetch(imageUrl)).pipe(
switchMap((response) => {
if (response.status === 404) {
return of({
size: 0,
url: IMG_NOT_FOUND_KEY
});
}

return this.dotUploadService.uploadFile({ file: imageUrl });
})
return response.clone().blob();
}),
map(({ size }) => {
return {
length: size,
url: imageUrl
};
}),
catchError(() => {
return from(this.dotUploadService.uploadFile({ file: imageUrl })).pipe(
catchError((uploadError) => {
console.warn('Error while uploading:', uploadError);

return of({
length: 0,
url: IMG_NOT_FOUND_KEY
});
})
);
})
);
}
}
Expand Up @@ -33,6 +33,7 @@ <h3 class="dot-device-selector__header-title">
</button>
</li>
</ul>
<p-divider [align]="'center'" layout="vertical"></p-divider>
<ul class="device-list">
<li
class="device-list-item"
Expand Down
Expand Up @@ -48,7 +48,7 @@ $device-selector-name-margin: 0.375rem;

.dot-device-selector__grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr 0fr 1fr;
width: $device-selector-grid-width;
height: 22.75rem;
overflow: auto;
Expand All @@ -59,10 +59,7 @@ $device-selector-name-margin: 0.375rem;
padding: $spacing-1 0;
margin: 0;
color: $color-palette-gray-600;

&:first-child {
border-right: 1px solid $color-palette-gray-300;
}
overflow: auto;
}

.device-list-item {
Expand Down Expand Up @@ -131,4 +128,11 @@ $device-selector-name-margin: 0.375rem;
color: $color-palette-primary;
}
}

.p-divider {
&.p-component {
color: $color-palette-gray-300;
margin: 0;
}
}
}
Expand Up @@ -44,7 +44,8 @@ import {
PanelModule,
DividerModule,
DotMessagePipe,
RouterLink
RouterLink,
DividerModule
],
providers: [DotDevicesService],
selector: 'dot-device-selector-seo',
Expand Down

0 comments on commit 054d1c7

Please sign in to comment.