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 @@

+
  • - favicon + *ngIf="noFavicon; else favicon" + data-testId="favicon-default"> + +
    + + favicon + {{ preview.hostName }}
    - preview image +
    {{ mainPreview.hostName }}
    {{ mainPreview.twitterTitle }}
    @@ -44,10 +51,7 @@
    {{ mainPreview.twitterTitle }}
    - preview image +
    {{ mainPreview.hostName }}
    {{ mainPreview.title }}
    diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.scss b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.scss index e0793240fe95..852bb4f2533d 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.scss +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.scss @@ -48,6 +48,7 @@ border-radius: 6.25rem; background: $color-palette-gray-200; padding: $spacing-1; + color: $color-palette-gray-800; } .results-seo-tool__search-title { @@ -110,10 +111,6 @@ color: $color-alert-green; } -.results-seo-tool__preview-image { - width: 100%; -} - .results-seo-tool__preview { margin-top: $spacing-1; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts index 41ab80ba5857..b4a3fdf4f455 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts @@ -352,4 +352,24 @@ describe('DotResultsSeoToolComponent', () => { '
    Meta Tags for SEO: A Simple Guide for Beginners. ' ); }); + + it('should display the default icon when noFavicon is true', () => { + const imageElement = spectator.query(byTestId('favicon-image')); + spectator.dispatchFakeEvent(imageElement, 'error'); + spectator.detectComponentChanges(); + + const defaultIcon = spectator.query(byTestId('favicon-default')); + expect(defaultIcon).toBeTruthy(); + expect(defaultIcon.querySelector('.pi-globe')).toBeTruthy(); + }); + + it('should display the favicon image when noFavicon is false', () => { + spectator.component.seoOGTags.favicon = 'favicon-image-url.png'; + + spectator.detectComponentChanges(); + + const faviconImage = spectator.query(byTestId('favicon-image')); + expect(faviconImage).toBeTruthy(); + expect(faviconImage.getAttribute('src')).toBe('favicon-image-url.png'); + }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.ts index 977a991a91f5..ed31c88c7235 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/dot-results-seo-tool.component.ts @@ -29,6 +29,7 @@ import { } from '../../../content/services/dot-edit-content-html/models/meta-tags-model'; import { DotSeoMetaTagsService } from '../../../content/services/html/dot-seo-meta-tags.service'; import { DotSelectSeoToolComponent } from '../dot-select-seo-tool/dot-select-seo-tool.component'; +import { DotSeoImagePreviewComponent } from '../dot-seo-image-preview/dot-seo-image-preview.component'; @Component({ selector: 'dot-results-seo-tool', @@ -46,7 +47,8 @@ import { DotSelectSeoToolComponent } from '../dot-select-seo-tool/dot-select-seo AsyncPipe, DotMessagePipe, DotPipesModule, - DotSelectSeoToolComponent + DotSelectSeoToolComponent, + DotSeoImagePreviewComponent ], providers: [DotSeoMetaTagsService], templateUrl: './dot-results-seo-tool.component.html', @@ -65,6 +67,7 @@ export class DotResultsSeoToolComponent implements OnInit, OnChanges { allPreview: MetaTagsPreview[]; mainPreview: MetaTagsPreview; seoMediaTypes = SEO_MEDIA_TYPES; + noFavicon = false; ngOnInit() { const title = @@ -93,7 +96,11 @@ export class DotResultsSeoToolComponent implements OnInit, OnChanges { this.seoOGTags['twitter:description']?.slice( 0, SEO_LIMITS.MAX_TWITTER_DESCRIPTION_LENGTH - ) ?? this.seoOGTags['og:description'], + ) ?? + this.seoOGTags['og:description']?.slice( + 0, + SEO_LIMITS.MAX_TWITTER_DESCRIPTION_LENGTH + ), twitterImage: this.seoOGTags['twitter:image'] }, { @@ -117,4 +124,8 @@ export class DotResultsSeoToolComponent implements OnInit, OnChanges { }) ); } + + onFaviconError(): void { + this.noFavicon = true; + } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/mocks.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/mocks.ts index 082357e84a94..3234634ae4cf 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/mocks.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-results-seo-tool/mocks.ts @@ -297,6 +297,12 @@ const seoOGTagsResultOgMock = [ keyIcon: 'pi-exclamation-triangle', keyColor: 'results-seo-tool__result-icon--alert-red', items: [ + { + message: + 'twitter:image meta tag found, with an appropriate sized image!', + color: 'results-seo-tool__result-icon--alert-green', + itemIcon: 'pi-check' + }, { message: 'twitter:image meta tag not found!', color: 'results-seo-tool__result-icon--alert-red', diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.html new file mode 100644 index 000000000000..e3223e2b6054 --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.html @@ -0,0 +1,15 @@ +
    + +
    {{ 'seo.rules.media.preview.not.defined' | dm }}
    +
    + + preview image + diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.scss b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.scss new file mode 100644 index 000000000000..5c4709cf1020 --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.scss @@ -0,0 +1,30 @@ +@use "variables" as *; + +$image-width: 45.61706rem; +$image-height: 23.875rem; + +.dot-seo-image-preview__image { + width: $image-width; + height: $image-height; + object-fit: contain; +} + +.dot-seo-image-preview__default { + width: $image-width; + height: $image-height; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + color: $black; + background-color: $color-palette-gray-100; +} + +:host ::ng-deep { + .dot-seo-image-preview__default { + .pi { + font-size: $font-size-xxxl; + color: $color-alert-red; + } + } +} diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.spec.ts new file mode 100644 index 000000000000..df31a992a28f --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.spec.ts @@ -0,0 +1,59 @@ +import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator'; + +import { DotMessageService } from '@dotcms/data-access'; +import { MockDotMessageService } from '@dotcms/utils-testing'; + +import { DotSeoImagePreviewComponent } from './dot-seo-image-preview.component'; + +describe('DotSeoImagePreviewComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: DotSeoImagePreviewComponent, + providers: [ + { + provide: DotMessageService, + useValue: new MockDotMessageService({ + 'seo.rules.media.preview.not.defined': 'Social Media Preview Image Not Defined!' + }) + } + ] + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should create', () => { + expect(spectator.component).toBeTruthy(); + }); + + it('should display an error message when noImageAvailable is true', () => { + spectator.component.noImageAvailable = true; + spectator.detectComponentChanges(); + + const errorMessage = spectator.query(byTestId('seo-image-default')); + expect(errorMessage).toBeTruthy(); + }); + + it('should display an image when noImageAvailable is false', () => { + spectator.component.noImageAvailable = false; + spectator.setInput({ + image: 'sample-image-url.jpg' + }); + spectator.detectComponentChanges(); + + const imageElement = spectator.query(byTestId('seo-image-preview')); + expect(imageElement).toBeTruthy(); + expect(imageElement.getAttribute('src')).toBe('sample-image-url.jpg'); + }); + + it('should call onImageError() when the image fails to load', () => { + const imageElement = spectator.query(byTestId('seo-image-preview')); + + spectator.dispatchFakeEvent(imageElement, 'error'); + spectator.detectComponentChanges(); + + expect(spectator.component.noImageAvailable).toBe(true); + }); +}); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.ts new file mode 100644 index 000000000000..f7810bed9499 --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-seo-image-preview/dot-seo-image-preview.component.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { DotMessagePipe } from '@dotcms/ui'; + +@Component({ + selector: 'dot-seo-image-preview', + templateUrl: './dot-seo-image-preview.component.html', + styleUrls: ['./dot-seo-image-preview.component.scss'], + imports: [DotMessagePipe, CommonModule], + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotSeoImagePreviewComponent { + @Input() image: string; + noImageAvailable = false; + + onImageError() { + this.noImageAvailable = true; + } +} diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 2497888a1307..42799c0981e8 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -5437,7 +5437,7 @@ experiments.chart.xAxisLabel.bayesian=Conversion Rate (%) seo.rules.read-more.title=Read More -seo.rules.favicon.not.found=Favicon not found! +seo.rules.favicon.not.found=Favicon not found, but Image Link is not valid! seo.rules.favicon.more.one.found=More than 1 Favicon found! seo.rules.favicon.found=Favicon found! @@ -5446,13 +5446,13 @@ seo.rules.description.more.one.found=More than 1 Meta Description found! seo.rules.description.found.empty=Meta Description found, but is empty! seo.rules.description.greater=Meta Description found, but it has more than 160 characters. seo.rules.description.less=Meta Description found, but it has fewer than 55 characters of content. -seo.rules.description.greater=Meta Description found! +seo.rules.description.found=Meta Description found! seo.rules.og-description.more.one.found=more than 1 og:description meta tag found! seo.rules.og-description.found.empty=og:description meta tag found, but is empty! seo.rules.og-description.not.found=og:description meta tag not found! Showing Meta Description instead. seo.rules.og-description.description.not.found=og:description meta tag, and Meta Description not found! -seo.rules.og-description.greater=og:description meta tag found, but it has more than 160 characters. +seo.rules.og-description.greater=og:description meta tag found, but it has more than 150 characters. seo.rules.og-description.less=og:description meta tag found, but has fewer than 55 characters of content. seo.rules.og-description.found=og:description meta tag with valid content found! @@ -5473,7 +5473,7 @@ seo.rules.og-title.more.one.found=more than 1 og:title meta tag found! seo.rules.og-title.found.empty=og:title meta tag found, but is empty! seo.rules.og-title.greater=og:title meta tag found, but has more than 60 characters. seo.rules.og-title.less=og:title meta tag found, but has fewer than 30 characters of content. -seo.rules.og-title.found=og:title meta tag found, with an appropriate amount of content! +seo.rules.og-title.found=og:title meta tag with valid content found! seo.rules.og-title.not.found.title=og:title meta tag not found, and HTML Title not found! seo.rules.og-image.not.found=og:image meta tag not found! @@ -5488,6 +5488,7 @@ seo.rules.twitter-card.more.one.found.empty=twitter:card meta tag found, but is seo.rules.twitter-card.found=twitter:card meta tag found! seo.rules.twitter-card-title.not.found=twitter:title meta tag not found! Showing HTML Title instead. +seo.rules.twitter-card-title.title.not.found=twitter:title meta tag not found! Showing HTML Title instead. seo.rules.twitter-card-title.more.one.found=more than 1 twitter:title meta tag found! seo.rules.twitter-card-title.found.empty=twitter:title meta tag found, but is empty! seo.rules.twitter-card.title.greater=twitter:title meta tag found, but has more than 70 characters. @@ -5499,6 +5500,7 @@ seo.rules.twitter-card-description.more.one.found=more than 1 twitter:descriptio seo.rules.twitter-card-description.more.one.found.empty=twitter:description meta tag found, but is empty! seo.rules.twitter-card-description.found=twitter:description meta tag with valid content found! seo.rules.twitter-card-description.greater=twitter:description meta tag found, but has more than 200 characters. +seo.rules.twitter-card-description.less=twitter:description meta tag found, but it has fewer than 30 characters of content. seo.rules.twitter-image.not.found=twitter:image meta tag not found! seo.rules.twitter-image.more.one.found=more than 1 twitter:image meta tag found! @@ -5539,3 +5541,5 @@ seo.rules.read-more.google.image-sizes=Read more about social media tile < seo.rules.media.search.engine=Search Engine Results Page seo.rules.media.preview.tile=Social Media Preview Tile + +seo.rules.media.preview.not.defined=Social Media Preview Image Not Defined! \ No newline at end of file