@elementor/editor-audits 4.3.2 → 4.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +570 -295
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +559 -274
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -13
- package/src/api/page-context-client.ts +8 -5
- package/src/audits/accessibility-statement.ts +39 -0
- package/src/audits/accessibility-widget.ts +41 -0
- package/src/audits/google-consent-mode.ts +4 -7
- package/src/audits/images-inefficient-format.ts +67 -0
- package/src/audits/images-too-large.ts +23 -9
- package/src/audits/page-title.ts +4 -0
- package/src/components/violation-cta-button.tsx +3 -2
- package/src/components/violation-row.tsx +16 -5
- package/src/register-audits.ts +6 -0
- package/src/register-promotions.ts +23 -0
- package/src/runner.ts +9 -5
- package/src/types.ts +10 -1
- package/src/utils/atomic-background-image-sources.ts +56 -0
- package/src/utils/atomic-image-sources.ts +62 -0
- package/src/utils/background-image-sources.ts +32 -0
- package/src/utils/image-alt.ts +3 -1
- package/src/utils/image-like-sources.ts +5 -2
- package/src/utils/image-size-key.ts +8 -0
- package/src/utils/page-attachments.ts +16 -7
- package/src/utils/scan-accessibility-widgets.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-audits",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Elementor Team",
|
|
6
6
|
"homepage": "https://elementor.com/",
|
|
@@ -39,19 +39,21 @@
|
|
|
39
39
|
"dev": "tsup --config=../../tsup.dev.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@elementor/editor": "4.3.
|
|
43
|
-
"@elementor/editor-mcp": "4.3.
|
|
44
|
-
"@elementor/editor-app-bar": "4.3.
|
|
45
|
-
"@elementor/editor-elements": "4.3.
|
|
46
|
-
"@elementor/editor-floating-panels": "4.3.
|
|
47
|
-
"@elementor/editor-notifications": "4.3.
|
|
48
|
-
"@elementor/editor-
|
|
49
|
-
"@elementor/
|
|
50
|
-
"@elementor/
|
|
42
|
+
"@elementor/editor": "4.3.4",
|
|
43
|
+
"@elementor/editor-mcp": "4.3.4",
|
|
44
|
+
"@elementor/editor-app-bar": "4.3.4",
|
|
45
|
+
"@elementor/editor-elements": "4.3.4",
|
|
46
|
+
"@elementor/editor-floating-panels": "4.3.4",
|
|
47
|
+
"@elementor/editor-notifications": "4.3.4",
|
|
48
|
+
"@elementor/editor-props": "4.3.4",
|
|
49
|
+
"@elementor/editor-styles": "4.3.4",
|
|
50
|
+
"@elementor/editor-v1-adapters": "4.3.4",
|
|
51
|
+
"@elementor/events": "4.3.4",
|
|
52
|
+
"@elementor/http-client": "4.3.4",
|
|
51
53
|
"@elementor/icons": "~1.77.0",
|
|
52
|
-
"@elementor/locations": "4.3.
|
|
53
|
-
"@elementor/session": "4.3.
|
|
54
|
-
"@elementor/store": "4.3.
|
|
54
|
+
"@elementor/locations": "4.3.4",
|
|
55
|
+
"@elementor/session": "4.3.4",
|
|
56
|
+
"@elementor/store": "4.3.4",
|
|
55
57
|
"@elementor/ui": "1.37.5"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
@@ -4,13 +4,16 @@ import { type PageContextResponse } from '../types';
|
|
|
4
4
|
import { isNonceInvalidError, refreshAuditsNonce } from '../utils/session-expiration';
|
|
5
5
|
import { getWindowConfig } from '../utils/window-config';
|
|
6
6
|
|
|
7
|
-
export async function fetchPageContext(
|
|
8
|
-
|
|
7
|
+
export async function fetchPageContext(
|
|
8
|
+
documentId: number,
|
|
9
|
+
imageSizeRequests: string[]
|
|
10
|
+
): Promise< PageContextResponse > {
|
|
11
|
+
return requestPageContext( documentId, imageSizeRequests, true );
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
async function requestPageContext(
|
|
12
15
|
documentId: number,
|
|
13
|
-
|
|
16
|
+
imageSizeRequests: string[],
|
|
14
17
|
allowNonceRetry: boolean
|
|
15
18
|
): Promise< PageContextResponse > {
|
|
16
19
|
const { restNamespace, nonce } = getWindowConfig();
|
|
@@ -20,7 +23,7 @@ async function requestPageContext(
|
|
|
20
23
|
const response = await httpService().get< PageContextResponse >( url, {
|
|
21
24
|
params: {
|
|
22
25
|
document_id: documentId,
|
|
23
|
-
|
|
26
|
+
image_size_requests: imageSizeRequests,
|
|
24
27
|
},
|
|
25
28
|
headers: { 'X-WP-Nonce': nonce },
|
|
26
29
|
} );
|
|
@@ -33,6 +36,6 @@ async function requestPageContext(
|
|
|
33
36
|
|
|
34
37
|
await refreshAuditsNonce();
|
|
35
38
|
|
|
36
|
-
return requestPageContext( documentId,
|
|
39
|
+
return requestPageContext( documentId, imageSizeRequests, false );
|
|
37
40
|
}
|
|
38
41
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
|
|
5
|
+
export const audit: Audit = {
|
|
6
|
+
id: 'audits/accessibility-statement',
|
|
7
|
+
title: __( 'Create an accessibility statement', 'elementor' ),
|
|
8
|
+
description: __(
|
|
9
|
+
'An accessibility statement showcases your efforts to create an inclusive online space, highlighting helpful features and a commitment to accessibility.',
|
|
10
|
+
'elementor'
|
|
11
|
+
),
|
|
12
|
+
fixHint: __( 'Use Ally to create and publish an accessibility statement for your site.', 'elementor' ),
|
|
13
|
+
categories: [ 'accessibility' ],
|
|
14
|
+
severity: 'info',
|
|
15
|
+
weight: 1,
|
|
16
|
+
evaluate: ( ctx ) => {
|
|
17
|
+
const isReady = ctx.pageContext.ally_plugin_active;
|
|
18
|
+
|
|
19
|
+
if ( isReady && ctx.pageContext.ally_accessibility_statement_created ) {
|
|
20
|
+
return { status: 'pass' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
status: 'fail',
|
|
25
|
+
violations: [
|
|
26
|
+
{
|
|
27
|
+
auditId: audit.id,
|
|
28
|
+
label: __( 'No accessibility statement has been created for this site.', 'elementor' ),
|
|
29
|
+
externalUrl: isReady
|
|
30
|
+
? ctx.pageContext.ally_accessibility_statement_url
|
|
31
|
+
: ctx.pageContext.ally_plugin_url,
|
|
32
|
+
ctaLabel: isReady ? __( 'Create', 'elementor' ) : undefined,
|
|
33
|
+
secondaryCtaLabel: __( 'Learn more', 'elementor' ),
|
|
34
|
+
secondaryCtaUrl: 'https://go.elementor.com/acc-plg-learn-more',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
import { hasAccessibilityWidgetScript } from '../utils/scan-accessibility-widgets';
|
|
5
|
+
|
|
6
|
+
export const audit: Audit = {
|
|
7
|
+
id: 'audits/accessibility-widget',
|
|
8
|
+
title: __( 'Add an accessibility widget', 'elementor' ),
|
|
9
|
+
description: __( 'Make your site more inclusive by adding an accessibility widget.', 'elementor' ),
|
|
10
|
+
fixHint: __( 'Activate Ally’s accessibility widget to give visitors on-page accessibility tools.', 'elementor' ),
|
|
11
|
+
categories: [ 'accessibility' ],
|
|
12
|
+
severity: 'info',
|
|
13
|
+
weight: 1,
|
|
14
|
+
evaluate: ( ctx ) => {
|
|
15
|
+
if ( ! ctx.pageContext.frontend_url ) {
|
|
16
|
+
return { status: 'skipped', reason: __( 'Page is not published.', 'elementor' ) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if ( ! ctx.renderedHtml ) {
|
|
20
|
+
return { status: 'skipped', reason: __( 'Could not fetch the published page.', 'elementor' ) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ( hasAccessibilityWidgetScript( ctx.renderedHtml ) ) {
|
|
24
|
+
return { status: 'pass' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isReady = ctx.pageContext.ally_plugin_active;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
status: 'fail',
|
|
31
|
+
violations: [
|
|
32
|
+
{
|
|
33
|
+
auditId: audit.id,
|
|
34
|
+
label: __( 'No accessibility widget is active on this site.', 'elementor' ),
|
|
35
|
+
externalUrl: isReady ? ctx.pageContext.ally_widget_settings_url : ctx.pageContext.ally_plugin_url,
|
|
36
|
+
ctaLabel: __( 'Activate', 'elementor' ),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { __ } from '@wordpress/i18n';
|
|
2
2
|
|
|
3
3
|
import { type Audit } from '../types';
|
|
4
|
-
import { fetchRenderedHtml } from '../utils/fetch-rendered-html';
|
|
5
4
|
import { hasConsentDefaultCall, hasGoogleTracking } from '../utils/scan-consent-signals';
|
|
6
5
|
|
|
7
6
|
export const audit: Audit = {
|
|
@@ -18,22 +17,20 @@ export const audit: Audit = {
|
|
|
18
17
|
categories: [ 'compliance' ],
|
|
19
18
|
severity: 'warning',
|
|
20
19
|
weight: 1,
|
|
21
|
-
evaluate:
|
|
20
|
+
evaluate: ( ctx ) => {
|
|
22
21
|
if ( ! ctx.pageContext.frontend_url ) {
|
|
23
22
|
return { status: 'skipped', reason: __( 'Page is not published.', 'elementor' ) };
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if ( ! html ) {
|
|
25
|
+
if ( ! ctx.renderedHtml ) {
|
|
29
26
|
return { status: 'skipped', reason: __( 'Could not fetch the published page.', 'elementor' ) };
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
if ( ! hasGoogleTracking(
|
|
29
|
+
if ( ! hasGoogleTracking( ctx.renderedHtml ) ) {
|
|
33
30
|
return { status: 'skipped', reason: __( 'No Google tracking product detected.', 'elementor' ) };
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
if ( hasConsentDefaultCall(
|
|
33
|
+
if ( hasConsentDefaultCall( ctx.renderedHtml ) ) {
|
|
37
34
|
return { status: 'pass' };
|
|
38
35
|
}
|
|
39
36
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit, type AuditViolation, type ElementSnapshotNode } from '../types';
|
|
4
|
+
import { walkAtomicBackgroundImageSources } from '../utils/atomic-background-image-sources';
|
|
5
|
+
import { walkAtomicImageSources } from '../utils/atomic-image-sources';
|
|
6
|
+
import { walkBackgroundImageSources } from '../utils/background-image-sources';
|
|
7
|
+
import { type ImageLikeMedia, walkImageLikeSources } from '../utils/image-like-sources';
|
|
8
|
+
import { buildImageSizeKey } from '../utils/image-size-key';
|
|
9
|
+
|
|
10
|
+
const LEGACY_RASTER_MIME_TYPES = new Set( [ 'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff' ] );
|
|
11
|
+
|
|
12
|
+
export const audit: Audit = {
|
|
13
|
+
id: 'audits/images-inefficient-format',
|
|
14
|
+
title: __( 'Inefficient image format', 'elementor' ),
|
|
15
|
+
description: __( "Image isn't served in a modern, efficient format like WebP or AVIF.", 'elementor' ),
|
|
16
|
+
fixHint: __( 'Convert the image to a modern format like WebP or AVIF, or enable image optimization.', 'elementor' ),
|
|
17
|
+
categories: [ 'performance' ],
|
|
18
|
+
severity: 'info',
|
|
19
|
+
weight: 1,
|
|
20
|
+
evaluate: ( ctx ) => {
|
|
21
|
+
const offendingElementIds = new Set< string >();
|
|
22
|
+
let hasAnyImage = false;
|
|
23
|
+
let inefficientImageFormatCount = 0;
|
|
24
|
+
|
|
25
|
+
const evaluateSource = ( node: ElementSnapshotNode, media: ImageLikeMedia ) => {
|
|
26
|
+
if ( media.id || media.url ) {
|
|
27
|
+
hasAnyImage = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const id = media.id;
|
|
31
|
+
|
|
32
|
+
if ( ! id ) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const mime = ctx.pageContext.image_sizes[ buildImageSizeKey( { id, size: media.size } ) ]?.mime;
|
|
37
|
+
|
|
38
|
+
if ( ! mime || ! LEGACY_RASTER_MIME_TYPES.has( mime ) ) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
inefficientImageFormatCount++;
|
|
43
|
+
offendingElementIds.add( node.id );
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
47
|
+
walkBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
48
|
+
walkAtomicImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
49
|
+
walkAtomicBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
50
|
+
|
|
51
|
+
if ( ! hasAnyImage ) {
|
|
52
|
+
return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const violations: AuditViolation[] = Array.from( offendingElementIds ).map( ( elementId ) => ( {
|
|
56
|
+
auditId: audit.id,
|
|
57
|
+
elementId,
|
|
58
|
+
targetHint: 'element-settings' as const,
|
|
59
|
+
label: __( "Image isn't served in a modern format like WebP or AVIF.", 'elementor' ),
|
|
60
|
+
externalUrl: ctx.pageContext.image_optimization_plugin_url,
|
|
61
|
+
} ) );
|
|
62
|
+
|
|
63
|
+
return violations.length === 0
|
|
64
|
+
? { status: 'pass' }
|
|
65
|
+
: { status: 'fail', violations, metadata: { inefficientImageFormatCount } };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { __, sprintf } from '@wordpress/i18n';
|
|
2
2
|
|
|
3
|
-
import { type Audit, type AuditViolation } from '../types';
|
|
4
|
-
import {
|
|
3
|
+
import { type Audit, type AuditViolation, type ElementSnapshotNode } from '../types';
|
|
4
|
+
import { walkAtomicBackgroundImageSources } from '../utils/atomic-background-image-sources';
|
|
5
|
+
import { walkAtomicImageSources } from '../utils/atomic-image-sources';
|
|
6
|
+
import { walkBackgroundImageSources } from '../utils/background-image-sources';
|
|
7
|
+
import { type ImageLikeMedia, walkImageLikeSources } from '../utils/image-like-sources';
|
|
8
|
+
import { buildImageSizeKey } from '../utils/image-size-key';
|
|
5
9
|
|
|
6
10
|
const SIZE_THRESHOLD_BYTES = 500 * 1024;
|
|
7
11
|
const BYTES_PER_KB = 1024;
|
|
@@ -15,21 +19,22 @@ export const audit: Audit = {
|
|
|
15
19
|
severity: 'warning',
|
|
16
20
|
weight: 7,
|
|
17
21
|
evaluate: ( ctx ) => {
|
|
18
|
-
if ( ! hasPageImages( ctx.elements.tree ) ) {
|
|
19
|
-
return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
22
|
const widgetMaxKb = new Map< string, number >();
|
|
23
23
|
let oversizedImageCount = 0;
|
|
24
|
+
let hasAnyImage = false;
|
|
25
|
+
|
|
26
|
+
const evaluateSource = ( node: ElementSnapshotNode, media: ImageLikeMedia ) => {
|
|
27
|
+
if ( media.id || media.url ) {
|
|
28
|
+
hasAnyImage = true;
|
|
29
|
+
}
|
|
24
30
|
|
|
25
|
-
walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => {
|
|
26
31
|
const id = media.id;
|
|
27
32
|
|
|
28
33
|
if ( ! id ) {
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
const size = ctx.pageContext.image_sizes[ id ];
|
|
37
|
+
const size = ctx.pageContext.image_sizes[ buildImageSizeKey( { id, size: media.size } ) ];
|
|
33
38
|
|
|
34
39
|
if ( ! size || size.filesize_bytes <= SIZE_THRESHOLD_BYTES ) {
|
|
35
40
|
return;
|
|
@@ -40,7 +45,16 @@ export const audit: Audit = {
|
|
|
40
45
|
const kb = Math.round( size.filesize_bytes / BYTES_PER_KB );
|
|
41
46
|
const currentMax = widgetMaxKb.get( node.id ) ?? 0;
|
|
42
47
|
widgetMaxKb.set( node.id, Math.max( currentMax, kb ) );
|
|
43
|
-
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
51
|
+
walkBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
52
|
+
walkAtomicImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
53
|
+
walkAtomicBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
|
|
54
|
+
|
|
55
|
+
if ( ! hasAnyImage ) {
|
|
56
|
+
return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
|
|
57
|
+
}
|
|
44
58
|
|
|
45
59
|
const violations: AuditViolation[] = Array.from( widgetMaxKb.entries() ).map( ( [ elementId, kb ] ) => ( {
|
|
46
60
|
auditId: audit.id,
|
package/src/audits/page-title.ts
CHANGED
|
@@ -24,6 +24,10 @@ export const audit: Audit = {
|
|
|
24
24
|
label: __( 'Page has no title.', 'elementor' ),
|
|
25
25
|
targetHint: 'page-settings',
|
|
26
26
|
angieFix: true,
|
|
27
|
+
angiePrompt: __(
|
|
28
|
+
"Based on this page's contents, generate a title for this page.",
|
|
29
|
+
'elementor'
|
|
30
|
+
),
|
|
27
31
|
},
|
|
28
32
|
],
|
|
29
33
|
};
|
|
@@ -4,9 +4,10 @@ import { Button } from '@elementor/ui';
|
|
|
4
4
|
type Props = {
|
|
5
5
|
ctaLabel: string;
|
|
6
6
|
externalUrl: string;
|
|
7
|
+
variant?: 'outlined' | 'text';
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
|
|
10
|
+
export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined' }: Props ) {
|
|
10
11
|
const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
|
|
11
12
|
event.stopPropagation();
|
|
12
13
|
event.preventDefault();
|
|
@@ -15,7 +16,7 @@ export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
|
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
return (
|
|
18
|
-
<Button variant=
|
|
19
|
+
<Button variant={ variant } color="secondary" size="small" onClick={ handleClick }>
|
|
19
20
|
{ ctaLabel }
|
|
20
21
|
</Button>
|
|
21
22
|
);
|
|
@@ -159,13 +159,24 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
|
|
|
159
159
|
) }
|
|
160
160
|
</Box>
|
|
161
161
|
{ violation.angieFix && (
|
|
162
|
-
<FixViolationWithAngie
|
|
162
|
+
<FixViolationWithAngie
|
|
163
|
+
prompt={ violation.angiePrompt ?? buildAngiePrompt( rowLabel ) }
|
|
164
|
+
/>
|
|
163
165
|
) }
|
|
164
166
|
{ violation.ctaLabel && violation.externalUrl ? (
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
<Box sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
|
|
168
|
+
<ViolationCtaButton
|
|
169
|
+
ctaLabel={ violation.ctaLabel }
|
|
170
|
+
externalUrl={ violation.externalUrl }
|
|
171
|
+
/>
|
|
172
|
+
{ violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
|
|
173
|
+
<ViolationCtaButton
|
|
174
|
+
ctaLabel={ violation.secondaryCtaLabel }
|
|
175
|
+
externalUrl={ violation.secondaryCtaUrl }
|
|
176
|
+
variant="text"
|
|
177
|
+
/>
|
|
178
|
+
) }
|
|
179
|
+
</Box>
|
|
169
180
|
) : (
|
|
170
181
|
<EyeIcon
|
|
171
182
|
className="violation-hover-icon"
|
package/src/register-audits.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as accessibilityPolicy from './audits/accessibility-policy';
|
|
2
|
+
import * as accessibilityStatement from './audits/accessibility-statement';
|
|
3
|
+
import * as accessibilityWidget from './audits/accessibility-widget';
|
|
2
4
|
import * as cookiePolicy from './audits/cookie-policy';
|
|
3
5
|
import * as deepNesting from './audits/deep-nesting';
|
|
4
6
|
import * as defaultDesignSystem from './audits/default-design-system';
|
|
@@ -7,6 +9,7 @@ import * as googleConsentMode from './audits/google-consent-mode';
|
|
|
7
9
|
import * as headingStructure from './audits/heading-structure';
|
|
8
10
|
import * as hiddenElements from './audits/hidden-elements';
|
|
9
11
|
import * as imagesAltText from './audits/images-alt-text';
|
|
12
|
+
import * as imagesInefficientFormat from './audits/images-inefficient-format';
|
|
10
13
|
import * as imagesTooLarge from './audits/images-too-large';
|
|
11
14
|
import * as nestedBoxedContainers from './audits/nested-boxed-containers';
|
|
12
15
|
import * as pageExcerpt from './audits/page-excerpt';
|
|
@@ -43,11 +46,14 @@ const AUDITS: Audit[] = [
|
|
|
43
46
|
headingStructure.audit,
|
|
44
47
|
imagesAltText.audit,
|
|
45
48
|
imagesTooLarge.audit,
|
|
49
|
+
imagesInefficientFormat.audit,
|
|
46
50
|
|
|
47
51
|
robotsNoindex.audit,
|
|
48
52
|
|
|
49
53
|
privacyPolicy.audit,
|
|
50
54
|
accessibilityPolicy.audit,
|
|
55
|
+
accessibilityStatement.audit,
|
|
56
|
+
accessibilityWidget.audit,
|
|
51
57
|
cookiePolicy.audit,
|
|
52
58
|
scanForCookies.audit,
|
|
53
59
|
googleConsentMode.audit,
|
|
@@ -80,4 +80,27 @@ export const PROMOTIONS: PromotionConfig[] = [
|
|
|
80
80
|
},
|
|
81
81
|
getCtaUrl: firstFailExternalUrl,
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
auditId: 'audits/images-inefficient-format',
|
|
85
|
+
icon: ShieldHalfFilledIcon,
|
|
86
|
+
ctaLabel: __( 'Optimize all', 'elementor' ),
|
|
87
|
+
formatSubtitle: ( run ) => {
|
|
88
|
+
if ( run.result.status !== 'fail' ) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const count = run.result.metadata?.inefficientImageFormatCount ?? 0;
|
|
93
|
+
|
|
94
|
+
if ( count === 0 ) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return sprintf(
|
|
99
|
+
/* translators: %d: number of images served in an inefficient format. */
|
|
100
|
+
_n( '%d image', '%d images', count, 'elementor' ),
|
|
101
|
+
count
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
getCtaUrl: firstFailExternalUrl,
|
|
105
|
+
},
|
|
83
106
|
];
|
package/src/runner.ts
CHANGED
|
@@ -12,19 +12,23 @@ import {
|
|
|
12
12
|
type PageAuditReport,
|
|
13
13
|
} from './types';
|
|
14
14
|
import { computeReport } from './utils/compute-report';
|
|
15
|
-
import {
|
|
15
|
+
import { fetchRenderedHtml } from './utils/fetch-rendered-html';
|
|
16
|
+
import { extractImageSizeRequests } from './utils/page-attachments';
|
|
16
17
|
import { readKitSnapshot } from './utils/read-kit-snapshot';
|
|
17
18
|
import { buildSnapshotTree } from './utils/v1-snapshot';
|
|
18
19
|
|
|
19
20
|
export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
|
|
20
21
|
const tree = buildSnapshotTree( getElements() );
|
|
21
|
-
const
|
|
22
|
-
const pageContext = await fetchPageContext( documentId,
|
|
22
|
+
const imageSizeRequests = extractImageSizeRequests( tree );
|
|
23
|
+
const pageContext = await fetchPageContext( documentId, imageSizeRequests );
|
|
23
24
|
|
|
24
25
|
const elements: ElementsModelSnapshot = { documentId, tree };
|
|
25
|
-
const kit: KitSnapshot = await
|
|
26
|
+
const [ kit, renderedHtml ]: [ KitSnapshot, string | null ] = await Promise.all( [
|
|
27
|
+
readKitSnapshot( pageContext.kit_id ),
|
|
28
|
+
fetchRenderedHtml( pageContext.frontend_url ),
|
|
29
|
+
] );
|
|
26
30
|
|
|
27
|
-
const ctx: AuditContext = { documentId, elements, pageContext, kit };
|
|
31
|
+
const ctx: AuditContext = { documentId, elements, pageContext, kit, renderedHtml };
|
|
28
32
|
const registered = getRegisteredAudits();
|
|
29
33
|
|
|
30
34
|
const auditResults: AuditRun[] = await Promise.all(
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type AuditRun = {
|
|
|
23
23
|
export type AuditFailMetadata = {
|
|
24
24
|
missingAltImageCount?: number;
|
|
25
25
|
oversizedImageCount?: number;
|
|
26
|
+
inefficientImageFormatCount?: number;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export type AuditResult =
|
|
@@ -38,15 +39,19 @@ export type AuditViolation = {
|
|
|
38
39
|
label: string;
|
|
39
40
|
detail?: string;
|
|
40
41
|
angieFix?: boolean;
|
|
42
|
+
angiePrompt?: string;
|
|
41
43
|
ctaLabel?: string;
|
|
44
|
+
secondaryCtaLabel?: string;
|
|
45
|
+
secondaryCtaUrl?: string;
|
|
42
46
|
};
|
|
43
47
|
|
|
44
48
|
export type PageContextResponse = {
|
|
45
49
|
post_title: string | null;
|
|
46
50
|
post_excerpt: string | null;
|
|
47
51
|
featured_image_id: number | null;
|
|
52
|
+
/** Keyed by the composite `buildImageSizeKey` result (`"{attachmentId}:{size}"`), since one attachment can resolve to different files at different sizes. */
|
|
48
53
|
image_sizes: Record<
|
|
49
|
-
|
|
54
|
+
string,
|
|
50
55
|
{
|
|
51
56
|
width: number;
|
|
52
57
|
height: number;
|
|
@@ -64,6 +69,9 @@ export type PageContextResponse = {
|
|
|
64
69
|
privacy_settings_url: string;
|
|
65
70
|
ally_plugin_active: boolean;
|
|
66
71
|
ally_plugin_url: string;
|
|
72
|
+
ally_accessibility_statement_created: boolean;
|
|
73
|
+
ally_accessibility_statement_url: string;
|
|
74
|
+
ally_widget_settings_url: string;
|
|
67
75
|
cookiez_plugin_active: boolean;
|
|
68
76
|
cookiez_plugin_url: string;
|
|
69
77
|
cookiez_plugin_installed: boolean;
|
|
@@ -107,6 +115,7 @@ export type AuditContext = {
|
|
|
107
115
|
elements: ElementsModelSnapshot;
|
|
108
116
|
pageContext: PageContextResponse;
|
|
109
117
|
kit: KitSnapshot;
|
|
118
|
+
renderedHtml: string | null;
|
|
110
119
|
};
|
|
111
120
|
|
|
112
121
|
export type PageAuditReport = {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getElementStyles } from '@elementor/editor-elements';
|
|
2
|
+
import {
|
|
3
|
+
backgroundImageOverlayPropTypeUtil,
|
|
4
|
+
backgroundOverlayPropTypeUtil,
|
|
5
|
+
backgroundPropTypeUtil,
|
|
6
|
+
} from '@elementor/editor-props';
|
|
7
|
+
import { type StyleDefinition } from '@elementor/editor-styles';
|
|
8
|
+
|
|
9
|
+
import { type ElementSnapshotNode } from '../types';
|
|
10
|
+
import { extractAtomicImageMedia } from './atomic-image-sources';
|
|
11
|
+
import { type ImageLikeMedia } from './image-like-sources';
|
|
12
|
+
import { walkElements } from './walk';
|
|
13
|
+
|
|
14
|
+
export type AtomicBackgroundImageSourceVisit = {
|
|
15
|
+
node: ElementSnapshotNode;
|
|
16
|
+
media: ImageLikeMedia;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function walkAtomicBackgroundImageSources(
|
|
20
|
+
tree: ElementSnapshotNode[],
|
|
21
|
+
visit: ( source: AtomicBackgroundImageSourceVisit ) => void
|
|
22
|
+
): void {
|
|
23
|
+
walkElements( tree, ( node ) => {
|
|
24
|
+
const styles = getElementStyles( node.id );
|
|
25
|
+
|
|
26
|
+
if ( ! styles ) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for ( const media of extractBackgroundImageMedia( styles ) ) {
|
|
31
|
+
visit( { node, media } );
|
|
32
|
+
}
|
|
33
|
+
} );
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractBackgroundImageMedia( styles: Record< string, StyleDefinition > ): ImageLikeMedia[] {
|
|
37
|
+
const sources: ImageLikeMedia[] = [];
|
|
38
|
+
|
|
39
|
+
for ( const style of Object.values( styles ) ) {
|
|
40
|
+
for ( const variant of style.variants ) {
|
|
41
|
+
const background = backgroundPropTypeUtil.extract( variant.props.background );
|
|
42
|
+
const overlays = backgroundOverlayPropTypeUtil.extract( background?.[ 'background-overlay' ] ) ?? [];
|
|
43
|
+
|
|
44
|
+
for ( const overlay of overlays ) {
|
|
45
|
+
const imageOverlay = backgroundImageOverlayPropTypeUtil.extract( overlay );
|
|
46
|
+
const media = extractAtomicImageMedia( imageOverlay?.image );
|
|
47
|
+
|
|
48
|
+
if ( media ) {
|
|
49
|
+
sources.push( media );
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return sources;
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
imageAttachmentIdPropType,
|
|
3
|
+
imagePropTypeUtil,
|
|
4
|
+
imageSrcPropTypeUtil,
|
|
5
|
+
stringPropTypeUtil,
|
|
6
|
+
urlPropTypeUtil,
|
|
7
|
+
} from '@elementor/editor-props';
|
|
8
|
+
|
|
9
|
+
import { type ElementSnapshotNode } from '../types';
|
|
10
|
+
import { type ImageLikeMedia } from './image-like-sources';
|
|
11
|
+
import { walkElements } from './walk';
|
|
12
|
+
|
|
13
|
+
const ATOMIC_IMAGE_PROP_KEYS = [ 'image', 'poster' ] as const;
|
|
14
|
+
|
|
15
|
+
export type AtomicImageSourceVisit = {
|
|
16
|
+
node: ElementSnapshotNode;
|
|
17
|
+
media: ImageLikeMedia;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function walkAtomicImageSources(
|
|
21
|
+
tree: ElementSnapshotNode[],
|
|
22
|
+
visit: ( source: AtomicImageSourceVisit ) => void
|
|
23
|
+
): void {
|
|
24
|
+
walkElements( tree, ( node ) => {
|
|
25
|
+
if ( node.elType !== 'widget' ) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for ( const key of ATOMIC_IMAGE_PROP_KEYS ) {
|
|
30
|
+
const media = extractAtomicImageMedia( node.settings[ key ] );
|
|
31
|
+
|
|
32
|
+
if ( media ) {
|
|
33
|
+
visit( { node, media } );
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} );
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function extractAtomicImageMedia( imageProp: unknown ): ImageLikeMedia | null {
|
|
40
|
+
const image = imagePropTypeUtil.extract( imageProp );
|
|
41
|
+
|
|
42
|
+
if ( ! image ) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const src = imageSrcPropTypeUtil.extract( image.src );
|
|
47
|
+
|
|
48
|
+
if ( ! src ) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const id = imageAttachmentIdPropType.extract( src.id ) ?? undefined;
|
|
53
|
+
const url = urlPropTypeUtil.extract( src.url ) ?? undefined;
|
|
54
|
+
|
|
55
|
+
if ( ! id && ! url ) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const size = stringPropTypeUtil.extract( image.size ) ?? undefined;
|
|
60
|
+
|
|
61
|
+
return { id, url, size };
|
|
62
|
+
}
|