-
Notifications
You must be signed in to change notification settings - Fork 141
feat: add skeleton loaders for improved UX #223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add skeleton loaders for improved UX #223
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds skeleton loaders across the blog website to improve perceived loading times and provide visual feedback during content loading. The implementation uses shadcn/ui's skeleton component as a foundation and creates custom skeleton components for different content types.
Key changes:
- Added base skeleton component and 4 custom skeleton variations (HeroPostSkeleton, PostCardSkeleton, PostGridSkeleton, TopBlogsSkeleton)
- Implemented client-side loading states with artificial 500ms delays on homepage, community, and technology pages
- Removed the global PageLoader component and route change loading indicators from _app.tsx
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
lib/utils.ts |
Adds cn() utility function for merging Tailwind classes |
components/ui/skeleton.tsx |
Base skeleton component with pulse animation |
components/skeletons/HeroPostSkeleton.tsx |
Skeleton loader for hero post layout |
components/skeletons/PostCardSkeleton.tsx |
Skeleton loader for individual post cards |
components/skeletons/PostGridSkeleton.tsx |
Skeleton loader for post grid layouts |
components/skeletons/TopBlogsSkeleton.tsx |
Skeleton loader for top blogs list (unused) |
pages/index.tsx |
Adds loading state with artificial delay for homepage posts |
pages/community/index.tsx |
Adds loading state with artificial delay for community posts |
pages/technology/index.tsx |
Adds loading state with artificial delay for technology posts |
components/topBlogs.tsx |
Integrates skeleton loaders for technology and community blog sections |
components/more-stories.tsx |
Adds skeleton loader for initial load state |
pages/_app.tsx |
Removes PageLoader component and route transition loading logic |
package-lock.json |
Updates package name from "blog-website" to "keploy-blog-website" |
Comments suppressed due to low confidence (1)
pages/technology/index.tsx:42
- During the loading state, the Layout component receives undefined values for featuredImage and Title props (heroPost is undefined when displayPosts is empty). This could cause issues with SEO meta tags and OpenGraph data. Consider using the original edges data for Layout props or provide fallback values.
<Layout
preview={preview}
featuredImage={heroPost?.featuredImage?.node.sourceUrl}
Title={heroPost?.title}
Description={`Blog from the Technology Page`}
>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pages/index.tsx
Outdated
| const [isLoading, setIsLoading] = useState(true); | ||
| const [displayPosts, setDisplayPosts] = useState({ | ||
| community: [], | ||
| technology: [], | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| if (typeof window !== "undefined") { | ||
| const timer = setTimeout(() => { | ||
| setDisplayPosts({ | ||
| community: communityPosts || [], | ||
| technology: technologyPosts || [], | ||
| }); | ||
| setIsLoading(false); | ||
| }, 500); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| } | ||
| }, [communityPosts, technologyPosts]); | ||
|
|
||
| return ( | ||
| <Layout | ||
| preview={preview} | ||
| featuredImage={HOME_OG_IMAGE_URL} | ||
| Title={`Blog - Keploy`} | ||
| Description={"The Keploy Blog offers in-depth articles and expert insights on software testing, automation, and quality assurance, empowering developers to enhance their testing strategies and deliver robust applications."}> | ||
| Description={ | ||
| "The Keploy Blog offers in-depth articles and expert insights on software testing, automation, and quality assurance, empowering developers to enhance their testing strategies and deliver robust applications." | ||
| } | ||
| > |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loading state starts as true, which means the statically generated HTML (from getStaticProps) will contain skeleton loaders instead of actual content. This defeats the purpose of static generation and harms SEO, as search engines and users will initially see skeleton loaders. For statically generated pages, the content should be rendered immediately without a loading state.
pages/technology/index.tsx
Outdated
| } | ||
| }, [edges]); | ||
|
|
||
| console.log("tech posts: ", edges.length); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console.log statements should be removed from production code. This debug statement can cause performance issues and expose implementation details in production.
| console.log("tech posts: ", edges.length); |
| function MyApp({ Component, pageProps }: AppProps) { | ||
| const [loading, setLoading] = useState(false); | ||
| useEffect(() => { | ||
| // Used for page transition | ||
| const startLoader = () => { | ||
| setLoading(true); | ||
| }; | ||
| const stopLoader = () => { | ||
| setLoading(false); | ||
| }; | ||
| Router.events.on("routeChangeStart", startLoader); | ||
| Router.events.on("routeChangeComplete", stopLoader); | ||
| Router.events.on("routeChangeError", stopLoader); | ||
| return () => { | ||
| Router.events.off("routeChangeStart", startLoader); | ||
| Router.events.off("routeChangeComplete", stopLoader); | ||
| Router.events.off("routeChangeError", stopLoader); | ||
| }; | ||
| }, []); | ||
|
|
||
| return ( | ||
| <> | ||
| <AnimatePresence> | ||
| {loading ? <PageLoader /> : <Component {...pageProps} />} | ||
| <Component {...pageProps} /> | ||
| </AnimatePresence> | ||
| </> | ||
| ); | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The removal of the PageLoader component and route change listeners means users will no longer see loading feedback during page navigation. This could negatively impact user experience, especially on slower connections. Consider keeping some form of loading indicator for route transitions, or document why this was intentionally removed.
| <Layout | ||
| preview={preview} | ||
| featuredImage={heroPost?.featuredImage?.node.sourceUrl} | ||
| Title={heroPost?.title} | ||
| Description={`Blog from the Technology Page`} | ||
| > |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During the loading state, the Layout component receives undefined values for featuredImage and Title props (heroPost is undefined when displayPosts is empty). This could cause issues with SEO meta tags and OpenGraph data. Consider using the original edges data for Layout props or provide fallback values.
pages/technology/index.tsx
Outdated
| if (typeof window !== "undefined") { | ||
| const timer = setTimeout(() => { | ||
| setDisplayPosts(edges || []); | ||
| setIsLoading(false); | ||
| }, 500); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition "typeof window !== 'undefined'" is unnecessary in a useEffect hook. useEffect only runs on the client side, so window is always defined. This check should be removed to simplify the code.
| if (typeof window !== "undefined") { | |
| const timer = setTimeout(() => { | |
| setDisplayPosts(edges || []); | |
| setIsLoading(false); | |
| }, 500); | |
| return () => clearTimeout(timer); | |
| } | |
| const timer = setTimeout(() => { | |
| setDisplayPosts(edges || []); | |
| setIsLoading(false); | |
| }, 500); | |
| return () => clearTimeout(timer); |
pages/community/index.tsx
Outdated
| if (typeof window !== "undefined") { | ||
| const timer = setTimeout(() => { | ||
| setDisplayPosts(edges || []); | ||
| setIsLoading(false); | ||
| }, 500); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition "typeof window !== 'undefined'" is unnecessary in a useEffect hook. useEffect only runs on the client side, so window is always defined. This check should be removed to simplify the code.
| if (typeof window !== "undefined") { | |
| const timer = setTimeout(() => { | |
| setDisplayPosts(edges || []); | |
| setIsLoading(false); | |
| }, 500); | |
| return () => clearTimeout(timer); | |
| } | |
| const timer = setTimeout(() => { | |
| setDisplayPosts(edges || []); | |
| setIsLoading(false); | |
| }, 500); | |
| return () => clearTimeout(timer); |
pages/index.tsx
Outdated
| if (typeof window !== "undefined") { | ||
| const timer = setTimeout(() => { | ||
| setDisplayPosts({ | ||
| community: communityPosts || [], | ||
| technology: technologyPosts || [], | ||
| }); | ||
| setIsLoading(false); | ||
| }, 500); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition "typeof window !== 'undefined'" is unnecessary in a useEffect hook. useEffect only runs on the client side, so window is always defined. This check should be removed to simplify the code.
| if (typeof window !== "undefined") { | |
| const timer = setTimeout(() => { | |
| setDisplayPosts({ | |
| community: communityPosts || [], | |
| technology: technologyPosts || [], | |
| }); | |
| setIsLoading(false); | |
| }, 500); | |
| return () => clearTimeout(timer); | |
| } | |
| const timer = setTimeout(() => { | |
| setDisplayPosts({ | |
| community: communityPosts || [], | |
| technology: technologyPosts || [], | |
| }); | |
| setIsLoading(false); | |
| }, 500); | |
| return () => clearTimeout(timer); |
components/ui/skeleton.tsx
Outdated
| className={cn( | ||
| "animate-pulse rounded-md bg-gray-200 dark:bg-gray-800", | ||
| className | ||
| )} | ||
| {...props} | ||
| /> |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The skeleton loader lacks ARIA attributes to indicate loading state to screen readers. Add role="status" and aria-live="polite" attributes along with visually hidden text like "Loading content..." to improve accessibility for users with assistive technologies.
| className={cn( | |
| "animate-pulse rounded-md bg-gray-200 dark:bg-gray-800", | |
| className | |
| )} | |
| {...props} | |
| /> | |
| role="status" | |
| aria-live="polite" | |
| className={cn( | |
| "animate-pulse rounded-md bg-gray-200 dark:bg-gray-800", | |
| className | |
| )} | |
| {...props} | |
| > | |
| <span className="sr-only">Loading content...</span> | |
| </div> |
components/more-stories.tsx
Outdated
| {loading && visibleCount === 0 ? ( | ||
| <PostGridSkeleton count={12} /> | ||
| ) : ( |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The skeleton loader will never display because visibleCount is initialized to 12 on line 40 and is never set to 0. The condition "loading && visibleCount === 0" will always be false. Consider using a different condition, such as checking if this is the initial load or if allPosts is empty.
83f914b to
d190e37
Compare
cbf96a7 to
f29ac47
Compare
Signed-off-by: manthansubhash01 <manthanziman20@gmail.com>
Signed-off-by: manthansubhash01 <manthanziman20@gmail.com>
Signed-off-by: manthansubhash01 <manthanziman20@gmail.com>
f29ac47 to
103d1d5
Compare
|
Hey @manthansubhash01 Thanks for raising the pr, preview looks good to me. Can you please take a look at the copilot review and the merge conflicts? |
|
@amaan-bhati |
Signed-off-by: Manthan Ziman <manthanziman20@gmail.com>
Related Tickets & Documents
Fixes: #3293
Description
Added skeleton loaders across the blog website to improve user experience during content loading. This provides visual feedback to users while data is being fetched, making the application feel more responsive and reducing perceived loading times.
The implementation uses shadcn/ui's skeleton component as the base and creates custom skeleton loaders for different content types (hero posts, post cards, post grids, and top blogs).
Changes
Type of Change
Testing
Demo
Before
After
Environment and Dependencies
New Dependencies:
@radix-ui/react-slot(added by shadcn/ui)class-variance-authority(for component variants)tailwind-merge(for className utilities)lucide-react(for icons, if not already present)Configuration Changes:
components.jsonfor shadcn/ui configurationlib/utils.tsfor cn() utility functionChecklist