-
Notifications
You must be signed in to change notification settings - Fork 142
feat: add professional skeleton loaders for blog and post pages (#3293) #225
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?
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 replaces a basic Lottie animation spinner with professional skeleton loaders that match the actual page layouts, improving the user experience during route transitions on slow networks.
- Introduces a reusable
Skeletonprimitive component with Tailwind-based pulse animation - Implements layout-aware skeleton components for blog listing and individual post pages
- Updates the main app to conditionally render appropriate skeletons based on the target route
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
pages/_app.tsx |
Replaces PageLoader with skeleton components; adds route-based skeleton selection logic and framer-motion animations for smooth transitions |
components/ui/skeleton.tsx |
Introduces base Skeleton component with pulse animation and customizable styling |
components/skeletons/BlogPageSkeleton.tsx |
Creates skeleton loader matching blog listing page layout with header, hero section, and post grid |
components/skeletons/PostPageSkeleton.tsx |
Creates skeleton loader matching individual post page layout with header, author info, and content blocks |
components/skeletons/PostCardSkeleton.tsx |
Creates skeleton loader for individual post cards used in the blog grid |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { AnimatePresence } from "framer-motion"; | ||
| import { AnimatePresence, motion } from "framer-motion"; | ||
| import { useEffect, useState } from "react"; | ||
| import dynamic from 'next/dynamic' |
Copilot
AI
Dec 25, 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 dynamic import from 'next/dynamic' is no longer used and should be removed since PageLoader has been replaced with skeleton loaders.
| import dynamic from 'next/dynamic' |
pages/_app.tsx
Outdated
| if (targetPath.includes('/technology/') || targetPath.includes('/community/')) { | ||
| if (targetPath === '/technology' || targetPath === '/community' || targetPath === '/technology/' || targetPath === '/community/') { | ||
| return <BlogPageSkeleton />; | ||
| } | ||
| return <PostPageSkeleton />; | ||
| } |
Copilot
AI
Dec 25, 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 logic for determining whether to show BlogPageSkeleton has a flaw. When navigating to '/technology/' or '/community/', the condition targetPath.includes('/technology/') will match, but then the exact path comparison checks for '/technology/' which will also match, returning BlogPageSkeleton. However, for paths like '/technology/my-post', the includes check will match but the exact comparison will fail, returning PostPageSkeleton as intended. This logic works correctly but could be simplified and made more maintainable by checking if the path ends with the section name first, then checking if it includes the section. Consider refactoring to: check if path starts with '/technology' or '/community', then check if it's exactly that path (with or without trailing slash) to return BlogPageSkeleton, otherwise return PostPageSkeleton.
| if (targetPath.includes('/technology/') || targetPath.includes('/community/')) { | |
| if (targetPath === '/technology' || targetPath === '/community' || targetPath === '/technology/' || targetPath === '/community/') { | |
| return <BlogPageSkeleton />; | |
| } | |
| return <PostPageSkeleton />; | |
| } | |
| const startsWithTechnology = targetPath.startsWith('/technology'); | |
| const startsWithCommunity = targetPath.startsWith('/community'); | |
| if (startsWithTechnology || startsWithCommunity) { | |
| const normalizedPath = | |
| targetPath.length > 1 && targetPath.endsWith('/') | |
| ? targetPath.slice(0, -1) | |
| : targetPath; | |
| if (normalizedPath === '/technology' || normalizedPath === '/community') { | |
| return <BlogPageSkeleton />; | |
| } | |
| return <PostPageSkeleton />; | |
| } |
pages/_app.tsx
Outdated
| if (targetPath.includes('/technology/') || targetPath.includes('/community/')) { | ||
| if (targetPath === '/technology' || targetPath === '/community' || targetPath === '/technology/' || targetPath === '/community/') { | ||
| return <BlogPageSkeleton />; | ||
| } | ||
| return <PostPageSkeleton />; |
Copilot
AI
Dec 25, 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 selection logic doesn't account for the '/community/search' route. When navigating to '/community/search', the condition targetPath.includes('/community/') will be true, and since it's not exactly '/community' or '/community/', it will show PostPageSkeleton instead of a more appropriate skeleton for a search page. Consider adding specific handling for search routes or using BlogPageSkeleton for search pages.
| if (targetPath.includes('/technology/') || targetPath.includes('/community/')) { | |
| if (targetPath === '/technology' || targetPath === '/community' || targetPath === '/technology/' || targetPath === '/community/') { | |
| return <BlogPageSkeleton />; | |
| } | |
| return <PostPageSkeleton />; | |
| // Use blog-style skeleton for community search routes | |
| if (targetPath.startsWith('/community/search')) { | |
| return <BlogPageSkeleton />; | |
| } | |
| if (targetPath.includes('/technology/') || targetPath.includes('/community/')) { | |
| if ( | |
| targetPath === '/technology' || | |
| targetPath === '/community' || | |
| targetPath === '/technology/' || | |
| targetPath === '/community/' | |
| ) { | |
| return <BlogPageSkeleton />; | |
| } | |
| return <PostPageSkeleton />; |
components/ui/skeleton.tsx
Outdated
| }: React.HTMLAttributes<HTMLDivElement>) { | ||
| return ( | ||
| <div | ||
| className={cn("animate-pulse rounded-md bg-muted bg-gray-200 dark:bg-gray-800", className)} |
Copilot
AI
Dec 25, 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 class string contains redundant background color definitions. The string includes both bg-muted and explicit bg-gray-200 dark:bg-gray-800. If bg-muted is defined in your Tailwind theme to provide background colors, the explicit bg-gray-200 dark:bg-gray-800 may override it or create conflicts. Consider using only one approach - either use the semantic bg-muted token or the explicit color classes, not both.
| className={cn("animate-pulse rounded-md bg-muted bg-gray-200 dark:bg-gray-800", className)} | |
| className={cn("animate-pulse rounded-md bg-gray-200 dark:bg-gray-800", className)} |
|
|
||
| export default function BlogPageSkeleton() { | ||
| return ( | ||
| <div className="min-h-screen bg-white"> |
Copilot
AI
Dec 25, 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 components lack ARIA attributes for accessibility. Screen readers should be informed that content is loading. Consider adding role="status" and aria-live="polite" to the root div, along with a visually hidden loading message using aria-label="Loading content" or a screen reader-only text element.
| <div className="min-h-screen bg-white"> | |
| <div | |
| className="min-h-screen bg-white" | |
| role="status" | |
| aria-live="polite" | |
| > | |
| <span className="sr-only">Loading blog content</span> |
|
|
||
| export default function PostPageSkeleton() { | ||
| return ( | ||
| <div className="min-h-screen bg-white"> |
Copilot
AI
Dec 25, 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 components lack ARIA attributes for accessibility. Screen readers should be informed that content is loading. Consider adding role="status" and aria-live="polite" to the root div, along with a visually hidden loading message using aria-label="Loading content" or a screen reader-only text element.
| <div className="min-h-screen bg-white"> | |
| <div | |
| className="min-h-screen bg-white" | |
| role="status" | |
| aria-live="polite" | |
| aria-label="Loading content" | |
| > | |
| <span className="sr-only">Loading content</span> |
|
|
||
| export default function PostCardSkeleton() { | ||
| return ( | ||
| <div className="bg-white rounded-xl shadow-[0_6px_18px_rgba(0,0,0,0.08)] border border-gray-200 overflow-hidden h-full"> |
Copilot
AI
Dec 25, 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 components lack ARIA attributes for accessibility. Screen readers should be informed that content is loading. Consider adding role="status" and aria-live="polite" to the root div, along with a visually hidden loading message using aria-label="Loading content" or a screen reader-only text element.
| <div className="bg-white rounded-xl shadow-[0_6px_18px_rgba(0,0,0,0.08)] border border-gray-200 overflow-hidden h-full"> | |
| <div | |
| className="bg-white rounded-xl shadow-[0_6px_18px_rgba(0,0,0,0.08)] border border-gray-200 overflow-hidden h-full" | |
| role="status" | |
| aria-live="polite" | |
| aria-label="Loading content" | |
| > |
- Convert author-description.jsx to TypeScript with proper interfaces - Convert waitlistBanner.jsx to TypeScript with React.FC typing - Add asChild prop to Header Sign In buttons for better semantics - Fix placeholder Home URL in footer (changed from #0 to https://keploy.io/) - Add proper TypeScript types and null checks in author-description - Improve accessibility by avoiding nested interactive elements
Implemented professional skeleton loaders to improve the user experience on slow networks (3G/4G).
Key changes:
Skeletonprimitive incomponents/ui/skeleton.tsx.BlogPageSkeletonandPostPageSkeletonto match existing layouts._app.tsxfor smooth route transitions.Fixes #3293 in the main keploy repository.