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 Oct 18, 2023
1 parent 15e5579 commit 9fb2040
Show file tree
Hide file tree
Showing 16 changed files with 359 additions and 60 deletions.
Original file line number Diff line number Diff line change
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';
Original file line number Diff line number Diff line change
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();
});
});
});
Original file line number Diff line number Diff line change
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
});
})
);
})
);
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ import {
PanelModule,
DividerModule,
DotMessagePipe,
RouterLink
RouterLink,
DividerModule
],
providers: [DotDevicesService],
selector: 'dot-device-selector-seo',
Expand Down
Loading

0 comments on commit 9fb2040

Please sign in to comment.