diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/dot-edit-content-html/models/meta-tags-model.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/dot-edit-content-html/models/meta-tags-model.ts
index 57ab05e70ffc..47e0e9a6deeb 100644
--- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/dot-edit-content-html/models/meta-tags-model.ts
+++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/dot-edit-content-html/models/meta-tags-model.ts
@@ -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
}
@@ -185,3 +186,5 @@ export interface SocialMediaOption {
icon: string;
description: string;
}
+
+export const IMG_NOT_FOUND_KEY = 'not-found';
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.spec.ts
index c8c4e5bdef2f..5b2f4551f537 100644
--- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.spec.ts
+++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.spec.ts
@@ -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({
@@ -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'
@@ -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('og:image
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(
+ 'og:description
meta tag found, but has more than 150 characters.'
+ );
+ done();
+ });
+ });
});
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.ts
index 8c3944867c78..2f41abe1f769 100644
--- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.ts
+++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/services/html/dot-seo-meta-tags.service.ts
@@ -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';
@@ -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;
+ seoMedia: string;
constructor(
private dotMessageService: DotMessageService,
@@ -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;
@@ -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'))
);
@@ -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')
@@ -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')
@@ -522,8 +533,10 @@ 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')
@@ -531,6 +544,14 @@ export class DotSeoMetaTagsService {
);
}
+ 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(
@@ -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(
@@ -638,10 +671,12 @@ 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')
@@ -649,7 +684,10 @@ export class DotSeoMetaTagsService {
);
}
- 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')
@@ -657,6 +695,16 @@ export class DotSeoMetaTagsService {
);
}
+ 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(
@@ -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 {
- 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
+ });
+ })
+ );
+ })
);
}
}
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-device-selector-seo/dot-device-selector-seo.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-device-selector-seo/dot-device-selector-seo.component.html
index 8e890d2fb243..329f4f7d9a9e 100644
--- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-device-selector-seo/dot-device-selector-seo.component.html
+++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-device-selector-seo/dot-device-selector-seo.component.html
@@ -33,6 +33,7 @@