Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions components/post-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,20 @@ export default function PostBody({
}

const decodeHtmlEntities = (str: string): string => {
const textarea = document.createElement("textarea");
textarea.innerHTML = str;
return textarea.value;
// document.createElement is browser-only; use a pure-JS fallback during SSR
if (typeof document !== "undefined") {
const textarea = document.createElement("textarea");
textarea.innerHTML = str;
return textarea.value;
}
return str
.replace(/&/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&#39;/g, "'")
.replace(/&nbsp;/g, "\u00A0");
};

return safeContent
Expand Down Expand Up @@ -407,4 +418,4 @@ export default function PostBody({
</div>
</div>
);
}
}
4 changes: 1 addition & 3 deletions pages/community/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ import {
} from "../../lib/structured-data";
import { sanitizeTitle, getSafeDescription } from "../../utils/seo";

const PostBody = dynamic(() => import("../../components/post-body"), {
ssr: false,
});
const PostBody = dynamic(() => import("../../components/post-body"));

// Apply all HTML transformations in one synchronous pass so the
// transformed content is available at render time (SSR-friendly) and
Expand Down
4 changes: 1 addition & 3 deletions pages/technology/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ import {
} from "../../lib/structured-data";
import { sanitizeTitle, getSafeDescription } from "../../utils/seo";

const PostBody = dynamic(() => import("../../components/post-body"), {
ssr: false,
});
const PostBody = dynamic(() => import("../../components/post-body"));

const postBody = ({ content, post }) => {
const urlPattern = /https:\/\/keploy\.io\/wp\/author\/[^\/]+\//g;
Expand Down
24 changes: 24 additions & 0 deletions tests/e2e/SeoMeta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,30 @@ test.describe('SEO and Meta Tags Configuration', () => {
expect(body).toContain('<loc>https://keploy.io/blog/technology</loc>');
});

test('Post page article body should be in server-rendered HTML before JavaScript runs', async ({ request, baseURL }) => {
// GPTBot / ClaudeBot / PerplexityBot fetch raw HTML without executing JS.
// This test uses Playwright's HTTP client (no browser, no JS) to verify
// that the article prose appears in the initial SSR payload — not only
// inside the __NEXT_DATA__ JSON blob.
const response = await request.get(`${baseURL!}/technology/understanding-api-testing-with-keploy`);
expect(response.status()).toBe(200);

const html = await response.text();

// __NEXT_DATA__ always contains the full post JSON — strip it so we only
// check the server-rendered DOM markup.
const htmlWithoutNextData = html.replace(
/<script id="__NEXT_DATA__"[\s\S]*?<\/script>/i,
''
);

// Scope the assertion to the rendered article body so title/SEO/schema
// text cannot satisfy the check when the prose itself is missing.
expect(htmlWithoutNextData).toMatch(
/<div[^>]*data-testid=["']post-content["'][^>]*>[\s\S]*<p>API testing is a critical part of the software development lifecycle\.[\s\S]*<\/p>/i
);
});

test('AI referral tracker should push event to dataLayer on UTM-attributed landing', async ({ page, baseURL }) => {
await page.goto(`${baseURL!}/?utm_source=chatgpt`);

Expand Down
Loading