From d2bd412dc764e6ab86394fdecceca78181c2bd9e Mon Sep 17 00:00:00 2001 From: Heet Mehta Date: Tue, 24 Feb 2026 21:37:11 +0530 Subject: [PATCH 1/3] feat: redesign testimonial marquee with manual controls, add security headers, and fix SEO issues Signed-off-by: Heet Mehta --- components/Marquee.tsx | 4 +- components/cover-image.tsx | 6 +- components/footer.tsx | 7 +- components/meta.tsx | 10 +- components/testimonials.tsx | 182 ++++++++++++++++++++++++++++++------ next.config.js | 18 +++- package-lock.json | 13 --- pages/index.tsx | 11 +-- 8 files changed, 191 insertions(+), 60 deletions(-) diff --git a/components/Marquee.tsx b/components/Marquee.tsx index 4b6082a9..07851b7d 100644 --- a/components/Marquee.tsx +++ b/components/Marquee.tsx @@ -5,6 +5,7 @@ interface MarqueeProps { children?: React.ReactNode; vertical?: boolean; repeat?: number; + isPaused?: boolean; [key: string]: any; } @@ -15,6 +16,7 @@ export function Marquee({ children, vertical = false, repeat = 4, + isPaused = false, ...props }: MarqueeProps) { return ( @@ -27,7 +29,7 @@ export function Marquee({ .map((_, i) => (
{children}
diff --git a/components/cover-image.tsx b/components/cover-image.tsx index 9f6f4fa4..73911f81 100644 --- a/components/cover-image.tsx +++ b/components/cover-image.tsx @@ -21,8 +21,8 @@ export default function CoverImage({ {`Cover ); return ( - +
{slug ? ( diff --git a/components/footer.tsx b/components/footer.tsx index 7b482a41..a9994f15 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -82,6 +82,7 @@ export default function Footer() { className="w-4 h-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" >
-

Copyright © {new Date().getFullYear()} Keploy Inc

+

Copyright © {new Date().getFullYear()} Keploy Inc

diff --git a/components/meta.tsx b/components/meta.tsx index b04dd086..1b8f2bb6 100644 --- a/components/meta.tsx +++ b/components/meta.tsx @@ -32,9 +32,9 @@ export default function Meta({ sizes="16x16" href="/blog/favicon/Group.png" /> - - - + + + @@ -45,10 +45,10 @@ export default function Meta({ - + diff --git a/components/testimonials.tsx b/components/testimonials.tsx index 662d8fb3..e1729aa4 100644 --- a/components/testimonials.tsx +++ b/components/testimonials.tsx @@ -1,9 +1,28 @@ -import React from "react"; +import React, { useState, useCallback, useMemo } from "react"; import { useRouter } from "next/router"; import { Marquee } from "./Marquee"; import Tweets from "../services/Tweets"; -const firstRow = Tweets.slice(0, Tweets.length / 2); -const secondRow = Tweets.slice(Tweets.length / 2); + +const firstRow = Tweets.slice(0, Math.ceil(Tweets.length / 2)); +const secondRow = Tweets.slice(Math.ceil(Tweets.length / 2)); + +import { User } from "lucide-react"; + +/** Generates a two-letter initial from a name string */ +const getInitials = (name: string): string => { + const parts = name.trim().split(/\s+/); + if (parts.length >= 2) { + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); +}; + +/** Fallback avatar component – renders a user icon/initials in a branded circle */ +const FallbackAvatar = ({ name }: { name: string }) => ( +
+ +
+); const ReviewCard = ({ avatar, @@ -18,53 +37,158 @@ const ReviewCard = ({ id: string; content: string; }) => { - const { basePath } = useRouter(); + const router = useRouter(); + const [imgError, setImgError] = React.useState(false); + + // Safe basePath handling for SSR/Hydration + const basePath = router?.basePath || ""; + + // Determine if we should use the proxy const isExternal = typeof avatar === "string" && /^https?:\/\//i.test(avatar); - const proxiedAvatar = isExternal ? `${basePath}/api/proxy-image?url=${encodeURIComponent(avatar)}` : avatar; + const proxiedAvatar = isExternal + ? `${basePath}/api/proxy-image?url=${encodeURIComponent(avatar)}` + : avatar; + + // Handle case where avatar might be empty or invalid + React.useEffect(() => { + if (!avatar) { + setImgError(true); + } else { + setImgError(false); + } + }, [avatar]); + return ( - -
-
- + +
+ {/* Quote icon */} + + +
+
+ {imgError ? ( + + ) : ( + {name setImgError(true)} + loading="lazy" + /> + )} +
-
{name}
-

{id}

+
+ {name} +
+

@{id}

-
{content}
+ +
+ {content} +
+ + {/* Bottom accent bar */} +
); }; const TwitterTestimonials = () => { + const [mounted, setMounted] = useState(false); + const [isPaused, setIsPaused] = useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( +
+

+ What our community thinks +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + return ( -
-

+
+
+

What our community thinks

-
- - + + + +
+ {/* Row 1 – scrolls left */} + {firstRow.map((tweet) => ( - + ))} - + + {/* Row 2 – scrolls right */} + {secondRow.map((tweet) => ( - + ))} -
-
+ + {/* Gradient fade overlays */} +
+
-
+ ); }; diff --git a/next.config.js b/next.config.js index db8d55d5..911b0b21 100644 --- a/next.config.js +++ b/next.config.js @@ -21,7 +21,7 @@ const contentSecurityPolicy = ` module.exports = { basePath: '/blog', assetPrefix: "/blog", - + // --- ADD THIS BLOCK --- // This exposes the server-side variable to the browser env: { @@ -55,6 +55,22 @@ module.exports = { key: 'Content-Security-Policy', value: contentSecurityPolicy, }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', + }, ], }, ] diff --git a/package-lock.json b/package-lock.json index 26a31355..0f92b1ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -270,7 +270,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz", "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", @@ -1454,7 +1453,6 @@ "version": "18.2.36", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.36.tgz", "integrity": "sha512-o9XFsHYLLZ4+sb9CWUYwHqFVoG61SesydF353vFMMsQziiyRu8np4n2OYMUSDZ8XuImxDr9c5tR7gidlH29Vnw==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1465,7 +1463,6 @@ "version": "18.2.14", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", - "peer": true, "dependencies": { "@types/react": "*" } @@ -1524,7 +1521,6 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -1796,7 +1792,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2185,7 +2180,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -2780,7 +2774,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2933,7 +2926,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -4918,7 +4910,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -5098,7 +5089,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5110,7 +5100,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -5808,7 +5797,6 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6053,7 +6041,6 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/pages/index.tsx b/pages/index.tsx index 70c33ff9..433c4cb0 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -32,9 +32,6 @@ export default function Index({ communityPosts, technologyPosts, preview }) { 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."} structuredData={structuredData} > - - {`Engineering | Keploy Blog`} -
@@ -84,7 +81,7 @@ export default function Index({ communityPosts, technologyPosts, preview }) { export const getStaticProps: GetStaticProps = async ({ preview = false }) => { const allCommunityPosts = await getAllPostsForCommunity(preview); - const allTehcnologyPosts = await getAllPostsForTechnology(preview); + const allTechnologyPosts = await getAllPostsForTechnology(preview); return { props: { @@ -93,9 +90,9 @@ export const getStaticProps: GetStaticProps = async ({ preview = false }) => { ? allCommunityPosts?.edges?.slice(0, 3) : allCommunityPosts?.edges, technologyPosts: - allTehcnologyPosts?.edges?.length > 3 - ? allTehcnologyPosts?.edges?.slice(0, 3) - : allTehcnologyPosts.edges, + allTechnologyPosts?.edges?.length > 3 + ? allTechnologyPosts?.edges?.slice(0, 3) + : allTechnologyPosts.edges, preview, }, revalidate: 10, From 7e29072d1f5f1e6cd734d7e5d06ca28f8978a6ed Mon Sep 17 00:00:00 2001 From: Heet Mehta Date: Tue, 24 Feb 2026 21:47:41 +0530 Subject: [PATCH 2/3] feat: redesign testimonial marquee, add security headers, and fix SEO issues Signed-off-by: Heet Mehta --- components/Marquee.tsx | 4 +--- components/testimonials.tsx | 30 +++++------------------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/components/Marquee.tsx b/components/Marquee.tsx index 07851b7d..4b6082a9 100644 --- a/components/Marquee.tsx +++ b/components/Marquee.tsx @@ -5,7 +5,6 @@ interface MarqueeProps { children?: React.ReactNode; vertical?: boolean; repeat?: number; - isPaused?: boolean; [key: string]: any; } @@ -16,7 +15,6 @@ export function Marquee({ children, vertical = false, repeat = 4, - isPaused = false, ...props }: MarqueeProps) { return ( @@ -29,7 +27,7 @@ export function Marquee({ .map((_, i) => (
{children}
diff --git a/components/testimonials.tsx b/components/testimonials.tsx index e1729aa4..527cf6bd 100644 --- a/components/testimonials.tsx +++ b/components/testimonials.tsx @@ -113,7 +113,6 @@ const ReviewCard = ({ const TwitterTestimonials = () => { const [mounted, setMounted] = useState(false); - const [isPaused, setIsPaused] = useState(false); React.useEffect(() => { setMounted(true); @@ -146,39 +145,20 @@ const TwitterTestimonials = () => { return (
-
-

- What our community thinks -

- -
+

+ What our community thinks +

{/* Row 1 – scrolls left */} - + {firstRow.map((tweet) => ( ))} {/* Row 2 – scrolls right */} - + {secondRow.map((tweet) => ( ))} From 797948911bd8e825c66e6a7cdc72d18413768393 Mon Sep 17 00:00:00 2001 From: Heet Mehta Date: Tue, 24 Feb 2026 22:00:47 +0530 Subject: [PATCH 3/3] refactor: remove unused useCallback and useMemo imports Signed-off-by: Heet Mehta --- components/testimonials.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/testimonials.tsx b/components/testimonials.tsx index 527cf6bd..da405060 100644 --- a/components/testimonials.tsx +++ b/components/testimonials.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo } from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; import { Marquee } from "./Marquee"; import Tweets from "../services/Tweets";