Skip to content

Commit 69719f6

Browse files
authored
Merge pull request #6446 from layer5io/leecalcote/perf/cls
[SEO] Improve site performance with reduction of layout shifts (CLS)
2 parents baf3ece + f3ceb65 commit 69719f6

12 files changed

Lines changed: 227 additions & 75 deletions

File tree

.husky/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/bin/sh
22
. "$(dirname "$0")/_/husky.sh"
33

4-
npm run checklint
4+
npm run lint

root-wrapper.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ import CTA_FullWidth from "./src/components/Call-To-Actions/CTA_FullWidth";
66
import CTA_Bottom from "./src/components/Call-To-Actions/CTA_Bottom";
77
import { ContextWrapper } from "./context-wrapper";
88

9+
// Custom image component for better CLS scores
10+
const OptimizedImage = props => {
11+
return (
12+
<div style={{ width: "100%", height: "auto", aspectRatio: props.aspectRatio || "16/9", overflow: "hidden" }}>
13+
<img
14+
{...props}
15+
width={props.width || "100%"}
16+
height={props.height || "auto"}
17+
style={{
18+
objectFit: props.objectFit || "cover",
19+
aspectRatio: props.aspectRatio || "16/9",
20+
...props.style
21+
}}
22+
loading="lazy"
23+
alt={props.alt || "Blog content image"}
24+
/>
25+
</div>
26+
);
27+
};
28+
929
const components = {
1030
pre: ({ children: { props } }) => {
1131
if (props.mdxType === "code") {
@@ -20,6 +40,7 @@ const components = {
2040
);
2141
}
2242
},
43+
img: OptimizedImage,
2344
CTA_ImageOnly,
2445
CTA_FullWidth,
2546
CTA_Bottom

src/components/Card/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ const Card = ({ frontmatter, fields }) => {
1313
return (
1414
<CardWrapper fixed={!!frontmatter.abstract}>
1515
<div className="post-block">
16-
<div className="post-thumb-block">
16+
<div className="post-thumb-block" style={{ aspectRatio: "16/9", minHeight: "200px" }}>
1717
<Image
1818
{...((isDark && frontmatter.darkthumbnail && frontmatter.darkthumbnail.publicURL !== frontmatter.thumbnail.publicURL)
1919
? frontmatter.darkthumbnail : frontmatter.thumbnail)}
20-
imgStyle={{ objectFit: "contain" }}
20+
imgStyle={{ objectFit: "cover" }}
2121
alt={frontmatter.title}
2222
/>
2323
</div>

src/components/Inline-quotes/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,13 @@ const InlineQuotes = ({ person, title, quote,image }) => {
111111
{(image || person || title) && <hr />}
112112
{
113113
image &&
114-
<img src={image}></img>
114+
<img
115+
src={image}
116+
alt={`${person || "Quote author"}`}
117+
width="96"
118+
height="96"
119+
style={{ objectFit: "cover" }}
120+
/>
115121
}
116122
<div className="quote-source">
117123
<h5>{person}</h5>

src/components/Related-Posts/index.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import React from "react";
32
import { Link } from "gatsby";
43
import { IoIosArrowRoundForward } from "@react-icons/all-files/io/IoIosArrowRoundForward";
@@ -32,13 +31,13 @@ const RelatedPosts = props => {
3231
{
3332
postType === "blogs" ? relatedPosts.map(({ post }) => {
3433
return (
35-
<Col className="cardCol" $xs={12} key={post.fields.slug}>
34+
<Col className="cardCol" $xs={12} key={post.fields.slug} style={{ minHeight: "320px" }}>
3635
<Card frontmatter={post.frontmatter} fields={post.fields}/>
3736
</Col>
3837
);
3938
}) : relatedPosts.map((post) => {
4039
return (
41-
<Col className="cardCol" $xs={12} key={post.fields.slug}>
40+
<Col className="cardCol" $xs={12} key={post.fields.slug} style={{ minHeight: "320px" }}>
4241
<Card frontmatter={post.frontmatter} fields={post.fields}/>
4342
</Col>
4443
);

src/components/image.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
import React from "react";
22
import { GatsbyImage } from "gatsby-plugin-image";
33

4-
const Image = ({ childImageSharp, extension, publicURL, alt, ...rest }) => {
4+
const Image = ({ childImageSharp, extension, publicURL, alt, imgStyle, ...rest }) => {
55

66
if (!childImageSharp && extension === "svg") {
77
return (
8-
<div className="old-gatsby-image-wrapper">
9-
<img src={publicURL} alt={alt} />
8+
<div className="old-gatsby-image-wrapper" style={{ width: "100%", height: "auto", aspectRatio: "16/9" }}>
9+
<img
10+
src={publicURL}
11+
alt={alt || "Blog image"}
12+
width="100%"
13+
height="auto"
14+
style={{
15+
aspectRatio: "16/9",
16+
objectFit: imgStyle?.objectFit || "cover",
17+
...imgStyle
18+
}}
19+
/>
1020
</div>
1121
);
1222
}
1323

1424
return <GatsbyImage
15-
key={publicURL} image={childImageSharp?.gatsbyImageData} {...rest} alt={alt}/>;
25+
key={publicURL}
26+
image={childImageSharp?.gatsbyImageData}
27+
alt={alt || "Blog image"}
28+
{...rest}
29+
/>;
1630
};
1731

1832
export default Image;

src/reusecore/PageHeader/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const PageHeader = ({ category, title, img, feedlink, subtitle, author, thumbnai
4545
return (
4646
<PageHeaderWrapper>
4747
<div className="page-header">
48-
{ thumbnail && <div className="feature-image">
48+
{ thumbnail && <div className="feature-image" style={{ aspectRatio: "16/9", minHeight: "250px" }}>
4949
<Image {...thumbnail} imgStyle={{ objectFit: "contain" }} alt={title}/>
5050
</div>}
5151
<h1 className="page-title" >{title} <sup className="supscript">{superscript}</sup>{ img && feedlink && (<a href= {feedlink} target="_blank" rel="noreferrer"> <img src={img} alt="RSS Feed"/> </a>) } </h1>

src/sections/Blog/Blog-single/author.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ const AboutTheAuthor = (props) => {
1818
<div className="author-info-section">
1919
<div className="authors-info-container">
2020
<h3>About the Author</h3>
21-
<div className="authors-head-shot">
21+
<div className="authors-head-shot" style={{ aspectRatio: "1/1", width: "150px", height: "150px", overflow: "hidden" }}>
2222
<Link to={`${authorInformation?.fields?.slug}`}>
23-
<Image {...authorInformation?.frontmatter?.image_path} imgStyle={{ objectFit: "cover" }} alt={authorInformation.frontmatter?.name} className="authors-image" />
23+
<Image {...authorInformation?.frontmatter?.image_path} imgStyle={{ objectFit: "cover", width: "100%", height: "100%" }} alt={authorInformation.frontmatter?.name} className="authors-image" />
2424
</Link>
2525
</div>
2626
<h4>{authorInformation.frontmatter?.name}</h4>

src/sections/Home/Banner-4/banner4.style.js

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,48 +36,77 @@ const Banner1SectionWrapper = styled.section`
3636
}
3737
}
3838
}
39-
.embedVideo {
39+
40+
/* Video container with fixed aspect ratio to prevent layout shifts */
41+
.video-wrapper {
4042
position: relative;
41-
min-width:25%;
42-
max-width:100%;
43+
width: 90%;
44+
margin: auto;
45+
height: 0;
46+
padding-bottom: 50.625%; /* 16:9 aspect ratio (9/16 = 0.5625) */
47+
overflow: hidden;
48+
border-radius: 8px;
49+
background: ${props => props.theme.DarkTheme ? "rgba(0, 0, 0, 0.2)" : "rgba(0, 0, 0, 0.05)"};
50+
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
51+
52+
/* Loading placeholder */
53+
&::before {
54+
content: "Loading video...";
55+
position: absolute;
56+
top: 50%;
57+
left: 50%;
58+
transform: translate(-50%, -50%);
59+
color: ${props => props.theme.DarkTheme ? "#ffffff80" : "#00000080"};
60+
z-index: 0; /* Lower z-index so it doesn't block interactions */
61+
opacity: 0.7;
62+
transition: opacity 0.3s ease, visibility 0.3s ease;
63+
}
64+
65+
&.video-loaded::before {
66+
opacity: 0;
67+
visibility: hidden; /* Completely hide the element */
68+
}
69+
}
70+
71+
.embedVideo {
72+
position: absolute !important;
73+
top: 0;
74+
left: 0;
75+
width: 100% !important;
76+
height: 100% !important;
4377
object-fit: cover;
44-
/* height: 44vw !important;
45-
border-radius: 2.5%;
46-
transition: 0.2s ease-in-out;
47-
box-shadow: 0px 3px 20px 4px rgba(0, 179, 159, 0.5); */
78+
z-index: 1; /* Ensure video player is above the loading message */
4879
4980
.react-player__preview {
50-
border-radius: 1.5%;
81+
border-radius: 8px;
82+
width: 100%;
83+
height: 100%;
84+
object-fit: cover;
85+
background-size: cover;
86+
z-index: 2; /* Ensure preview is clickable */
5187
}
5288
5389
.react-player__play-icon {
54-
transform: scale(3, 3);
90+
transform: scale(3, 3);
5591
}
5692
5793
iframe {
58-
border-radius: 2.5%;
59-
}
60-
61-
@media (max-width: 768px) {
62-
height: 54vw !important;
94+
border-radius: 8px;
6395
}
6496
6597
&:hover {
66-
/* box-shadow: 0px 3px 20px 4px rgba(0, 179, 159, 0.75);
67-
.react-player__play-icon {
68-
border-color: transparent transparent transparent #EBC017 !important;
69-
} */
70-
.playBtn {
71-
box-shadow: 0px 0px 16px 3px #00B39F;
72-
}
98+
.playBtn {
99+
box-shadow: 0px 0px 16px 3px #00B39F;
100+
}
73101
}
74102
}
103+
75104
.kanvasVideo {
76105
position: relative;
77106
min-width:25%;
78107
max-width:100%;
79108
object-fit: cover;
80-
}
109+
}
81110
.vintage-box-container {
82111
display: flex;
83112
}
@@ -132,6 +161,7 @@ const Banner1SectionWrapper = styled.section`
132161
border-radius: 50%;
133162
height: 4rem;
134163
width: 4rem;
164+
z-index: 3; /* Highest z-index to ensure it's clickable */
135165
}
136166
@media only screen and (max-width: 1200px) {
137167
.section-title {

src/sections/Home/Banner-4/index.js

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useState, useEffect, useRef } from "react";
22
// import { Link } from "gatsby";
33

44
import { Container, Row, Col } from "../../../reusecore/Layout";
@@ -22,6 +22,8 @@ import { getImage } from "gatsby-plugin-image";
2222
import useHasMounted from "../../../utils/useHasMounted";
2323

2424
const Banner1 = (props) => {
25+
const [videoReady, setVideoReady] = useState(false);
26+
const thumbnailRef = useRef(null);
2527

2628
const { heroImage } = useStaticQuery(
2729
graphql`
@@ -46,6 +48,37 @@ const Banner1 = (props) => {
4648

4749
const hasMounted = useHasMounted();
4850

51+
// Set video as ready immediately after mount to avoid loading message
52+
useEffect(() => {
53+
if (hasMounted) {
54+
// Force the video to be marked as ready after a short delay
55+
// This ensures that even if events don't fire, the loading message will disappear
56+
const timer = setTimeout(() => {
57+
setVideoReady(true);
58+
}, 1000);
59+
60+
// Preload the thumbnail
61+
const img = new Image();
62+
img.src = videoThumbnail;
63+
img.onload = () => {
64+
// Mark video as ready when thumbnail loads
65+
setVideoReady(true);
66+
};
67+
68+
return () => clearTimeout(timer);
69+
}
70+
}, [hasMounted]);
71+
72+
// Multiple handlers to ensure the video gets marked as ready
73+
const handleVideoReady = () => {
74+
setVideoReady(true);
75+
};
76+
77+
const handleThumbnailClick = () => {
78+
// Set ready when user clicks on thumbnail
79+
setVideoReady(true);
80+
};
81+
4982
return (
5083
<Banner1SectionWrapper {...props}>
5184
<BGImg title="heroImage" image={pluginImage}>
@@ -61,10 +94,6 @@ const Banner1 = (props) => {
6194
<h2>
6295
Collaborate to innovate
6396
</h2>
64-
{/* <h1>Take the blinders off</h1>
65-
<h2>
66-
cloud native management
67-
</h2> */}
6897
</SectionTitle>
6998
<span className="vintage-box-container">
7099
<VintageBox $right={true} $vintageOne={true}>
@@ -80,32 +109,45 @@ const Banner1 = (props) => {
80109
</Col>
81110
{hasMounted && window.innerWidth > 760 && (
82111
<Col $sm={4} $lg={6} className="section-title-wrapper video-col">
83-
<ReactPlayer
84-
url="https://youtu.be/034nVaQUyME?si=Yya8m6i7JUoSdZm4"
85-
playing
86-
controls
87-
light={videoThumbnail}
88-
playIcon={
89-
<img
90-
src={playIcon}
91-
className="playBtn"
92-
loading="lazy"
93-
alt="Play"
94-
role="button"
95-
aria-label="Play"
96-
style={{ fontSize: "24px" }}
97-
/>
98-
}
99-
width="90%"
100-
height="30vw"
101-
style={{ margin: "auto" }}
102-
className="embedVideo"
103-
/>
104-
{/* <Link to="/cloud-native-management/kanvas">
105-
<video autoPlay muted loop preload="metadata" className="kanvasVideo">
106-
<source src={kanvasVideo} type="video/mp4"></source>
107-
</video>
108-
</Link> */}
112+
<div
113+
className={`video-wrapper ${videoReady ? "video-loaded" : ""}`}
114+
ref={thumbnailRef}
115+
onClick={handleThumbnailClick}
116+
>
117+
<ReactPlayer
118+
url="https://youtu.be/034nVaQUyME?si=Yya8m6i7JUoSdZm4"
119+
playing
120+
controls
121+
light={videoThumbnail}
122+
playIcon={
123+
<img
124+
src={playIcon}
125+
className="playBtn"
126+
loading="eager"
127+
alt="Play"
128+
role="button"
129+
aria-label="Play"
130+
style={{ fontSize: "24px" }}
131+
/>
132+
}
133+
width="100%"
134+
height="100%"
135+
className="embedVideo"
136+
onReady={handleVideoReady}
137+
onStart={handleVideoReady}
138+
onPlay={handleVideoReady}
139+
onBufferEnd={handleVideoReady}
140+
onClickPreview={handleVideoReady}
141+
config={{
142+
youtube: {
143+
playerVars: {
144+
rel: 0,
145+
modestbranding: 1,
146+
}
147+
}
148+
}}
149+
/>
150+
</div>
109151
</Col>
110152
)}
111153
</Row>

0 commit comments

Comments
 (0)