Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
be87ca1
feat(schema): give Organization a stable @id and link publisher nodes…
dhananjay6561 Aug 7, 2026
c605fde
fix(schema): always emit article image as ImageObject with OG fallback
dhananjay6561 Aug 7, 2026
f394942
fix(schema): guarantee a valid datePublished, never empty or invalid
dhananjay6561 Aug 7, 2026
1644c1d
fix(schema): sanitize description (strip HTML/entities) before emitting
dhananjay6561 Aug 7, 2026
dbc577f
fix(schema): fall back to a linked Keploy Team Person when author is …
dhananjay6561 Aug 7, 2026
29dd4b6
test(schema): assert Article stays valid for null/malformed WP inputs
dhananjay6561 Aug 7, 2026
46eb1ee
feat(seo): add on-page h1 to tag, community, and technology archive t…
dhananjay6561 Aug 7, 2026
8bfaadd
feat(seo): cap post <title> at 60 chars via buildPageTitle helper
dhananjay6561 Aug 7, 2026
5651d04
test(seo): pin buildPageTitle 60-char invariant; fix empty-title edge…
dhananjay6561 Aug 7, 2026
81a2554
fix(a11y): give cover-image link a non-empty aria-label and alt fallback
dhananjay6561 Aug 7, 2026
aec2eba
fix(a11y): fall back to default cover when WP featuredImage URL is mi…
dhananjay6561 Aug 7, 2026
cc741b9
feat(schema): add reusable getImageObjectSchema and use it for articl…
dhananjay6561 Aug 7, 2026
dfb459b
feat(schema): add getItemListSchema and getCollectionPageSchema builders
dhananjay6561 Aug 7, 2026
96bc97f
feat(schema): emit CollectionPage/ItemList on community, technology, …
dhananjay6561 Aug 7, 2026
65f94ff
feat(schema): wrap author in ProfilePage, enrich Person (image/sameAs…
dhananjay6561 Aug 7, 2026
6e36858
feat(schema): add FAQPage, DefinedTermSet, SoftwareSourceCode, Search…
dhananjay6561 Aug 7, 2026
92546c1
feat(schema): server-safe detectors for code languages and FAQ pairs
dhananjay6561 Aug 7, 2026
937e5da
feat(schema): wire FAQPage, SoftwareSourceCode, DefinedTermSet, depen…
dhananjay6561 Aug 7, 2026
a379171
feat(schema): emit SearchResultsPage on /search and /community/search
dhananjay6561 Aug 7, 2026
b52aa7b
feat(seo): add visible intro prose to archive templates (lifts thin-c…
dhananjay6561 Aug 7, 2026
3f26aa5
feat(schema): homepage ItemList of featured posts; drop no-op single-…
dhananjay6561 Aug 7, 2026
bd2f7c9
feat(schema): tag hub CollectionPage/ItemList of the tag directory
dhananjay6561 Aug 7, 2026
03e8970
fix(schema): drop raw inline JSON-LD from 404 (noindex page needs no …
dhananjay6561 Aug 7, 2026
736f19a
feat(schema): link post author/reviewer Person nodes to the author-pa…
dhananjay6561 Aug 7, 2026
388f2d1
feat(schema): attach ImageObject to ItemList entries (models listing …
dhananjay6561 Aug 7, 2026
dc3e534
chore(schema): delete 4 dead components; set author jobTitle to Author
dhananjay6561 Aug 7, 2026
8b87c38
feat(schema): add HowToTool (Keploy) to HowTo and an Occupation node …
dhananjay6561 Aug 7, 2026
3eaa5e1
fix(test): wait for the ISR fallback route before reading author JSON-LD
dhananjay6561 Aug 7, 2026
6641fc0
feat(schema): link the post author Person to the Keploy Organization …
dhananjay6561 Aug 7, 2026
66db353
docs(ai): document the JSON-LD coverage and entity @ids in llms.txt (…
dhananjay6561 Aug 7, 2026
53a383d
fix(schema): give the Blog and WebSite entities stable @ids
dhananjay6561 Aug 7, 2026
3b8f66e
feat(schema): model the home route as a CollectionPage
dhananjay6561 Aug 7, 2026
842012a
fix(seo): give the archive templates real titles (A9 title-too-short)
dhananjay6561 Aug 7, 2026
6b62768
feat(schema): emit community testimonials as Review nodes on the Kepl…
dhananjay6561 Aug 10, 2026
49a1f57
test(schema): pin getReviewSchema validity — rating-less, shared org …
dhananjay6561 Aug 10, 2026
dfe5780
feat(seo): add visible intro prose to the authors archive (A4 thin-co…
dhananjay6561 Aug 10, 2026
bacdb2f
fix(schema): url-less author fallback; make SoftwareSourceCode repo o…
dhananjay6561 Aug 10, 2026
5f55009
feat(schema): model Keploy as a SoftwareApplication + emit article co…
dhananjay6561 Aug 10, 2026
3d3f860
fix(schema): address review comments (valid schema, word-boundary tit…
dhananjay6561 Aug 10, 2026
3c190ee
fix(schema): harden entity decode, lang detection, tag + image URLs
dhananjay6561 Aug 10, 2026
9e884ed
fix(schema): drop ItemList entries with empty name/url (invalid ListI…
dhananjay6561 Aug 13, 2026
2627001
fix(schema): omit empty ItemList mainEntity on CollectionPage (orphan…
dhananjay6561 Aug 14, 2026
445663e
fix(a11y): add aria-label to author LinkedIn icon link (links-no-anchor)
dhananjay6561 Aug 14, 2026
edcbfab
fix(schema): guard getItemListSchema against empty itemListElement
dhananjay6561 Aug 14, 2026
57677ca
fix(schema): omit dates instead of fabricating new Date() (hydration …
dhananjay6561 Aug 15, 2026
04776ae
fix(schema): drop free/offers claim from SoftwareApplication (Keploy …
dhananjay6561 Aug 15, 2026
5495f4c
fix(schema): percent-encode tag slug in tag/[slug] URLs to match the …
dhananjay6561 Aug 15, 2026
05d723e
fix(schema): memoize content extractors; drop auto-FAQPage from artic…
dhananjay6561 Aug 15, 2026
1cdb21d
fix(schema): keep angle brackets encoded in stripTags (defence-in-depth)
dhananjay6561 Aug 15, 2026
a9bf2e1
test(schema): assert SoftwareApplication emits no price/free claim (r…
dhananjay6561 Aug 15, 2026
21c7236
fix(schema): drop SearchResultsPage from noindex search pages (dead m…
dhananjay6561 Aug 15, 2026
da5be34
fix(seo): route home <title> through buildPageTitle to hold the 60-ch…
dhananjay6561 Aug 15, 2026
dc414a4
feat(schema): re-add FAQPage behind an explicit FAQ-section marker
dhananjay6561 Aug 17, 2026
2932bb6
fix(sitemap): resolve redirects so no <loc> 301s (A8/A9 incorrect-pag…
dhananjay6561 Aug 17, 2026
1291b4b
fix(schema): include <li> FAQ answers, decode angle brackets in answers
dhananjay6561 Aug 17, 2026
e4b34bd
fix(sitemap): bucket lastmod by resolved category, guard resolver, ad…
dhananjay6561 Aug 17, 2026
42ef85b
fix(schema): bound FAQ section length against malformed markup
dhananjay6561 Aug 17, 2026
f24c43f
refactor(schema): single source of truth for JSON-LD builders
dhananjay6561 Aug 19, 2026
965dbef
feat(schema): add isAccessibleForFree, numberOfItems, WebSite publisher
dhananjay6561 Aug 19, 2026
ec43569
fix(sitemap): resolve + dedup static archive roots, bucket by resolve…
dhananjay6561 Aug 19, 2026
94c6774
fix(schema): stop emitting Review on the homepage (client-only wall)
dhananjay6561 Aug 19, 2026
8f4223f
fix(author): never render a broken avatar from junk WordPress data
dhananjay6561 Aug 19, 2026
339d760
chore(seo): refresh public/llms-full.txt content index
dhananjay6561 Aug 19, 2026
04f3484
fix(assets): align author avatar to author.webp (resolve cross-PR F1 …
dhananjay6561 Aug 20, 2026
4483325
feat: track banner impressions (for per-banner CTR) (#416)
dhananjay6561 Aug 25, 2026
a1023c1
Merge branch 'main' into ai-cite-health
nehagup Aug 25, 2026
ee87d3b
Merge branch 'main' into ai-cite-health
dhananjay6561 Aug 26, 2026
ba09062
fix(sitemap): fail loud when redirect config can't be loaded
dhananjay6561 Aug 27, 2026
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
10 changes: 7 additions & 3 deletions components/cover-image.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Image from "next/image";
import Link from "next/link";
import { Post } from "../types/post";
import { DEFAULT_ARTICLE_IMAGE_URL } from "../lib/structured-data";

interface Props extends Partial<Pick<Post, "title" | "slug">> {
coverImage: Post["featuredImage"];
Expand All @@ -22,13 +23,16 @@ export default function CoverImage({
sizes = "(max-width: 768px) 100vw, (max-width: 1200px) 780px, 780px",
}: Props) {
const basePath = isCommunity ? "/community/" : "/technology/";
// Never let the link's accessible name or the image alt end up empty (some
// cards render without a title) — an empty aria-label is an anchor-less link.
const safeTitle = title || "Keploy blog article";

const image = (
<Image
width={2000}
height={1000}
alt={`Cover Image for ${title}`}
src={coverImage?.node.sourceUrl}
alt={`Cover Image for ${safeTitle}`}
src={coverImage?.node?.sourceUrl || DEFAULT_ARTICLE_IMAGE_URL}
className={`w-full h-auto object-cover${imgClassName ? ` ${imgClassName}` : ""}${slug ? " transition-transform duration-300 hover:scale-[1.01]" : ""}`}
priority={priority}
loading={priority ? "eager" : "lazy"}
Expand All @@ -39,7 +43,7 @@ export default function CoverImage({
return (
<div className="w-full overflow-hidden">
{slug ? (
<Link href={`${basePath}${slug}`} aria-label={title}>
<Link href={`${basePath}${slug}`} aria-label={safeTitle}>
{image}
</Link>
) : (
Expand Down
138 changes: 129 additions & 9 deletions lib/structured-data.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { sanitizeAuthorSlug } from "../utils/sanitizeAuthorSlug";
import { decodeEntities } from "../utils/seo";

export const SITE_URL = "https://keploy.io/blog";
export const MAIN_SITE_URL = "https://keploy.io";
export const ORG_NAME = "Keploy";
export const BLOG_NAME = "Keploy Blog";
export const ORG_LOGO_URL = `${SITE_URL}/favicon/android-chrome-512x512.png`;
// Stable @id for the Keploy Organization entity. Every place that emits an
// Organization node (global node, per-post publisher, author worksFor) points
// at this same @id so AI/search engines resolve ONE Keploy entity instead of
// treating each inline copy as a separate organization (entity fragmentation).
Comment thread
dhananjay6561 marked this conversation as resolved.
export const ORG_ID = `${MAIN_SITE_URL}/#organization`;
Comment thread
dhananjay6561 marked this conversation as resolved.
// Default article image when a post's WordPress featuredImage is null (common on
// older/migrated posts). Mirrors HOME_OG_IMAGE_URL in lib/constants.ts so the
// schema image matches the og:image the page actually renders.
export const DEFAULT_ARTICLE_IMAGE_URL =
Comment thread
dhananjay6561 marked this conversation as resolved.
"https://wp.keploy.io/wp-content/uploads/2023/11/thumbnil-.png";
// Author fallback when a post has no usable ppmaAuthorName. A named team
// Person with its own profile URL is a valid author node; the previous
// fallback ("Keploy") produced a URL-less Person, a weaker E-E-A-T signal.
export const AUTHOR_FALLBACK_NAME = "Keploy Team";
export const SOCIAL_LINKS = [
"https://twitter.com/Keployio",
"https://www.linkedin.com/company/keploy/",
Expand Down Expand Up @@ -81,6 +96,7 @@ type BlogPostingInput = {
export const getOrganizationSchema = () => ({
"@context": "https://schema.org",
"@type": "Organization",
"@id": ORG_ID,
name: ORG_NAME,
url: MAIN_SITE_URL,
logo: ORG_LOGO_URL,
Expand Down Expand Up @@ -128,6 +144,29 @@ export const getWebSiteSchema = (searchTarget = `${SITE_URL}/search?q={search_te
},
});

/**
* Reusable ImageObject node. Google/AI engines prefer a typed ImageObject over
Comment thread
dhananjay6561 marked this conversation as resolved.
Outdated
* a bare URL string (it can carry caption + dimensions), and it's the shape the
* Article `image` field and listing/cover images should share.
*/
export const getImageObjectSchema = ({
url,
caption,
width,
height,
}: {
url: string;
caption?: string;
width?: number;
height?: number;
}) => ({
"@type": "ImageObject",
url,
...(caption ? { caption } : {}),
...(typeof width === "number" ? { width } : {}),
...(typeof height === "number" ? { height } : {}),
});

export const getBreadcrumbListSchema = (items: BreadcrumbItem[]) => ({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
Expand All @@ -139,6 +178,69 @@ export const getBreadcrumbListSchema = (items: BreadcrumbItem[]) => ({
})),
});

// Coerce a possibly-null/malformed WordPress date into a valid ISO 8601 string,
// or null if unparseable. Stops empty / "Invalid Date" values from reaching the
// schema, which is a structured-data validation error.
const toISODate = (value?: string): string | null => {
if (!value) return null;
const d = new Date(value);
return isNaN(d.getTime()) ? null : d.toISOString();
};

type ListEntry = { url: string; name: string };

const toListItems = (items: ListEntry[]) =>
items.map((it, index) => ({
"@type": "ListItem",
position: index + 1,
url: it.url,
name: it.name,
}));

/**
* ItemList for a grid/collection of posts. Emitted standalone (has @context) so
* AI/search engines can map the ordered set of links on listing pages, which
* currently ship zero collection schema.
*/
export const getItemListSchema = (items: ListEntry[], listName?: string) => ({
"@context": "https://schema.org",
"@type": "ItemList",
...(listName ? { name: listName } : {}),
itemListElement: toListItems(items),
Comment thread
dhananjay6561 marked this conversation as resolved.
Outdated
});

/**
* CollectionPage for an archive/listing route (community, technology, tag,
* authors). Wraps the post/author grid as an ItemList mainEntity so the whole
* archive is a modeled collection rather than an unlabeled wall of cards.
*/
export const getCollectionPageSchema = ({
name,
url,
description,
items,
}: {
name: string;
url: string;
description?: string;
items: ListEntry[];
}) => ({
"@context": "https://schema.org",
"@type": "CollectionPage",
name,
url,
...(description ? { description } : {}),
isPartOf: {
"@type": "Blog",
name: BLOG_NAME,
url: SITE_URL,
},
mainEntity: {
"@type": "ItemList",
itemListElement: toListItems(items),
},
});

export const getBlogPostingSchema = ({
title,
url,
Expand All @@ -156,9 +258,8 @@ export const getBlogPostingSchema = ({
reviewerImage,
reviewerDescription,
}: BlogPostingInput) => {
const resolvedAuthorName = Array.isArray(authorName)
? (authorName[0] || ORG_NAME)
: (authorName || ORG_NAME);
const resolvedAuthorName =
(Array.isArray(authorName) ? authorName[0] : authorName) || AUTHOR_FALLBACK_NAME;
const authorSlug = sanitizeAuthorSlug(resolvedAuthorName);

// GEO-13: blog/technology posts render as TechArticle
Expand All @@ -177,6 +278,12 @@ export const getBlogPostingSchema = ({
authorNode.image = authorImage;
}

// Never emit an empty/invalid datePublished. Prefer the post's own date,
// then its modified date, and only as a last resort the current build time.
const resolvedPublished =
toISODate(datePublished) || toISODate(dateModified) || new Date().toISOString();
const resolvedModified = toISODate(dateModified) || resolvedPublished;

const schema: Record<string, unknown> = {
"@context": "https://schema.org",
"@type": schemaType,
Expand All @@ -185,11 +292,12 @@ export const getBlogPostingSchema = ({
"@type": "WebPage",
"@id": url,
},
datePublished,
dateModified: dateModified || datePublished,
datePublished: resolvedPublished,
dateModified: resolvedModified,
author: authorNode,
publisher: {
"@type": "Organization",
"@id": ORG_ID,
name: ORG_NAME,
logo: {
"@type": "ImageObject",
Expand Down Expand Up @@ -232,13 +340,24 @@ export const getBlogPostingSchema = ({
schema.articleSection = articleSection;
}

// Sanitize the WP excerpt before it enters the schema: strip HTML tags and
// decode entities (script-safe) so unescaped markup/entities can't produce a
// structured-data parse warning. Only emit when something survives.
if (description) {
schema.description = description;
const cleanDescription = decodeEntities(description);
Comment thread
dhananjay6561 marked this conversation as resolved.
if (cleanDescription) {
schema.description = cleanDescription;
}
}

if (imageUrl) {
schema.image = [imageUrl];
}
// Always emit an image so the Article schema never trips the "missing field
// image" validation error — WordPress returns a null featuredImage on many
// older/migrated posts. Fall back to the site's default OG cover, and emit a
// typed ImageObject (not a bare URL) so rich results can use its caption.
schema.image = getImageObjectSchema({
url: imageUrl || DEFAULT_ARTICLE_IMAGE_URL,
caption: title,
});

// TechArticle-specific fields — only emit when set AND when we're
// actually rendering a TechArticle.
Expand All @@ -263,6 +382,7 @@ export const getBlogSchema = () => ({
"Technical blog covering AI-powered API test generation, eBPF-based testing, production behavior replay, dependency virtualization, and developer productivity by Keploy.",
publisher: {
"@type": "Organization",
"@id": ORG_ID,
name: ORG_NAME,
url: MAIN_SITE_URL,
logo: {
Expand Down
15 changes: 14 additions & 1 deletion pages/authors/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import Container from "../../components/container";
import AuthorMapping from "../../components/AuthorMapping";
import { HOME_OG_IMAGE_URL } from "../../lib/constants";
import { Post } from "../../types/post";
import { getBreadcrumbListSchema, SITE_URL } from "../../lib/structured-data";
import { getBreadcrumbListSchema, getCollectionPageSchema, SITE_URL } from "../../lib/structured-data";
import { sanitizeAuthorSlug } from "../../utils/sanitizeAuthorSlug";
import { REVALIDATE_CONTENT } from "../../lib/isr";

export default function Authors({
Expand Down Expand Up @@ -34,6 +35,18 @@ export default function Authors({
{ name: "Home", url: SITE_URL },
{ name: "Authors", url: `${SITE_URL}/authors` },
]),
getCollectionPageSchema({
name: "Keploy Blog Authors",
url: `${SITE_URL}/authors`,
description:
"Engineers, developers, and QA experts writing on the Keploy blog about testing, automation, and software quality.",
items: authorArray
.filter((node) => node.ppmaAuthorName)
.map((node) => ({
url: `${SITE_URL}/authors/${sanitizeAuthorSlug(node.ppmaAuthorName)}`,
name: node.ppmaAuthorName,
})),
}),
]}
canonicalUrl={`${SITE_URL}/authors`}
>
Expand Down
4 changes: 2 additions & 2 deletions pages/community/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
getBreadcrumbListSchema,
SITE_URL,
} from "../../lib/structured-data";
import { sanitizeTitle, getSafeDescription } from "../../utils/seo";
import { sanitizeTitle, getSafeDescription, buildPageTitle } from "../../utils/seo";
import { getHowToSchema } from "../../lib/howToSchema";

const PostBody = dynamic(() => import("../../components/post-body"));
Expand Down Expand Up @@ -219,7 +219,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) {
<>
<article>
<Head>
<title>{`${post?.title || "Loading..."} | Keploy Blog`}</title>
<title>{buildPageTitle(post?.title)}</title>
{/* DM Sans + Baloo 2 are preloaded globally in _document.tsx */}
</Head>
<PostHeader
Expand Down
15 changes: 14 additions & 1 deletion pages/community/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import HeroPost from "../../components/hero-post";
import Layout from "../../components/layout";
import { getAllPostsForCommunity } from "../../lib/api";
import Header from "../../components/header";
import { getBreadcrumbListSchema, SITE_URL } from "../../lib/structured-data";
import { getBreadcrumbListSchema, getCollectionPageSchema, SITE_URL } from "../../lib/structured-data";
import { REVALIDATE_CONTENT } from "../../lib/isr";

export default function Community({ allPosts: { edges, pageInfo }, preview }) {
Expand All @@ -18,6 +18,16 @@ export default function Community({ allPosts: { edges, pageInfo }, preview }) {
{ name: "Home", url: SITE_URL },
{ name: "Community", url: `${SITE_URL}/community` },
]),
getCollectionPageSchema({
name: "Keploy Community Blog",
url: `${SITE_URL}/community`,
description:
"Developer stories, open-source contributions, API testing tutorials, and hands-on engineering guides from the Keploy community.",
items: edges.map(({ node }) => ({
url: `${SITE_URL}/community/${node.slug}`,
name: node.title,
})),
}),
];
function getExcerpt(content) {
const maxWords = 50;
Expand Down Expand Up @@ -46,6 +56,9 @@ export default function Community({ allPosts: { edges, pageInfo }, preview }) {
</Head>
<Header />
<Container>
<h1 className="text-3xl md:text-4xl font-bold tracking-tight mt-4 mb-8">
Keploy Community Blog
</h1>
{/* <Intro /> */}
{heroPost && (
<HeroPost
Expand Down
18 changes: 17 additions & 1 deletion pages/tag/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Container from "../../components/container";
import { getAllPostsFromTags, getAllTags } from "../../lib/api";
import TagsStories from "../../components/TagsStories";
import { useRouter } from "next/router";
import { getBreadcrumbListSchema, SITE_URL } from "../../lib/structured-data";
import { getBreadcrumbListSchema, getCollectionPageSchema, SITE_URL } from "../../lib/structured-data";
import { REVALIDATE_CONTENT, REVALIDATE_ERROR, REVALIDATE_NOT_FOUND } from "../../lib/isr";
export default function PostByTags({ postsByTags, preview, tagSlug: tagSlugProp }) {
const posts = postsByTags?.edges || [];
Expand All @@ -26,6 +26,19 @@ export default function PostByTags({ postsByTags, preview, tagSlug: tagSlugProp
{ name: "Tags", url: `${SITE_URL}/tag` },
{ name: `${tagDisplay || "Tag"}`, url: `${SITE_URL}/tag/${tagSlug || ""}` },
]),
getCollectionPageSchema({
name: `${tagDisplay} posts`,
url: `${SITE_URL}/tag/${tagSlug || ""}`,
description: `Keploy blog posts tagged "${tagDisplay}".`,
items: posts.map(({ node }: any) => {
const isCommunity =
node.categories?.edges?.[0]?.node?.name === "community";
return {
url: `${SITE_URL}/${isCommunity ? "community" : "technology"}/${node.slug}`,
name: node.title,
};
}),
}),
]}
canonicalUrl={tagSlug ? `${SITE_URL}/tag/${tagSlug}` : `${SITE_URL}/tag`}
>
Expand All @@ -34,6 +47,9 @@ export default function PostByTags({ postsByTags, preview, tagSlug: tagSlugProp
</Head>
<Header />
<Container>
<h1 className="text-3xl md:text-4xl font-bold tracking-tight mt-4 mb-8">
{tagDisplay} posts
</h1>
<TagsStories posts={posts} />
</Container>
</Layout>
Expand Down
4 changes: 2 additions & 2 deletions pages/technology/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
getBreadcrumbListSchema,
SITE_URL,
} from "../../lib/structured-data";
import { sanitizeTitle, getSafeDescription } from "../../utils/seo";
import { sanitizeTitle, getSafeDescription, buildPageTitle } from "../../utils/seo";
import { getHowToSchema } from "../../lib/howToSchema";

const PostBody = dynamic(() => import("../../components/post-body"));
Expand Down Expand Up @@ -197,7 +197,7 @@ export default function Post({ post, posts, reviewAuthorDetails, preview }) {
<>
<article>
<Head>
<title>{`${post?.title || "Loading..."} | Keploy Blog`}</title>
<title>{buildPageTitle(post?.title)}</title>
{/* DM Sans + Baloo 2 are preloaded globally in _document.tsx */}
</Head>
<PostHeader
Expand Down
Loading
Loading